changeset 814:aead999345e0

reorganization
author Sebastien Jodogne <s.jodogne@gmail.com>
date Tue, 28 May 2019 21:16:39 +0200
parents bc7ee59420a1
children df442f1ba0c6
files Framework/Loaders/OrthancSeriesVolumeProgressiveLoader.cpp Framework/Loaders/OrthancSeriesVolumeProgressiveLoader.h Framework/Scene2D/GrayscaleStyleConfigurator.cpp Framework/Scene2D/GrayscaleStyleConfigurator.h Framework/Scene2D/ILayerStyleConfigurator.h Framework/Scene2D/LookupTableStyleConfigurator.cpp Framework/Scene2D/LookupTableStyleConfigurator.h Framework/Volumes/DicomVolumeImage.cpp Framework/Volumes/DicomVolumeImage.h Framework/Volumes/DicomVolumeImageMPRSlicer.cpp Framework/Volumes/DicomVolumeImageMPRSlicer.h Framework/Volumes/IVolumeSlicer.cpp Framework/Volumes/IVolumeSlicer.cpp~ Framework/Volumes/IVolumeSlicer.h Framework/Volumes/IVolumeSlicer.h~ Resources/CMake/OrthancStoneConfiguration.cmake Samples/Sdl/Loader.cpp
diffstat 17 files changed, 1735 insertions(+), 1027 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Loaders/OrthancSeriesVolumeProgressiveLoader.cpp	Tue May 28 21:16:39 2019 +0200
@@ -0,0 +1,483 @@
+/**
+ * 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 "OrthancSeriesVolumeProgressiveLoader.h"
+
+#include "../Toolbox/GeometryToolbox.h"
+#include "../Volumes/DicomVolumeImageMPRSlicer.h"
+#include "BasicFetchingItemsSorter.h"
+#include "BasicFetchingStrategy.h"
+
+#include <Core/Images/ImageProcessing.h>
+#include <Core/OrthancException.h>
+
+namespace OrthancStone
+{
+  class OrthancSeriesVolumeProgressiveLoader::ExtractedSlice : public DicomVolumeImageMPRSlicer::Slice
+  {
+  private:
+    const OrthancSeriesVolumeProgressiveLoader&  that_;
+
+  protected:
+    virtual uint64_t GetRevisionInternal(VolumeProjection projection,
+                                         unsigned int sliceIndex) const
+    {
+      if (projection == VolumeProjection_Axial)
+      {
+        return that_.seriesGeometry_.GetSliceRevision(sliceIndex);
+      }
+      else
+      {
+        // For coronal and sagittal projections, we take the global
+        // revision of the volume because even if a single slice changes,
+        // this means the projection will yield a different result --> 
+        // we must increase the revision as soon as any slice changes 
+        return that_.volume_->GetRevision();
+      }
+    }
+
+  public:
+    ExtractedSlice(const OrthancSeriesVolumeProgressiveLoader& that,
+                   const CoordinateSystem3D& plane) :
+      DicomVolumeImageMPRSlicer::Slice(*that.volume_, plane),
+      that_(that)
+    {
+      if (that_.strategy_.get() != NULL &&
+          IsValid() &&
+          GetProjection() == VolumeProjection_Axial)
+      {
+        that_.strategy_->SetCurrent(GetSliceIndex());
+      }
+    }
+  };
+
+    
+    
+  void OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::CheckSlice(size_t index,
+                                                                        const DicomInstanceParameters& reference) const
+  {
+    const DicomInstanceParameters& slice = *slices_[index];
+      
+    if (!GeometryToolbox::IsParallel(
+          reference.GetGeometry().GetNormal(),
+          slice.GetGeometry().GetNormal()))
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadGeometry,
+                                      "A slice in the volume image is not parallel to the others");
+    }
+
+    if (reference.GetExpectedPixelFormat() != slice.GetExpectedPixelFormat())
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageFormat,
+                                      "The pixel format changes across the slices of the volume image");
+    }
+
+    if (reference.GetImageInformation().GetWidth() != slice.GetImageInformation().GetWidth() ||
+        reference.GetImageInformation().GetHeight() != slice.GetImageInformation().GetHeight())
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageSize,
+                                      "The width/height of slices are not constant in the volume image");
+    }
+
+    if (!LinearAlgebra::IsNear(reference.GetPixelSpacingX(), slice.GetPixelSpacingX()) ||
+        !LinearAlgebra::IsNear(reference.GetPixelSpacingY(), slice.GetPixelSpacingY()))
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadGeometry,
+                                      "The pixel spacing of the slices change across the volume image");
+    }
+  }
+
+    
+  void OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::CheckVolume() const
+  {
+    for (size_t i = 0; i < slices_.size(); i++)
+    {
+      assert(slices_[i] != NULL);
+      if (slices_[i]->GetImageInformation().GetNumberOfFrames() != 1)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadGeometry,
+                                        "This class does not support multi-frame images");
+      }
+    }
+
+    if (slices_.size() != 0)
+    {
+      const DicomInstanceParameters& reference = *slices_[0];
+
+      for (size_t i = 1; i < slices_.size(); i++)
+      {
+        CheckSlice(i, reference);
+      }
+    }
+  }
+
+
+  void OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::Clear()
+  {
+    for (size_t i = 0; i < slices_.size(); i++)
+    {
+      assert(slices_[i] != NULL);
+      delete slices_[i];
+    }
+
+    slices_.clear();
+    slicesRevision_.clear();
+  }
+
+
+  void OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::CheckSliceIndex(size_t index) const
+  {
+    if (!HasGeometry())
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+    }
+    else if (index >= slices_.size())
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+    else
+    {
+      assert(slices_.size() == GetImageGeometry().GetDepth() &&
+             slices_.size() == slicesRevision_.size());
+    }
+  }
+
+
+  // WARNING: The payload of "slices" must be of class "DicomInstanceParameters"
+  // (called with the slices created in LoadGeometry)
+  void OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::ComputeGeometry(SlicesSorter& slices)
+  {
+    Clear();
+      
+    if (!slices.Sort())
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange,
+                                      "Cannot sort the 3D slices of a DICOM series");          
+    }
+
+    if (slices.GetSlicesCount() == 0)
+    {
+      geometry_.reset(new VolumeImageGeometry);
+    }
+    else
+    {
+      slices_.reserve(slices.GetSlicesCount());
+      slicesRevision_.resize(slices.GetSlicesCount(), 0);
+
+      for (size_t i = 0; i < slices.GetSlicesCount(); i++)
+      {
+        const DicomInstanceParameters& slice =
+          dynamic_cast<const DicomInstanceParameters&>(slices.GetSlicePayload(i));
+        slices_.push_back(new DicomInstanceParameters(slice));
+      }
+
+      CheckVolume();
+
+      const double spacingZ = slices.ComputeSpacingBetweenSlices();
+      LOG(INFO) << "Computed spacing between slices: " << spacingZ << "mm";
+      
+      const DicomInstanceParameters& parameters = *slices_[0];
+
+      geometry_.reset(new VolumeImageGeometry);
+      geometry_->SetSize(parameters.GetImageInformation().GetWidth(),
+                         parameters.GetImageInformation().GetHeight(),
+                         static_cast<unsigned int>(slices.GetSlicesCount()));
+      geometry_->SetAxialGeometry(slices.GetSliceGeometry(0));
+      geometry_->SetVoxelDimensions(parameters.GetPixelSpacingX(),
+                                    parameters.GetPixelSpacingY(), spacingZ);
+    }
+  }
+
+
+  const VolumeImageGeometry& OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::GetImageGeometry() const
+  {
+    if (!HasGeometry())
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      assert(slices_.size() == geometry_->GetDepth());
+      return *geometry_;
+    }
+  }
+
+
+  const DicomInstanceParameters& OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::GetSliceParameters(size_t index) const
+  {
+    CheckSliceIndex(index);
+    return *slices_[index];
+  }
+
+
+  uint64_t OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::GetSliceRevision(size_t index) const
+  {
+    CheckSliceIndex(index);
+    return slicesRevision_[index];
+  }
+
+
+  void OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::IncrementSliceRevision(size_t index)
+  {
+    CheckSliceIndex(index);
+    slicesRevision_[index] ++;
+  }
+
+
+  static unsigned int GetSliceIndexPayload(const OracleCommandWithPayload& command)
+  {
+    return dynamic_cast< const Orthanc::SingleValueObject<unsigned int>& >(command.GetPayload()).GetValue();
+  }
+
+
+  void OrthancSeriesVolumeProgressiveLoader::ScheduleNextSliceDownload()
+  {
+    assert(strategy_.get() != NULL);
+      
+    unsigned int sliceIndex, quality;
+      
+    if (strategy_->GetNext(sliceIndex, quality))
+    {
+      assert(quality <= BEST_QUALITY);
+
+      const DicomInstanceParameters& slice = seriesGeometry_.GetSliceParameters(sliceIndex);
+          
+      const std::string& instance = slice.GetOrthancInstanceIdentifier();
+      if (instance.empty())
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+      }
+
+      std::auto_ptr<OracleCommandWithPayload> command;
+        
+      if (quality == BEST_QUALITY)
+      {
+        std::auto_ptr<GetOrthancImageCommand> tmp(new GetOrthancImageCommand);
+        tmp->SetHttpHeader("Accept-Encoding", "gzip");
+        tmp->SetHttpHeader("Accept", std::string(Orthanc::EnumerationToString(Orthanc::MimeType_Pam)));
+        tmp->SetInstanceUri(instance, slice.GetExpectedPixelFormat());          
+        tmp->SetExpectedPixelFormat(slice.GetExpectedPixelFormat());
+        command.reset(tmp.release());
+      }
+      else
+      {
+        std::auto_ptr<GetOrthancWebViewerJpegCommand> tmp(new GetOrthancWebViewerJpegCommand);
+        tmp->SetHttpHeader("Accept-Encoding", "gzip");
+        tmp->SetInstance(instance);
+        tmp->SetQuality((quality == 0 ? 50 : 90));
+        tmp->SetExpectedPixelFormat(slice.GetExpectedPixelFormat());
+        command.reset(tmp.release());
+      }
+
+      command->SetPayload(new Orthanc::SingleValueObject<unsigned int>(sliceIndex));
+      oracle_.Schedule(*this, command.release());
+    }
+  }
+
+/**
+   This is called in response to GET "/series/XXXXXXXXXXXXX/instances-tags"
+*/
+  void OrthancSeriesVolumeProgressiveLoader::LoadGeometry(const OrthancRestApiCommand::SuccessMessage& message)
+  {
+    Json::Value body;
+    message.ParseJsonBody(body);
+      
+    if (body.type() != Json::objectValue)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol);
+    }
+
+    {
+      Json::Value::Members instances = body.getMemberNames();
+
+      SlicesSorter slices;
+        
+      for (size_t i = 0; i < instances.size(); i++)
+      {
+        Orthanc::DicomMap dicom;
+        dicom.FromDicomAsJson(body[instances[i]]);
+
+        std::auto_ptr<DicomInstanceParameters> instance(new DicomInstanceParameters(dicom));
+        instance->SetOrthancInstanceIdentifier(instances[i]);
+
+        // the 3D plane corresponding to the slice
+        CoordinateSystem3D geometry = instance->GetGeometry();
+        slices.AddSlice(geometry, instance.release());
+      }
+
+      seriesGeometry_.ComputeGeometry(slices);
+    }
+
+    size_t slicesCount = seriesGeometry_.GetImageGeometry().GetDepth();
+
+    if (slicesCount == 0)
+    {
+      volume_->Initialize(seriesGeometry_.GetImageGeometry(), Orthanc::PixelFormat_Grayscale8);
+    }
+    else
+    {
+      const DicomInstanceParameters& parameters = seriesGeometry_.GetSliceParameters(0);
+        
+      volume_->Initialize(seriesGeometry_.GetImageGeometry(), parameters.GetExpectedPixelFormat());
+      volume_->SetDicomParameters(parameters);
+      volume_->GetPixelData().Clear();
+
+      strategy_.reset(new BasicFetchingStrategy(sorter_->CreateSorter(static_cast<unsigned int>(slicesCount)), BEST_QUALITY));
+        
+      assert(simultaneousDownloads_ != 0);
+      for (unsigned int i = 0; i < simultaneousDownloads_; i++)
+      {
+        ScheduleNextSliceDownload();
+      }
+    }
+
+    slicesQuality_.resize(slicesCount, 0);
+
+    BroadcastMessage(DicomVolumeImage::GeometryReadyMessage(*volume_));
+  }
+
+
+  void OrthancSeriesVolumeProgressiveLoader::SetSliceContent(unsigned int sliceIndex,
+                                                             const Orthanc::ImageAccessor& image,
+                                                             unsigned int quality)
+  {
+    assert(sliceIndex < slicesQuality_.size() &&
+           slicesQuality_.size() == volume_->GetPixelData().GetDepth());
+      
+    if (quality >= slicesQuality_[sliceIndex])
+    {
+      {
+        ImageBuffer3D::SliceWriter writer(volume_->GetPixelData(), VolumeProjection_Axial, sliceIndex);
+        Orthanc::ImageProcessing::Copy(writer.GetAccessor(), image);
+      }
+
+      volume_->IncrementRevision();
+      seriesGeometry_.IncrementSliceRevision(sliceIndex);
+      slicesQuality_[sliceIndex] = quality;
+
+      BroadcastMessage(DicomVolumeImage::ContentUpdatedMessage(*volume_));
+    }
+
+    ScheduleNextSliceDownload();
+  }
+
+
+  void OrthancSeriesVolumeProgressiveLoader::LoadBestQualitySliceContent(const GetOrthancImageCommand::SuccessMessage& message)
+  {
+    SetSliceContent(GetSliceIndexPayload(message.GetOrigin()), message.GetImage(), BEST_QUALITY);
+  }
+
+
+  void OrthancSeriesVolumeProgressiveLoader::LoadJpegSliceContent(const GetOrthancWebViewerJpegCommand::SuccessMessage& message)
+  {
+    unsigned int quality;
+      
+    switch (message.GetOrigin().GetQuality())
+    {
+      case 50:
+        quality = LOW_QUALITY;
+        break;
+
+      case 90:
+        quality = MIDDLE_QUALITY;
+        break;
+
+      default:
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+    }
+      
+    SetSliceContent(GetSliceIndexPayload(message.GetOrigin()), message.GetImage(), quality);
+  }
+
+
+  OrthancSeriesVolumeProgressiveLoader::OrthancSeriesVolumeProgressiveLoader(const boost::shared_ptr<DicomVolumeImage>& volume,
+                                                                             IOracle& oracle,
+                                                                             IObservable& oracleObservable) :
+    IObserver(oracleObservable.GetBroker()),
+    IObservable(oracleObservable.GetBroker()),
+    oracle_(oracle),
+    active_(false),
+    simultaneousDownloads_(4),
+    volume_(volume),
+    sorter_(new BasicFetchingItemsSorter::Factory)
+  {
+    oracleObservable.RegisterObserverCallback(
+      new Callable<OrthancSeriesVolumeProgressiveLoader, OrthancRestApiCommand::SuccessMessage>
+      (*this, &OrthancSeriesVolumeProgressiveLoader::LoadGeometry));
+
+    oracleObservable.RegisterObserverCallback(
+      new Callable<OrthancSeriesVolumeProgressiveLoader, GetOrthancImageCommand::SuccessMessage>
+      (*this, &OrthancSeriesVolumeProgressiveLoader::LoadBestQualitySliceContent));
+
+    oracleObservable.RegisterObserverCallback(
+      new Callable<OrthancSeriesVolumeProgressiveLoader, GetOrthancWebViewerJpegCommand::SuccessMessage>
+      (*this, &OrthancSeriesVolumeProgressiveLoader::LoadJpegSliceContent));
+  }
+
+
+  void OrthancSeriesVolumeProgressiveLoader::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;
+    }
+  }
+
+
+  void OrthancSeriesVolumeProgressiveLoader::LoadSeries(const std::string& seriesId)
+  {
+    if (active_)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      active_ = true;
+
+      std::auto_ptr<OrthancRestApiCommand> command(new OrthancRestApiCommand);
+      command->SetUri("/series/" + seriesId + "/instances-tags");
+
+      oracle_.Schedule(*this, command.release());
+    }
+  }
+  
+
+  IVolumeSlicer::IExtractedSlice* 
+  OrthancSeriesVolumeProgressiveLoader::ExtractSlice(const CoordinateSystem3D& cuttingPlane)
+  {
+    if (volume_->HasGeometry())
+    {
+      return new ExtractedSlice(*this, cuttingPlane);
+    }
+    else
+    {
+      return new IVolumeSlicer::InvalidSlice;
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Loaders/OrthancSeriesVolumeProgressiveLoader.h	Tue May 28 21:16:39 2019 +0200
@@ -0,0 +1,134 @@
+/**
+ * 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/OrthancRestApiCommand.h"
+#include "../Toolbox/SlicesSorter.h"
+#include "../Volumes/DicomVolumeImage.h"
+#include "../Volumes/IVolumeSlicer.h"
+#include "IFetchingItemsSorter.h"
+#include "IFetchingStrategy.h"
+
+#include <boost/shared_ptr.hpp>
+
+namespace OrthancStone
+{
+  /**
+    This class is used to manage the progressive loading of a volume that
+    is stored in a Dicom series.
+  */
+  class OrthancSeriesVolumeProgressiveLoader : 
+    public IObserver,
+    public IObservable,
+    public IVolumeSlicer
+  {
+  private:
+    static const unsigned int LOW_QUALITY = 0;
+    static const unsigned int MIDDLE_QUALITY = 1;
+    static const unsigned int BEST_QUALITY = 2;
+    
+    class ExtractedSlice;
+    
+    /** Helper class internal to OrthancSeriesVolumeProgressiveLoader */
+    class SeriesGeometry : public boost::noncopyable
+    {
+    private:
+      void CheckSlice(size_t index,
+                      const DicomInstanceParameters& reference) const;
+    
+      void CheckVolume() const;
+
+      void Clear();
+
+      void CheckSliceIndex(size_t index) const;
+
+      std::auto_ptr<VolumeImageGeometry>     geometry_;
+      std::vector<DicomInstanceParameters*>  slices_;
+      std::vector<uint64_t>                  slicesRevision_;
+
+    public:
+      ~SeriesGeometry()
+      {
+        Clear();
+      }
+
+      void ComputeGeometry(SlicesSorter& slices);
+
+      bool HasGeometry() const
+      {
+        return geometry_.get() != NULL;
+      }
+
+      const VolumeImageGeometry& GetImageGeometry() const;
+
+      const DicomInstanceParameters& GetSliceParameters(size_t index) const;
+
+      uint64_t GetSliceRevision(size_t index) const;
+
+      void IncrementSliceRevision(size_t index);
+    };
+
+
+    void ScheduleNextSliceDownload();
+
+    void LoadGeometry(const OrthancRestApiCommand::SuccessMessage& message);
+
+    void SetSliceContent(unsigned int sliceIndex,
+                         const Orthanc::ImageAccessor& image,
+                         unsigned int quality);
+
+    void LoadBestQualitySliceContent(const GetOrthancImageCommand::SuccessMessage& message);
+
+    void LoadJpegSliceContent(const GetOrthancWebViewerJpegCommand::SuccessMessage& message);
+
+    IOracle&                                       oracle_;
+    bool                                           active_;
+    unsigned int                                   simultaneousDownloads_;
+    SeriesGeometry                                 seriesGeometry_;
+    boost::shared_ptr<DicomVolumeImage>            volume_;
+    std::auto_ptr<IFetchingItemsSorter::IFactory>  sorter_;
+    std::auto_ptr<IFetchingStrategy>               strategy_;
+    std::vector<unsigned int>                      slicesQuality_;
+
+
+  public:
+    OrthancSeriesVolumeProgressiveLoader(const boost::shared_ptr<DicomVolumeImage>& volume,
+                                         IOracle& oracle,
+                                         IObservable& oracleObservable);
+
+    void SetSimultaneousDownloads(unsigned int count);
+
+    void LoadSeries(const std::string& seriesId);
+
+    /**
+    When a slice is requested, the strategy algorithm (that defines the 
+    sequence of resources to be loaded from the server) is modified to 
+    take into account this request (this is done in the ExtractedSlice ctor)
+    */
+    virtual IExtractedSlice* ExtractSlice(const CoordinateSystem3D& cuttingPlane);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Scene2D/GrayscaleStyleConfigurator.cpp	Tue May 28 21:16:39 2019 +0200
@@ -0,0 +1,32 @@
+/**
+ * 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 "GrayscaleStyleConfigurator.h"
+
+#include <Core/OrthancException.h>
+
+namespace OrthancStone
+{
+  TextureBaseSceneLayer* GrayscaleStyleConfigurator::CreateTextureFromImage(const Orthanc::ImageAccessor& image) const
+  {
+    throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Scene2D/GrayscaleStyleConfigurator.h	Tue May 28 21:16:39 2019 +0200
@@ -0,0 +1,62 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-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 "ILayerStyleConfigurator.h"
+
+namespace OrthancStone
+{
+  /**
+  Creates layers to display the supplied image in grayscale. No dynamic 
+  style is available.
+  */
+  class GrayscaleStyleConfigurator : public ILayerStyleConfigurator
+  {
+  private:
+    uint64_t revision_;
+
+    // TODO - Add windowing
+    
+  public:
+    GrayscaleStyleConfigurator() :
+      revision_(0)
+    {
+    }
+
+    virtual uint64_t GetRevision() const
+    {
+      return revision_;
+    }
+    
+    virtual TextureBaseSceneLayer* CreateTextureFromImage(const Orthanc::ImageAccessor& image) const;
+
+    virtual TextureBaseSceneLayer* CreateTextureFromDicom(const Orthanc::ImageAccessor& frame,
+                                                          const DicomInstanceParameters& parameters) const
+    {
+      return parameters.CreateTexture(frame);
+    }
+
+    virtual void ApplyStyle(ISceneLayer& layer) const
+    {
+    }
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Scene2D/ILayerStyleConfigurator.h	Tue May 28 21:16:39 2019 +0200
@@ -0,0 +1,52 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-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/DicomInstanceParameters.h"
+
+namespace OrthancStone
+{
+  /**
+  This interface is implemented by objects able to create an ISceneLayer 
+  suitable to display the Orthanc image supplied to the CreateTextureXX 
+  factory methods (taking Dicom parameters into account if relevant).
+
+  It can also refresh the style of an existing layer afterwards, to match
+  the configurator settings.
+  */
+  class ILayerStyleConfigurator
+  {
+  public:
+    virtual ~ILayerStyleConfigurator()
+    {
+    }
+    
+    virtual uint64_t GetRevision() const = 0;
+    
+    virtual TextureBaseSceneLayer* CreateTextureFromImage(const Orthanc::ImageAccessor& image) const = 0;
+
+    virtual TextureBaseSceneLayer* CreateTextureFromDicom(const Orthanc::ImageAccessor& frame,
+                                                          const DicomInstanceParameters& parameters) const = 0;
+
+    virtual void ApplyStyle(ISceneLayer& layer) const = 0;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Scene2D/LookupTableStyleConfigurator.cpp	Tue May 28 21:16:39 2019 +0200
@@ -0,0 +1,90 @@
+/**
+ * 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 "LookupTableStyleConfigurator.h"
+
+#include <Core/OrthancException.h>
+
+namespace OrthancStone
+{
+  LookupTableStyleConfigurator::LookupTableStyleConfigurator() :
+    revision_(0),
+    hasLut_(false),
+    hasRange_(false)
+  {
+  }
+
+
+  void LookupTableStyleConfigurator::SetLookupTable(Orthanc::EmbeddedResources::FileResourceId resource)
+  {
+    hasLut_ = true;
+    Orthanc::EmbeddedResources::GetFileResource(lut_, resource);
+  }
+
+
+  void LookupTableStyleConfigurator::SetLookupTable(const std::string& lut)
+  {
+    hasLut_ = true;
+    lut_ = lut;
+  }
+
+
+  void LookupTableStyleConfigurator::SetRange(float minValue,
+                                              float maxValue)
+  {
+    if (minValue > maxValue)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+    else
+    {
+      hasRange_ = true;
+      minValue_ = minValue;
+      maxValue_ = maxValue;
+    }
+  }
+
+    
+  TextureBaseSceneLayer* LookupTableStyleConfigurator::CreateTextureFromImage(const Orthanc::ImageAccessor& image) const
+  {
+    throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+  }
+
+
+  void LookupTableStyleConfigurator::ApplyStyle(ISceneLayer& layer) const
+  {
+    LookupTableTextureSceneLayer& l = dynamic_cast<LookupTableTextureSceneLayer&>(layer);
+      
+    if (hasLut_)
+    {
+      l.SetLookupTable(lut_);
+    }
+
+    if (hasRange_)
+    {
+      l.SetRange(minValue_, maxValue_);
+    }
+    else
+    {
+      l.FitRange();
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Scene2D/LookupTableStyleConfigurator.h	Tue May 28 21:16:39 2019 +0200
@@ -0,0 +1,68 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-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 "ILayerStyleConfigurator.h"
+
+#include <EmbeddedResources.h>
+
+namespace OrthancStone
+{
+  /**
+  This configurator supplies an API to set a display range and a LUT.
+  */
+  class LookupTableStyleConfigurator : public ILayerStyleConfigurator
+  {
+  private:
+    uint64_t     revision_;
+    bool         hasLut_;
+    std::string  lut_;
+    bool         hasRange_;
+    float        minValue_;
+    float        maxValue_;
+    
+  public:
+    LookupTableStyleConfigurator();
+
+    void SetLookupTable(Orthanc::EmbeddedResources::FileResourceId resource);
+
+    void SetLookupTable(const std::string& lut);
+
+    void SetRange(float minValue,
+                  float maxValue);
+
+    virtual uint64_t GetRevision() const
+    {
+      return revision_;
+    }
+    
+    virtual TextureBaseSceneLayer* CreateTextureFromImage(const Orthanc::ImageAccessor& image) const;
+
+    virtual TextureBaseSceneLayer* CreateTextureFromDicom(const Orthanc::ImageAccessor& frame,
+                                                          const DicomInstanceParameters& parameters) const
+    {
+      return parameters.CreateLookupTableTexture(frame);
+    }
+
+    virtual void ApplyStyle(ISceneLayer& layer) const;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Volumes/DicomVolumeImage.cpp	Tue May 28 21:16:39 2019 +0200
@@ -0,0 +1,94 @@
+/**
+ * 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 "DicomVolumeImage.h"
+
+#include <Core/OrthancException.h>
+
+namespace OrthancStone
+{
+  void DicomVolumeImage::CheckHasGeometry() const
+  {
+    if (!HasGeometry())
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+    }
+  }
+    
+
+  void DicomVolumeImage::Initialize(const VolumeImageGeometry& geometry,
+                                    Orthanc::PixelFormat format)
+  {
+    geometry_.reset(new VolumeImageGeometry(geometry));
+    image_.reset(new ImageBuffer3D(format, geometry_->GetWidth(), geometry_->GetHeight(),
+                                   geometry_->GetDepth(), false /* don't compute range */));
+
+    revision_ ++;
+  }
+
+
+  void DicomVolumeImage::SetDicomParameters(const DicomInstanceParameters& parameters)
+  {
+    parameters_.reset(parameters.Clone());
+    revision_ ++;
+  }
+    
+
+  bool DicomVolumeImage::HasGeometry() const
+  {
+    return (geometry_.get() != NULL &&
+            image_.get() != NULL);
+  }
+
+
+  ImageBuffer3D& DicomVolumeImage::GetPixelData()
+  {
+    CheckHasGeometry();
+    return *image_;
+  }
+
+
+  const ImageBuffer3D& DicomVolumeImage::GetPixelData() const
+  {
+    CheckHasGeometry();
+    return *image_;
+  }
+
+
+  const VolumeImageGeometry& DicomVolumeImage::GetGeometry() const
+  {
+    CheckHasGeometry();
+    return *geometry_;
+  }
+
+
+  const DicomInstanceParameters& DicomVolumeImage::GetDicomParameters() const
+  {
+    if (HasDicomParameters())
+    {
+      return *parameters_;
+    }
+    else
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+    }      
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Volumes/DicomVolumeImage.h	Tue May 28 21:16:39 2019 +0200
@@ -0,0 +1,86 @@
+/**
+ * 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/IMessage.h"
+#include "../Toolbox/DicomInstanceParameters.h"
+#include "ImageBuffer3D.h"
+#include "VolumeImageGeometry.h"
+
+namespace OrthancStone
+{
+  /**
+  This class combines a 3D image buffer, a 3D volume geometry and
+  information about the DICOM parameters of the series.
+  (MPR means MultiPlanar Reconstruction)
+  */ 
+  class DicomVolumeImage : public boost::noncopyable
+  {
+  public:
+    ORTHANC_STONE_DEFINE_ORIGIN_MESSAGE(__FILE__, __LINE__, GeometryReadyMessage, DicomVolumeImage);
+    ORTHANC_STONE_DEFINE_ORIGIN_MESSAGE(__FILE__, __LINE__, ContentUpdatedMessage, DicomVolumeImage);
+
+  private:
+    uint64_t                                revision_;
+    std::auto_ptr<VolumeImageGeometry>      geometry_;
+    std::auto_ptr<ImageBuffer3D>            image_;
+    std::auto_ptr<DicomInstanceParameters>  parameters_;
+
+    void CheckHasGeometry() const;
+    
+  public:
+    DicomVolumeImage() :
+      revision_(0)
+    {
+    }
+
+    void IncrementRevision()
+    {
+      revision_ ++;
+    }
+
+    void Initialize(const VolumeImageGeometry& geometry,
+                    Orthanc::PixelFormat format);
+
+    void SetDicomParameters(const DicomInstanceParameters& parameters);
+    
+    uint64_t GetRevision() const
+    {
+      return revision_;
+    }
+
+    bool HasGeometry() const;
+
+    ImageBuffer3D& GetPixelData();
+
+    const ImageBuffer3D& GetPixelData() const;
+
+    const VolumeImageGeometry& GetGeometry() const;
+
+    bool HasDicomParameters() const
+    {
+      return parameters_.get() != NULL;
+    }      
+
+    const DicomInstanceParameters& GetDicomParameters() const;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Volumes/DicomVolumeImageMPRSlicer.cpp	Tue May 28 21:16:39 2019 +0200
@@ -0,0 +1,120 @@
+/**
+ * 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 "DicomVolumeImageMPRSlicer.h"
+
+#include <Core/OrthancException.h>
+
+namespace OrthancStone
+{
+  void DicomVolumeImageMPRSlicer::Slice::CheckValid() const
+  {
+    if (!valid_)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+    }
+  }
+
+
+  DicomVolumeImageMPRSlicer::Slice::Slice(const DicomVolumeImage& volume,
+                                          const CoordinateSystem3D& cuttingPlane) :
+    volume_(volume)
+  {
+    valid_ = (volume_.HasDicomParameters() &&
+              volume_.GetGeometry().DetectSlice(projection_, sliceIndex_, cuttingPlane));
+  }
+
+
+  VolumeProjection DicomVolumeImageMPRSlicer::Slice::GetProjection() const
+  {
+    CheckValid();
+    return projection_;
+  }
+
+
+  unsigned int DicomVolumeImageMPRSlicer::Slice::GetSliceIndex() const
+  {
+    CheckValid();
+    return sliceIndex_;
+  }
+
+  uint64_t DicomVolumeImageMPRSlicer::Slice::GetRevision()
+  {
+    CheckValid();
+    return GetRevisionInternal(projection_, sliceIndex_);
+  }
+
+
+  ISceneLayer* DicomVolumeImageMPRSlicer::Slice::CreateSceneLayer(const ILayerStyleConfigurator* configurator,
+                                                                  const CoordinateSystem3D& cuttingPlane)
+  {
+    CheckValid();
+
+    if (configurator == NULL)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer,
+                                      "A style configurator is mandatory for textures");
+    }
+
+    std::auto_ptr<TextureBaseSceneLayer> texture;
+        
+    {
+      const DicomInstanceParameters& parameters = volume_.GetDicomParameters();
+      ImageBuffer3D::SliceReader reader(volume_.GetPixelData(), projection_, sliceIndex_);
+      texture.reset(dynamic_cast<TextureBaseSceneLayer*>
+                    (configurator->CreateTextureFromDicom(reader.GetAccessor(), parameters)));
+    }
+
+    const CoordinateSystem3D& system = volume_.GetGeometry().GetProjectionGeometry(projection_);
+      
+    double x0, y0, x1, y1;
+    cuttingPlane.ProjectPoint(x0, y0, system.GetOrigin());
+    cuttingPlane.ProjectPoint(x1, y1, system.GetOrigin() + system.GetAxisX());
+    texture->SetOrigin(x0, y0);
+
+    double dx = x1 - x0;
+    double dy = y1 - y0;
+    if (!LinearAlgebra::IsCloseToZero(dx) ||
+        !LinearAlgebra::IsCloseToZero(dy))
+    {
+      texture->SetAngle(atan2(dy, dx));
+    }
+        
+    Vector tmp = volume_.GetGeometry().GetVoxelDimensions(projection_);
+    texture->SetPixelSpacing(tmp[0], tmp[1]);
+
+    return texture.release();
+  }
+
+
+  IVolumeSlicer::IExtractedSlice* 
+  DicomVolumeImageMPRSlicer::ExtractSlice(const CoordinateSystem3D& cuttingPlane)
+  {
+    if (volume_->HasGeometry())
+    {
+      return new Slice(*volume_, cuttingPlane);
+    }
+    else
+    {
+      return new IVolumeSlicer::InvalidSlice;
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Volumes/DicomVolumeImageMPRSlicer.h	Tue May 28 21:16:39 2019 +0200
@@ -0,0 +1,93 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-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 <boost/shared_ptr.hpp>
+
+namespace OrthancStone
+{
+  /**
+     Implements the IVolumeSlicer on Dicom volume data when the cutting plane
+     that is supplied to the slicer is either axial, sagittal or coronal. 
+     Arbitrary planes are *not* supported
+  */
+  class DicomVolumeImageMPRSlicer : public IVolumeSlicer
+  {
+  public:
+    class Slice : public IExtractedSlice
+    {
+    private:
+      const DicomVolumeImage&  volume_;
+      bool                     valid_;
+      VolumeProjection         projection_;
+      unsigned int             sliceIndex_;
+
+      void CheckValid() const;
+
+    protected:
+      // Can be overloaded in subclasses
+      virtual uint64_t GetRevisionInternal(VolumeProjection projection,
+                                           unsigned int sliceIndex) const
+      {
+        return volume_.GetRevision();
+      }
+
+    public:
+      /**
+         Represents a slice of a volume image that is parallel to the 
+         coordinate system axis. 
+         The constructor initializes the type of projection (axial, sagittal or
+         coronal) and the corresponding slice index, from the cutting plane.
+      */
+      Slice(const DicomVolumeImage& volume,
+            const CoordinateSystem3D& cuttingPlane);
+
+      VolumeProjection GetProjection() const;
+
+      unsigned int GetSliceIndex() const;
+
+      virtual bool IsValid()
+      {
+        return valid_;
+      }
+
+      virtual uint64_t GetRevision();
+
+      virtual ISceneLayer* CreateSceneLayer(const ILayerStyleConfigurator* configurator,
+                                            const CoordinateSystem3D& cuttingPlane);
+    };
+
+  private:
+    boost::shared_ptr<DicomVolumeImage>  volume_;
+
+  public:
+    DicomVolumeImageMPRSlicer(const boost::shared_ptr<DicomVolumeImage>& volume) :
+      volume_(volume)
+    {
+    }
+
+    virtual IExtractedSlice* ExtractSlice(const CoordinateSystem3D& cuttingPlane) ORTHANC_OVERRIDE;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Volumes/IVolumeSlicer.cpp	Tue May 28 21:16:39 2019 +0200
@@ -0,0 +1,38 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-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 "IVolumeSlicer.h"
+
+#include <Core/OrthancException.h>
+
+namespace OrthancStone
+{
+  uint64_t IVolumeSlicer::InvalidSlice::GetRevision()
+  {
+    throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+  }
+
+  ISceneLayer* IVolumeSlicer::InvalidSlice::CreateSceneLayer(const ILayerStyleConfigurator* configurator,
+                                                             const CoordinateSystem3D& cuttingPlane)
+  {
+    throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Volumes/IVolumeSlicer.cpp~	Tue May 28 21:16:39 2019 +0200
@@ -0,0 +1,113 @@
+/**
+ * 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
+
+namespace OrthancStone
+{
+  /**
+  This interface is implemented by objects representing 3D volume data and 
+  that are able to return an object that:
+  - represent a slice of their data 
+  - are able to create the corresponding slice visual representation.
+  */
+  class IVolumeSlicer : public boost::noncopyable
+  {
+  public:
+    /**
+    This interface is implemented by objects representing a slice of 
+    volume data and that are able to create a 2D layer to display a this 
+    slice.
+
+    The CreateSceneLayer factory method is called with an optional
+    configurator that possibly impacts the ISceneLayer subclass that is 
+    created (for instance, if a LUT must be applied on the texture when
+    displaying it)
+    */
+    class IExtractedSlice : public boost::noncopyable
+    {
+    public:
+      virtual ~IExtractedSlice()
+      {
+      }
+
+      /**
+      Invalid slices are created when the data is not ready yet or if the
+      cut is outside of the available geometry.
+      */
+      virtual bool IsValid() = 0;
+
+      /**
+      This retrieves the *revision* that gets incremented every time the 
+      underlying object undergoes a mutable operation (that it, changes its 
+      state).
+      This **must** be a cheap call.
+      */
+      virtual uint64_t GetRevision() = 0;
+
+      /** Creates the slice visual representation */
+      virtual ISceneLayer* CreateSceneLayer(
+        const ILayerStyleConfigurator* configurator,  // possibly absent
+        const CoordinateSystem3D& cuttingPlane) = 0;
+    };
+
+    /**
+    See IExtractedSlice.IsValid()
+    */
+    class InvalidSlice : public IExtractedSlice
+    {
+    public:
+      virtual bool IsValid()
+      {
+        return false;
+      }
+
+      virtual uint64_t GetRevision()
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+      }
+
+      virtual ISceneLayer* CreateSceneLayer(const ILayerStyleConfigurator* configurator,
+                                            const CoordinateSystem3D& cuttingPlane)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+      }
+    };
+
+
+    virtual ~IVolumeSlicer()
+    {
+    }
+
+    /**
+    This method is implemented by the objects representing volumetric data
+    and must returns an IExtractedSlice subclass that contains all the data
+    needed to, later on, create its visual representation through
+    CreateSceneLayer.
+    Subclasses a.o.: 
+    - InvalidSlice, 
+    - DicomVolumeImageMPRSlicer::Slice, 
+    - DicomVolumeImageReslicer::Slice
+    - DicomStructureSetLoader::Slice 
+    */
+    virtual IExtractedSlice* ExtractSlice(const CoordinateSystem3D& cuttingPlane) = 0;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Volumes/IVolumeSlicer.h	Tue May 28 21:16:39 2019 +0200
@@ -0,0 +1,110 @@
+/**
+ * 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/ILayerStyleConfigurator.h"
+#include "../Toolbox/CoordinateSystem3D.h"
+
+namespace OrthancStone
+{
+  /**
+  This interface is implemented by objects representing 3D volume data and 
+  that are able to return an object that:
+  - represent a slice of their data 
+  - are able to create the corresponding slice visual representation.
+  */
+  class IVolumeSlicer : public boost::noncopyable
+  {
+  public:
+    /**
+    This interface is implemented by objects representing a slice of 
+    volume data and that are able to create a 2D layer to display a this 
+    slice.
+
+    The CreateSceneLayer factory method is called with an optional
+    configurator that possibly impacts the ISceneLayer subclass that is 
+    created (for instance, if a LUT must be applied on the texture when
+    displaying it)
+    */
+    class IExtractedSlice : public boost::noncopyable
+    {
+    public:
+      virtual ~IExtractedSlice()
+      {
+      }
+
+      /**
+      Invalid slices are created when the data is not ready yet or if the
+      cut is outside of the available geometry.
+      */
+      virtual bool IsValid() = 0;
+
+      /**
+      This retrieves the *revision* that gets incremented every time the 
+      underlying object undergoes a mutable operation (that it, changes its 
+      state).
+      This **must** be a cheap call.
+      */
+      virtual uint64_t GetRevision() = 0;
+
+      /** Creates the slice visual representation */
+      virtual ISceneLayer* CreateSceneLayer(
+        const ILayerStyleConfigurator* configurator,  // possibly absent
+        const CoordinateSystem3D& cuttingPlane) = 0;
+    };
+
+    /**
+    See IExtractedSlice.IsValid()
+    */
+    class InvalidSlice : public IExtractedSlice
+    {
+    public:
+      virtual bool IsValid()
+      {
+        return false;
+      }
+
+      virtual uint64_t GetRevision();
+
+      virtual ISceneLayer* CreateSceneLayer(const ILayerStyleConfigurator* configurator,
+                                            const CoordinateSystem3D& cuttingPlane);
+    };
+
+
+    virtual ~IVolumeSlicer()
+    {
+    }
+
+    /**
+    This method is implemented by the objects representing volumetric data
+    and must returns an IExtractedSlice subclass that contains all the data
+    needed to, later on, create its visual representation through
+    CreateSceneLayer.
+    Subclasses a.o.: 
+    - InvalidSlice, 
+    - DicomVolumeImageMPRSlicer::Slice, 
+    - DicomVolumeImageReslicer::Slice
+    - DicomStructureSetLoader::Slice 
+    */
+    virtual IExtractedSlice* ExtractSlice(const CoordinateSystem3D& cuttingPlane) = 0;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Volumes/IVolumeSlicer.h~	Tue May 28 21:16:39 2019 +0200
@@ -0,0 +1,113 @@
+/**
+ * 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
+
+namespace OrthancStone
+{
+  /**
+  This interface is implemented by objects representing 3D volume data and 
+  that are able to return an object that:
+  - represent a slice of their data 
+  - are able to create the corresponding slice visual representation.
+  */
+  class IVolumeSlicer : public boost::noncopyable
+  {
+  public:
+    /**
+    This interface is implemented by objects representing a slice of 
+    volume data and that are able to create a 2D layer to display a this 
+    slice.
+
+    The CreateSceneLayer factory method is called with an optional
+    configurator that possibly impacts the ISceneLayer subclass that is 
+    created (for instance, if a LUT must be applied on the texture when
+    displaying it)
+    */
+    class IExtractedSlice : public boost::noncopyable
+    {
+    public:
+      virtual ~IExtractedSlice()
+      {
+      }
+
+      /**
+      Invalid slices are created when the data is not ready yet or if the
+      cut is outside of the available geometry.
+      */
+      virtual bool IsValid() = 0;
+
+      /**
+      This retrieves the *revision* that gets incremented every time the 
+      underlying object undergoes a mutable operation (that it, changes its 
+      state).
+      This **must** be a cheap call.
+      */
+      virtual uint64_t GetRevision() = 0;
+
+      /** Creates the slice visual representation */
+      virtual ISceneLayer* CreateSceneLayer(
+        const ILayerStyleConfigurator* configurator,  // possibly absent
+        const CoordinateSystem3D& cuttingPlane) = 0;
+    };
+
+    /**
+    See IExtractedSlice.IsValid()
+    */
+    class InvalidSlice : public IExtractedSlice
+    {
+    public:
+      virtual bool IsValid()
+      {
+        return false;
+      }
+
+      virtual uint64_t GetRevision()
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+      }
+
+      virtual ISceneLayer* CreateSceneLayer(const ILayerStyleConfigurator* configurator,
+                                            const CoordinateSystem3D& cuttingPlane)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+      }
+    };
+
+
+    virtual ~IVolumeSlicer()
+    {
+    }
+
+    /**
+    This method is implemented by the objects representing volumetric data
+    and must returns an IExtractedSlice subclass that contains all the data
+    needed to, later on, create its visual representation through
+    CreateSceneLayer.
+    Subclasses a.o.: 
+    - InvalidSlice, 
+    - DicomVolumeImageMPRSlicer::Slice, 
+    - DicomVolumeImageReslicer::Slice
+    - DicomStructureSetLoader::Slice 
+    */
+    virtual IExtractedSlice* ExtractSlice(const CoordinateSystem3D& cuttingPlane) = 0;
+  };
+}
--- a/Resources/CMake/OrthancStoneConfiguration.cmake	Tue May 28 18:31:44 2019 +0200
+++ b/Resources/CMake/OrthancStoneConfiguration.cmake	Tue May 28 21:16:39 2019 +0200
@@ -382,9 +382,46 @@
   ${ORTHANC_ROOT}/Plugins/Samples/Common/FullOrthancDataset.cpp
   ${ORTHANC_ROOT}/Plugins/Samples/Common/IOrthancConnection.cpp
 
+  ${ORTHANC_STONE_ROOT}/Framework/Fonts/FontRenderer.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Fonts/Glyph.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Fonts/GlyphAlphabet.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Fonts/GlyphBitmapAlphabet.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Fonts/GlyphTextureAlphabet.cpp
+  ${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/OrthancSeriesVolumeProgressiveLoader.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Messages/ICallable.h
+  ${ORTHANC_STONE_ROOT}/Framework/Messages/IMessage.h
+  ${ORTHANC_STONE_ROOT}/Framework/Messages/IObservable.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Messages/IObserver.h
+  ${ORTHANC_STONE_ROOT}/Framework/Messages/MessageBroker.h
+  ${ORTHANC_STONE_ROOT}/Framework/Messages/MessageForwarder.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Messages/Promise.h
+  ${ORTHANC_STONE_ROOT}/Framework/Oracle/GetOrthancImageCommand.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Oracle/GetOrthancWebViewerJpegCommand.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Oracle/OracleCommandWithPayload.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Oracle/OrthancRestApiCommand.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographyAlphaLayer.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographyDicomLayer.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographyLayer.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographyLayerCropTracker.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographyLayerMaskTracker.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographyLayerMoveTracker.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographyLayerResizeTracker.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographyLayerRotateTracker.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographyMaskLayer.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographyScene.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographySceneCommand.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographySceneReader.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographySceneWriter.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographyTextLayer.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographyWidget.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographyWindowingTracker.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Scene2D/CairoCompositor.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Scene2D/ColorTextureSceneLayer.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Scene2D/FloatTextureSceneLayer.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Scene2D/GrayscaleStyleConfigurator.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Scene2D/InfoPanelSceneLayer.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/CairoColorTextureRenderer.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/CairoFloatTextureRenderer.cpp
@@ -394,6 +431,7 @@
   ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/CairoTextRenderer.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/CompositorHelper.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/FixedPointAligner.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Scene2D/LookupTableStyleConfigurator.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Scene2D/LookupTableTextureSceneLayer.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Scene2D/PanSceneTracker.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Scene2D/PointerEvent.cpp
@@ -438,41 +476,6 @@
   ${ORTHANC_STONE_ROOT}/Framework/Scene2DViewport/PointerTypes.h
   ${ORTHANC_STONE_ROOT}/Framework/Scene2DViewport/ViewportController.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Scene2DViewport/ViewportController.h
-  ${ORTHANC_STONE_ROOT}/Framework/Fonts/FontRenderer.cpp
-  ${ORTHANC_STONE_ROOT}/Framework/Fonts/Glyph.cpp
-  ${ORTHANC_STONE_ROOT}/Framework/Fonts/GlyphAlphabet.cpp
-  ${ORTHANC_STONE_ROOT}/Framework/Fonts/GlyphBitmapAlphabet.cpp
-  ${ORTHANC_STONE_ROOT}/Framework/Fonts/GlyphTextureAlphabet.cpp
-  ${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/Messages/ICallable.h
-  ${ORTHANC_STONE_ROOT}/Framework/Messages/IMessage.h
-  ${ORTHANC_STONE_ROOT}/Framework/Messages/IObservable.cpp
-  ${ORTHANC_STONE_ROOT}/Framework/Messages/IObserver.h
-  ${ORTHANC_STONE_ROOT}/Framework/Messages/MessageBroker.h
-  ${ORTHANC_STONE_ROOT}/Framework/Messages/MessageForwarder.cpp
-  ${ORTHANC_STONE_ROOT}/Framework/Messages/Promise.h
-  ${ORTHANC_STONE_ROOT}/Framework/Oracle/GetOrthancImageCommand.cpp
-  ${ORTHANC_STONE_ROOT}/Framework/Oracle/GetOrthancWebViewerJpegCommand.cpp
-  ${ORTHANC_STONE_ROOT}/Framework/Oracle/OracleCommandWithPayload.cpp
-  ${ORTHANC_STONE_ROOT}/Framework/Oracle/OrthancRestApiCommand.cpp
-  ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographyAlphaLayer.cpp
-  ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographyDicomLayer.cpp
-  ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographyLayer.cpp
-  ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographyLayerCropTracker.cpp
-  ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographyLayerMaskTracker.cpp
-  ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographyLayerMoveTracker.cpp
-  ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographyLayerResizeTracker.cpp
-  ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographyLayerRotateTracker.cpp
-  ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographyMaskLayer.cpp
-  ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographyScene.cpp
-  ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographySceneCommand.cpp
-  ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographySceneReader.cpp
-  ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographySceneWriter.cpp
-  ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographyTextLayer.cpp
-  ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographyWidget.cpp
-  ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographyWindowingTracker.cpp
   ${ORTHANC_STONE_ROOT}/Framework/StoneEnumerations.cpp
   ${ORTHANC_STONE_ROOT}/Framework/StoneException.h
   ${ORTHANC_STONE_ROOT}/Framework/StoneInitialization.cpp
@@ -489,12 +492,15 @@
   ${ORTHANC_STONE_ROOT}/Framework/Toolbox/ShearWarpProjectiveTransform.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Toolbox/SlicesSorter.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Toolbox/UndoRedoStack.cpp
-  ${ORTHANC_STONE_ROOT}/Framework/Wrappers/CairoContext.cpp
-  ${ORTHANC_STONE_ROOT}/Framework/Wrappers/CairoSurface.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Volumes/DicomVolumeImage.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Volumes/DicomVolumeImageMPRSlicer.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/Wrappers/CairoContext.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Wrappers/CairoSurface.cpp
 
   ${PLATFORM_SOURCES}
   ${APPLICATIONS_SOURCES}
--- a/Samples/Sdl/Loader.cpp	Tue May 28 18:31:44 2019 +0200
+++ b/Samples/Sdl/Loader.cpp	Tue May 28 21:16:39 2019 +0200
@@ -19,7 +19,10 @@
  **/
 
 
-#include "../../Framework/Toolbox/DicomInstanceParameters.h"
+#include "../../Framework/Loaders/OrthancSeriesVolumeProgressiveLoader.h"
+#include "../../Framework/Volumes/DicomVolumeImageMPRSlicer.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"
@@ -60,995 +63,6 @@
 namespace OrthancStone
 {
   /**
-  This interface is implemented by objects able to create an ISceneLayer 
-  suitable to display the Orthanc image supplied to the CreateTextureXX 
-  factory methods (taking Dicom parameters into account if relevant).
-
-  It can also refresh the style of an existing layer afterwards, to match
-  the configurator settings.
-  */
-  class ILayerStyleConfigurator
-  {
-  public:
-    virtual ~ILayerStyleConfigurator()
-    {
-    }
-    
-    virtual uint64_t GetRevision() const = 0;
-    
-    virtual TextureBaseSceneLayer* CreateTextureFromImage(const Orthanc::ImageAccessor& image) const = 0;
-
-    virtual TextureBaseSceneLayer* CreateTextureFromDicom(const Orthanc::ImageAccessor& frame,
-                                                          const DicomInstanceParameters& parameters) const = 0;
-
-    virtual void ApplyStyle(ISceneLayer& layer) const = 0;
-  };
-
-  /**
-  This configurator supplies an API to set a display range and a LUT.
-  */
-  class LookupTableStyleConfigurator : public ILayerStyleConfigurator
-  {
-  private:
-    uint64_t     revision_;
-    bool         hasLut_;
-    std::string  lut_;
-    bool         hasRange_;
-    float        minValue_;
-    float        maxValue_;
-    
-  public:
-    LookupTableStyleConfigurator() :
-      revision_(0),
-      hasLut_(false),
-      hasRange_(false)
-    {
-    }
-
-    void SetLookupTable(Orthanc::EmbeddedResources::FileResourceId resource)
-    {
-      hasLut_ = true;
-      Orthanc::EmbeddedResources::GetFileResource(lut_, resource);
-    }
-
-    void SetLookupTable(const std::string& lut)
-    {
-      hasLut_ = true;
-      lut_ = lut;
-    }
-
-    void SetRange(float minValue,
-                  float maxValue)
-    {
-      if (minValue > maxValue)
-      {
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
-      }
-      else
-      {
-        hasRange_ = true;
-        minValue_ = minValue;
-        maxValue_ = maxValue;
-      }
-    }
-
-    virtual uint64_t GetRevision() const
-    {
-      return revision_;
-    }
-    
-    virtual TextureBaseSceneLayer* CreateTextureFromImage(const Orthanc::ImageAccessor& image) const
-    {
-      throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
-    }
-
-    virtual TextureBaseSceneLayer* CreateTextureFromDicom(const Orthanc::ImageAccessor& frame,
-                                                          const DicomInstanceParameters& parameters) const
-    {
-      return parameters.CreateLookupTableTexture(frame);
-    }
-
-    virtual void ApplyStyle(ISceneLayer& layer) const
-    {
-      LookupTableTextureSceneLayer& l = dynamic_cast<LookupTableTextureSceneLayer&>(layer);
-      
-      if (hasLut_)
-      {
-        l.SetLookupTable(lut_);
-      }
-
-      if (hasRange_)
-      {
-        l.SetRange(minValue_, maxValue_);
-      }
-      else
-      {
-        l.FitRange();
-      }
-    }
-  };
-
-  /**
-  Creates layers to display the supplied image in grayscale. No dynamic 
-  style is available.
-  */
-  class GrayscaleStyleConfigurator : public ILayerStyleConfigurator
-  {
-  private:
-    uint64_t revision_;
-    
-  public:
-    GrayscaleStyleConfigurator() :
-      revision_(0)
-    {
-    }
-
-    virtual uint64_t GetRevision() const
-    {
-      return revision_;
-    }
-    
-    virtual TextureBaseSceneLayer* CreateTextureFromImage(const Orthanc::ImageAccessor& image) const
-    {
-      throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
-    }
-
-    virtual TextureBaseSceneLayer* CreateTextureFromDicom(const Orthanc::ImageAccessor& frame,
-                                                          const DicomInstanceParameters& parameters) const
-    {
-      return parameters.CreateTexture(frame);
-    }
-
-    virtual void ApplyStyle(ISceneLayer& layer) const
-    {
-    }
-  };
-
-  /**
-  This interface is implemented by objects representing 3D volume data and 
-  that are able to return an object that:
-  - represent a slice of their data 
-  - are able to create the corresponding slice visual representation.
-  */
-  class IVolumeSlicer : public boost::noncopyable
-  {
-  public:
-    /**
-    This interface is implemented by objects representing a slice of 
-    volume data and that are able to create a 2D layer to display a this 
-    slice.
-
-    The CreateSceneLayer factory method is called with an optional
-    configurator that possibly impacts the ISceneLayer subclass that is 
-    created (for instance, if a LUT must be applied on the texture when
-    displaying it)
-    */
-    class IExtractedSlice : public boost::noncopyable
-    {
-    public:
-      virtual ~IExtractedSlice()
-      {
-      }
-
-      /**
-      Invalid slices are created when the data is not ready yet or if the
-      cut is outside of the available geometry.
-      */
-      virtual bool IsValid() = 0;
-
-      /**
-      This retrieves the *revision* that gets incremented every time the 
-      underlying object undergoes a mutable operation (that it, changes its 
-      state).
-      This **must** be a cheap call.
-      */
-      virtual uint64_t GetRevision() = 0;
-
-      /** Creates the slice visual representation */
-      virtual ISceneLayer* CreateSceneLayer(
-        const ILayerStyleConfigurator* configurator,  // possibly absent
-        const CoordinateSystem3D& cuttingPlane) = 0;
-    };
-
-    /**
-    See IExtractedSlice.IsValid()
-    */
-    class InvalidSlice : public IExtractedSlice
-    {
-    public:
-      virtual bool IsValid()
-      {
-        return false;
-      }
-
-      virtual uint64_t GetRevision()
-      {
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
-      }
-
-      virtual ISceneLayer* CreateSceneLayer(const ILayerStyleConfigurator* configurator,
-                                            const CoordinateSystem3D& cuttingPlane)
-      {
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
-      }
-    };
-
-
-    virtual ~IVolumeSlicer()
-    {
-    }
-
-    /**
-    This method is implemented by the objects representing volumetric data
-    and must returns an IExtractedSlice subclass that contains all the data
-    needed to, later on, create its visual representation through
-    CreateSceneLayer.
-    Subclasses a.o.: 
-    - InvalidSlice, 
-    - DicomVolumeImageMPRSlicer::Slice, 
-    - DicomVolumeImageReslicer::Slice
-    - DicomStructureSetLoader::Slice 
-    */
-    virtual IExtractedSlice* ExtractSlice(const CoordinateSystem3D& cuttingPlane) = 0;
-  };
-
-  /**
-  This class combines a 3D image buffer, a 3D volume geometry and
-  information about the DICOM parameters of the series.
-  (MPR means MultiPlanar Reconstruction)
-  */ 
-  class DicomVolumeImage : public boost::noncopyable
-  {
-  public:
-    ORTHANC_STONE_DEFINE_ORIGIN_MESSAGE(__FILE__, __LINE__, GeometryReadyMessage, DicomVolumeImage);
-    ORTHANC_STONE_DEFINE_ORIGIN_MESSAGE(__FILE__, __LINE__, ContentUpdatedMessage, DicomVolumeImage);
-
-  private:
-    uint64_t                                revision_;
-    std::auto_ptr<VolumeImageGeometry>      geometry_;
-    std::auto_ptr<ImageBuffer3D>            image_;
-    std::auto_ptr<DicomInstanceParameters>  parameters_;
-
-    void CheckHasGeometry() const
-    {
-      if (!HasGeometry())
-      {
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
-      }
-    }
-    
-  public:
-    DicomVolumeImage() :
-      revision_(0)
-    {
-    }
-
-    void IncrementRevision()
-    {
-      revision_ ++;
-    }
-
-    void Initialize(const VolumeImageGeometry& geometry,
-                    Orthanc::PixelFormat format)
-    {
-      geometry_.reset(new VolumeImageGeometry(geometry));
-      image_.reset(new ImageBuffer3D(format, geometry_->GetWidth(), geometry_->GetHeight(),
-                                     geometry_->GetDepth(), false /* don't compute range */));
-
-      revision_ ++;
-    }
-
-    void SetDicomParameters(const DicomInstanceParameters& parameters)
-    {
-      parameters_.reset(parameters.Clone());
-      revision_ ++;
-    }
-    
-    uint64_t GetRevision() const
-    {
-      return revision_;
-    }
-
-    bool HasGeometry() const
-    {
-      return (geometry_.get() != NULL &&
-              image_.get() != NULL);
-    }
-
-    ImageBuffer3D& GetPixelData()
-    {
-      CheckHasGeometry();
-      return *image_;
-    }
-
-    const ImageBuffer3D& GetPixelData() const
-    {
-      CheckHasGeometry();
-      return *image_;
-    }
-
-    const VolumeImageGeometry& GetGeometry() const
-    {
-      CheckHasGeometry();
-      return *geometry_;
-    }
-
-    bool HasDicomParameters() const
-    {
-      return parameters_.get() != NULL;
-    }      
-
-    const DicomInstanceParameters& GetDicomParameters() const
-    {
-      if (HasDicomParameters())
-      {
-        return *parameters_;
-      }
-      else
-      {
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
-      }      
-    }
-  };
-
-  /**
-  Implements the IVolumeSlicer on Dicom volume data when the cutting plane
-  that is supplied to the slicer is either axial, sagittal or coronal. 
-  Arbitrary planes are *not* supported
-  */
-  class DicomVolumeImageMPRSlicer : public IVolumeSlicer
-  {
-  public:
-    class Slice : public IExtractedSlice
-    {
-    private:
-      const DicomVolumeImage&  volume_;
-      bool                 valid_;
-      VolumeProjection     projection_;
-      unsigned int         sliceIndex_;
-
-      void CheckValid() const
-      {
-        if (!valid_)
-        {
-          throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
-        }
-      }
-
-    protected:
-      // Can be overloaded in subclasses
-      virtual uint64_t GetRevisionInternal(VolumeProjection projection,
-                                           unsigned int sliceIndex) const
-      {
-        return volume_.GetRevision();
-      }
-
-    public:
-      /**
-      Represents a slice of a volume image that is parallel to the 
-      coordinate system axis. 
-      The constructor initializes the type of projection (axial, sagittal or
-      coronal) and the corresponding slice index, from the cutting plane.
-      */
-      Slice(const DicomVolumeImage& volume,
-            const CoordinateSystem3D& cuttingPlane) :
-        volume_(volume)
-      {
-        valid_ = (volume_.HasDicomParameters() &&
-                  volume_.GetGeometry().DetectSlice(projection_, sliceIndex_, cuttingPlane));
-      }
-
-      VolumeProjection GetProjection() const
-      {
-        CheckValid();
-        return projection_;
-      }
-
-      unsigned int GetSliceIndex() const
-      {
-        CheckValid();
-        return sliceIndex_;
-      }
-
-      virtual bool IsValid()
-      {
-        return valid_;
-      }
-
-      virtual uint64_t GetRevision()
-      {
-        CheckValid();
-        return GetRevisionInternal(projection_, sliceIndex_);
-      }
-
-      virtual ISceneLayer* CreateSceneLayer(const ILayerStyleConfigurator* configurator,
-                                            const CoordinateSystem3D& cuttingPlane)
-      {
-        CheckValid();
-
-        if (configurator == NULL)
-        {
-          throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer,
-                                          "A style configurator is mandatory for textures");
-        }
-
-        std::auto_ptr<TextureBaseSceneLayer> texture;
-        
-        {
-          const DicomInstanceParameters& parameters = volume_.GetDicomParameters();
-          ImageBuffer3D::SliceReader reader(volume_.GetPixelData(), projection_, sliceIndex_);
-          texture.reset(dynamic_cast<TextureBaseSceneLayer*>
-                        (configurator->CreateTextureFromDicom(reader.GetAccessor(), parameters)));
-        }
-
-        const CoordinateSystem3D& system = volume_.GetGeometry().GetProjectionGeometry(projection_);
-      
-        double x0, y0, x1, y1;
-        cuttingPlane.ProjectPoint(x0, y0, system.GetOrigin());
-        cuttingPlane.ProjectPoint(x1, y1, system.GetOrigin() + system.GetAxisX());
-        texture->SetOrigin(x0, y0);
-
-        double dx = x1 - x0;
-        double dy = y1 - y0;
-        if (!LinearAlgebra::IsCloseToZero(dx) ||
-            !LinearAlgebra::IsCloseToZero(dy))
-        {
-          texture->SetAngle(atan2(dy, dx));
-        }
-        
-        Vector tmp = volume_.GetGeometry().GetVoxelDimensions(projection_);
-        texture->SetPixelSpacing(tmp[0], tmp[1]);
-
-        return texture.release();
-
-#if 0
-        double w = texture->GetTexture().GetWidth() * tmp[0];
-        double h = texture->GetTexture().GetHeight() * tmp[1];
-        printf("%.1f %.1f %.1f => %.1f %.1f => %.1f %.1f\n",
-               system.GetOrigin() [0],
-               system.GetOrigin() [1],
-               system.GetOrigin() [2],
-               x0, y0, x0 + w, y0 + h);
-
-        std::auto_ptr<PolylineSceneLayer> toto(new PolylineSceneLayer);
-
-        PolylineSceneLayer::Chain c;
-        c.push_back(ScenePoint2D(x0, y0));
-        c.push_back(ScenePoint2D(x0 + w, y0));
-        c.push_back(ScenePoint2D(x0 + w, y0 + h));
-        c.push_back(ScenePoint2D(x0, y0 + h));
-      
-        toto->AddChain(c, true);
-
-        return toto.release();
-#endif
-      }
-    };
-
-  private:
-    boost::shared_ptr<DicomVolumeImage>  volume_;
-
-  public:
-    DicomVolumeImageMPRSlicer(const boost::shared_ptr<DicomVolumeImage>& volume) :
-      volume_(volume)
-    {
-    }
-
-    virtual IExtractedSlice* ExtractSlice(const CoordinateSystem3D& cuttingPlane) ORTHANC_OVERRIDE
-    {
-      if (volume_->HasGeometry())
-      {
-        return new Slice(*volume_, cuttingPlane);
-      }
-      else
-      {
-        return new IVolumeSlicer::InvalidSlice;
-      }
-    }
-  };
-
-  
-
-
-  /**
-    This class is used to manage the progressive loading of a volume that
-    is stored in a Dicom series.
-  
-  // TODO - Refactor using LoaderStateMachine?
-  // TODO:
-  */
-  class OrthancSeriesVolumeProgressiveLoader : 
-    public IObserver,
-    public IObservable,
-    public IVolumeSlicer
-  {
-  private:
-    static const unsigned int LOW_QUALITY = 0;
-    static const unsigned int MIDDLE_QUALITY = 1;
-    static const unsigned int BEST_QUALITY = 2;
-    
-    /** Helper class internal to OrthancSeriesVolumeProgressiveLoader */
-    class SeriesGeometry : public boost::noncopyable
-    {
-    private:
-      void CheckSlice(size_t index,
-                      const DicomInstanceParameters& reference) const
-      {
-        const DicomInstanceParameters& slice = *slices_[index];
-      
-        if (!GeometryToolbox::IsParallel(
-              reference.GetGeometry().GetNormal(),
-              slice.GetGeometry().GetNormal()))
-        {
-          throw Orthanc::OrthancException(Orthanc::ErrorCode_BadGeometry,
-                                          "A slice in the volume image is not parallel to the others");
-        }
-
-        if (reference.GetExpectedPixelFormat() != slice.GetExpectedPixelFormat())
-        {
-          throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageFormat,
-                                          "The pixel format changes across the slices of the volume image");
-        }
-
-        if (reference.GetImageInformation().GetWidth() != slice.GetImageInformation().GetWidth() ||
-            reference.GetImageInformation().GetHeight() != slice.GetImageInformation().GetHeight())
-        {
-          throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageSize,
-                                          "The width/height of slices are not constant in the volume image");
-        }
-
-        if (!LinearAlgebra::IsNear(reference.GetPixelSpacingX(), slice.GetPixelSpacingX()) ||
-            !LinearAlgebra::IsNear(reference.GetPixelSpacingY(), slice.GetPixelSpacingY()))
-        {
-          throw Orthanc::OrthancException(Orthanc::ErrorCode_BadGeometry,
-                                          "The pixel spacing of the slices change across the volume image");
-        }
-      }
-
-    
-      void CheckVolume() const
-      {
-        for (size_t i = 0; i < slices_.size(); i++)
-        {
-          assert(slices_[i] != NULL);
-          if (slices_[i]->GetImageInformation().GetNumberOfFrames() != 1)
-          {
-            throw Orthanc::OrthancException(Orthanc::ErrorCode_BadGeometry,
-                                            "This class does not support multi-frame images");
-          }
-        }
-
-        if (slices_.size() != 0)
-        {
-          const DicomInstanceParameters& reference = *slices_[0];
-
-          for (size_t i = 1; i < slices_.size(); i++)
-          {
-            CheckSlice(i, reference);
-          }
-        }
-      }
-
-
-      void Clear()
-      {
-        for (size_t i = 0; i < slices_.size(); i++)
-        {
-          assert(slices_[i] != NULL);
-          delete slices_[i];
-        }
-
-        slices_.clear();
-        slicesRevision_.clear();
-      }
-
-
-      void CheckSliceIndex(size_t index) const
-      {
-        if (!HasGeometry())
-        {
-          throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
-        }
-        else if (index >= slices_.size())
-        {
-          throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
-        }
-        else
-        {
-          assert(slices_.size() == GetImageGeometry().GetDepth() &&
-                 slices_.size() == slicesRevision_.size());
-        }
-      }
-
-    
-      std::auto_ptr<VolumeImageGeometry>     geometry_;
-      std::vector<DicomInstanceParameters*>  slices_;
-      std::vector<uint64_t>                  slicesRevision_;
-
-    public:
-      ~SeriesGeometry()
-      {
-        Clear();
-      }
-
-      // WARNING: The payload of "slices" must be of class "DicomInstanceParameters"
-      // (called with the slices created in LoadGeometry)
-      void ComputeGeometry(SlicesSorter& slices)
-      {
-        Clear();
-      
-        if (!slices.Sort())
-        {
-          throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange,
-                                          "Cannot sort the 3D slices of a DICOM series");          
-        }
-
-        if (slices.GetSlicesCount() == 0)
-        {
-          geometry_.reset(new VolumeImageGeometry);
-        }
-        else
-        {
-          slices_.reserve(slices.GetSlicesCount());
-          slicesRevision_.resize(slices.GetSlicesCount(), 0);
-
-          for (size_t i = 0; i < slices.GetSlicesCount(); i++)
-          {
-            const DicomInstanceParameters& slice =
-              dynamic_cast<const DicomInstanceParameters&>(slices.GetSlicePayload(i));
-            slices_.push_back(new DicomInstanceParameters(slice));
-          }
-
-          CheckVolume();
-
-          const double spacingZ = slices.ComputeSpacingBetweenSlices();
-          LOG(INFO) << "Computed spacing between slices: " << spacingZ << "mm";
-      
-          const DicomInstanceParameters& parameters = *slices_[0];
-
-          geometry_.reset(new VolumeImageGeometry);
-          geometry_->SetSize(parameters.GetImageInformation().GetWidth(),
-                             parameters.GetImageInformation().GetHeight(),
-                             static_cast<unsigned int>(slices.GetSlicesCount()));
-          geometry_->SetAxialGeometry(slices.GetSliceGeometry(0));
-          geometry_->SetVoxelDimensions(parameters.GetPixelSpacingX(),
-                                        parameters.GetPixelSpacingY(), spacingZ);
-        }
-      }
-
-      bool HasGeometry() const
-      {
-        return geometry_.get() != NULL;
-      }
-
-      const VolumeImageGeometry& GetImageGeometry() const
-      {
-        if (!HasGeometry())
-        {
-          throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
-        }
-        else
-        {
-          assert(slices_.size() == geometry_->GetDepth());
-          return *geometry_;
-        }
-      }
-
-      const DicomInstanceParameters& GetSliceParameters(size_t index) const
-      {
-        CheckSliceIndex(index);
-        return *slices_[index];
-      }
-
-      uint64_t GetSliceRevision(size_t index) const
-      {
-        CheckSliceIndex(index);
-        return slicesRevision_[index];
-      }
-
-      void IncrementSliceRevision(size_t index)
-      {
-        CheckSliceIndex(index);
-        slicesRevision_[index] ++;
-      }
-    };
-
-
-    class ExtractedSlice : public DicomVolumeImageMPRSlicer::Slice
-    {
-    private:
-      const OrthancSeriesVolumeProgressiveLoader&  that_;
-
-    protected:
-      virtual uint64_t GetRevisionInternal(VolumeProjection projection,
-                                           unsigned int sliceIndex) const
-      {
-        if (projection == VolumeProjection_Axial)
-        {
-          return that_.seriesGeometry_.GetSliceRevision(sliceIndex);
-        }
-        else
-        {
-          // For coronal and sagittal projections, we take the global
-          // revision of the volume because even if a single slice changes,
-          // this means the projection will yield a different result --> 
-          // we must increase the revision as soon as any slice changes 
-          return that_.volume_->GetRevision();
-        }
-      }
-
-    public:
-      ExtractedSlice(const OrthancSeriesVolumeProgressiveLoader& that,
-                     const CoordinateSystem3D& plane) :
-        DicomVolumeImageMPRSlicer::Slice(*that.volume_, plane),
-        that_(that)
-      {
-        if (that_.strategy_.get() != NULL &&
-            IsValid() &&
-            GetProjection() == VolumeProjection_Axial)
-        {
-          that_.strategy_->SetCurrent(GetSliceIndex());
-        }
-      }
-    };
-
-    
-    
-    static unsigned int GetSliceIndexPayload(const OracleCommandWithPayload& command)
-    {
-      return dynamic_cast< const Orthanc::SingleValueObject<unsigned int>& >(command.GetPayload()).GetValue();
-    }
-
-
-    void ScheduleNextSliceDownload()
-    {
-      assert(strategy_.get() != NULL);
-      
-      unsigned int sliceIndex, quality;
-      
-      if (strategy_->GetNext(sliceIndex, quality))
-      {
-        assert(quality <= BEST_QUALITY);
-
-        const DicomInstanceParameters& slice = seriesGeometry_.GetSliceParameters(sliceIndex);
-          
-        const std::string& instance = slice.GetOrthancInstanceIdentifier();
-        if (instance.empty())
-        {
-          throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
-        }
-
-        std::auto_ptr<OracleCommandWithPayload> command;
-        
-        if (quality == BEST_QUALITY)
-        {
-          std::auto_ptr<GetOrthancImageCommand> tmp(new GetOrthancImageCommand);
-          tmp->SetHttpHeader("Accept-Encoding", "gzip");
-          tmp->SetHttpHeader("Accept", std::string(Orthanc::EnumerationToString(Orthanc::MimeType_Pam)));
-          tmp->SetInstanceUri(instance, slice.GetExpectedPixelFormat());          
-          tmp->SetExpectedPixelFormat(slice.GetExpectedPixelFormat());
-          command.reset(tmp.release());
-        }
-        else
-        {
-          std::auto_ptr<GetOrthancWebViewerJpegCommand> tmp(new GetOrthancWebViewerJpegCommand);
-          tmp->SetHttpHeader("Accept-Encoding", "gzip");
-          tmp->SetInstance(instance);
-          tmp->SetQuality((quality == 0 ? 50 : 90));
-          tmp->SetExpectedPixelFormat(slice.GetExpectedPixelFormat());
-          command.reset(tmp.release());
-        }
-
-        command->SetPayload(new Orthanc::SingleValueObject<unsigned int>(sliceIndex));
-        oracle_.Schedule(*this, command.release());
-      }
-    }
-
-    /**
-    This is called in response to GET "/series/XXXXXXXXXXXXX/instances-tags"
-    */
-    void LoadGeometry(const OrthancRestApiCommand::SuccessMessage& message)
-    {
-      Json::Value body;
-      message.ParseJsonBody(body);
-      
-      if (body.type() != Json::objectValue)
-      {
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol);
-      }
-
-      {
-        Json::Value::Members instances = body.getMemberNames();
-
-        SlicesSorter slices;
-        
-        for (size_t i = 0; i < instances.size(); i++)
-        {
-          Orthanc::DicomMap dicom;
-          dicom.FromDicomAsJson(body[instances[i]]);
-
-          std::auto_ptr<DicomInstanceParameters> instance(new DicomInstanceParameters(dicom));
-          instance->SetOrthancInstanceIdentifier(instances[i]);
-
-          // the 3D plane corresponding to the slice
-          CoordinateSystem3D geometry = instance->GetGeometry();
-          slices.AddSlice(geometry, instance.release());
-        }
-
-        seriesGeometry_.ComputeGeometry(slices);
-      }
-
-      size_t slicesCount = seriesGeometry_.GetImageGeometry().GetDepth();
-
-      if (slicesCount == 0)
-      {
-        volume_->Initialize(seriesGeometry_.GetImageGeometry(), Orthanc::PixelFormat_Grayscale8);
-      }
-      else
-      {
-        const DicomInstanceParameters& parameters = seriesGeometry_.GetSliceParameters(0);
-        
-        volume_->Initialize(seriesGeometry_.GetImageGeometry(), parameters.GetExpectedPixelFormat());
-        volume_->SetDicomParameters(parameters);
-        volume_->GetPixelData().Clear();
-
-        strategy_.reset(new BasicFetchingStrategy(sorter_->CreateSorter(static_cast<unsigned int>(slicesCount)), BEST_QUALITY));
-        
-        assert(simultaneousDownloads_ != 0);
-        for (unsigned int i = 0; i < simultaneousDownloads_; i++)
-        {
-          ScheduleNextSliceDownload();
-        }
-      }
-
-      slicesQuality_.resize(slicesCount, 0);
-
-      BroadcastMessage(DicomVolumeImage::GeometryReadyMessage(*volume_));
-    }
-
-
-    void SetSliceContent(unsigned int sliceIndex,
-                         const Orthanc::ImageAccessor& image,
-                         unsigned int quality)
-    {
-      assert(sliceIndex < slicesQuality_.size() &&
-             slicesQuality_.size() == volume_->GetPixelData().GetDepth());
-      
-      if (quality >= slicesQuality_[sliceIndex])
-      {
-        {
-          ImageBuffer3D::SliceWriter writer(volume_->GetPixelData(), VolumeProjection_Axial, sliceIndex);
-          Orthanc::ImageProcessing::Copy(writer.GetAccessor(), image);
-        }
-
-        volume_->IncrementRevision();
-        seriesGeometry_.IncrementSliceRevision(sliceIndex);
-        slicesQuality_[sliceIndex] = quality;
-
-        BroadcastMessage(DicomVolumeImage::ContentUpdatedMessage(*volume_));
-      }
-
-      ScheduleNextSliceDownload();
-    }
-
-
-    void LoadBestQualitySliceContent(const GetOrthancImageCommand::SuccessMessage& message)
-    {
-      SetSliceContent(GetSliceIndexPayload(message.GetOrigin()), message.GetImage(), BEST_QUALITY);
-    }
-
-
-    void LoadJpegSliceContent(const GetOrthancWebViewerJpegCommand::SuccessMessage& message)
-    {
-      unsigned int quality;
-      
-      switch (message.GetOrigin().GetQuality())
-      {
-        case 50:
-          quality = LOW_QUALITY;
-          break;
-
-        case 90:
-          quality = MIDDLE_QUALITY;
-          break;
-
-        default:
-          throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
-      }
-      
-      SetSliceContent(GetSliceIndexPayload(message.GetOrigin()), message.GetImage(), quality);
-    }
-
-
-    IOracle&        oracle_;
-    bool            active_;
-    unsigned int    simultaneousDownloads_;
-    SeriesGeometry  seriesGeometry_;
-    
-    boost::shared_ptr<DicomVolumeImage>      volume_;
-    std::auto_ptr<IFetchingItemsSorter::IFactory>  sorter_;
-    std::auto_ptr<IFetchingStrategy>               strategy_;
-    std::vector<unsigned int>   slicesQuality_;
-
-
-  public:
-    OrthancSeriesVolumeProgressiveLoader(const boost::shared_ptr<DicomVolumeImage>& volume,
-                                         IOracle& oracle,
-                                         IObservable& oracleObservable) :
-      IObserver(oracleObservable.GetBroker()),
-      IObservable(oracleObservable.GetBroker()),
-      oracle_(oracle),
-      active_(false),
-      simultaneousDownloads_(4),
-      volume_(volume),
-      sorter_(new BasicFetchingItemsSorter::Factory)
-    {
-      oracleObservable.RegisterObserverCallback(
-        new Callable<OrthancSeriesVolumeProgressiveLoader, OrthancRestApiCommand::SuccessMessage>
-        (*this, &OrthancSeriesVolumeProgressiveLoader::LoadGeometry));
-
-      oracleObservable.RegisterObserverCallback(
-        new Callable<OrthancSeriesVolumeProgressiveLoader, GetOrthancImageCommand::SuccessMessage>
-        (*this, &OrthancSeriesVolumeProgressiveLoader::LoadBestQualitySliceContent));
-
-      oracleObservable.RegisterObserverCallback(
-        new Callable<OrthancSeriesVolumeProgressiveLoader, GetOrthancWebViewerJpegCommand::SuccessMessage>
-        (*this, &OrthancSeriesVolumeProgressiveLoader::LoadJpegSliceContent));
-    }
-
-    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;
-      }
-    }
-
-    void LoadSeries(const std::string& seriesId)
-    {
-      if (active_)
-      {
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
-      }
-      else
-      {
-        active_ = true;
-
-        std::auto_ptr<OrthancRestApiCommand> command(new OrthancRestApiCommand);
-        command->SetUri("/series/" + seriesId + "/instances-tags");
-
-        oracle_.Schedule(*this, command.release());
-      }
-    }
-
-    /**
-    When a slice is requested, the strategy algorithm (that defines the 
-    sequence of resources to be loaded from the server) is modified to 
-    take into account this request (this is done in the ExtractedSlice ctor)
-    */
-    virtual IExtractedSlice* ExtractSlice(const CoordinateSystem3D& cuttingPlane)
-    {
-      if (volume_->HasGeometry())
-      {
-        return new ExtractedSlice(*this, cuttingPlane);
-      }
-      else
-      {
-        return new IVolumeSlicer::InvalidSlice;
-      }
-    }
-  };
-
-
-  /**
   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