changeset 102:fcec0ab44054 wasm

display volumes
author Sebastien Jodogne <s.jodogne@gmail.com>
date Wed, 31 May 2017 17:01:18 +0200
parents af312ce4fe59
children 474d85e76499
files Applications/BasicApplicationContext.cpp Applications/BasicApplicationContext.h Applications/IBasicApplication.cpp Applications/Samples/SingleFrameApplication.h Applications/Samples/SingleVolumeApplication.h Applications/Sdl/SdlEngine.cpp Applications/Sdl/SdlSurface.cpp Applications/Sdl/SdlSurface.cpp~ Applications/Sdl/SdlWindow.cpp CMakeLists.txt Framework/Layers/OrthancFrameLayerSource.cpp Framework/Layers/OrthancFrameLayerSource.h Framework/Toolbox/Slice.cpp Framework/Toolbox/Slice.h Framework/dev.h UnitTestsSources/UnitTestsMain.cpp
diffstat 16 files changed, 919 insertions(+), 353 deletions(-) [+]
line wrap: on
line diff
--- a/Applications/BasicApplicationContext.cpp	Wed May 31 10:35:20 2017 +0200
+++ b/Applications/BasicApplicationContext.cpp	Wed May 31 17:01:18 2017 +0200
@@ -80,33 +80,17 @@
   }
 
 
-  VolumeImage& BasicApplicationContext::AddSeriesVolume(const std::string& series,
-                                                        bool isProgressiveDownload,
-                                                        size_t downloadThreadCount)
+  ISlicedVolume& BasicApplicationContext::AddVolume(ISlicedVolume* volume)
   {
-    /*std::auto_ptr<VolumeImage> volume
-      (new VolumeImage(new OrthancSeriesLoader(GetWebService().GetConnection(), series)));
-
-    if (isProgressiveDownload)
+    if (volume == NULL)
     {
-      volume->SetDownloadPolicy(new VolumeImageProgressivePolicy);
-    }
-    else
-    {
-      volume->SetDownloadPolicy(new VolumeImageSimplePolicy);
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
     }
 
-    volume->SetThreadCount(downloadThreadCount);
-
-    VolumeImage& result = *volume;
-    volumes_.push_back(volume.release());
-
-    return result;*/
-
-    throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+    volumes_.push_back(volume);
+    return *volume;
   }
 
-
   DicomStructureSet& BasicApplicationContext::AddStructureSet(const std::string& instance)
   {
     /*std::auto_ptr<DicomStructureSet> structureSet
@@ -125,7 +109,7 @@
   {
     if (interactor == NULL)
     {
-      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
     }
 
     interactors_.push_back(interactor);
@@ -138,13 +122,6 @@
   {
     oracle_.Start();
 
-    // TODO REMOVE THIS
-    for (Volumes::iterator it = volumes_.begin(); it != volumes_.end(); ++it)
-    {
-      assert(*it != NULL);
-      (*it)->Start();
-    }
-
     if (viewport_.HasUpdateContent())
     {
       stopped_ = false;
@@ -162,13 +139,6 @@
       updateThread_.join();
     }
     
-    // TODO REMOVE THIS
-    for (Volumes::iterator it = volumes_.begin(); it != volumes_.end(); ++it)
-    {
-      assert(*it != NULL);
-      (*it)->Stop();
-    }
-
     oracle_.Stop();
   }
 }
--- a/Applications/BasicApplicationContext.h	Wed May 31 10:35:20 2017 +0200
+++ b/Applications/BasicApplicationContext.h	Wed May 31 17:01:18 2017 +0200
@@ -21,7 +21,7 @@
 
 #pragma once
 
-#include "../../Framework/Volumes/VolumeImage.h"
+#include "../../Framework/Volumes/ISlicedVolume.h"
 #include "../../Framework/Viewport/WidgetViewport.h"
 #include "../../Framework/Widgets/IWorldSceneInteractor.h"
 #include "../../Framework/Toolbox/DicomStructureSet.h"
@@ -35,7 +35,7 @@
   class BasicApplicationContext : public boost::noncopyable
   {
   private:
-    typedef std::list<ISliceableVolume*>       Volumes;
+    typedef std::list<ISlicedVolume*>          Volumes;
     typedef std::list<IWorldSceneInteractor*>  Interactors;
     typedef std::list<DicomStructureSet*>      StructureSets;
 
@@ -84,9 +84,7 @@
       return webService_;
     }
     
-    VolumeImage& AddSeriesVolume(const std::string& series,
-                                 bool isProgressiveDownload,
-                                 size_t downloadThreadCount);
+    ISlicedVolume& AddVolume(ISlicedVolume* volume);
 
     DicomStructureSet& AddStructureSet(const std::string& instance);
 
--- a/Applications/IBasicApplication.cpp	Wed May 31 10:35:20 2017 +0200
+++ b/Applications/IBasicApplication.cpp	Wed May 31 17:01:18 2017 +0200
@@ -21,10 +21,12 @@
 
 #include "IBasicApplication.h"
 
+#include "../Framework/Toolbox/MessagingToolbox.h"
+#include "Sdl/SdlEngine.h"
+
 #include "../../Resources/Orthanc/Core/Logging.h"
 #include "../../Resources/Orthanc/Core/HttpClient.h"
 #include "../../Resources/Orthanc/Plugins/Samples/Common/OrthancHttpConnection.h"
-#include "Sdl/SdlEngine.h"
 
 namespace OrthancStone
 {
--- a/Applications/Samples/SingleFrameApplication.h	Wed May 31 10:35:20 2017 +0200
+++ b/Applications/Samples/SingleFrameApplication.h	Wed May 31 17:01:18 2017 +0200
@@ -22,7 +22,6 @@
 #pragma once
 
 #include "SampleApplicationBase.h"
-#include "SampleInteractor.h"
 
 #include "../../Framework/Layers/OrthancFrameLayerSource.h"
 #include "../../Framework/Widgets/LayerWidget.h"
@@ -81,7 +80,7 @@
                                 KeyboardModifiers modifiers,
                                 IStatusBar* statusBar)
         {
-          unsigned int scale = (modifiers & KeyboardModifiers_Control ? 10 : 1);
+          int scale = (modifiers & KeyboardModifiers_Control ? 10 : 1);
           
           switch (direction)
           {
@@ -163,9 +162,6 @@
 #endif
           
           SliceGeometry s(source_->GetSlice(slice_).GetGeometry().GetOrigin(), x, y);
-          GeometryToolbox::Print(s.GetAxisX());
-          GeometryToolbox::Print(s.GetAxisY());
-          GeometryToolbox::Print(s.GetNormal());
           widget_->SetSlice(s);
 #endif
         }
@@ -251,7 +247,7 @@
 
         std::auto_ptr<LayerWidget> widget(new LayerWidget);
 
-#if 0
+#if 1
         std::auto_ptr<OrthancFrameLayerSource> layer
           (new OrthancFrameLayerSource(context.GetWebService()));
         //layer->SetImageQuality(SliceImageQuality_Jpeg50);
--- a/Applications/Samples/SingleVolumeApplication.h	Wed May 31 10:35:20 2017 +0200
+++ b/Applications/Samples/SingleVolumeApplication.h	Wed May 31 17:01:18 2017 +0200
@@ -21,7 +21,10 @@
 
 #pragma once
 
-#include "SampleInteractor.h"
+#include "SampleApplicationBase.h"
+#include "../../Framework/dev.h"
+//#include "SampleInteractor.h"
+#include "../../Framework/Widgets/LayerWidget.h"
 
 #include "../../Resources/Orthanc/Core/Toolbox.h"
 #include "../../Framework/Layers/LineMeasureTracker.h"
@@ -32,9 +35,160 @@
 {
   namespace Samples
   {
-    class SingleVolumeApplication : public SampleApplicationBase
+    class SingleVolumeApplication :
+      public SampleApplicationBase,
+      private ILayerSource::IObserver
     {
     private:
+      class Interactor : public IWorldSceneInteractor
+      {
+      private:
+        SingleVolumeApplication&  application_;
+        
+      public:
+        Interactor(SingleVolumeApplication&  application) :
+          application_(application)
+        {
+        }
+        
+        virtual IWorldSceneMouseTracker* CreateMouseTracker(WorldSceneWidget& widget,
+                                                            const ViewportGeometry& view,
+                                                            MouseButton button,
+                                                            double x,
+                                                            double y,
+                                                            IStatusBar* statusBar)
+        {
+          return NULL;
+        }
+
+        virtual void MouseOver(CairoContext& context,
+                               WorldSceneWidget& widget,
+                               const ViewportGeometry& view,
+                               double x,
+                               double y,
+                               IStatusBar* statusBar)
+        {
+          if (statusBar != NULL)
+          {
+            Vector p = dynamic_cast<LayerWidget&>(widget).GetSlice().MapSliceToWorldCoordinates(x, y);
+            
+            char buf[64];
+            sprintf(buf, "X = %.02f Y = %.02f Z = %.02f (in cm)", 
+                    p[0] / 10.0, p[1] / 10.0, p[2] / 10.0);
+            statusBar->SetMessage(buf);
+          }
+        }
+
+        virtual void MouseWheel(WorldSceneWidget& widget,
+                                MouseWheelDirection direction,
+                                KeyboardModifiers modifiers,
+                                IStatusBar* statusBar)
+        {
+          int scale = (modifiers & KeyboardModifiers_Control ? 10 : 1);
+          
+          switch (direction)
+          {
+            case MouseWheelDirection_Up:
+              application_.OffsetSlice(-scale);
+              break;
+
+            case MouseWheelDirection_Down:
+              application_.OffsetSlice(scale);
+              break;
+
+            default:
+              break;
+          }
+        }
+
+        virtual void KeyPressed(WorldSceneWidget& widget,
+                                char key,
+                                KeyboardModifiers modifiers,
+                                IStatusBar* statusBar)
+        {
+          switch (key)
+          {
+            case 's':
+              widget.SetDefaultView();
+              break;
+
+            default:
+              break;
+          }
+        }
+      };
+
+
+      LayerWidget*                        widget_;
+      OrthancVolumeImage*                 volume_;
+      VolumeProjection                    projection_;
+      std::auto_ptr<VolumeImageGeometry>  slices_;
+      size_t                              slice_;
+
+      void OffsetSlice(int offset)
+      {
+        if (slices_.get() != NULL)
+        {
+          int slice = static_cast<int>(slice_) + offset;
+
+          if (slice < 0)
+          {
+            slice = 0;
+          }
+
+          if (slice >= static_cast<int>(slices_->GetSliceCount()))
+          {
+            slice = slices_->GetSliceCount() - 1;
+          }
+
+          if (slice != static_cast<int>(slice_)) 
+          {
+            SetSlice(slice);
+          }   
+        }
+      }
+      
+      void SetSlice(size_t slice)
+      {
+        if (slices_.get() != NULL)
+        {
+          slice_ = slice;
+          widget_->SetSlice(slices_->GetSlice(slice_).GetGeometry());
+        }
+      }
+      
+      virtual void NotifyGeometryReady(const ILayerSource& source)
+      {
+        if (slices_.get() == NULL)
+        {
+          slices_.reset(new VolumeImageGeometry(*volume_, projection_));
+          SetSlice(slices_->GetSliceCount() / 2);
+          
+          widget_->SetDefaultView();
+        }
+      }
+      
+      virtual void NotifyGeometryError(const ILayerSource& source)
+      {
+      }
+      
+      virtual void NotifyContentChange(const ILayerSource& source)
+      {
+      }
+
+      virtual void NotifySliceChange(const ILayerSource& source,
+                                     const Slice& slice)
+      {
+      }
+ 
+      virtual void NotifyLayerReady(std::auto_ptr<ILayerRenderer>& layer,
+                                    const ILayerSource& source,
+                                    const Slice& slice,
+                                    bool isError)
+      {
+      }
+
+#if 0
       class Interactor : public SampleInteractor
       {
       private:
@@ -204,9 +358,16 @@
           }
         }
       };
-
+#endif
 
+      
     public:
+      SingleVolumeApplication() : 
+        widget_(NULL),
+        volume_(NULL)
+      {
+      }
+      
       virtual void DeclareCommandLineOptions(boost::program_options::options_description& options)
       {
         boost::program_options::options_description generic("Sample options");
@@ -243,18 +404,17 @@
         std::string tmp = parameters["projection"].as<std::string>();
         Orthanc::Toolbox::ToLowerCase(tmp);
         
-        VolumeProjection projection;
         if (tmp == "axial")
         {
-          projection = VolumeProjection_Axial;
+          projection_ = VolumeProjection_Axial;
         }
         else if (tmp == "sagittal")
         {
-          projection = VolumeProjection_Sagittal;
+          projection_ = VolumeProjection_Sagittal;
         }
         else if (tmp == "coronal")
         {
-          projection = VolumeProjection_Coronal;
+          projection_ = VolumeProjection_Coronal;
         }
         else
         {
@@ -262,22 +422,74 @@
           throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
         }
 
-        VolumeImage& volume = context.AddSeriesVolume(series, true /* progressive download */, threads);
+        std::auto_ptr<LayerWidget> widget(new LayerWidget);
+        widget_ = widget.get();
+
+#if 0
+        std::auto_ptr<OrthancVolumeImage> volume(new OrthancVolumeImage(context.GetWebService()));
+        volume->ScheduleLoadSeries(series);
 
-        std::auto_ptr<Interactor> interactor(new Interactor(volume, projection, reverse));
+        volume_ = volume.get();
+        
+        {
+          std::auto_ptr<VolumeImageSource> source(new VolumeImageSource(*volume));
+          source->Register(*this);
+          widget->AddLayer(source.release());
+        }
+
+        context.AddVolume(volume.release());
+#else
+        std::auto_ptr<OrthancVolumeImage> ct(new OrthancVolumeImage(context.GetWebService()));
+        ct->ScheduleLoadSeries("dd069910-4f090474-7d2bba07-e5c10783-f9e4fb1d");
+
+        std::auto_ptr<OrthancVolumeImage> pet(new OrthancVolumeImage(context.GetWebService()));
+        pet->ScheduleLoadSeries("aabad2e7-80702b5d-e599d26c-4f13398e-38d58a9e");
 
-        std::auto_ptr<LayeredSceneWidget> widget(new LayeredSceneWidget);
-        widget->AddLayer(new VolumeImage::LayerFactory(volume));
-        widget->SetSlice(interactor->GetCursor().GetCurrentSlice());
-        widget->SetInteractor(*interactor);
+        volume_ = pet.get();
+        
+        {
+          std::auto_ptr<VolumeImageSource> source(new VolumeImageSource(*ct));
+          //source->Register(*this);
+          widget->AddLayer(source.release());
+        }
+
+        {
+          std::auto_ptr<VolumeImageSource> source(new VolumeImageSource(*pet));
+          source->Register(*this);
+          widget->AddLayer(source.release());
+        }
+
+        context.AddVolume(ct.release());
+        context.AddVolume(pet.release());
 
-        context.AddInteractor(interactor.release());
-        context.SetCentralWidget(widget.release());
+        {
+          RenderStyle s;
+          //s.drawGrid_ = true;
+          s.alpha_ = 1;
+          widget->SetLayerStyle(0, s);
+        }
+
+        {
+          RenderStyle s;
+          //s.drawGrid_ = true;
+          s.SetColor(255, 0, 0);  // Draw missing PET layer in red
+          s.alpha_ = 0.5;
+          s.applyLut_ = true;
+          s.lut_ = Orthanc::EmbeddedResources::COLORMAP_JET;
+          s.interpolation_ = ImageInterpolation_Linear;
+          widget->SetLayerStyle(1, s);
+        }
+#endif
+
 
         statusBar.SetMessage("Use the keys \"b\", \"l\" and \"d\" to change Hounsfield windowing");
         statusBar.SetMessage("Use the keys \"t\" to track the (X,Y,Z) mouse coordinates");
         statusBar.SetMessage("Use the keys \"m\" to measure distances");
         statusBar.SetMessage("Use the keys \"c\" to draw circles");
+
+        widget->SetTransmitMouseOver(true);
+        widget->SetInteractor(context.AddInteractor(new Interactor(*this)));
+        context.SetCentralWidget(widget.release());
       }
     };
   }
--- a/Applications/Sdl/SdlEngine.cpp	Wed May 31 10:35:20 2017 +0200
+++ b/Applications/Sdl/SdlEngine.cpp	Wed May 31 17:01:18 2017 +0200
@@ -23,7 +23,7 @@
 
 #if ORTHANC_ENABLE_SDL == 1
 
-#include "../../../Resources/Orthanc/Core/Logging.h"
+#include "../../Resources/Orthanc/Core/Logging.h"
 
 #include <SDL.h>
 
--- a/Applications/Sdl/SdlSurface.cpp	Wed May 31 10:35:20 2017 +0200
+++ b/Applications/Sdl/SdlSurface.cpp	Wed May 31 17:01:18 2017 +0200
@@ -23,8 +23,8 @@
 
 #if ORTHANC_ENABLE_SDL == 1
 
-#include "../../../Resources/Orthanc/Core/Logging.h"
-#include "../../../Resources/Orthanc/Core/OrthancException.h"
+#include "../../Resources/Orthanc/Core/Logging.h"
+#include "../../Resources/Orthanc/Core/OrthancException.h"
 
 namespace OrthancStone
 {
--- a/Applications/Sdl/SdlSurface.cpp~	Wed May 31 10:35:20 2017 +0200
+++ b/Applications/Sdl/SdlSurface.cpp~	Wed May 31 17:01:18 2017 +0200
@@ -19,7 +19,7 @@
  **/
 
 
-#include "SdlBuffering.h"
+#include "SdlSurface.h"
 
 #if ORTHANC_ENABLE_SDL == 1
 
@@ -28,14 +28,14 @@
 
 namespace OrthancStone
 {
-  SdlBuffering::SdlBuffering() :
-    sdlSurface_(NULL),
-    pendingFrame_(false)
+  SdlSurface::SdlSurface(SdlWindow& window) :
+    window_(window),
+    sdlSurface_(NULL)
   {
   }
 
 
-  SdlBuffering::~SdlBuffering()
+  SdlSurface::~SdlSurface()
   {
     if (sdlSurface_)
     {
@@ -44,26 +44,14 @@
   }
 
 
-  void SdlBuffering::SetSize(unsigned int width,
-                             unsigned int height,
-                             IViewport& viewport)
+  void SdlSurface::SetSize(unsigned int width,
+                           unsigned int height)
   {
-    boost::mutex::scoped_lock lock(mutex_);
-
-    viewport.SetSize(width, height);
-
-    if (offscreenSurface_.get() == NULL ||
-        offscreenSurface_->GetWidth() != width ||
-        offscreenSurface_->GetHeight() != height)
+    if (cairoSurface_.get() == NULL ||
+        cairoSurface_->GetWidth() != width ||
+        cairoSurface_->GetHeight() != height)
     {
-      offscreenSurface_.reset(new CairoSurface(width, height));
-    }
-
-    if (onscreenSurface_.get() == NULL ||
-        onscreenSurface_->GetWidth() != width ||
-        onscreenSurface_->GetHeight() != height)
-    {
-      onscreenSurface_.reset(new CairoSurface(width, height));
+      cairoSurface_.reset(new CairoSurface(width, height));
 
       // TODO Big endian?
       static const uint32_t rmask = 0x00ff0000;
@@ -75,59 +63,30 @@
         SDL_FreeSurface(sdlSurface_);
       }
 
-      sdlSurface_ = SDL_CreateRGBSurfaceFrom(onscreenSurface_->GetBuffer(), width, height, 32,
-                                             onscreenSurface_->GetPitch(), rmask, gmask, bmask, 0);
+      sdlSurface_ = SDL_CreateRGBSurfaceFrom(cairoSurface_->GetBuffer(), width, height, 32,
+                                             cairoSurface_->GetPitch(), rmask, gmask, bmask, 0);
       if (!sdlSurface_)
       {
         LOG(ERROR) << "Cannot create a SDL surface from a Cairo surface";
         throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
-      }    
-    }
-
-    pendingFrame_ = false;
-  }
-
-
-  bool SdlBuffering::RenderOffscreen(IViewport& viewport)
-  {
-    boost::mutex::scoped_lock lock(mutex_);
-
-    if (offscreenSurface_.get() == NULL)
-    {
-      return false;
-    }
-
-    Orthanc::ImageAccessor target = offscreenSurface_->GetAccessor();
-
-    if (viewport.Render(target) &&
-        !pendingFrame_)
-    {
-      pendingFrame_ = true;
-      return true;
-    }
-    else
-    {
-      return false;
+      }
     }
   }
 
 
-  void SdlBuffering::SwapToScreen(SdlWindow& window)
+  void SdlSurface::Render(IViewport& viewport)
   {
-    if (!pendingFrame_ ||
-        offscreenSurface_.get() == NULL ||
-        onscreenSurface_.get() == NULL)
+    if (cairoSurface_.get() == NULL)
     {
-      return;
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
     }
 
+    Orthanc::ImageAccessor target = cairoSurface_->GetAccessor();
+
+    if (viewport.Render(target))
     {
-      boost::mutex::scoped_lock lock(mutex_);
-      onscreenSurface_->Copy(*offscreenSurface_);
+      window_.Render(sdlSurface_);
     }
-    
-    window.Render(sdlSurface_);
-    pendingFrame_ = false;
   }
 }
 
--- a/Applications/Sdl/SdlWindow.cpp	Wed May 31 10:35:20 2017 +0200
+++ b/Applications/Sdl/SdlWindow.cpp	Wed May 31 17:01:18 2017 +0200
@@ -23,8 +23,8 @@
 
 #if ORTHANC_ENABLE_SDL == 1
 
-#include "../../../Resources/Orthanc/Core/Logging.h"
-#include "../../../Resources/Orthanc/Core/OrthancException.h"
+#include "../../Resources/Orthanc/Core/Logging.h"
+#include "../../Resources/Orthanc/Core/OrthancException.h"
 
 namespace OrthancStone
 {
--- a/CMakeLists.txt	Wed May 31 10:35:20 2017 +0200
+++ b/CMakeLists.txt	Wed May 31 17:01:18 2017 +0200
@@ -34,7 +34,7 @@
 BuildSample(OrthancStoneEmpty 1)
 BuildSample(OrthancStoneTestPattern 2)
 BuildSample(OrthancStoneSingleFrame 3)
-#BuildSample(OrthancStoneSingleVolume 4)
+BuildSample(OrthancStoneSingleVolume 4)
 #BuildSample(OrthancStoneBasicPetCtFusion 5)
 #BuildSample(OrthancStoneSynchronizedSeries 6)
 #BuildSample(OrthancStoneLayoutPetCtFusion 7)
--- a/Framework/Layers/OrthancFrameLayerSource.cpp	Wed May 31 10:35:20 2017 +0200
+++ b/Framework/Layers/OrthancFrameLayerSource.cpp	Wed May 31 17:01:18 2017 +0200
@@ -94,19 +94,7 @@
     if (loader_.IsGeometryReady() &&
         loader_.LookupSlice(index, viewportSlice))
     {
-      const Slice& slice = loader_.GetSlice(index);
-      const SliceGeometry& plane = slice.GetGeometry();
-
-      double sx = slice.GetPixelSpacingX();
-      double sy = slice.GetPixelSpacingY();
-      double w = static_cast<double>(slice.GetWidth());
-      double h = static_cast<double>(slice.GetHeight());
-
-      points.clear();
-      points.push_back(plane.MapSliceToWorldCoordinates(-0.5      * sx, -0.5      * sy));
-      points.push_back(plane.MapSliceToWorldCoordinates((w - 0.5) * sx, -0.5      * sy));
-      points.push_back(plane.MapSliceToWorldCoordinates(-0.5      * sx, (h - 0.5) * sy));
-      points.push_back(plane.MapSliceToWorldCoordinates((w - 0.5) * sx, (h - 0.5) * sy));
+      loader_.GetSlice(index).GetExtent(points);
       return true;
     }
     else
--- a/Framework/Layers/OrthancFrameLayerSource.h	Wed May 31 10:35:20 2017 +0200
+++ b/Framework/Layers/OrthancFrameLayerSource.h	Wed May 31 17:01:18 2017 +0200
@@ -63,12 +63,12 @@
       quality_ = quality;
     }
 
-    virtual size_t GetSliceCount() const
+    size_t GetSliceCount() const
     {
       return loader_.GetSliceCount();
     }
 
-    virtual const Slice& GetSlice(size_t slice) const 
+    const Slice& GetSlice(size_t slice) const 
     {
       return loader_.GetSlice(slice);
     }
--- a/Framework/Toolbox/Slice.cpp	Wed May 31 10:35:20 2017 +0200
+++ b/Framework/Toolbox/Slice.cpp	Wed May 31 17:01:18 2017 +0200
@@ -187,4 +187,19 @@
                                     GetGeometry().ProjectAlongNormal(plane.GetOrigin()),
                                     thickness_ / 2.0));
   }
+
+  
+  void Slice::GetExtent(std::vector<Vector>& points) const
+  {
+    double sx = GetPixelSpacingX();
+    double sy = GetPixelSpacingY();
+    double w = static_cast<double>(GetWidth());
+    double h = static_cast<double>(GetHeight());
+
+    points.clear();
+    points.push_back(GetGeometry().MapSliceToWorldCoordinates(-0.5      * sx, -0.5      * sy));
+    points.push_back(GetGeometry().MapSliceToWorldCoordinates((w - 0.5) * sx, -0.5      * sy));
+    points.push_back(GetGeometry().MapSliceToWorldCoordinates(-0.5      * sx, (h - 0.5) * sy));
+    points.push_back(GetGeometry().MapSliceToWorldCoordinates((w - 0.5) * sx, (h - 0.5) * sy));
+  }
 }
--- a/Framework/Toolbox/Slice.h	Wed May 31 10:35:20 2017 +0200
+++ b/Framework/Toolbox/Slice.h	Wed May 31 17:01:18 2017 +0200
@@ -32,7 +32,7 @@
     enum Type
     {
       Type_Invalid,
-      Type_Detached,
+      Type_Standalone,
       Type_OrthancInstance
       // TODO A slice could come from some DICOM file (URL)
     };
@@ -57,7 +57,7 @@
     // layers within LayerWidget?
     Slice(const SliceGeometry& plane,
           double thickness) :
-      type_(Type_Detached),
+      type_(Type_Standalone),
       frame_(0),
       geometry_(plane),
       pixelSpacingX_(1),
@@ -68,6 +68,24 @@
     {      
     }
 
+    Slice(const SliceGeometry& plane,
+          double pixelSpacingX,
+          double pixelSpacingY,
+          double thickness,
+          unsigned int width,
+          unsigned int height,
+          const DicomFrameConverter& converter) :
+      type_(Type_Standalone),
+      geometry_(plane),
+      pixelSpacingX_(pixelSpacingX),
+      pixelSpacingY_(pixelSpacingY),
+      thickness_(thickness),
+      width_(width),
+      height_(height),
+      converter_(converter)
+    {
+    }
+
     bool IsValid() const
     {
       return type_ != Type_Invalid;
@@ -101,5 +119,7 @@
     const DicomFrameConverter& GetConverter() const;
 
     bool ContainsPlane(const SliceGeometry& plane) const;
+
+    void GetExtent(std::vector<Vector>& points) const;
   };
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/dev.h	Wed May 31 17:01:18 2017 +0200
@@ -0,0 +1,605 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017 Osimis, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You 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 "Layers/FrameRenderer.h"
+#include "Layers/LayerSourceBase.h"
+#include "Layers/SliceOutlineRenderer.h"
+#include "Toolbox/DownloadStack.h"
+#include "Toolbox/OrthancSlicesLoader.h"
+#include "Volumes/ImageBuffer3D.h"
+#include "Volumes/SlicedVolumeBase.h"
+
+#include "../Resources/Orthanc/Core/Logging.h"
+#include "../Resources/Orthanc/Core/Images/ImageProcessing.h"
+
+#include <boost/math/special_functions/round.hpp>
+
+
+namespace OrthancStone
+{
+  class OrthancVolumeImage : 
+    public SlicedVolumeBase,
+    private OrthancSlicesLoader::ICallback
+  { 
+  private:
+    OrthancSlicesLoader           loader_;
+    std::auto_ptr<ImageBuffer3D>  image_;
+    std::auto_ptr<DownloadStack>  downloadStack_;
+
+    
+    void ScheduleSliceDownload()
+    {
+      assert(downloadStack_.get() != NULL);
+
+      unsigned int slice;
+      if (downloadStack_->Pop(slice))
+      {
+        loader_.ScheduleLoadSliceImage(slice, SliceImageQuality_Jpeg90);
+      }
+    }
+
+
+    static bool IsCompatible(const Slice& a, 
+                             const Slice& b)
+    {
+      if (!GeometryToolbox::IsParallel(a.GetGeometry().GetNormal(),
+                                       b.GetGeometry().GetNormal()))
+      {
+        LOG(ERROR) << "Some slice in the volume image is not parallel to the others";
+        return false;
+      }
+
+      if (a.GetConverter().GetExpectedPixelFormat() != b.GetConverter().GetExpectedPixelFormat())
+      {
+        LOG(ERROR) << "The pixel format changes across the slices of the volume image";
+        return false;
+      }
+
+      if (a.GetWidth() != b.GetWidth() ||
+          a.GetHeight() != b.GetHeight())
+      {
+        LOG(ERROR) << "The width/height of the slices change across the volume image";
+        return false;
+      }
+
+      if (!GeometryToolbox::IsNear(a.GetPixelSpacingX(), b.GetPixelSpacingX()) ||
+          !GeometryToolbox::IsNear(a.GetPixelSpacingY(), b.GetPixelSpacingY()))
+      {
+        LOG(ERROR) << "The pixel spacing of the slices change across the volume image";
+        return false;
+      }
+
+      return true;
+    }
+
+
+    static double GetDistance(const Slice& a, 
+                              const Slice& b)
+    {
+      return fabs(a.GetGeometry().ProjectAlongNormal(a.GetGeometry().GetOrigin()) - 
+                  a.GetGeometry().ProjectAlongNormal(b.GetGeometry().GetOrigin()));
+    }
+
+
+    virtual void NotifyGeometryReady(const OrthancSlicesLoader& loader)
+    {
+      if (loader.GetSliceCount() == 0)
+      {
+        LOG(ERROR) << "Empty volume image";
+        SlicedVolumeBase::NotifyGeometryError();
+        return;
+      }
+
+      for (size_t i = 1; i < loader.GetSliceCount(); i++)
+      {
+        if (!IsCompatible(loader.GetSlice(0), loader.GetSlice(i)))
+        {
+          SlicedVolumeBase::NotifyGeometryError();
+          return;
+        }
+      }
+
+      double spacingZ;
+
+      if (loader.GetSliceCount() > 1)
+      {
+        spacingZ = GetDistance(loader.GetSlice(0), loader.GetSlice(1));
+      }
+      else
+      {
+        // This is a volume with one single slice: Choose a dummy
+        // z-dimension for voxels
+        spacingZ = 1;
+      }
+      
+      for (size_t i = 1; i < loader.GetSliceCount(); i++)
+      {
+        if (!GeometryToolbox::IsNear(spacingZ, GetDistance(loader.GetSlice(i - 1), loader.GetSlice(i)),
+                                     0.001 /* this is expressed in mm */))
+        {
+          LOG(ERROR) << "The distance between successive slices is not constant in a volume image";
+          SlicedVolumeBase::NotifyGeometryError();
+          return;
+        }
+      }
+
+      unsigned int width = loader.GetSlice(0).GetWidth();
+      unsigned int height = loader.GetSlice(0).GetHeight();
+      Orthanc::PixelFormat format = loader.GetSlice(0).GetConverter().GetExpectedPixelFormat();
+      LOG(INFO) << "Creating a volume image of size " << width << "x" << height 
+                << "x" << loader.GetSliceCount() << " in " << Orthanc::EnumerationToString(format);
+
+      image_.reset(new ImageBuffer3D(format, width, height, loader.GetSliceCount()));
+      image_->SetAxialGeometry(loader.GetSlice(0).GetGeometry());
+      image_->SetVoxelDimensions(loader.GetSlice(0).GetPixelSpacingX(), 
+                                 loader.GetSlice(0).GetPixelSpacingY(), spacingZ);
+      image_->Clear();
+
+      downloadStack_.reset(new DownloadStack(loader.GetSliceCount()));
+
+      for (unsigned int i = 0; i < 4; i++)  // Limit to 4 simultaneous downloads
+      {
+        ScheduleSliceDownload();
+      }
+
+      // TODO Check the DicomFrameConverter are constant
+
+      SlicedVolumeBase::NotifyGeometryReady();
+    }
+
+    virtual void NotifyGeometryError(const OrthancSlicesLoader& loader)
+    {
+      LOG(ERROR) << "Unable to download a volume image";
+      SlicedVolumeBase::NotifyGeometryError();
+    }
+
+    virtual void NotifySliceImageReady(const OrthancSlicesLoader& loader,
+                                       unsigned int sliceIndex,
+                                       const Slice& slice,
+                                       std::auto_ptr<Orthanc::ImageAccessor>& image,
+                                       SliceImageQuality quality)
+    {
+      {
+        ImageBuffer3D::SliceWriter writer(*image_, VolumeProjection_Axial, sliceIndex);
+        Orthanc::ImageProcessing::Copy(writer.GetAccessor(), *image);
+      }
+
+      SlicedVolumeBase::NotifySliceChange(sliceIndex, slice);
+
+      ScheduleSliceDownload();
+    }
+
+    virtual void NotifySliceImageError(const OrthancSlicesLoader& loader,
+                                       unsigned int sliceIndex,
+                                       const Slice& slice,
+                                       SliceImageQuality quality)
+    {
+      LOG(ERROR) << "Cannot download slice " << sliceIndex << " in a volume image";
+      ScheduleSliceDownload();
+    }
+
+  public:
+    OrthancVolumeImage(IWebService& orthanc) : 
+      loader_(*this, orthanc)
+    {
+    }
+
+    void ScheduleLoadSeries(const std::string& seriesId)
+    {
+      loader_.ScheduleLoadSeries(seriesId);
+    }
+
+    void ScheduleLoadInstance(const std::string& instanceId,
+                              unsigned int frame)
+    {
+      loader_.ScheduleLoadInstance(instanceId, frame);
+    }
+
+    virtual size_t GetSliceCount() const
+    {
+      return loader_.GetSliceCount();
+    }
+
+    virtual const Slice& GetSlice(size_t index) const
+    {
+      return loader_.GetSlice(index);
+    }
+
+    ImageBuffer3D& GetImage() const
+    {
+      if (image_.get() == NULL)
+      {
+        // The geometry is not ready yet
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+      }
+      else
+      {
+        return *image_;
+      }
+    }
+  };
+
+
+  class VolumeImageGeometry
+  {
+  private:
+    unsigned int         width_;
+    unsigned int         height_;
+    size_t               depth_;
+    double               pixelSpacingX_;
+    double               pixelSpacingY_;
+    double               sliceThickness_;
+    SliceGeometry        reference_;
+    DicomFrameConverter  converter_;
+    
+    double ComputeAxialThickness(const OrthancVolumeImage& volume) const
+    {
+      double thickness;
+      
+      size_t n = volume.GetSliceCount();
+      if (n > 1)
+      {
+        const Slice& a = volume.GetSlice(0);
+        const Slice& b = volume.GetSlice(n - 1);
+        thickness = ((reference_.ProjectAlongNormal(b.GetGeometry().GetOrigin()) -
+                      reference_.ProjectAlongNormal(a.GetGeometry().GetOrigin())) /
+                     (static_cast<double>(n) - 1.0));
+      }
+      else
+      {
+        thickness = volume.GetSlice(0).GetThickness();
+      }
+
+      if (thickness <= 0)
+      {
+        // The slices should have been sorted with increasing Z
+        // (along the normal) by the OrthancSlicesLoader
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+      }
+      else
+      {
+        return thickness;
+      }
+    }
+    
+    void SetupAxial(const OrthancVolumeImage& volume)
+    {
+      const Slice& axial = volume.GetSlice(0);
+      
+      width_ = axial.GetWidth();
+      height_ = axial.GetHeight();
+      depth_ = volume.GetSliceCount();
+
+      pixelSpacingX_ = axial.GetPixelSpacingX();
+      pixelSpacingY_ = axial.GetPixelSpacingY();
+      sliceThickness_ = ComputeAxialThickness(volume);
+
+      reference_ = axial.GetGeometry();
+    }
+
+    void SetupCoronal(const OrthancVolumeImage& volume)
+    {
+      const Slice& axial = volume.GetSlice(0);
+      double axialThickness = ComputeAxialThickness(volume);
+
+      width_ = axial.GetWidth();
+      height_ = volume.GetSliceCount();
+      depth_ = axial.GetHeight();
+
+      pixelSpacingX_ = axial.GetPixelSpacingX();
+      pixelSpacingY_ = axialThickness;
+      sliceThickness_ = axial.GetPixelSpacingY();
+
+      Vector origin = axial.GetGeometry().GetOrigin();
+      origin += (static_cast<double>(volume.GetSliceCount() - 1) *
+                 axialThickness * axial.GetGeometry().GetNormal());
+      
+      reference_ = SliceGeometry(origin,
+                                 axial.GetGeometry().GetAxisX(), 
+                                 -axial.GetGeometry().GetNormal());
+    }
+
+    void SetupSagittal(const OrthancVolumeImage& volume)
+    {
+      const Slice& axial = volume.GetSlice(0);
+      double axialThickness = ComputeAxialThickness(volume);
+
+      width_ = axial.GetHeight();
+      height_ = volume.GetSliceCount();
+      depth_ = axial.GetWidth();
+
+      pixelSpacingX_ = axial.GetPixelSpacingY();
+      pixelSpacingY_ = axialThickness;
+      sliceThickness_ = axial.GetPixelSpacingX();
+
+      Vector origin = axial.GetGeometry().GetOrigin();
+      origin += (static_cast<double>(volume.GetSliceCount() - 1) *
+                 axialThickness * axial.GetGeometry().GetNormal());
+      
+      reference_ = SliceGeometry(origin,
+                                 axial.GetGeometry().GetAxisY(), 
+                                 axial.GetGeometry().GetNormal());
+    }
+
+  public:
+    VolumeImageGeometry(const OrthancVolumeImage& volume,
+                        VolumeProjection projection)
+    {
+      if (volume.GetSliceCount() == 0)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+      }
+
+      converter_ = volume.GetSlice(0).GetConverter();
+
+      switch (projection)
+      {
+        case VolumeProjection_Axial:
+          SetupAxial(volume);
+          break;
+
+        case VolumeProjection_Coronal:
+          SetupCoronal(volume);
+          break;
+
+        case VolumeProjection_Sagittal:
+          SetupSagittal(volume);
+          break;
+
+        default:
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+      }
+    }
+
+    size_t GetSliceCount() const
+    {
+      return depth_;
+    }
+
+    const Vector& GetNormal() const
+    {
+      return reference_.GetNormal();
+    }
+    
+    bool LookupSlice(size_t& index,
+                     const SliceGeometry& slice) const
+    {
+      bool opposite;
+      if (!GeometryToolbox::IsParallelOrOpposite(opposite,
+                                                 reference_.GetNormal(),
+                                                 slice.GetNormal()))
+      {
+        return false;
+      }
+      
+      double z = (reference_.ProjectAlongNormal(slice.GetOrigin()) -
+                  reference_.ProjectAlongNormal(reference_.GetOrigin())) / sliceThickness_;
+
+      int s = static_cast<int>(boost::math::iround(z));
+
+      if (s < 0 ||
+          s >= static_cast<int>(depth_))
+      {
+        return false;
+      }
+      else
+      {
+        index = static_cast<size_t>(s);
+        return true;
+      }
+    }
+
+    Slice GetSlice(size_t slice) const
+    {
+      if (slice < 0 ||
+          slice >= depth_)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+      }
+      else
+      {
+        SliceGeometry origin(reference_.GetOrigin() +
+                             static_cast<double>(slice) * sliceThickness_ * reference_.GetNormal(),
+                             reference_.GetAxisX(),
+                             reference_.GetAxisY());
+
+        return Slice(origin, pixelSpacingX_, pixelSpacingY_, sliceThickness_,
+                     width_, height_, converter_);
+      }
+    }
+  };
+
+
+
+  class VolumeImageSource :
+    public LayerSourceBase,
+    private ISlicedVolume::IObserver
+  {
+  private:
+    OrthancVolumeImage&                 volume_;
+    std::auto_ptr<VolumeImageGeometry>  axialGeometry_;
+    std::auto_ptr<VolumeImageGeometry>  coronalGeometry_;
+    std::auto_ptr<VolumeImageGeometry>  sagittalGeometry_;
+
+    
+    bool IsGeometryReady() const
+    {
+      return axialGeometry_.get() != NULL;
+    }
+
+    
+    virtual void NotifyGeometryReady(const ISlicedVolume& volume)
+    {
+      // These 3 values are only used to speed up the ILayerSource
+      axialGeometry_.reset(new VolumeImageGeometry(volume_, VolumeProjection_Axial));
+      coronalGeometry_.reset(new VolumeImageGeometry(volume_, VolumeProjection_Coronal));
+      sagittalGeometry_.reset(new VolumeImageGeometry(volume_, VolumeProjection_Sagittal));
+      
+      LayerSourceBase::NotifyGeometryReady();
+    }
+      
+    virtual void NotifyGeometryError(const ISlicedVolume& volume)
+    {
+      LayerSourceBase::NotifyGeometryError();
+    }
+      
+    virtual void NotifyContentChange(const ISlicedVolume& volume)
+    {
+      LayerSourceBase::NotifyContentChange();
+    }
+
+    virtual void NotifySliceChange(const ISlicedVolume& volume,
+                                   const size_t& sliceIndex,
+                                   const Slice& slice)
+    {
+      //LayerSourceBase::NotifySliceChange(slice);
+
+      // TODO Improve this?
+      LayerSourceBase::NotifyContentChange();
+    }
+
+
+    const VolumeImageGeometry& GetProjectionGeometry(VolumeProjection projection)
+    {
+      if (!IsGeometryReady())
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+      }
+
+      switch (projection)
+      {
+        case VolumeProjection_Axial:
+          return *axialGeometry_;
+
+        case VolumeProjection_Sagittal:
+          return *sagittalGeometry_;
+
+        case VolumeProjection_Coronal:
+          return *coronalGeometry_;
+
+        default:
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+      }
+    }
+
+
+    bool DetectProjection(VolumeProjection& projection,
+                          const SliceGeometry& viewportSlice)
+    {
+      bool isOpposite;  // Ignored
+
+      if (GeometryToolbox::IsParallelOrOpposite(isOpposite,
+                                                viewportSlice.GetNormal(),
+                                                axialGeometry_->GetNormal()))
+      {
+        projection = VolumeProjection_Axial;
+        return true;
+      }
+      else if (GeometryToolbox::IsParallelOrOpposite(isOpposite,
+                                                     viewportSlice.GetNormal(),
+                                                     sagittalGeometry_->GetNormal()))
+      {
+        projection = VolumeProjection_Sagittal;
+        return true;
+      }
+      else if (GeometryToolbox::IsParallelOrOpposite(isOpposite,
+                                                     viewportSlice.GetNormal(),
+                                                     coronalGeometry_->GetNormal()))
+      {
+        projection = VolumeProjection_Coronal;
+        return true;
+      }
+      else
+      {
+        return false;
+      }
+    }
+
+
+  public:
+    VolumeImageSource(OrthancVolumeImage&  volume) :
+      volume_(volume)
+    {
+      volume_.Register(*this);
+    }
+
+    virtual bool GetExtent(std::vector<Vector>& points,
+                           const SliceGeometry& viewportSlice)
+    {
+      VolumeProjection projection;
+      
+      if (!IsGeometryReady() ||
+          !DetectProjection(projection, viewportSlice))
+      {
+        return false;
+      }
+      else
+      {       
+        // As the slices of the volumic image are arranged in a box,
+        // we only consider one single reference slice (the one with index 0).
+        GetProjectionGeometry(projection).GetSlice(0).GetExtent(points);
+        
+        return true;
+      }
+    }
+    
+
+    virtual void ScheduleLayerCreation(const SliceGeometry& viewportSlice)
+    {
+      VolumeProjection projection;
+      
+      if (IsGeometryReady() &&
+          DetectProjection(projection, viewportSlice))
+      {
+        const VolumeImageGeometry& geometry = GetProjectionGeometry(projection);
+
+        size_t closest;
+
+        if (geometry.LookupSlice(closest, viewportSlice))
+        {
+          bool isFullQuality = true;  // TODO
+
+          std::auto_ptr<Orthanc::Image> frame;
+
+          {
+            ImageBuffer3D::SliceReader reader(volume_.GetImage(), projection, closest);
+
+            // TODO Transfer ownership if non-axial, to avoid memcpy
+            frame.reset(Orthanc::Image::Clone(reader.GetAccessor()));
+          }
+
+          Slice slice = geometry.GetSlice(closest);
+          LayerSourceBase::NotifyLayerReady(
+            FrameRenderer::CreateRenderer(frame.release(), slice, isFullQuality),
+            //new SliceOutlineRenderer(slice),
+            slice, false);
+          return;
+        }
+      }
+
+      // Error
+      Slice slice;
+      LayerSourceBase::NotifyLayerReady(NULL, slice, true);
+    }
+  };
+}
--- a/UnitTestsSources/UnitTestsMain.cpp	Wed May 31 10:35:20 2017 +0200
+++ b/UnitTestsSources/UnitTestsMain.cpp	Wed May 31 17:01:18 2017 +0200
@@ -19,6 +19,7 @@
  **/
 
 
+#include "../Framework/dev.h"
 #include "gtest/gtest.h"
 
 #include "../Platforms/Generic/OracleWebService.h"
@@ -31,11 +32,15 @@
 #include "../Framework/Volumes/ImageBuffer3D.h"
 #include "../Framework/Volumes/SlicedVolumeBase.h"
 #include "../Framework/Toolbox/DownloadStack.h"
+#include "../Framework/Layers/LayerSourceBase.h"
+#include "../Framework/Layers/FrameRenderer.h"
 #include "../Resources/Orthanc/Core/Images/ImageProcessing.h"
 
 #include <boost/lexical_cast.hpp>
 #include <boost/date_time/posix_time/posix_time.hpp>
 #include <boost/thread/thread.hpp> 
+#include <boost/math/special_functions/round.hpp>
+
 
 namespace OrthancStone
 {
@@ -60,7 +65,7 @@
     virtual void NotifySliceImageReady(const OrthancSlicesLoader& loader,
                                        unsigned int sliceIndex,
                                        const Slice& slice,
-                                       Orthanc::ImageAccessor* image,
+                                       std::auto_ptr<Orthanc::ImageAccessor>& image,
                                        SliceImageQuality quality)
     {
       std::auto_ptr<Orthanc::ImageAccessor> tmp(image);
@@ -75,210 +80,6 @@
       printf("ERROR 2\n");
     }
   };
-
-
-  class OrthancVolumeImage : 
-    public SlicedVolumeBase,
-    private OrthancSlicesLoader::ICallback
-  { 
-  private:
-    OrthancSlicesLoader           loader_;
-    std::auto_ptr<ImageBuffer3D>  image_;
-    std::auto_ptr<DownloadStack>  downloadStack_;
-
-    
-    void ScheduleSliceDownload()
-    {
-      assert(downloadStack_.get() != NULL);
-
-      unsigned int slice;
-      if (downloadStack_->Pop(slice))
-      {
-        loader_.ScheduleLoadSliceImage(slice, SliceImageQuality_Full);
-      }
-    }
-
-
-    static bool IsCompatible(const Slice& a, 
-                             const Slice& b)
-    {
-      if (!GeometryToolbox::IsParallel(a.GetGeometry().GetNormal(),
-                                       b.GetGeometry().GetNormal()))
-      {
-        LOG(ERROR) << "Some slice in the volume image is not parallel to the others";
-        return false;
-      }
-
-      if (a.GetConverter().GetExpectedPixelFormat() != b.GetConverter().GetExpectedPixelFormat())
-      {
-        LOG(ERROR) << "The pixel format changes across the slices of the volume image";
-        return false;
-      }
-
-      if (a.GetWidth() != b.GetWidth() ||
-          a.GetHeight() != b.GetHeight())
-      {
-        LOG(ERROR) << "The width/height of the slices change across the volume image";
-        return false;
-      }
-
-      if (!GeometryToolbox::IsNear(a.GetPixelSpacingX(), b.GetPixelSpacingX()) ||
-          !GeometryToolbox::IsNear(a.GetPixelSpacingY(), b.GetPixelSpacingY()))
-      {
-        LOG(ERROR) << "The pixel spacing of the slices change across the volume image";
-        return false;
-      }
-
-      return true;
-    }
-
-
-    static double GetDistance(const Slice& a, 
-                              const Slice& b)
-    {
-      return fabs(a.GetGeometry().ProjectAlongNormal(a.GetGeometry().GetOrigin()) - 
-                  a.GetGeometry().ProjectAlongNormal(b.GetGeometry().GetOrigin()));
-    }
-
-
-    virtual void NotifyGeometryReady(const OrthancSlicesLoader& loader)
-    {
-      if (loader.GetSliceCount() == 0)
-      {
-        LOG(ERROR) << "Empty volume image";
-        SlicedVolumeBase::NotifyGeometryError();
-        return;
-      }
-
-      for (size_t i = 1; i < loader.GetSliceCount(); i++)
-      {
-        if (!IsCompatible(loader.GetSlice(0), loader.GetSlice(i)))
-        {
-          SlicedVolumeBase::NotifyGeometryError();
-          return;
-        }
-      }
-
-      double spacingZ;
-
-      if (loader.GetSliceCount() > 1)
-      {
-        spacingZ = GetDistance(loader.GetSlice(0), loader.GetSlice(1));
-      }
-      else
-      {
-        // This is a volume with one single slice: Choose a dummy
-        // z-dimension for voxels
-        spacingZ = 1;
-      }
-      
-      for (size_t i = 1; i < loader.GetSliceCount(); i++)
-      {
-        printf("%d %s %f\n", i, loader.GetSlice(i).GetOrthancInstanceId().c_str(),
-               GetDistance(loader.GetSlice(i - 1), loader.GetSlice(i)));
-        
-        if (!GeometryToolbox::IsNear(spacingZ, GetDistance(loader.GetSlice(i - 1), loader.GetSlice(i)),
-                                     0.001 /* this is expressed in mm */))
-        {
-          LOG(ERROR) << "The distance between successive slices is not constant in a volume image";
-          SlicedVolumeBase::NotifyGeometryError();
-          return;
-        }
-      }
-
-      unsigned int width = loader.GetSlice(0).GetWidth();
-      unsigned int height = loader.GetSlice(0).GetHeight();
-      Orthanc::PixelFormat format = loader.GetSlice(0).GetConverter().GetExpectedPixelFormat();
-      LOG(INFO) << "Creating a volume image of size " << width << "x" << height 
-                << "x" << loader.GetSliceCount() << " in " << Orthanc::EnumerationToString(format);
-
-      image_.reset(new ImageBuffer3D(format, width, height, loader.GetSliceCount()));
-      image_->SetAxialGeometry(loader.GetSlice(0).GetGeometry());
-      image_->SetVoxelDimensions(loader.GetSlice(0).GetPixelSpacingX(), 
-                                 loader.GetSlice(0).GetPixelSpacingY(), spacingZ);
-      image_->Clear();
-
-      downloadStack_.reset(new DownloadStack(loader.GetSliceCount()));
-
-      SlicedVolumeBase::NotifyGeometryReady();
-
-      for (unsigned int i = 0; i < 4; i++)  // Limit to 4 simultaneous downloads
-      {
-        ScheduleSliceDownload();
-      }
-    }
-
-    virtual void NotifyGeometryError(const OrthancSlicesLoader& loader)
-    {
-      LOG(ERROR) << "Unable to download a volume image";
-    }
-
-    virtual void NotifySliceImageReady(const OrthancSlicesLoader& loader,
-                                       unsigned int sliceIndex,
-                                       const Slice& slice,
-                                       Orthanc::ImageAccessor* image,
-                                       SliceImageQuality quality)
-    {
-      std::auto_ptr<Orthanc::ImageAccessor> protection(image);
-
-      {
-        ImageBuffer3D::SliceWriter writer(*image_, VolumeProjection_Axial, sliceIndex);
-        Orthanc::ImageProcessing::Copy(writer.GetAccessor(), *protection);
-      }
-
-      SlicedVolumeBase::NotifySliceChange(sliceIndex, slice);
-
-      ScheduleSliceDownload();
-    }
-
-    virtual void NotifySliceImageError(const OrthancSlicesLoader& loader,
-                                       unsigned int sliceIndex,
-                                       const Slice& slice,
-                                       SliceImageQuality quality)
-    {
-      LOG(ERROR) << "Cannot download slice " << sliceIndex << " in a volume image";
-      ScheduleSliceDownload();
-    }
-
-  public:
-    OrthancVolumeImage(IWebService& orthanc) : 
-      loader_(*this, orthanc)
-    {
-    }
-
-    void ScheduleLoadSeries(const std::string& seriesId)
-    {
-      loader_.ScheduleLoadSeries(seriesId);
-    }
-
-    void ScheduleLoadInstance(const std::string& instanceId,
-                              unsigned int frame)
-    {
-      loader_.ScheduleLoadInstance(instanceId, frame);
-    }
-
-    virtual size_t GetSliceCount() const
-    {
-      return loader_.GetSliceCount();
-    }
-
-    virtual const Slice& GetSlice(size_t index) const
-    {
-      return loader_.GetSlice(index);
-    }
-
-    ImageBuffer3D& GetImage()
-    {
-      if (image_.get() == NULL)
-      {
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
-      }
-      else
-      {
-        return *image_;
-      }
-    }
-  };
 }