changeset 815:df442f1ba0c6

reorganization
author Sebastien Jodogne <s.jodogne@gmail.com>
date Tue, 28 May 2019 21:59:20 +0200
parents aead999345e0
children 1f85e9c7d020
files Framework/Loaders/DicomStructureSetLoader.cpp Framework/Loaders/DicomStructureSetLoader.h Framework/Loaders/LoaderStateMachine.cpp Framework/Loaders/LoaderStateMachine.h Framework/Loaders/OrthancMultiframeVolumeLoader.cpp Framework/Loaders/OrthancMultiframeVolumeLoader.h Framework/Volumes/DicomVolumeImageReslicer.cpp Framework/Volumes/DicomVolumeImageReslicer.h Framework/Volumes/VolumeSceneLayerSource.cpp Framework/Volumes/VolumeSceneLayerSource.h Resources/CMake/OrthancStoneConfiguration.cmake Samples/Sdl/Loader.cpp
diffstat 12 files changed, 1440 insertions(+), 1067 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Loaders/DicomStructureSetLoader.cpp	Tue May 28 21:59:20 2019 +0200
@@ -0,0 +1,254 @@
+/**
+ * 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 "DicomStructureSetLoader.h"
+
+#include "../Scene2D/PolylineSceneLayer.h"
+#include "../Toolbox/GeometryToolbox.h"
+
+namespace OrthancStone
+{
+  class DicomStructureSetLoader::AddReferencedInstance : public LoaderStateMachine::State
+  {
+  private:
+    std::string instanceId_;
+      
+  public:
+    AddReferencedInstance(DicomStructureSetLoader& that,
+                          const std::string& instanceId) :
+      State(that),
+      instanceId_(instanceId)
+    {
+    }
+
+    virtual void Handle(const OrthancRestApiCommand::SuccessMessage& message)
+    {
+      Json::Value tags;
+      message.ParseJsonBody(tags);
+        
+      Orthanc::DicomMap dicom;
+      dicom.FromDicomAsJson(tags);
+
+      DicomStructureSetLoader& loader = GetLoader<DicomStructureSetLoader>();
+      loader.content_->AddReferencedSlice(dicom);
+
+      loader.countProcessedInstances_ ++;
+      assert(loader.countProcessedInstances_ <= loader.countReferencedInstances_);
+
+      if (loader.countProcessedInstances_ == loader.countReferencedInstances_)
+      {
+        // All the referenced instances have been loaded, finalize the RT-STRUCT
+        loader.content_->CheckReferencedSlices();
+        loader.revision_++;
+      }
+    }
+  };
+
+
+  // State that converts a "SOP Instance UID" to an Orthanc identifier
+  class DicomStructureSetLoader::LookupInstance : public LoaderStateMachine::State
+  {
+  private:
+    std::string  sopInstanceUid_;
+      
+  public:
+    LookupInstance(DicomStructureSetLoader& that,
+                   const std::string& sopInstanceUid) :
+      State(that),
+      sopInstanceUid_(sopInstanceUid)
+    {
+    }
+
+    virtual void Handle(const OrthancRestApiCommand::SuccessMessage& message)
+    {
+      DicomStructureSetLoader& loader = GetLoader<DicomStructureSetLoader>();
+
+      Json::Value lookup;
+      message.ParseJsonBody(lookup);
+
+      if (lookup.type() != Json::arrayValue ||
+          lookup.size() != 1 ||
+          !lookup[0].isMember("Type") ||
+          !lookup[0].isMember("Path") ||
+          lookup[0]["Type"].type() != Json::stringValue ||
+          lookup[0]["ID"].type() != Json::stringValue ||
+          lookup[0]["Type"].asString() != "Instance")
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_UnknownResource);          
+      }
+
+      const std::string instanceId = lookup[0]["ID"].asString();
+
+      {
+        std::auto_ptr<OrthancRestApiCommand> command(new OrthancRestApiCommand);
+        command->SetHttpHeader("Accept-Encoding", "gzip");
+        command->SetUri("/instances/" + instanceId + "/tags");
+        command->SetPayload(new AddReferencedInstance(loader, instanceId));
+        Schedule(command.release());
+      }
+    }
+  };
+
+
+  class DicomStructureSetLoader::LoadStructure : public LoaderStateMachine::State
+  {
+  public:
+    LoadStructure(DicomStructureSetLoader& that) :
+    State(that)
+    {
+    }
+
+    virtual void Handle(const OrthancRestApiCommand::SuccessMessage& message)
+    {
+      DicomStructureSetLoader& loader = GetLoader<DicomStructureSetLoader>();
+        
+      {
+        OrthancPlugins::FullOrthancDataset dicom(message.GetAnswer());
+        loader.content_.reset(new DicomStructureSet(dicom));
+      }
+
+      std::set<std::string> instances;
+      loader.content_->GetReferencedInstances(instances);
+
+      loader.countReferencedInstances_ = instances.size();
+
+      for (std::set<std::string>::const_iterator
+             it = instances.begin(); it != instances.end(); ++it)
+      {
+        std::auto_ptr<OrthancRestApiCommand> command(new OrthancRestApiCommand);
+        command->SetUri("/tools/lookup");
+        command->SetMethod(Orthanc::HttpMethod_Post);
+        command->SetBody(*it);
+        command->SetPayload(new LookupInstance(loader, *it));
+        Schedule(command.release());
+      }
+    }
+  };
+    
+
+  class DicomStructureSetLoader::Slice : public IExtractedSlice
+  {
+  private:
+    const DicomStructureSet&  content_;
+    uint64_t                  revision_;
+    bool                      isValid_;
+      
+  public:
+    Slice(const DicomStructureSet& content,
+          uint64_t revision,
+          const CoordinateSystem3D& cuttingPlane) :
+      content_(content),
+      revision_(revision)
+    {
+      bool opposite;
+
+      const Vector normal = content.GetNormal();
+      isValid_ = (
+        GeometryToolbox::IsParallelOrOpposite(opposite, normal, cuttingPlane.GetNormal()) ||
+        GeometryToolbox::IsParallelOrOpposite(opposite, normal, cuttingPlane.GetAxisX()) ||
+        GeometryToolbox::IsParallelOrOpposite(opposite, normal, cuttingPlane.GetAxisY()));
+    }
+      
+    virtual bool IsValid()
+    {
+      return isValid_;
+    }
+
+    virtual uint64_t GetRevision()
+    {
+      return revision_;
+    }
+
+    virtual ISceneLayer* CreateSceneLayer(const ILayerStyleConfigurator* configurator,
+                                          const CoordinateSystem3D& cuttingPlane)
+    {
+      assert(isValid_);
+
+      std::auto_ptr<PolylineSceneLayer> layer(new PolylineSceneLayer);
+      layer->SetThickness(2);
+
+      for (size_t i = 0; i < content_.GetStructuresCount(); i++)
+      {
+        const Color& color = content_.GetStructureColor(i);
+
+        std::vector< std::vector<DicomStructureSet::PolygonPoint> > polygons;
+          
+        if (content_.ProjectStructure(polygons, i, cuttingPlane))
+        {
+          for (size_t j = 0; j < polygons.size(); j++)
+          {
+            PolylineSceneLayer::Chain chain;
+            chain.resize(polygons[j].size());
+            
+            for (size_t k = 0; k < polygons[j].size(); k++)
+            {
+              chain[k] = ScenePoint2D(polygons[j][k].first, polygons[j][k].second);
+            }
+
+            layer->AddChain(chain, true /* closed */, color);
+          }
+        }
+      }
+
+      return layer.release();
+    }
+  };
+    
+
+  DicomStructureSetLoader::DicomStructureSetLoader(IOracle& oracle,
+                                                   IObservable& oracleObservable) :
+    LoaderStateMachine(oracle, oracleObservable),
+    revision_(0),
+    countProcessedInstances_(0),
+    countReferencedInstances_(0)
+  {
+  }
+    
+    
+  void DicomStructureSetLoader::LoadInstance(const std::string& instanceId)
+  {
+    Start();
+      
+    instanceId_ = instanceId;
+      
+    {
+      std::auto_ptr<OrthancRestApiCommand> command(new OrthancRestApiCommand);
+      command->SetHttpHeader("Accept-Encoding", "gzip");
+      command->SetUri("/instances/" + instanceId + "/tags?ignore-length=3006-0050");
+      command->SetPayload(new LoadStructure(*this));
+      Schedule(command.release());
+    }
+  }
+
+
+  IVolumeSlicer::IExtractedSlice* DicomStructureSetLoader::ExtractSlice(const CoordinateSystem3D& cuttingPlane)
+  {
+    if (content_.get() == NULL)
+    {
+      // Geometry is not available yet
+      return new IVolumeSlicer::InvalidSlice;
+    }
+    else
+    {
+      return new Slice(*content_, revision_, cuttingPlane);
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Loaders/DicomStructureSetLoader.h	Tue May 28 21:59:20 2019 +0200
@@ -0,0 +1,56 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-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/>.
+ **/
+
+
+#pragma once
+
+#include "../Toolbox/DicomStructureSet.h"
+#include "../Volumes/IVolumeSlicer.h"
+#include "LoaderStateMachine.h"
+
+namespace OrthancStone
+{
+  class DicomStructureSetLoader :
+    public LoaderStateMachine,
+    public IVolumeSlicer
+  {
+  private:
+    class Slice;
+
+    // States of LoaderStateMachine
+    class AddReferencedInstance;   // 3rd state
+    class LookupInstance;          // 2nd state
+    class LoadStructure;           // 1st state
+    
+    std::auto_ptr<DicomStructureSet>  content_;
+    uint64_t                          revision_;
+    std::string                       instanceId_;
+    unsigned int                      countProcessedInstances_;
+    unsigned int                      countReferencedInstances_;  
+    
+  public:
+    DicomStructureSetLoader(IOracle& oracle,
+                            IObservable& oracleObservable);    
+    
+    void LoadInstance(const std::string& instanceId);
+
+    virtual IExtractedSlice* ExtractSlice(const CoordinateSystem3D& cuttingPlane);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Loaders/LoaderStateMachine.cpp	Tue May 28 21:59:20 2019 +0200
@@ -0,0 +1,174 @@
+/**
+ * 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 "LoaderStateMachine.h"
+
+#include <Core/OrthancException.h>
+
+namespace OrthancStone
+{
+  void LoaderStateMachine::State::Handle(const OrthancRestApiCommand::SuccessMessage& message)
+  {
+    throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+  }
+      
+
+  void LoaderStateMachine::State::Handle(const GetOrthancImageCommand::SuccessMessage& message)
+  {
+    throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+  }
+
+      
+  void LoaderStateMachine::State::Handle(const GetOrthancWebViewerJpegCommand::SuccessMessage& message)
+  {
+    throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+  }
+
+
+  void LoaderStateMachine::Schedule(OracleCommandWithPayload* command)
+  {
+    std::auto_ptr<OracleCommandWithPayload> protection(command);
+
+    if (command == NULL)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
+    }
+      
+    if (!command->HasPayload())
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange,
+                                      "The payload must contain the next state");
+    }
+
+    pendingCommands_.push_back(protection.release());
+    Step();
+  }
+
+
+  void LoaderStateMachine::Start()
+  {
+    if (active_)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+    }
+
+    active_ = true;
+
+    for (size_t i = 0; i < simultaneousDownloads_; i++)
+    {
+      Step();
+    }
+  }
+
+
+  void LoaderStateMachine::Step()
+  {
+    if (!pendingCommands_.empty() &&
+        activeCommands_ < simultaneousDownloads_)
+    {
+      oracle_.Schedule(*this, pendingCommands_.front());
+      pendingCommands_.pop_front();
+
+      activeCommands_++;
+    }
+  }
+
+
+  void LoaderStateMachine::Clear()
+  {
+    for (PendingCommands::iterator it = pendingCommands_.begin();
+         it != pendingCommands_.end(); ++it)
+    {
+      delete *it;
+    }
+
+    pendingCommands_.clear();
+  }
+
+
+  void LoaderStateMachine::HandleExceptionMessage(const OracleCommandExceptionMessage& message)
+  {
+    LOG(ERROR) << "Error in the state machine, stopping all processing";
+    Clear();
+  }
+
+
+  template <typename T>
+  void LoaderStateMachine::HandleSuccessMessage(const T& message)
+  {
+    assert(activeCommands_ > 0);
+    activeCommands_--;
+
+    try
+    {
+      dynamic_cast<State&>(message.GetOrigin().GetPayload()).Handle(message);
+      Step();
+    }
+    catch (Orthanc::OrthancException& e)
+    {
+      LOG(ERROR) << "Error in the state machine, stopping all processing: " << e.What();
+      Clear();
+    }
+  }
+
+
+  LoaderStateMachine::LoaderStateMachine(IOracle& oracle,
+                                         IObservable& oracleObservable) :
+    IObserver(oracleObservable.GetBroker()),
+    oracle_(oracle),
+    active_(false),
+    simultaneousDownloads_(4),
+    activeCommands_(0)
+  {
+    oracleObservable.RegisterObserverCallback(
+      new Callable<LoaderStateMachine, OrthancRestApiCommand::SuccessMessage>
+      (*this, &LoaderStateMachine::HandleSuccessMessage));
+
+    oracleObservable.RegisterObserverCallback(
+      new Callable<LoaderStateMachine, GetOrthancImageCommand::SuccessMessage>
+      (*this, &LoaderStateMachine::HandleSuccessMessage));
+
+    oracleObservable.RegisterObserverCallback(
+      new Callable<LoaderStateMachine, GetOrthancWebViewerJpegCommand::SuccessMessage>
+      (*this, &LoaderStateMachine::HandleSuccessMessage));
+
+    oracleObservable.RegisterObserverCallback(
+      new Callable<LoaderStateMachine, OracleCommandExceptionMessage>
+      (*this, &LoaderStateMachine::HandleExceptionMessage));
+  }
+
+
+  void LoaderStateMachine::SetSimultaneousDownloads(unsigned int count)
+  {
+    if (active_)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+    }
+    else if (count == 0)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);        
+    }
+    else
+    {
+      simultaneousDownloads_ = count;
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Loaders/LoaderStateMachine.h	Tue May 28 21:59:20 2019 +0200
@@ -0,0 +1,119 @@
+/**
+ * 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/>.
+ **/
+
+
+#pragma once
+
+#include "../Messages/IObservable.h"
+#include "../Messages/IObserver.h"
+#include "../Oracle/GetOrthancImageCommand.h"
+#include "../Oracle/GetOrthancWebViewerJpegCommand.h"
+#include "../Oracle/IOracle.h"
+#include "../Oracle/OracleCommandExceptionMessage.h"
+#include "../Oracle/OrthancRestApiCommand.h"
+
+#include <Core/IDynamicObject.h>
+
+#include <list>
+
+namespace OrthancStone
+{
+  /**
+     This class is supplied with Oracle commands and will schedule up to 
+     simultaneousDownloads_ of them at the same time, then will schedule the 
+     rest once slots become available. It is used, a.o., by the 
+     OrtancMultiframeVolumeLoader class.
+  */
+  class LoaderStateMachine : public IObserver
+  {
+  protected:
+    class State : public Orthanc::IDynamicObject
+    {
+    private:
+      LoaderStateMachine&  that_;
+
+    public:
+      State(LoaderStateMachine& that) :
+      that_(that)
+      {
+      }
+
+      State(const State& currentState) :
+      that_(currentState.that_)
+      {
+      }
+
+      void Schedule(OracleCommandWithPayload* command) const
+      {
+        that_.Schedule(command);
+      }
+
+      template <typename T>
+      T& GetLoader() const
+      {
+        return dynamic_cast<T&>(that_);
+      }
+      
+      virtual void Handle(const OrthancRestApiCommand::SuccessMessage& message);
+      
+      virtual void Handle(const GetOrthancImageCommand::SuccessMessage& message);
+      
+      virtual void Handle(const GetOrthancWebViewerJpegCommand::SuccessMessage& message);
+    };
+
+    void Schedule(OracleCommandWithPayload* command);
+
+    void Start();
+
+  private:
+    void Step();
+
+    void Clear();
+
+    void HandleExceptionMessage(const OracleCommandExceptionMessage& message);
+
+    template <typename T>
+    void HandleSuccessMessage(const T& message);
+
+    typedef std::list<IOracleCommand*>  PendingCommands;
+
+    IOracle&         oracle_;
+    bool             active_;
+    unsigned int     simultaneousDownloads_;
+    PendingCommands  pendingCommands_;
+    unsigned int     activeCommands_;
+
+  public:
+    LoaderStateMachine(IOracle& oracle,
+                       IObservable& oracleObservable);
+
+    virtual ~LoaderStateMachine()
+    {
+      Clear();
+    }
+
+    bool IsActive() const
+    {
+      return active_;
+    }
+
+    void SetSimultaneousDownloads(unsigned int count);  
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Loaders/OrthancMultiframeVolumeLoader.cpp	Tue May 28 21:59:20 2019 +0200
@@ -0,0 +1,355 @@
+/**
+ * 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 "OrthancMultiframeVolumeLoader.h"
+
+#include <Core/Toolbox.h>
+
+namespace OrthancStone
+{
+  class OrthancMultiframeVolumeLoader::LoadRTDoseGeometry : public LoaderStateMachine::State
+  {
+  private:
+    std::auto_ptr<Orthanc::DicomMap>  dicom_;
+
+  public:
+    LoadRTDoseGeometry(OrthancMultiframeVolumeLoader& that,
+                       Orthanc::DicomMap* dicom) :
+      State(that),
+      dicom_(dicom)
+    {
+      if (dicom == NULL)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
+      }
+
+    }
+
+    virtual void Handle(const OrthancRestApiCommand::SuccessMessage& message)
+    {
+      // Complete the DICOM tags with just-received "Grid Frame Offset Vector"
+      std::string s = Orthanc::Toolbox::StripSpaces(message.GetAnswer());
+      dicom_->SetValue(Orthanc::DICOM_TAG_GRID_FRAME_OFFSET_VECTOR, s, false);
+
+      GetLoader<OrthancMultiframeVolumeLoader>().SetGeometry(*dicom_);
+    }      
+  };
+
+
+  static std::string GetSopClassUid(const Orthanc::DicomMap& dicom)
+  {
+    std::string s;
+    if (!dicom.CopyToString(s, Orthanc::DICOM_TAG_SOP_CLASS_UID, false))
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat,
+                                      "DICOM file without SOP class UID");
+    }
+    else
+    {
+      return s;
+    }
+  }
+    
+
+  class OrthancMultiframeVolumeLoader::LoadGeometry : public State
+  {
+  public:
+    LoadGeometry(OrthancMultiframeVolumeLoader& that) :
+    State(that)
+    {
+    }
+      
+    virtual void Handle(const OrthancRestApiCommand::SuccessMessage& message)
+    {
+      OrthancMultiframeVolumeLoader& loader = GetLoader<OrthancMultiframeVolumeLoader>();
+        
+      Json::Value body;
+      message.ParseJsonBody(body);
+        
+      if (body.type() != Json::objectValue)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol);
+      }
+
+      std::auto_ptr<Orthanc::DicomMap> dicom(new Orthanc::DicomMap);
+      dicom->FromDicomAsJson(body);
+
+      if (StringToSopClassUid(GetSopClassUid(*dicom)) == SopClassUid_RTDose)
+      {
+        // Download the "Grid Frame Offset Vector" DICOM tag, that is
+        // mandatory for RT-DOSE, but is too long to be returned by default
+          
+        std::auto_ptr<OrthancRestApiCommand> command(new OrthancRestApiCommand);
+        command->SetUri("/instances/" + loader.GetInstanceId() + "/content/" +
+                        Orthanc::DICOM_TAG_GRID_FRAME_OFFSET_VECTOR.Format());
+        command->SetPayload(new LoadRTDoseGeometry(loader, dicom.release()));
+
+        Schedule(command.release());
+      }
+      else
+      {
+        loader.SetGeometry(*dicom);
+      }
+    }
+  };
+
+
+
+  class OrthancMultiframeVolumeLoader::LoadTransferSyntax : public State
+  {
+  public:
+    LoadTransferSyntax(OrthancMultiframeVolumeLoader& that) :
+    State(that)
+    {
+    }
+      
+    virtual void Handle(const OrthancRestApiCommand::SuccessMessage& message)
+    {
+      GetLoader<OrthancMultiframeVolumeLoader>().SetTransferSyntax(message.GetAnswer());
+    }
+  };
+   
+    
+  class OrthancMultiframeVolumeLoader::LoadUncompressedPixelData : public State
+  {
+  public:
+    LoadUncompressedPixelData(OrthancMultiframeVolumeLoader& that) :
+    State(that)
+    {
+    }
+      
+    virtual void Handle(const OrthancRestApiCommand::SuccessMessage& message)
+    {
+      GetLoader<OrthancMultiframeVolumeLoader>().SetUncompressedPixelData(message.GetAnswer());
+    }
+  };
+   
+    
+  const std::string& OrthancMultiframeVolumeLoader::GetInstanceId() const
+  {
+    if (IsActive())
+    {
+      return instanceId_;
+    }
+    else
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+    }
+  }
+
+
+  void OrthancMultiframeVolumeLoader::ScheduleFrameDownloads()
+  {
+    if (transferSyntaxUid_.empty() ||
+        !volume_->HasGeometry())
+    {
+      return;
+    }
+    /*
+      1.2.840.10008.1.2	Implicit VR Endian: Default Transfer Syntax for DICOM
+      1.2.840.10008.1.2.1	Explicit VR Little Endian
+      1.2.840.10008.1.2.2	Explicit VR Big Endian
+
+      See https://www.dicomlibrary.com/dicom/transfer-syntax/
+    */
+    if (transferSyntaxUid_ == "1.2.840.10008.1.2" ||
+        transferSyntaxUid_ == "1.2.840.10008.1.2.1" ||
+        transferSyntaxUid_ == "1.2.840.10008.1.2.2")
+    {
+      std::auto_ptr<OrthancRestApiCommand> command(new OrthancRestApiCommand);
+      command->SetHttpHeader("Accept-Encoding", "gzip");
+      command->SetUri("/instances/" + instanceId_ + "/content/" +
+                      Orthanc::DICOM_TAG_PIXEL_DATA.Format() + "/0");
+      command->SetPayload(new LoadUncompressedPixelData(*this));
+      Schedule(command.release());
+    }
+    else
+    {
+      throw Orthanc::OrthancException(
+        Orthanc::ErrorCode_NotImplemented,
+        "No support for multiframe instances with transfer syntax: " + transferSyntaxUid_);
+    }
+  }
+      
+
+  void OrthancMultiframeVolumeLoader::SetTransferSyntax(const std::string& transferSyntax)
+  {
+    transferSyntaxUid_ = Orthanc::Toolbox::StripSpaces(transferSyntax);
+    ScheduleFrameDownloads();
+  }
+    
+
+  void OrthancMultiframeVolumeLoader::SetGeometry(const Orthanc::DicomMap& dicom)
+  {
+    DicomInstanceParameters parameters(dicom);
+    volume_->SetDicomParameters(parameters);
+      
+    Orthanc::PixelFormat format;
+    if (!parameters.GetImageInformation().ExtractPixelFormat(format, true))
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+    }
+
+    double spacingZ;
+    switch (parameters.GetSopClassUid())
+    {
+      case SopClassUid_RTDose:
+        spacingZ = parameters.GetThickness();
+        break;
+
+      default:
+        throw Orthanc::OrthancException(
+          Orthanc::ErrorCode_NotImplemented,
+          "No support for multiframe instances with SOP class UID: " + GetSopClassUid(dicom));
+    }
+
+    const unsigned int width = parameters.GetImageInformation().GetWidth();
+    const unsigned int height = parameters.GetImageInformation().GetHeight();
+    const unsigned int depth = parameters.GetImageInformation().GetNumberOfFrames();
+
+    {
+      VolumeImageGeometry geometry;
+      geometry.SetSize(width, height, depth);
+      geometry.SetAxialGeometry(parameters.GetGeometry());
+      geometry.SetVoxelDimensions(parameters.GetPixelSpacingX(),
+                                  parameters.GetPixelSpacingY(), spacingZ);
+      volume_->Initialize(geometry, format);
+    }
+
+    volume_->GetPixelData().Clear();
+
+    ScheduleFrameDownloads();
+
+    BroadcastMessage(DicomVolumeImage::GeometryReadyMessage(*volume_));
+  }
+
+
+  ORTHANC_FORCE_INLINE
+  static void CopyPixel(uint32_t& target,
+                        const void* source)
+  {
+    // TODO - check alignement?
+    target = le32toh(*reinterpret_cast<const uint32_t*>(source));
+  }
+      
+
+  template <typename T>
+  void OrthancMultiframeVolumeLoader::CopyPixelData(const std::string& pixelData)
+  {
+    ImageBuffer3D& target = volume_->GetPixelData();
+      
+    const unsigned int bpp = target.GetBytesPerPixel();
+    const unsigned int width = target.GetWidth();
+    const unsigned int height = target.GetHeight();
+    const unsigned int depth = target.GetDepth();
+
+    if (pixelData.size() != bpp * width * height * depth)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat,
+                                      "The pixel data has not the proper size");
+    }
+
+    if (pixelData.empty())
+    {
+      return;
+    }
+
+    const uint8_t* source = reinterpret_cast<const uint8_t*>(pixelData.c_str());
+
+    for (unsigned int z = 0; z < depth; z++)
+    {
+      ImageBuffer3D::SliceWriter writer(target, VolumeProjection_Axial, z);
+
+      assert (writer.GetAccessor().GetWidth() == width &&
+              writer.GetAccessor().GetHeight() == height);
+
+      for (unsigned int y = 0; y < height; y++)
+      {
+        assert(sizeof(T) == Orthanc::GetBytesPerPixel(target.GetFormat()));
+
+        T* target = reinterpret_cast<T*>(writer.GetAccessor().GetRow(y));
+
+        for (unsigned int x = 0; x < width; x++)
+        {
+          CopyPixel(*target, source);
+            
+          target ++;
+          source += bpp;
+        }
+      }
+    }
+  }
+    
+
+  void OrthancMultiframeVolumeLoader::SetUncompressedPixelData(const std::string& pixelData)
+  {
+    switch (volume_->GetPixelData().GetFormat())
+    {
+      case Orthanc::PixelFormat_Grayscale32:
+        CopyPixelData<uint32_t>(pixelData);
+        break;
+
+      default:
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+    }
+
+    volume_->IncrementRevision();
+
+    BroadcastMessage(DicomVolumeImage::ContentUpdatedMessage(*volume_));
+  }
+
+
+  OrthancMultiframeVolumeLoader::OrthancMultiframeVolumeLoader(const boost::shared_ptr<DicomVolumeImage>& volume,
+                                                               IOracle& oracle,
+                                                               IObservable& oracleObservable) :
+    LoaderStateMachine(oracle, oracleObservable),
+    IObservable(oracleObservable.GetBroker()),
+    volume_(volume)
+  {
+    if (volume.get() == NULL)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
+    }
+  }
+
+
+  void OrthancMultiframeVolumeLoader::LoadInstance(const std::string& instanceId)
+  {
+    Start();
+
+    instanceId_ = instanceId;
+
+    {
+      std::auto_ptr<OrthancRestApiCommand> command(new OrthancRestApiCommand);
+      command->SetHttpHeader("Accept-Encoding", "gzip");
+      command->SetUri("/instances/" + instanceId + "/tags");
+      command->SetPayload(new LoadGeometry(*this));
+      Schedule(command.release());
+    }
+
+    {
+      std::auto_ptr<OrthancRestApiCommand> command(new OrthancRestApiCommand);
+      command->SetUri("/instances/" + instanceId + "/metadata/TransferSyntax");
+      command->SetPayload(new LoadTransferSyntax(*this));
+      Schedule(command.release());
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Loaders/OrthancMultiframeVolumeLoader.h	Tue May 28 21:59:20 2019 +0200
@@ -0,0 +1,64 @@
+/**
+ * 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/>.
+ **/
+
+
+#pragma once
+
+#include "LoaderStateMachine.h"
+#include "../Volumes/DicomVolumeImage.h"
+
+namespace OrthancStone
+{
+  class OrthancMultiframeVolumeLoader :
+    public LoaderStateMachine,
+    public IObservable
+  {
+  private:
+    class LoadRTDoseGeometry;
+    class LoadGeometry;
+    class LoadTransferSyntax;    
+    class LoadUncompressedPixelData;
+
+    boost::shared_ptr<DicomVolumeImage>  volume_;
+    std::string                          instanceId_;
+    std::string                          transferSyntaxUid_;
+
+
+    const std::string& GetInstanceId() const;
+
+    void ScheduleFrameDownloads();
+
+    void SetTransferSyntax(const std::string& transferSyntax);
+
+    void SetGeometry(const Orthanc::DicomMap& dicom);
+
+    template <typename T>
+    void CopyPixelData(const std::string& pixelData);
+
+    void SetUncompressedPixelData(const std::string& pixelData);
+
+  public:
+    OrthancMultiframeVolumeLoader(const boost::shared_ptr<DicomVolumeImage>& volume,
+                                  IOracle& oracle,
+                                  IObservable& oracleObservable);
+
+    void LoadInstance(const std::string& instanceId);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Volumes/DicomVolumeImageReslicer.cpp	Tue May 28 21:59:20 2019 +0200
@@ -0,0 +1,116 @@
+/**
+ * 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 "DicomVolumeImageReslicer.h"
+
+#include <Core/OrthancException.h>
+
+namespace OrthancStone
+{
+  class DicomVolumeImageReslicer::Slice : public IVolumeSlicer::IExtractedSlice
+  {
+  private:
+    DicomVolumeImageReslicer&  that_;
+    CoordinateSystem3D         cuttingPlane_;
+      
+  public:
+    Slice(DicomVolumeImageReslicer& that,
+          const CoordinateSystem3D& cuttingPlane) :
+      that_(that),
+      cuttingPlane_(cuttingPlane)
+    {
+    }
+      
+    virtual bool IsValid()
+    {
+      return true;
+    }
+
+    virtual uint64_t GetRevision()
+    {
+      return that_.volume_->GetRevision();
+    }
+
+    virtual ISceneLayer* CreateSceneLayer(const ILayerStyleConfigurator* configurator,
+                                          const CoordinateSystem3D& cuttingPlane)
+    {
+      VolumeReslicer& reslicer = that_.reslicer_;
+        
+      if (configurator == NULL)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError,
+                                        "Must provide a layer style configurator");
+      }
+        
+      reslicer.SetOutputFormat(that_.volume_->GetPixelData().GetFormat());
+      reslicer.Apply(that_.volume_->GetPixelData(),
+                     that_.volume_->GetGeometry(),
+                     cuttingPlane);
+
+      if (reslicer.IsSuccess())
+      {
+        std::auto_ptr<TextureBaseSceneLayer> layer
+          (configurator->CreateTextureFromDicom(reslicer.GetOutputSlice(),
+                                                that_.volume_->GetDicomParameters()));
+        if (layer.get() == NULL)
+        {
+          return NULL;
+        }
+
+        double s = reslicer.GetPixelSpacing();
+        layer->SetPixelSpacing(s, s);
+        layer->SetOrigin(reslicer.GetOutputExtent().GetX1() + 0.5 * s,
+                         reslicer.GetOutputExtent().GetY1() + 0.5 * s);
+
+        // TODO - Angle!!
+                           
+        return layer.release();
+      }
+      else
+      {
+        return NULL;
+      }          
+    }
+  };
+    
+
+  DicomVolumeImageReslicer::DicomVolumeImageReslicer(const boost::shared_ptr<DicomVolumeImage>& volume) :
+    volume_(volume)
+  {
+    if (volume.get() == NULL)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
+    }
+  }
+
+    
+  IVolumeSlicer::IExtractedSlice* DicomVolumeImageReslicer::ExtractSlice(const CoordinateSystem3D& cuttingPlane)
+  {
+    if (volume_->HasGeometry())
+    {
+      return new Slice(*this, cuttingPlane);
+    }
+    else
+    {
+      return new IVolumeSlicer::InvalidSlice;
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Volumes/DicomVolumeImageReslicer.h	Tue May 28 21:59:20 2019 +0200
@@ -0,0 +1,69 @@
+/**
+ * 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/>.
+ **/
+
+
+#pragma once
+
+#include "DicomVolumeImage.h"
+#include "IVolumeSlicer.h"
+#include "VolumeReslicer.h"
+
+#include <boost/shared_ptr.hpp>
+
+namespace OrthancStone
+{
+  /**
+  This class is able to supply an extract slice for an arbitrary cutting
+  plane through a volume image
+  */
+  class DicomVolumeImageReslicer : public IVolumeSlicer
+  {
+  private:
+    class Slice;
+    
+    boost::shared_ptr<DicomVolumeImage>  volume_;
+    VolumeReslicer                       reslicer_;
+
+  public:
+    DicomVolumeImageReslicer(const boost::shared_ptr<DicomVolumeImage>& volume);
+
+    ImageInterpolation GetInterpolation() const
+    {
+      return reslicer_.GetInterpolation();
+    }
+
+    void SetInterpolation(ImageInterpolation interpolation)
+    {
+      reslicer_.SetInterpolation(interpolation);
+    }
+
+    bool IsFastMode() const
+    {
+      return reslicer_.IsFastMode();
+    }
+
+    void SetFastMode(bool fast)
+    {
+      reslicer_.EnableFastMode(fast);
+    }
+    
+    virtual IExtractedSlice* ExtractSlice(const CoordinateSystem3D& cuttingPlane);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Volumes/VolumeSceneLayerSource.cpp	Tue May 28 21:59:20 2019 +0200
@@ -0,0 +1,143 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-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 "VolumeSceneLayerSource.h"
+
+#include <Core/OrthancException.h>
+
+namespace OrthancStone
+{
+  static bool IsSameCuttingPlane(const CoordinateSystem3D& a,
+                                 const CoordinateSystem3D& b)
+  {
+    // TODO - What if the normal is reversed?
+    double distance;
+    return (CoordinateSystem3D::ComputeDistance(distance, a, b) &&
+            LinearAlgebra::IsCloseToZero(distance));
+  }
+
+
+  void VolumeSceneLayerSource::ClearLayer()
+  {
+    scene_.DeleteLayer(layerDepth_);
+    lastPlane_.reset(NULL);
+  }
+
+
+  VolumeSceneLayerSource::VolumeSceneLayerSource(Scene2D& scene,
+                                                 int layerDepth,
+                                                 const boost::shared_ptr<IVolumeSlicer>& slicer) :
+    scene_(scene),
+    layerDepth_(layerDepth),
+    slicer_(slicer)
+  {
+    if (slicer == NULL)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
+    }
+  }
+
+
+  void VolumeSceneLayerSource::RemoveConfigurator()
+  {
+    configurator_.reset();
+    lastPlane_.reset();
+  }
+
+
+  void VolumeSceneLayerSource::SetConfigurator(ILayerStyleConfigurator* configurator)  // Takes ownership
+  {
+    if (configurator == NULL)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
+    }
+
+    configurator_.reset(configurator);
+
+    // Invalidate the layer
+    lastPlane_.reset(NULL);
+  }
+
+
+  ILayerStyleConfigurator& VolumeSceneLayerSource::GetConfigurator() const
+  {
+    if (configurator_.get() == NULL)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+    }
+      
+    return *configurator_;
+  }
+
+
+  void VolumeSceneLayerSource::Update(const CoordinateSystem3D& plane)
+  {
+    assert(slicer_.get() != NULL);
+    std::auto_ptr<IVolumeSlicer::IExtractedSlice> slice(slicer_->ExtractSlice(plane));
+
+    if (slice.get() == NULL)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);        
+    }
+
+    if (!slice->IsValid())
+    {
+      // The slicer cannot handle this cutting plane: Clear the layer
+      ClearLayer();
+    }
+    else if (lastPlane_.get() != NULL &&
+             IsSameCuttingPlane(*lastPlane_, plane) &&
+             lastRevision_ == slice->GetRevision())
+    {
+      // The content of the slice has not changed: Don't update the
+      // layer content, but possibly update its style
+
+      if (configurator_.get() != NULL &&
+          configurator_->GetRevision() != lastConfiguratorRevision_ &&
+          scene_.HasLayer(layerDepth_))
+      {
+        configurator_->ApplyStyle(scene_.GetLayer(layerDepth_));
+      }
+    }
+    else
+    {
+      // Content has changed: An update is needed
+      lastPlane_.reset(new CoordinateSystem3D(plane));
+      lastRevision_ = slice->GetRevision();
+
+      std::auto_ptr<ISceneLayer> layer(slice->CreateSceneLayer(configurator_.get(), plane));
+      if (layer.get() == NULL)
+      {
+        ClearLayer();
+      }
+      else
+      {
+        if (configurator_.get() != NULL)
+        {
+          lastConfiguratorRevision_ = configurator_->GetRevision();
+          configurator_->ApplyStyle(*layer);
+        }
+
+        scene_.SetLayer(layerDepth_, layer.release());
+      }
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Volumes/VolumeSceneLayerSource.h	Tue May 28 21:59:20 2019 +0200
@@ -0,0 +1,74 @@
+/**
+ * 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/>.
+ **/
+
+
+#pragma once
+
+#include "../Scene2D/Scene2D.h"
+#include "IVolumeSlicer.h"
+
+#include <boost/shared_ptr.hpp>
+
+namespace OrthancStone
+{
+  /**
+     This class applies one "volume slicer" to a "3D volume", in order
+     to create one "2D scene layer" that will be set onto the "2D
+     scene". The style of the layer can be fine-tuned using a "layer
+     style configurator". The class only changes the layer if the
+     cutting plane has been modified since the last call to "Update()".
+   **/
+  class VolumeSceneLayerSource : public boost::noncopyable
+  {
+  private:
+    Scene2D&                                scene_;
+    int                                     layerDepth_;
+    boost::shared_ptr<IVolumeSlicer>        slicer_;
+    std::auto_ptr<ILayerStyleConfigurator>  configurator_;
+    std::auto_ptr<CoordinateSystem3D>       lastPlane_;
+    uint64_t                                lastRevision_;
+    uint64_t                                lastConfiguratorRevision_;
+
+    void ClearLayer();
+
+  public:
+    VolumeSceneLayerSource(Scene2D& scene,
+                           int layerDepth,
+                           const boost::shared_ptr<IVolumeSlicer>& slicer);
+
+    const IVolumeSlicer& GetSlicer() const
+    {
+      return *slicer_;
+    }
+
+    void RemoveConfigurator();
+
+    void SetConfigurator(ILayerStyleConfigurator* configurator);
+
+    bool HasConfigurator() const
+    {
+      return configurator_.get() != NULL;
+    }
+
+    ILayerStyleConfigurator& GetConfigurator() const;
+
+    void Update(const CoordinateSystem3D& plane);  
+  };
+}
--- a/Resources/CMake/OrthancStoneConfiguration.cmake	Tue May 28 21:16:39 2019 +0200
+++ b/Resources/CMake/OrthancStoneConfiguration.cmake	Tue May 28 21:59:20 2019 +0200
@@ -390,6 +390,9 @@
   ${ORTHANC_STONE_ROOT}/Framework/Fonts/TextBoundingBox.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Loaders/BasicFetchingItemsSorter.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Loaders/BasicFetchingStrategy.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Loaders/DicomStructureSetLoader.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Loaders/LoaderStateMachine.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Loaders/OrthancMultiframeVolumeLoader.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Loaders/OrthancSeriesVolumeProgressiveLoader.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Messages/ICallable.h
   ${ORTHANC_STONE_ROOT}/Framework/Messages/IMessage.h
@@ -494,11 +497,13 @@
   ${ORTHANC_STONE_ROOT}/Framework/Toolbox/UndoRedoStack.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Volumes/DicomVolumeImage.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Volumes/DicomVolumeImageMPRSlicer.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Volumes/DicomVolumeImageReslicer.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Volumes/IVolumeSlicer.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Volumes/ImageBuffer3D.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Volumes/OrientedVolumeBoundingBox.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Volumes/VolumeImageGeometry.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Volumes/VolumeReslicer.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Volumes/VolumeSceneLayerSource.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Wrappers/CairoContext.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Wrappers/CairoSurface.cpp
 
--- a/Samples/Sdl/Loader.cpp	Tue May 28 21:16:39 2019 +0200
+++ b/Samples/Sdl/Loader.cpp	Tue May 28 21:59:20 2019 +0200
@@ -19,1091 +19,35 @@
  **/
 
 
+#include "../../Framework/Loaders/DicomStructureSetLoader.h"
+#include "../../Framework/Loaders/OrthancMultiframeVolumeLoader.h"
 #include "../../Framework/Loaders/OrthancSeriesVolumeProgressiveLoader.h"
-#include "../../Framework/Volumes/DicomVolumeImageMPRSlicer.h"
+#include "../../Framework/Oracle/SleepOracleCommand.h"
+#include "../../Framework/Oracle/ThreadedOracle.h"
+#include "../../Framework/Scene2D/CairoCompositor.h"
 #include "../../Framework/Scene2D/GrayscaleStyleConfigurator.h"
 #include "../../Framework/Scene2D/LookupTableStyleConfigurator.h"
-#include "../../Framework/Oracle/ThreadedOracle.h"
-#include "../../Framework/Oracle/GetOrthancWebViewerJpegCommand.h"
-#include "../../Framework/Oracle/GetOrthancImageCommand.h"
-#include "../../Framework/Oracle/OrthancRestApiCommand.h"
-#include "../../Framework/Oracle/SleepOracleCommand.h"
-#include "../../Framework/Oracle/OracleCommandExceptionMessage.h"
-
-// From Stone
-#include "../../Framework/Loaders/BasicFetchingItemsSorter.h"
-#include "../../Framework/Loaders/BasicFetchingStrategy.h"
-#include "../../Framework/Scene2D/CairoCompositor.h"
-#include "../../Framework/Scene2D/Scene2D.h"
-#include "../../Framework/Scene2D/PolylineSceneLayer.h"
-#include "../../Framework/Scene2D/LookupTableTextureSceneLayer.h"
 #include "../../Framework/StoneInitialization.h"
-#include "../../Framework/Toolbox/GeometryToolbox.h"
-#include "../../Framework/Toolbox/SlicesSorter.h"
-#include "../../Framework/Toolbox/DicomStructureSet.h"
-#include "../../Framework/Volumes/ImageBuffer3D.h"
-#include "../../Framework/Volumes/VolumeImageGeometry.h"
-#include "../../Framework/Volumes/VolumeReslicer.h"
+#include "../../Framework/Volumes/VolumeSceneLayerSource.h"
+#include "../../Framework/Volumes/DicomVolumeImageMPRSlicer.h"
+#include "../../Framework/Volumes/DicomVolumeImageReslicer.h"
 
 // From Orthanc framework
-#include <Core/DicomFormat/DicomArray.h>
-#include <Core/Images/Image.h>
 #include <Core/Images/ImageProcessing.h>
 #include <Core/Images/PngWriter.h>
-#include <Core/Endianness.h>
 #include <Core/Logging.h>
 #include <Core/OrthancException.h>
 #include <Core/SystemToolbox.h>
-#include <Core/Toolbox.h>
-
-
-#include <EmbeddedResources.h>
 
 
 namespace OrthancStone
 {
-  /**
-  This class is supplied with Oracle commands and will schedule up to 
-  simultaneousDownloads_ of them at the same time, then will schedule the 
-  rest once slots become available. It is used, a.o., by the 
-  OrtancMultiframeVolumeLoader class.
-  */
-  class LoaderStateMachine : public IObserver
-  {
-  protected:
-    class State : public Orthanc::IDynamicObject
-    {
-    private:
-      LoaderStateMachine&  that_;
-
-    public:
-      State(LoaderStateMachine& that) :
-        that_(that)
-      {
-      }
-
-      State(const State& currentState) :
-        that_(currentState.that_)
-      {
-      }
-
-      void Schedule(OracleCommandWithPayload* command) const
-      {
-        that_.Schedule(command);
-      }
-
-      template <typename T>
-      T& GetLoader() const
-      {
-        return dynamic_cast<T&>(that_);
-      }
-      
-      virtual void Handle(const OrthancRestApiCommand::SuccessMessage& message)
-      {
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
-      }
-      
-      virtual void Handle(const GetOrthancImageCommand::SuccessMessage& message)
-      {
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
-      }
-      
-      virtual void Handle(const GetOrthancWebViewerJpegCommand::SuccessMessage& message)
-      {
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
-      }
-    };
-
-    void Schedule(OracleCommandWithPayload* command)
-    {
-      std::auto_ptr<OracleCommandWithPayload> protection(command);
-
-      if (command == NULL)
-      {
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
-      }
-      
-      if (!command->HasPayload())
-      {
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange,
-                                        "The payload must contain the next state");
-      }
-
-      pendingCommands_.push_back(protection.release());
-      Step();
-    }
-
-    void Start()
-    {
-      if (active_)
-      {
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
-      }
-
-      active_ = true;
-
-      for (size_t i = 0; i < simultaneousDownloads_; i++)
-      {
-        Step();
-      }
-    }
-
-  private:
-    void Step()
-    {
-      if (!pendingCommands_.empty() &&
-          activeCommands_ < simultaneousDownloads_)
-      {
-        oracle_.Schedule(*this, pendingCommands_.front());
-        pendingCommands_.pop_front();
-
-        activeCommands_++;
-      }
-    }
-
-    void Clear()
-    {
-      for (PendingCommands::iterator it = pendingCommands_.begin();
-           it != pendingCommands_.end(); ++it)
-      {
-        delete *it;
-      }
-
-      pendingCommands_.clear();
-    }
-
-    void HandleExceptionMessage(const OracleCommandExceptionMessage& message)
-    {
-      LOG(ERROR) << "Error in the state machine, stopping all processing";
-      Clear();
-    }
-
-    template <typename T>
-    void HandleSuccessMessage(const T& message)
-    {
-      assert(activeCommands_ > 0);
-      activeCommands_--;
-
-      try
-      {
-        dynamic_cast<State&>(message.GetOrigin().GetPayload()).Handle(message);
-        Step();
-      }
-      catch (Orthanc::OrthancException& e)
-      {
-        LOG(ERROR) << "Error in the state machine, stopping all processing: " << e.What();
-        Clear();
-      }
-    }
-
-    typedef std::list<IOracleCommand*>  PendingCommands;
-
-    IOracle&         oracle_;
-    bool             active_;
-    unsigned int     simultaneousDownloads_;
-    PendingCommands  pendingCommands_;
-    unsigned int     activeCommands_;
-
-  public:
-    LoaderStateMachine(IOracle& oracle,
-                       IObservable& oracleObservable) :
-      IObserver(oracleObservable.GetBroker()),
-      oracle_(oracle),
-      active_(false),
-      simultaneousDownloads_(4),
-      activeCommands_(0)
-    {
-      oracleObservable.RegisterObserverCallback(
-        new Callable<LoaderStateMachine, OrthancRestApiCommand::SuccessMessage>
-        (*this, &LoaderStateMachine::HandleSuccessMessage));
-
-      oracleObservable.RegisterObserverCallback(
-        new Callable<LoaderStateMachine, GetOrthancImageCommand::SuccessMessage>
-        (*this, &LoaderStateMachine::HandleSuccessMessage));
-
-      oracleObservable.RegisterObserverCallback(
-        new Callable<LoaderStateMachine, GetOrthancWebViewerJpegCommand::SuccessMessage>
-        (*this, &LoaderStateMachine::HandleSuccessMessage));
-
-      oracleObservable.RegisterObserverCallback(
-        new Callable<LoaderStateMachine, OracleCommandExceptionMessage>
-        (*this, &LoaderStateMachine::HandleExceptionMessage));
-    }
-
-    virtual ~LoaderStateMachine()
-    {
-      Clear();
-    }
-
-    bool IsActive() const
-    {
-      return active_;
-    }
-
-    void SetSimultaneousDownloads(unsigned int count)
-    {
-      if (active_)
-      {
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
-      }
-      else if (count == 0)
-      {
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);        
-      }
-      else
-      {
-        simultaneousDownloads_ = count;
-      }
-    }
-  };
-
-
-
-  class OrthancMultiframeVolumeLoader :
-    public LoaderStateMachine,
-    public IObservable
-  {
-  private:
-    class LoadRTDoseGeometry : public State
-    {
-    private:
-      std::auto_ptr<Orthanc::DicomMap>  dicom_;
-
-    public:
-      LoadRTDoseGeometry(OrthancMultiframeVolumeLoader& that,
-                         Orthanc::DicomMap* dicom) :
-        State(that),
-        dicom_(dicom)
-      {
-        if (dicom == NULL)
-        {
-          throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
-        }
-
-      }
-
-      virtual void Handle(const OrthancRestApiCommand::SuccessMessage& message)
-      {
-        // Complete the DICOM tags with just-received "Grid Frame Offset Vector"
-        std::string s = Orthanc::Toolbox::StripSpaces(message.GetAnswer());
-        dicom_->SetValue(Orthanc::DICOM_TAG_GRID_FRAME_OFFSET_VECTOR, s, false);
-
-        GetLoader<OrthancMultiframeVolumeLoader>().SetGeometry(*dicom_);
-      }      
-    };
-
-
-    static std::string GetSopClassUid(const Orthanc::DicomMap& dicom)
-    {
-      std::string s;
-      if (!dicom.CopyToString(s, Orthanc::DICOM_TAG_SOP_CLASS_UID, false))
-      {
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat,
-                                        "DICOM file without SOP class UID");
-      }
-      else
-      {
-        return s;
-      }
-    }
-    
-
-    class LoadGeometry : public State
-    {
-    public:
-      LoadGeometry(OrthancMultiframeVolumeLoader& that) :
-        State(that)
-      {
-      }
-      
-      virtual void Handle(const OrthancRestApiCommand::SuccessMessage& message)
-      {
-        OrthancMultiframeVolumeLoader& loader = GetLoader<OrthancMultiframeVolumeLoader>();
-        
-        Json::Value body;
-        message.ParseJsonBody(body);
-        
-        if (body.type() != Json::objectValue)
-        {
-          throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol);
-        }
-
-        std::auto_ptr<Orthanc::DicomMap> dicom(new Orthanc::DicomMap);
-        dicom->FromDicomAsJson(body);
-
-        if (StringToSopClassUid(GetSopClassUid(*dicom)) == SopClassUid_RTDose)
-        {
-          // Download the "Grid Frame Offset Vector" DICOM tag, that is
-          // mandatory for RT-DOSE, but is too long to be returned by default
-          
-          std::auto_ptr<OrthancRestApiCommand> command(new OrthancRestApiCommand);
-          command->SetUri("/instances/" + loader.GetInstanceId() + "/content/" +
-                          Orthanc::DICOM_TAG_GRID_FRAME_OFFSET_VECTOR.Format());
-          command->SetPayload(new LoadRTDoseGeometry(loader, dicom.release()));
-
-          Schedule(command.release());
-        }
-        else
-        {
-          loader.SetGeometry(*dicom);
-        }
-      }
-    };
-
-
-
-    class LoadTransferSyntax : public State
-    {
-    public:
-      LoadTransferSyntax(OrthancMultiframeVolumeLoader& that) :
-        State(that)
-      {
-      }
-      
-      virtual void Handle(const OrthancRestApiCommand::SuccessMessage& message)
-      {
-        GetLoader<OrthancMultiframeVolumeLoader>().SetTransferSyntax(message.GetAnswer());
-      }
-    };
-   
-    
-    class LoadUncompressedPixelData : public State
-    {
-    public:
-      LoadUncompressedPixelData(OrthancMultiframeVolumeLoader& that) :
-        State(that)
-      {
-      }
-      
-      virtual void Handle(const OrthancRestApiCommand::SuccessMessage& message)
-      {
-        GetLoader<OrthancMultiframeVolumeLoader>().SetUncompressedPixelData(message.GetAnswer());
-      }
-    };
-   
-    
-
-    boost::shared_ptr<DicomVolumeImage>   volume_;
-    std::string  instanceId_;
-    std::string  transferSyntaxUid_;
-
-
-    const std::string& GetInstanceId() const
-    {
-      if (IsActive())
-      {
-        return instanceId_;
-      }
-      else
-      {
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
-      }
-    }
-
-
-    void ScheduleFrameDownloads()
-    {
-      if (transferSyntaxUid_.empty() ||
-          !volume_->HasGeometry())
-      {
-        return;
-      }
-      /*
-      1.2.840.10008.1.2	Implicit VR Endian: Default Transfer Syntax for DICOM
-      1.2.840.10008.1.2.1	Explicit VR Little Endian
-      1.2.840.10008.1.2.2	Explicit VR Big Endian
-
-      See https://www.dicomlibrary.com/dicom/transfer-syntax/
-      */
-      if (transferSyntaxUid_ == "1.2.840.10008.1.2" ||
-          transferSyntaxUid_ == "1.2.840.10008.1.2.1" ||
-          transferSyntaxUid_ == "1.2.840.10008.1.2.2")
-      {
-        std::auto_ptr<OrthancRestApiCommand> command(new OrthancRestApiCommand);
-        command->SetHttpHeader("Accept-Encoding", "gzip");
-        command->SetUri("/instances/" + instanceId_ + "/content/" +
-                        Orthanc::DICOM_TAG_PIXEL_DATA.Format() + "/0");
-        command->SetPayload(new LoadUncompressedPixelData(*this));
-        Schedule(command.release());
-      }
-      else
-      {
-        throw Orthanc::OrthancException(
-          Orthanc::ErrorCode_NotImplemented,
-          "No support for multiframe instances with transfer syntax: " + transferSyntaxUid_);
-      }
-    }
-      
-
-    void SetTransferSyntax(const std::string& transferSyntax)
-    {
-      transferSyntaxUid_ = Orthanc::Toolbox::StripSpaces(transferSyntax);
-      ScheduleFrameDownloads();
-    }
-    
-
-    void SetGeometry(const Orthanc::DicomMap& dicom)
-    {
-      DicomInstanceParameters parameters(dicom);
-      volume_->SetDicomParameters(parameters);
-      
-      Orthanc::PixelFormat format;
-      if (!parameters.GetImageInformation().ExtractPixelFormat(format, true))
-      {
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
-      }
-
-      double spacingZ;
-      switch (parameters.GetSopClassUid())
-      {
-        case SopClassUid_RTDose:
-          spacingZ = parameters.GetThickness();
-          break;
-
-        default:
-          throw Orthanc::OrthancException(
-            Orthanc::ErrorCode_NotImplemented,
-            "No support for multiframe instances with SOP class UID: " + GetSopClassUid(dicom));
-      }
-
-      const unsigned int width = parameters.GetImageInformation().GetWidth();
-      const unsigned int height = parameters.GetImageInformation().GetHeight();
-      const unsigned int depth = parameters.GetImageInformation().GetNumberOfFrames();
-
-      {
-        VolumeImageGeometry geometry;
-        geometry.SetSize(width, height, depth);
-        geometry.SetAxialGeometry(parameters.GetGeometry());
-        geometry.SetVoxelDimensions(parameters.GetPixelSpacingX(),
-                                    parameters.GetPixelSpacingY(), spacingZ);
-        volume_->Initialize(geometry, format);
-      }
-
-      volume_->GetPixelData().Clear();
-
-      ScheduleFrameDownloads();
-
-      BroadcastMessage(DicomVolumeImage::GeometryReadyMessage(*volume_));
-    }
-
-
-    ORTHANC_FORCE_INLINE
-    static void CopyPixel(uint32_t& target,
-                          const void* source)
-    {
-      // TODO - check alignement?
-      target = le32toh(*reinterpret_cast<const uint32_t*>(source));
-    }
-      
-
-    template <typename T>
-    void CopyPixelData(const std::string& pixelData)
-    {
-      ImageBuffer3D& target = volume_->GetPixelData();
-      
-      const unsigned int bpp = target.GetBytesPerPixel();
-      const unsigned int width = target.GetWidth();
-      const unsigned int height = target.GetHeight();
-      const unsigned int depth = target.GetDepth();
-
-      if (pixelData.size() != bpp * width * height * depth)
-      {
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat,
-                                        "The pixel data has not the proper size");
-      }
-
-      if (pixelData.empty())
-      {
-        return;
-      }
-
-      const uint8_t* source = reinterpret_cast<const uint8_t*>(pixelData.c_str());
-
-      for (unsigned int z = 0; z < depth; z++)
-      {
-        ImageBuffer3D::SliceWriter writer(target, VolumeProjection_Axial, z);
-
-        assert (writer.GetAccessor().GetWidth() == width &&
-                writer.GetAccessor().GetHeight() == height);
-
-        for (unsigned int y = 0; y < height; y++)
-        {
-          assert(sizeof(T) == Orthanc::GetBytesPerPixel(target.GetFormat()));
-
-          T* target = reinterpret_cast<T*>(writer.GetAccessor().GetRow(y));
-
-          for (unsigned int x = 0; x < width; x++)
-          {
-            CopyPixel(*target, source);
-            
-            target ++;
-            source += bpp;
-          }
-        }
-      }
-    }
-    
-
-    void SetUncompressedPixelData(const std::string& pixelData)
-    {
-      switch (volume_->GetPixelData().GetFormat())
-      {
-        case Orthanc::PixelFormat_Grayscale32:
-          CopyPixelData<uint32_t>(pixelData);
-          break;
-
-        default:
-          throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
-      }
-
-      volume_->IncrementRevision();
-
-      BroadcastMessage(DicomVolumeImage::ContentUpdatedMessage(*volume_));
-    }
-
-
-  public:
-    OrthancMultiframeVolumeLoader(const boost::shared_ptr<DicomVolumeImage>& volume,
-                                  IOracle& oracle,
-                                  IObservable& oracleObservable) :
-      LoaderStateMachine(oracle, oracleObservable),
-      IObservable(oracleObservable.GetBroker()),
-      volume_(volume)
-    {
-      if (volume.get() == NULL)
-      {
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
-      }
-    }
-
-
-    void LoadInstance(const std::string& instanceId)
-    {
-      Start();
-
-      instanceId_ = instanceId;
-
-      {
-        std::auto_ptr<OrthancRestApiCommand> command(new OrthancRestApiCommand);
-        command->SetHttpHeader("Accept-Encoding", "gzip");
-        command->SetUri("/instances/" + instanceId + "/tags");
-        command->SetPayload(new LoadGeometry(*this));
-        Schedule(command.release());
-      }
-
-      {
-        std::auto_ptr<OrthancRestApiCommand> command(new OrthancRestApiCommand);
-        command->SetUri("/instances/" + instanceId + "/metadata/TransferSyntax");
-        command->SetPayload(new LoadTransferSyntax(*this));
-        Schedule(command.release());
-      }
-    }
-  };
-
-
-  /**
-  This class is able to supply an extract slice for an arbitrary cutting
-  plane through a volume image
-  */
-  class DicomVolumeImageReslicer : public IVolumeSlicer
-  {
-  private:
-    class Slice : public IExtractedSlice
-    {
-    private:
-      DicomVolumeImageReslicer&  that_;
-      CoordinateSystem3D         cuttingPlane_;
-      
-    public:
-      Slice(DicomVolumeImageReslicer& that,
-            const CoordinateSystem3D& cuttingPlane) :
-        that_(that),
-        cuttingPlane_(cuttingPlane)
-      {
-      }
-      
-      virtual bool IsValid()
-      {
-        return true;
-      }
-
-      virtual uint64_t GetRevision()
-      {
-        return that_.volume_->GetRevision();
-      }
-
-      virtual ISceneLayer* CreateSceneLayer(const ILayerStyleConfigurator* configurator,
-                                            const CoordinateSystem3D& cuttingPlane)
-      {
-        VolumeReslicer& reslicer = that_.reslicer_;
-        
-        if (configurator == NULL)
-        {
-          throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError,
-                                          "Must provide a layer style configurator");
-        }
-        
-        reslicer.SetOutputFormat(that_.volume_->GetPixelData().GetFormat());
-        reslicer.Apply(that_.volume_->GetPixelData(),
-                       that_.volume_->GetGeometry(),
-                       cuttingPlane);
-
-        if (reslicer.IsSuccess())
-        {
-          std::auto_ptr<TextureBaseSceneLayer> layer
-            (configurator->CreateTextureFromDicom(reslicer.GetOutputSlice(),
-                                                  that_.volume_->GetDicomParameters()));
-          if (layer.get() == NULL)
-          {
-            return NULL;
-          }
-
-          double s = reslicer.GetPixelSpacing();
-          layer->SetPixelSpacing(s, s);
-          layer->SetOrigin(reslicer.GetOutputExtent().GetX1() + 0.5 * s,
-                           reslicer.GetOutputExtent().GetY1() + 0.5 * s);
-
-          // TODO - Angle!!
-                           
-          return layer.release();
-        }
-        else
-        {
-          return NULL;
-        }          
-      }
-    };
-    
-    boost::shared_ptr<DicomVolumeImage>  volume_;
-    VolumeReslicer                       reslicer_;
-
-  public:
-    DicomVolumeImageReslicer(const boost::shared_ptr<DicomVolumeImage>& volume) :
-      volume_(volume)
-    {
-      if (volume.get() == NULL)
-      {
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
-      }
-    }
-
-    ImageInterpolation GetInterpolation() const
-    {
-      return reslicer_.GetInterpolation();
-    }
-
-    void SetInterpolation(ImageInterpolation interpolation)
-    {
-      reslicer_.SetInterpolation(interpolation);
-    }
-
-    bool IsFastMode() const
-    {
-      return reslicer_.IsFastMode();
-    }
-
-    void SetFastMode(bool fast)
-    {
-      reslicer_.EnableFastMode(fast);
-    }
-    
-    virtual IExtractedSlice* ExtractSlice(const CoordinateSystem3D& cuttingPlane)
-    {
-      if (volume_->HasGeometry())
-      {
-        return new Slice(*this, cuttingPlane);
-      }
-      else
-      {
-        return new IVolumeSlicer::InvalidSlice;
-      }
-    }
-  };
-
-
-
-  class DicomStructureSetLoader :
-    public LoaderStateMachine,
-    public IVolumeSlicer
-  {
-  private:
-    class AddReferencedInstance : public State
-    {
-    private:
-      std::string instanceId_;
-      
-    public:
-      AddReferencedInstance(DicomStructureSetLoader& that,
-                            const std::string& instanceId) :
-        State(that),
-        instanceId_(instanceId)
-      {
-      }
-
-      virtual void Handle(const OrthancRestApiCommand::SuccessMessage& message)
-      {
-        Json::Value tags;
-        message.ParseJsonBody(tags);
-        
-        Orthanc::DicomMap dicom;
-        dicom.FromDicomAsJson(tags);
-
-        DicomStructureSetLoader& loader = GetLoader<DicomStructureSetLoader>();
-        loader.content_->AddReferencedSlice(dicom);
-
-        loader.countProcessedInstances_ ++;
-        assert(loader.countProcessedInstances_ <= loader.countReferencedInstances_);
-
-        if (loader.countProcessedInstances_ == loader.countReferencedInstances_)
-        {
-          // All the referenced instances have been loaded, finalize the RT-STRUCT
-          loader.content_->CheckReferencedSlices();
-          loader.revision_++;
-        }
-      }
-    };
-    
-    // State that converts a "SOP Instance UID" to an Orthanc identifier
-    class LookupInstance : public State
-    {
-    private:
-      std::string  sopInstanceUid_;
-      
-    public:
-      LookupInstance(DicomStructureSetLoader& that,
-                     const std::string& sopInstanceUid) :
-        State(that),
-        sopInstanceUid_(sopInstanceUid)
-      {
-      }
-
-      virtual void Handle(const OrthancRestApiCommand::SuccessMessage& message)
-      {
-        DicomStructureSetLoader& loader = GetLoader<DicomStructureSetLoader>();
-
-        Json::Value lookup;
-        message.ParseJsonBody(lookup);
-
-        if (lookup.type() != Json::arrayValue ||
-            lookup.size() != 1 ||
-            !lookup[0].isMember("Type") ||
-            !lookup[0].isMember("Path") ||
-            lookup[0]["Type"].type() != Json::stringValue ||
-            lookup[0]["ID"].type() != Json::stringValue ||
-            lookup[0]["Type"].asString() != "Instance")
-        {
-          throw Orthanc::OrthancException(Orthanc::ErrorCode_UnknownResource);          
-        }
-
-        const std::string instanceId = lookup[0]["ID"].asString();
-
-        {
-          std::auto_ptr<OrthancRestApiCommand> command(new OrthancRestApiCommand);
-          command->SetHttpHeader("Accept-Encoding", "gzip");
-          command->SetUri("/instances/" + instanceId + "/tags");
-          command->SetPayload(new AddReferencedInstance(loader, instanceId));
-          Schedule(command.release());
-        }
-      }
-    };
-    
-    class LoadStructure : public State
-    {
-    public:
-      LoadStructure(DicomStructureSetLoader& that) :
-        State(that)
-      {
-      }
-
-      virtual void Handle(const OrthancRestApiCommand::SuccessMessage& message)
-      {
-        DicomStructureSetLoader& loader = GetLoader<DicomStructureSetLoader>();
-        
-        {
-          OrthancPlugins::FullOrthancDataset dicom(message.GetAnswer());
-          loader.content_.reset(new DicomStructureSet(dicom));
-        }
-
-        std::set<std::string> instances;
-        loader.content_->GetReferencedInstances(instances);
-
-        loader.countReferencedInstances_ = instances.size();
-
-        for (std::set<std::string>::const_iterator
-               it = instances.begin(); it != instances.end(); ++it)
-        {
-          std::auto_ptr<OrthancRestApiCommand> command(new OrthancRestApiCommand);
-          command->SetUri("/tools/lookup");
-          command->SetMethod(Orthanc::HttpMethod_Post);
-          command->SetBody(*it);
-          command->SetPayload(new LookupInstance(loader, *it));
-          Schedule(command.release());
-        }
-      }
-    };
-
-
-    
-    std::auto_ptr<DicomStructureSet>  content_;
-    uint64_t                          revision_;
-    std::string                       instanceId_;
-    unsigned int                      countProcessedInstances_;
-    unsigned int                      countReferencedInstances_;
-
-    
-    class Slice : public IExtractedSlice
-    {
-    private:
-      const DicomStructureSet&  content_;
-      uint64_t                  revision_;
-      bool                      isValid_;
-      
-    public:
-      Slice(const DicomStructureSet& content,
-            uint64_t revision,
-            const CoordinateSystem3D& cuttingPlane) :
-        content_(content),
-        revision_(revision)
-      {
-        bool opposite;
-
-        const Vector normal = content.GetNormal();
-        isValid_ = (
-          GeometryToolbox::IsParallelOrOpposite(opposite, normal, cuttingPlane.GetNormal()) ||
-          GeometryToolbox::IsParallelOrOpposite(opposite, normal, cuttingPlane.GetAxisX()) ||
-          GeometryToolbox::IsParallelOrOpposite(opposite, normal, cuttingPlane.GetAxisY()));
-      }
-      
-      virtual bool IsValid()
-      {
-        return isValid_;
-      }
-
-      virtual uint64_t GetRevision()
-      {
-        return revision_;
-      }
-
-      virtual ISceneLayer* CreateSceneLayer(const ILayerStyleConfigurator* configurator,
-                                            const CoordinateSystem3D& cuttingPlane)
-      {
-        assert(isValid_);
-
-        std::auto_ptr<PolylineSceneLayer> layer(new PolylineSceneLayer);
-        layer->SetThickness(2);
-
-        for (size_t i = 0; i < content_.GetStructuresCount(); i++)
-        {
-          const Color& color = content_.GetStructureColor(i);
-
-          std::vector< std::vector<DicomStructureSet::PolygonPoint> > polygons;
-          
-          if (content_.ProjectStructure(polygons, i, cuttingPlane))
-          {
-            for (size_t j = 0; j < polygons.size(); j++)
-            {
-              PolylineSceneLayer::Chain chain;
-              chain.resize(polygons[j].size());
-            
-              for (size_t k = 0; k < polygons[j].size(); k++)
-              {
-                chain[k] = ScenePoint2D(polygons[j][k].first, polygons[j][k].second);
-              }
-
-              layer->AddChain(chain, true /* closed */, color);
-            }
-          }
-        }
-
-        return layer.release();
-      }
-    };
-    
-  public:
-    DicomStructureSetLoader(IOracle& oracle,
-                            IObservable& oracleObservable) :
-      LoaderStateMachine(oracle, oracleObservable),
-      revision_(0),
-      countProcessedInstances_(0),
-      countReferencedInstances_(0)
-    {
-    }
-    
-    
-    void LoadInstance(const std::string& instanceId)
-    {
-      Start();
-      
-      instanceId_ = instanceId;
-      
-      {
-        std::auto_ptr<OrthancRestApiCommand> command(new OrthancRestApiCommand);
-        command->SetHttpHeader("Accept-Encoding", "gzip");
-        command->SetUri("/instances/" + instanceId + "/tags?ignore-length=3006-0050");
-        command->SetPayload(new LoadStructure(*this));
-        Schedule(command.release());
-      }
-    }
-
-    virtual IExtractedSlice* ExtractSlice(const CoordinateSystem3D& cuttingPlane)
-    {
-      if (content_.get() == NULL)
-      {
-        // Geometry is not available yet
-        return new IVolumeSlicer::InvalidSlice;
-      }
-      else
-      {
-        return new Slice(*content_, revision_, cuttingPlane);
-      }
-    }
-  };
-
-
-
-  class VolumeSceneLayerSource : public boost::noncopyable
-  {
-  private:
-    Scene2D&                                scene_;
-    int                                     layerDepth_;
-    boost::shared_ptr<IVolumeSlicer>        slicer_;
-    std::auto_ptr<ILayerStyleConfigurator>  configurator_;
-    std::auto_ptr<CoordinateSystem3D>       lastPlane_;
-    uint64_t                                lastRevision_;
-    uint64_t                                lastConfiguratorRevision_;
-
-    static bool IsSameCuttingPlane(const CoordinateSystem3D& a,
-                                   const CoordinateSystem3D& b)
-    {
-      // TODO - What if the normal is reversed?
-      double distance;
-      return (CoordinateSystem3D::ComputeDistance(distance, a, b) &&
-              LinearAlgebra::IsCloseToZero(distance));
-    }
-
-    void ClearLayer()
-    {
-      scene_.DeleteLayer(layerDepth_);
-      lastPlane_.reset(NULL);
-    }
-
-  public:
-    VolumeSceneLayerSource(Scene2D& scene,
-                           int layerDepth,
-                           const boost::shared_ptr<IVolumeSlicer>& slicer) :
-      scene_(scene),
-      layerDepth_(layerDepth),
-      slicer_(slicer)
-    {
-      if (slicer == NULL)
-      {
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
-      }
-    }
-
-    const IVolumeSlicer& GetSlicer() const
-    {
-      return *slicer_;
-    }
-
-    void RemoveConfigurator()
-    {
-      configurator_.reset();
-      lastPlane_.reset();
-    }
-
-    void SetConfigurator(ILayerStyleConfigurator* configurator)  // Takes ownership
-    {
-      if (configurator == NULL)
-      {
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
-      }
-
-      configurator_.reset(configurator);
-
-      // Invalidate the layer
-      lastPlane_.reset(NULL);
-    }
-
-    bool HasConfigurator() const
-    {
-      return configurator_.get() != NULL;
-    }
-
-    ILayerStyleConfigurator& GetConfigurator() const
-    {
-      if (configurator_.get() == NULL)
-      {
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
-      }
-      
-      return *configurator_;
-    }
-
-    void Update(const CoordinateSystem3D& plane)
-    {
-      assert(slicer_.get() != NULL);
-      std::auto_ptr<IVolumeSlicer::IExtractedSlice> slice(slicer_->ExtractSlice(plane));
-
-      if (slice.get() == NULL)
-      {
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);        
-      }
-
-      if (!slice->IsValid())
-      {
-        // The slicer cannot handle this cutting plane: Clear the layer
-        ClearLayer();
-      }
-      else if (lastPlane_.get() != NULL &&
-               IsSameCuttingPlane(*lastPlane_, plane) &&
-               lastRevision_ == slice->GetRevision())
-      {
-        // The content of the slice has not changed: Don't update the
-        // layer content, but possibly update its style
-
-        if (configurator_.get() != NULL &&
-            configurator_->GetRevision() != lastConfiguratorRevision_ &&
-            scene_.HasLayer(layerDepth_))
-        {
-          configurator_->ApplyStyle(scene_.GetLayer(layerDepth_));
-        }
-      }
-      else
-      {
-        // Content has changed: An update is needed
-        lastPlane_.reset(new CoordinateSystem3D(plane));
-        lastRevision_ = slice->GetRevision();
-
-        std::auto_ptr<ISceneLayer> layer(slice->CreateSceneLayer(configurator_.get(), plane));
-        if (layer.get() == NULL)
-        {
-          ClearLayer();
-        }
-        else
-        {
-          if (configurator_.get() != NULL)
-          {
-            lastConfiguratorRevision_ = configurator_->GetRevision();
-            configurator_->ApplyStyle(*layer);
-          }
-
-          scene_.SetLayer(layerDepth_, layer.release());
-        }
-      }
-    }
-  };
-
-
-
-
-
   class NativeApplicationContext : public IMessageEmitter
   {
   private:
-    boost::shared_mutex            mutex_;
-    MessageBroker    broker_;
-    IObservable      oracleObservable_;
+    boost::shared_mutex  mutex_;
+    MessageBroker        broker_;
+    IObservable          oracleObservable_;
 
   public:
     NativeApplicationContext() :