view Samples/Sdl/Loader.cpp @ 812:abc08ac721d3

small bit of doc
author Benjamin Golinvaux <bgo@osimis.io>
date Tue, 28 May 2019 18:31:17 +0200
parents f4d64e088838
children aead999345e0
line wrap: on
line source

/**
 * 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 "../../Framework/Toolbox/DicomInstanceParameters.h"
#include "../../Framework/Oracle/ThreadedOracle.h"
#include "../../Framework/Oracle/GetOrthancWebViewerJpegCommand.h"
#include "../../Framework/Oracle/GetOrthancImageCommand.h"
#include "../../Framework/Oracle/OrthancRestApiCommand.h"
#include "../../Framework/Oracle/SleepOracleCommand.h"
#include "../../Framework/Oracle/OracleCommandExceptionMessage.h"

// From Stone
#include "../../Framework/Loaders/BasicFetchingItemsSorter.h"
#include "../../Framework/Loaders/BasicFetchingStrategy.h"
#include "../../Framework/Scene2D/CairoCompositor.h"
#include "../../Framework/Scene2D/Scene2D.h"
#include "../../Framework/Scene2D/PolylineSceneLayer.h"
#include "../../Framework/Scene2D/LookupTableTextureSceneLayer.h"
#include "../../Framework/StoneInitialization.h"
#include "../../Framework/Toolbox/GeometryToolbox.h"
#include "../../Framework/Toolbox/SlicesSorter.h"
#include "../../Framework/Toolbox/DicomStructureSet.h"
#include "../../Framework/Volumes/ImageBuffer3D.h"
#include "../../Framework/Volumes/VolumeImageGeometry.h"
#include "../../Framework/Volumes/VolumeReslicer.h"

// From Orthanc framework
#include <Core/DicomFormat/DicomArray.h>
#include <Core/Images/Image.h>
#include <Core/Images/ImageProcessing.h>
#include <Core/Images/PngWriter.h>
#include <Core/Endianness.h>
#include <Core/Logging.h>
#include <Core/OrthancException.h>
#include <Core/SystemToolbox.h>
#include <Core/Toolbox.h>


#include <EmbeddedResources.h>


namespace OrthancStone
{
  /**
  This 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 
  OrtancMultiframeVolumeLoader class.
  */
  class LoaderStateMachine : public IObserver
  {
  protected:
    class State : public Orthanc::IDynamicObject
    {
    private:
      LoaderStateMachine&  that_;

    public:
      State(LoaderStateMachine& that) :
        that_(that)
      {
      }

      State(const State& currentState) :
        that_(currentState.that_)
      {
      }

      void Schedule(OracleCommandWithPayload* command) const
      {
        that_.Schedule(command);
      }

      template <typename T>
      T& GetLoader() const
      {
        return dynamic_cast<T&>(that_);
      }
      
      virtual void Handle(const OrthancRestApiCommand::SuccessMessage& message)
      {
        throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
      }
      
      virtual void Handle(const GetOrthancImageCommand::SuccessMessage& message)
      {
        throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
      }
      
      virtual void Handle(const GetOrthancWebViewerJpegCommand::SuccessMessage& message)
      {
        throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
      }
    };

    void Schedule(OracleCommandWithPayload* command)
    {
      std::auto_ptr<OracleCommandWithPayload> protection(command);

      if (command == NULL)
      {
        throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
      }
      
      if (!command->HasPayload())
      {
        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange,
                                        "The payload must contain the next state");
      }

      pendingCommands_.push_back(protection.release());
      Step();
    }

    void Start()
    {
      if (active_)
      {
        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
      }

      active_ = true;

      for (size_t i = 0; i < simultaneousDownloads_; i++)
      {
        Step();
      }
    }

  private:
    void Step()
    {
      if (!pendingCommands_.empty() &&
          activeCommands_ < simultaneousDownloads_)
      {
        oracle_.Schedule(*this, pendingCommands_.front());
        pendingCommands_.pop_front();

        activeCommands_++;
      }
    }

    void Clear()
    {
      for (PendingCommands::iterator it = pendingCommands_.begin();
           it != pendingCommands_.end(); ++it)
      {
        delete *it;
      }

      pendingCommands_.clear();
    }

    void HandleExceptionMessage(const OracleCommandExceptionMessage& message)
    {
      LOG(ERROR) << "Error in the state machine, stopping all processing";
      Clear();
    }

    template <typename T>
    void HandleSuccessMessage(const T& message)
    {
      assert(activeCommands_ > 0);
      activeCommands_--;

      try
      {
        dynamic_cast<State&>(message.GetOrigin().GetPayload()).Handle(message);
        Step();
      }
      catch (Orthanc::OrthancException& e)
      {
        LOG(ERROR) << "Error in the state machine, stopping all processing: " << e.What();
        Clear();
      }
    }

    typedef std::list<IOracleCommand*>  PendingCommands;

    IOracle&         oracle_;
    bool             active_;
    unsigned int     simultaneousDownloads_;
    PendingCommands  pendingCommands_;
    unsigned int     activeCommands_;

  public:
    LoaderStateMachine(IOracle& oracle,
                       IObservable& oracleObservable) :
      IObserver(oracleObservable.GetBroker()),
      oracle_(oracle),
      active_(false),
      simultaneousDownloads_(4),
      activeCommands_(0)
    {
      oracleObservable.RegisterObserverCallback(
        new Callable<LoaderStateMachine, OrthancRestApiCommand::SuccessMessage>
        (*this, &LoaderStateMachine::HandleSuccessMessage));

      oracleObservable.RegisterObserverCallback(
        new Callable<LoaderStateMachine, GetOrthancImageCommand::SuccessMessage>
        (*this, &LoaderStateMachine::HandleSuccessMessage));

      oracleObservable.RegisterObserverCallback(
        new Callable<LoaderStateMachine, GetOrthancWebViewerJpegCommand::SuccessMessage>
        (*this, &LoaderStateMachine::HandleSuccessMessage));

      oracleObservable.RegisterObserverCallback(
        new Callable<LoaderStateMachine, OracleCommandExceptionMessage>
        (*this, &LoaderStateMachine::HandleExceptionMessage));
    }

    virtual ~LoaderStateMachine()
    {
      Clear();
    }

    bool IsActive() const
    {
      return active_;
    }

    void SetSimultaneousDownloads(unsigned int count)
    {
      if (active_)
      {
        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
      }
      else if (count == 0)
      {
        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);        
      }
      else
      {
        simultaneousDownloads_ = count;
      }
    }
  };



  class OrthancMultiframeVolumeLoader :
    public LoaderStateMachine,
    public IObservable
  {
  private:
    class LoadRTDoseGeometry : public State
    {
    private:
      std::auto_ptr<Orthanc::DicomMap>  dicom_;

    public:
      LoadRTDoseGeometry(OrthancMultiframeVolumeLoader& that,
                         Orthanc::DicomMap* dicom) :
        State(that),
        dicom_(dicom)
      {
        if (dicom == NULL)
        {
          throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
        }

      }

      virtual void Handle(const OrthancRestApiCommand::SuccessMessage& message)
      {
        // Complete the DICOM tags with just-received "Grid Frame Offset Vector"
        std::string s = Orthanc::Toolbox::StripSpaces(message.GetAnswer());
        dicom_->SetValue(Orthanc::DICOM_TAG_GRID_FRAME_OFFSET_VECTOR, s, false);

        GetLoader<OrthancMultiframeVolumeLoader>().SetGeometry(*dicom_);
      }      
    };


    static std::string GetSopClassUid(const Orthanc::DicomMap& dicom)
    {
      std::string s;
      if (!dicom.CopyToString(s, Orthanc::DICOM_TAG_SOP_CLASS_UID, false))
      {
        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat,
                                        "DICOM file without SOP class UID");
      }
      else
      {
        return s;
      }
    }
    

    class LoadGeometry : public State
    {
    public:
      LoadGeometry(OrthancMultiframeVolumeLoader& that) :
        State(that)
      {
      }
      
      virtual void Handle(const OrthancRestApiCommand::SuccessMessage& message)
      {
        OrthancMultiframeVolumeLoader& loader = GetLoader<OrthancMultiframeVolumeLoader>();
        
        Json::Value body;
        message.ParseJsonBody(body);
        
        if (body.type() != Json::objectValue)
        {
          throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol);
        }

        std::auto_ptr<Orthanc::DicomMap> dicom(new Orthanc::DicomMap);
        dicom->FromDicomAsJson(body);

        if (StringToSopClassUid(GetSopClassUid(*dicom)) == SopClassUid_RTDose)
        {
          // Download the "Grid Frame Offset Vector" DICOM tag, that is
          // mandatory for RT-DOSE, but is too long to be returned by default
          
          std::auto_ptr<OrthancRestApiCommand> command(new OrthancRestApiCommand);
          command->SetUri("/instances/" + loader.GetInstanceId() + "/content/" +
                          Orthanc::DICOM_TAG_GRID_FRAME_OFFSET_VECTOR.Format());
          command->SetPayload(new LoadRTDoseGeometry(loader, dicom.release()));

          Schedule(command.release());
        }
        else
        {
          loader.SetGeometry(*dicom);
        }
      }
    };



    class LoadTransferSyntax : public State
    {
    public:
      LoadTransferSyntax(OrthancMultiframeVolumeLoader& that) :
        State(that)
      {
      }
      
      virtual void Handle(const OrthancRestApiCommand::SuccessMessage& message)
      {
        GetLoader<OrthancMultiframeVolumeLoader>().SetTransferSyntax(message.GetAnswer());
      }
    };
   
    
    class LoadUncompressedPixelData : public State
    {
    public:
      LoadUncompressedPixelData(OrthancMultiframeVolumeLoader& that) :
        State(that)
      {
      }
      
      virtual void Handle(const OrthancRestApiCommand::SuccessMessage& message)
      {
        GetLoader<OrthancMultiframeVolumeLoader>().SetUncompressedPixelData(message.GetAnswer());
      }
    };
   
    

    boost::shared_ptr<DicomVolumeImage>   volume_;
    std::string  instanceId_;
    std::string  transferSyntaxUid_;


    const std::string& GetInstanceId() const
    {
      if (IsActive())
      {
        return instanceId_;
      }
      else
      {
        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
      }
    }


    void ScheduleFrameDownloads()
    {
      if (transferSyntaxUid_.empty() ||
          !volume_->HasGeometry())
      {
        return;
      }
      /*
      1.2.840.10008.1.2	Implicit VR Endian: Default Transfer Syntax for DICOM
      1.2.840.10008.1.2.1	Explicit VR Little Endian
      1.2.840.10008.1.2.2	Explicit VR Big Endian

      See https://www.dicomlibrary.com/dicom/transfer-syntax/
      */
      if (transferSyntaxUid_ == "1.2.840.10008.1.2" ||
          transferSyntaxUid_ == "1.2.840.10008.1.2.1" ||
          transferSyntaxUid_ == "1.2.840.10008.1.2.2")
      {
        std::auto_ptr<OrthancRestApiCommand> command(new OrthancRestApiCommand);
        command->SetHttpHeader("Accept-Encoding", "gzip");
        command->SetUri("/instances/" + instanceId_ + "/content/" +
                        Orthanc::DICOM_TAG_PIXEL_DATA.Format() + "/0");
        command->SetPayload(new LoadUncompressedPixelData(*this));
        Schedule(command.release());
      }
      else
      {
        throw Orthanc::OrthancException(
          Orthanc::ErrorCode_NotImplemented,
          "No support for multiframe instances with transfer syntax: " + transferSyntaxUid_);
      }
    }
      

    void SetTransferSyntax(const std::string& transferSyntax)
    {
      transferSyntaxUid_ = Orthanc::Toolbox::StripSpaces(transferSyntax);
      ScheduleFrameDownloads();
    }
    

    void SetGeometry(const Orthanc::DicomMap& dicom)
    {
      DicomInstanceParameters parameters(dicom);
      volume_->SetDicomParameters(parameters);
      
      Orthanc::PixelFormat format;
      if (!parameters.GetImageInformation().ExtractPixelFormat(format, true))
      {
        throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
      }

      double spacingZ;
      switch (parameters.GetSopClassUid())
      {
        case SopClassUid_RTDose:
          spacingZ = parameters.GetThickness();
          break;

        default:
          throw Orthanc::OrthancException(
            Orthanc::ErrorCode_NotImplemented,
            "No support for multiframe instances with SOP class UID: " + GetSopClassUid(dicom));
      }

      const unsigned int width = parameters.GetImageInformation().GetWidth();
      const unsigned int height = parameters.GetImageInformation().GetHeight();
      const unsigned int depth = parameters.GetImageInformation().GetNumberOfFrames();

      {
        VolumeImageGeometry geometry;
        geometry.SetSize(width, height, depth);
        geometry.SetAxialGeometry(parameters.GetGeometry());
        geometry.SetVoxelDimensions(parameters.GetPixelSpacingX(),
                                    parameters.GetPixelSpacingY(), spacingZ);
        volume_->Initialize(geometry, format);
      }

      volume_->GetPixelData().Clear();

      ScheduleFrameDownloads();

      BroadcastMessage(DicomVolumeImage::GeometryReadyMessage(*volume_));
    }


    ORTHANC_FORCE_INLINE
    static void CopyPixel(uint32_t& target,
                          const void* source)
    {
      // TODO - check alignement?
      target = le32toh(*reinterpret_cast<const uint32_t*>(source));
    }
      

    template <typename T>
    void CopyPixelData(const std::string& pixelData)
    {
      ImageBuffer3D& target = volume_->GetPixelData();
      
      const unsigned int bpp = target.GetBytesPerPixel();
      const unsigned int width = target.GetWidth();
      const unsigned int height = target.GetHeight();
      const unsigned int depth = target.GetDepth();

      if (pixelData.size() != bpp * width * height * depth)
      {
        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat,
                                        "The pixel data has not the proper size");
      }

      if (pixelData.empty())
      {
        return;
      }

      const uint8_t* source = reinterpret_cast<const uint8_t*>(pixelData.c_str());

      for (unsigned int z = 0; z < depth; z++)
      {
        ImageBuffer3D::SliceWriter writer(target, VolumeProjection_Axial, z);

        assert (writer.GetAccessor().GetWidth() == width &&
                writer.GetAccessor().GetHeight() == height);

        for (unsigned int y = 0; y < height; y++)
        {
          assert(sizeof(T) == Orthanc::GetBytesPerPixel(target.GetFormat()));

          T* target = reinterpret_cast<T*>(writer.GetAccessor().GetRow(y));

          for (unsigned int x = 0; x < width; x++)
          {
            CopyPixel(*target, source);
            
            target ++;
            source += bpp;
          }
        }
      }
    }
    

    void SetUncompressedPixelData(const std::string& pixelData)
    {
      switch (volume_->GetPixelData().GetFormat())
      {
        case Orthanc::PixelFormat_Grayscale32:
          CopyPixelData<uint32_t>(pixelData);
          break;

        default:
          throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
      }

      volume_->IncrementRevision();

      BroadcastMessage(DicomVolumeImage::ContentUpdatedMessage(*volume_));
    }


  public:
    OrthancMultiframeVolumeLoader(const boost::shared_ptr<DicomVolumeImage>& volume,
                                  IOracle& oracle,
                                  IObservable& oracleObservable) :
      LoaderStateMachine(oracle, oracleObservable),
      IObservable(oracleObservable.GetBroker()),
      volume_(volume)
    {
      if (volume.get() == NULL)
      {
        throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
      }
    }


    void LoadInstance(const std::string& instanceId)
    {
      Start();

      instanceId_ = instanceId;

      {
        std::auto_ptr<OrthancRestApiCommand> command(new OrthancRestApiCommand);
        command->SetHttpHeader("Accept-Encoding", "gzip");
        command->SetUri("/instances/" + instanceId + "/tags");
        command->SetPayload(new LoadGeometry(*this));
        Schedule(command.release());
      }

      {
        std::auto_ptr<OrthancRestApiCommand> command(new OrthancRestApiCommand);
        command->SetUri("/instances/" + instanceId + "/metadata/TransferSyntax");
        command->SetPayload(new LoadTransferSyntax(*this));
        Schedule(command.release());
      }
    }
  };


  /**
  This class is able to supply an extract slice for an arbitrary cutting
  plane through a volume image
  */
  class DicomVolumeImageReslicer : public IVolumeSlicer
  {
  private:
    class Slice : public IExtractedSlice
    {
    private:
      DicomVolumeImageReslicer&  that_;
      CoordinateSystem3D         cuttingPlane_;
      
    public:
      Slice(DicomVolumeImageReslicer& that,
            const CoordinateSystem3D& cuttingPlane) :
        that_(that),
        cuttingPlane_(cuttingPlane)
      {
      }
      
      virtual bool IsValid()
      {
        return true;
      }

      virtual uint64_t GetRevision()
      {
        return that_.volume_->GetRevision();
      }

      virtual ISceneLayer* CreateSceneLayer(const ILayerStyleConfigurator* configurator,
                                            const CoordinateSystem3D& cuttingPlane)
      {
        VolumeReslicer& reslicer = that_.reslicer_;
        
        if (configurator == NULL)
        {
          throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError,
                                          "Must provide a layer style configurator");
        }
        
        reslicer.SetOutputFormat(that_.volume_->GetPixelData().GetFormat());
        reslicer.Apply(that_.volume_->GetPixelData(),
                       that_.volume_->GetGeometry(),
                       cuttingPlane);

        if (reslicer.IsSuccess())
        {
          std::auto_ptr<TextureBaseSceneLayer> layer
            (configurator->CreateTextureFromDicom(reslicer.GetOutputSlice(),
                                                  that_.volume_->GetDicomParameters()));
          if (layer.get() == NULL)
          {
            return NULL;
          }

          double s = reslicer.GetPixelSpacing();
          layer->SetPixelSpacing(s, s);
          layer->SetOrigin(reslicer.GetOutputExtent().GetX1() + 0.5 * s,
                           reslicer.GetOutputExtent().GetY1() + 0.5 * s);

          // TODO - Angle!!
                           
          return layer.release();
        }
        else
        {
          return NULL;
        }          
      }
    };
    
    boost::shared_ptr<DicomVolumeImage>  volume_;
    VolumeReslicer                       reslicer_;

  public:
    DicomVolumeImageReslicer(const boost::shared_ptr<DicomVolumeImage>& volume) :
      volume_(volume)
    {
      if (volume.get() == NULL)
      {
        throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
      }
    }

    ImageInterpolation GetInterpolation() const
    {
      return reslicer_.GetInterpolation();
    }

    void SetInterpolation(ImageInterpolation interpolation)
    {
      reslicer_.SetInterpolation(interpolation);
    }

    bool IsFastMode() const
    {
      return reslicer_.IsFastMode();
    }

    void SetFastMode(bool fast)
    {
      reslicer_.EnableFastMode(fast);
    }
    
    virtual IExtractedSlice* ExtractSlice(const CoordinateSystem3D& cuttingPlane)
    {
      if (volume_->HasGeometry())
      {
        return new Slice(*this, cuttingPlane);
      }
      else
      {
        return new IVolumeSlicer::InvalidSlice;
      }
    }
  };



  class DicomStructureSetLoader :
    public LoaderStateMachine,
    public IVolumeSlicer
  {
  private:
    class AddReferencedInstance : public State
    {
    private:
      std::string instanceId_;
      
    public:
      AddReferencedInstance(DicomStructureSetLoader& that,
                            const std::string& instanceId) :
        State(that),
        instanceId_(instanceId)
      {
      }

      virtual void Handle(const OrthancRestApiCommand::SuccessMessage& message)
      {
        Json::Value tags;
        message.ParseJsonBody(tags);
        
        Orthanc::DicomMap dicom;
        dicom.FromDicomAsJson(tags);

        DicomStructureSetLoader& loader = GetLoader<DicomStructureSetLoader>();
        loader.content_->AddReferencedSlice(dicom);

        loader.countProcessedInstances_ ++;
        assert(loader.countProcessedInstances_ <= loader.countReferencedInstances_);

        if (loader.countProcessedInstances_ == loader.countReferencedInstances_)
        {
          // All the referenced instances have been loaded, finalize the RT-STRUCT
          loader.content_->CheckReferencedSlices();
          loader.revision_++;
        }
      }
    };
    
    // State that converts a "SOP Instance UID" to an Orthanc identifier
    class LookupInstance : public State
    {
    private:
      std::string  sopInstanceUid_;
      
    public:
      LookupInstance(DicomStructureSetLoader& that,
                     const std::string& sopInstanceUid) :
        State(that),
        sopInstanceUid_(sopInstanceUid)
      {
      }

      virtual void Handle(const OrthancRestApiCommand::SuccessMessage& message)
      {
        DicomStructureSetLoader& loader = GetLoader<DicomStructureSetLoader>();

        Json::Value lookup;
        message.ParseJsonBody(lookup);

        if (lookup.type() != Json::arrayValue ||
            lookup.size() != 1 ||
            !lookup[0].isMember("Type") ||
            !lookup[0].isMember("Path") ||
            lookup[0]["Type"].type() != Json::stringValue ||
            lookup[0]["ID"].type() != Json::stringValue ||
            lookup[0]["Type"].asString() != "Instance")
        {
          throw Orthanc::OrthancException(Orthanc::ErrorCode_UnknownResource);          
        }

        const std::string instanceId = lookup[0]["ID"].asString();

        {
          std::auto_ptr<OrthancRestApiCommand> command(new OrthancRestApiCommand);
          command->SetHttpHeader("Accept-Encoding", "gzip");
          command->SetUri("/instances/" + instanceId + "/tags");
          command->SetPayload(new AddReferencedInstance(loader, instanceId));
          Schedule(command.release());
        }
      }
    };
    
    class LoadStructure : public State
    {
    public:
      LoadStructure(DicomStructureSetLoader& that) :
        State(that)
      {
      }

      virtual void Handle(const OrthancRestApiCommand::SuccessMessage& message)
      {
        DicomStructureSetLoader& loader = GetLoader<DicomStructureSetLoader>();
        
        {
          OrthancPlugins::FullOrthancDataset dicom(message.GetAnswer());
          loader.content_.reset(new DicomStructureSet(dicom));
        }

        std::set<std::string> instances;
        loader.content_->GetReferencedInstances(instances);

        loader.countReferencedInstances_ = instances.size();

        for (std::set<std::string>::const_iterator
               it = instances.begin(); it != instances.end(); ++it)
        {
          std::auto_ptr<OrthancRestApiCommand> command(new OrthancRestApiCommand);
          command->SetUri("/tools/lookup");
          command->SetMethod(Orthanc::HttpMethod_Post);
          command->SetBody(*it);
          command->SetPayload(new LookupInstance(loader, *it));
          Schedule(command.release());
        }
      }
    };


    
    std::auto_ptr<DicomStructureSet>  content_;
    uint64_t                          revision_;
    std::string                       instanceId_;
    unsigned int                      countProcessedInstances_;
    unsigned int                      countReferencedInstances_;

    
    class Slice : public IExtractedSlice
    {
    private:
      const DicomStructureSet&  content_;
      uint64_t                  revision_;
      bool                      isValid_;
      
    public:
      Slice(const DicomStructureSet& content,
            uint64_t revision,
            const CoordinateSystem3D& cuttingPlane) :
        content_(content),
        revision_(revision)
      {
        bool opposite;

        const Vector normal = content.GetNormal();
        isValid_ = (
          GeometryToolbox::IsParallelOrOpposite(opposite, normal, cuttingPlane.GetNormal()) ||
          GeometryToolbox::IsParallelOrOpposite(opposite, normal, cuttingPlane.GetAxisX()) ||
          GeometryToolbox::IsParallelOrOpposite(opposite, normal, cuttingPlane.GetAxisY()));
      }
      
      virtual bool IsValid()
      {
        return isValid_;
      }

      virtual uint64_t GetRevision()
      {
        return revision_;
      }

      virtual ISceneLayer* CreateSceneLayer(const ILayerStyleConfigurator* configurator,
                                            const CoordinateSystem3D& cuttingPlane)
      {
        assert(isValid_);

        std::auto_ptr<PolylineSceneLayer> layer(new PolylineSceneLayer);
        layer->SetThickness(2);

        for (size_t i = 0; i < content_.GetStructuresCount(); i++)
        {
          const Color& color = content_.GetStructureColor(i);

          std::vector< std::vector<DicomStructureSet::PolygonPoint> > polygons;
          
          if (content_.ProjectStructure(polygons, i, cuttingPlane))
          {
            for (size_t j = 0; j < polygons.size(); j++)
            {
              PolylineSceneLayer::Chain chain;
              chain.resize(polygons[j].size());
            
              for (size_t k = 0; k < polygons[j].size(); k++)
              {
                chain[k] = ScenePoint2D(polygons[j][k].first, polygons[j][k].second);
              }

              layer->AddChain(chain, true /* closed */, color);
            }
          }
        }

        return layer.release();
      }
    };
    
  public:
    DicomStructureSetLoader(IOracle& oracle,
                            IObservable& oracleObservable) :
      LoaderStateMachine(oracle, oracleObservable),
      revision_(0),
      countProcessedInstances_(0),
      countReferencedInstances_(0)
    {
    }
    
    
    void LoadInstance(const std::string& instanceId)
    {
      Start();
      
      instanceId_ = instanceId;
      
      {
        std::auto_ptr<OrthancRestApiCommand> command(new OrthancRestApiCommand);
        command->SetHttpHeader("Accept-Encoding", "gzip");
        command->SetUri("/instances/" + instanceId + "/tags?ignore-length=3006-0050");
        command->SetPayload(new LoadStructure(*this));
        Schedule(command.release());
      }
    }

    virtual IExtractedSlice* ExtractSlice(const CoordinateSystem3D& cuttingPlane)
    {
      if (content_.get() == NULL)
      {
        // Geometry is not available yet
        return new IVolumeSlicer::InvalidSlice;
      }
      else
      {
        return new Slice(*content_, revision_, cuttingPlane);
      }
    }
  };



  class VolumeSceneLayerSource : public boost::noncopyable
  {
  private:
    Scene2D&                                scene_;
    int                                     layerDepth_;
    boost::shared_ptr<IVolumeSlicer>        slicer_;
    std::auto_ptr<ILayerStyleConfigurator>  configurator_;
    std::auto_ptr<CoordinateSystem3D>       lastPlane_;
    uint64_t                                lastRevision_;
    uint64_t                                lastConfiguratorRevision_;

    static bool IsSameCuttingPlane(const CoordinateSystem3D& a,
                                   const CoordinateSystem3D& b)
    {
      // TODO - What if the normal is reversed?
      double distance;
      return (CoordinateSystem3D::ComputeDistance(distance, a, b) &&
              LinearAlgebra::IsCloseToZero(distance));
    }

    void ClearLayer()
    {
      scene_.DeleteLayer(layerDepth_);
      lastPlane_.reset(NULL);
    }

  public:
    VolumeSceneLayerSource(Scene2D& scene,
                           int layerDepth,
                           const boost::shared_ptr<IVolumeSlicer>& slicer) :
      scene_(scene),
      layerDepth_(layerDepth),
      slicer_(slicer)
    {
      if (slicer == NULL)
      {
        throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
      }
    }

    const IVolumeSlicer& GetSlicer() const
    {
      return *slicer_;
    }

    void RemoveConfigurator()
    {
      configurator_.reset();
      lastPlane_.reset();
    }

    void SetConfigurator(ILayerStyleConfigurator* configurator)  // Takes ownership
    {
      if (configurator == NULL)
      {
        throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
      }

      configurator_.reset(configurator);

      // Invalidate the layer
      lastPlane_.reset(NULL);
    }

    bool HasConfigurator() const
    {
      return configurator_.get() != NULL;
    }

    ILayerStyleConfigurator& GetConfigurator() const
    {
      if (configurator_.get() == NULL)
      {
        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
      }
      
      return *configurator_;
    }

    void Update(const CoordinateSystem3D& plane)
    {
      assert(slicer_.get() != NULL);
      std::auto_ptr<IVolumeSlicer::IExtractedSlice> slice(slicer_->ExtractSlice(plane));

      if (slice.get() == NULL)
      {
        throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);        
      }

      if (!slice->IsValid())
      {
        // The slicer cannot handle this cutting plane: Clear the layer
        ClearLayer();
      }
      else if (lastPlane_.get() != NULL &&
               IsSameCuttingPlane(*lastPlane_, plane) &&
               lastRevision_ == slice->GetRevision())
      {
        // The content of the slice has not changed: Don't update the
        // layer content, but possibly update its style

        if (configurator_.get() != NULL &&
            configurator_->GetRevision() != lastConfiguratorRevision_ &&
            scene_.HasLayer(layerDepth_))
        {
          configurator_->ApplyStyle(scene_.GetLayer(layerDepth_));
        }
      }
      else
      {
        // Content has changed: An update is needed
        lastPlane_.reset(new CoordinateSystem3D(plane));
        lastRevision_ = slice->GetRevision();

        std::auto_ptr<ISceneLayer> layer(slice->CreateSceneLayer(configurator_.get(), plane));
        if (layer.get() == NULL)
        {
          ClearLayer();
        }
        else
        {
          if (configurator_.get() != NULL)
          {
            lastConfiguratorRevision_ = configurator_->GetRevision();
            configurator_->ApplyStyle(*layer);
          }

          scene_.SetLayer(layerDepth_, layer.release());
        }
      }
    }
  };





  class NativeApplicationContext : public IMessageEmitter
  {
  private:
    boost::shared_mutex            mutex_;
    MessageBroker    broker_;
    IObservable      oracleObservable_;

  public:
    NativeApplicationContext() :
      oracleObservable_(broker_)
    {
    }


    virtual void EmitMessage(const IObserver& observer,
                             const IMessage& message)
    {
      try
      {
        boost::unique_lock<boost::shared_mutex>  lock(mutex_);
        oracleObservable_.EmitMessage(observer, message);
      }
      catch (Orthanc::OrthancException& e)
      {
        LOG(ERROR) << "Exception while emitting a message: " << e.What();
      }
    }


    class ReaderLock : public boost::noncopyable
    {
    private:
      NativeApplicationContext&                that_;
      boost::shared_lock<boost::shared_mutex>  lock_;

    public:
      ReaderLock(NativeApplicationContext& that) : 
        that_(that),
        lock_(that.mutex_)
      {
      }
    };


    class WriterLock : public boost::noncopyable
    {
    private:
      NativeApplicationContext&                that_;
      boost::unique_lock<boost::shared_mutex>  lock_;

    public:
      WriterLock(NativeApplicationContext& that) : 
        that_(that),
        lock_(that.mutex_)
      {
      }

      MessageBroker& GetBroker() 
      {
        return that_.broker_;
      }

      IObservable& GetOracleObservable()
      {
        return that_.oracleObservable_;
      }
    };
  };
}



class Toto : public OrthancStone::IObserver
{
private:
  OrthancStone::CoordinateSystem3D  plane_;
  OrthancStone::IOracle&            oracle_;
  OrthancStone::Scene2D             scene_;
  std::auto_ptr<OrthancStone::VolumeSceneLayerSource>  source1_, source2_, source3_;


  void Refresh()
  {
    if (source1_.get() != NULL)
    {
      source1_->Update(plane_);
    }
      
    if (source2_.get() != NULL)
    {
      source2_->Update(plane_);
    }

    if (source3_.get() != NULL)
    {
      source3_->Update(plane_);
    }

    scene_.FitContent(1024, 768);
      
    {
      OrthancStone::CairoCompositor compositor(scene_, 1024, 768);
      compositor.Refresh();
        
      Orthanc::ImageAccessor accessor;
      compositor.GetCanvas().GetReadOnlyAccessor(accessor);

      Orthanc::Image tmp(Orthanc::PixelFormat_RGB24, accessor.GetWidth(), accessor.GetHeight(), false);
      Orthanc::ImageProcessing::Convert(tmp, accessor);
        
      static unsigned int count = 0;
      char buf[64];
      sprintf(buf, "scene-%06d.png", count++);
        
      Orthanc::PngWriter writer;
      writer.WriteToFile(buf, tmp);
    }
  }

  
  void Handle(const OrthancStone::DicomVolumeImage::GeometryReadyMessage& message)
  {
    printf("Geometry ready\n");
    
    //plane_ = message.GetOrigin().GetGeometry().GetSagittalGeometry();
    //plane_ = message.GetOrigin().GetGeometry().GetAxialGeometry();
    plane_ = message.GetOrigin().GetGeometry().GetCoronalGeometry();
    plane_.SetOrigin(message.GetOrigin().GetGeometry().GetCoordinates(0.5f, 0.5f, 0.5f));

    Refresh();
  }
  
  
  void Handle(const OrthancStone::SleepOracleCommand::TimeoutMessage& message)
  {
    if (message.GetOrigin().HasPayload())
    {
      printf("TIMEOUT! %d\n", dynamic_cast<const Orthanc::SingleValueObject<unsigned int>& >(message.GetOrigin().GetPayload()).GetValue());
    }
    else
    {
      printf("TIMEOUT\n");

      Refresh();

      /**
       * The sleep() leads to a crash if the oracle is still running,
       * while this object is destroyed. Always stop the oracle before
       * destroying active objects.  (*)
       **/
      // boost::this_thread::sleep(boost::posix_time::seconds(2));

      oracle_.Schedule(*this, new OrthancStone::SleepOracleCommand(message.GetOrigin().GetDelay()));
    }
  }

  void Handle(const OrthancStone::OrthancRestApiCommand::SuccessMessage& message)
  {
    Json::Value v;
    message.ParseJsonBody(v);

    printf("ICI [%s]\n", v.toStyledString().c_str());
  }

  void Handle(const OrthancStone::GetOrthancImageCommand::SuccessMessage& message)
  {
    printf("IMAGE %dx%d\n", message.GetImage().GetWidth(), message.GetImage().GetHeight());
  }

  void Handle(const OrthancStone::GetOrthancWebViewerJpegCommand::SuccessMessage& message)
  {
    printf("WebViewer %dx%d\n", message.GetImage().GetWidth(), message.GetImage().GetHeight());
  }

  void Handle(const OrthancStone::OracleCommandExceptionMessage& message)
  {
    printf("EXCEPTION: [%s] on command type %d\n", message.GetException().What(), message.GetCommand().GetType());

    switch (message.GetCommand().GetType())
    {
      case OrthancStone::IOracleCommand::Type_GetOrthancWebViewerJpeg:
        printf("URI: [%s]\n", dynamic_cast<const OrthancStone::GetOrthancWebViewerJpegCommand&>
               (message.GetCommand()).GetUri().c_str());
        break;
      
      default:
        break;
    }
  }

public:
  Toto(OrthancStone::IOracle& oracle,
       OrthancStone::IObservable& oracleObservable) :
    IObserver(oracleObservable.GetBroker()),
    oracle_(oracle)
  {
    oracleObservable.RegisterObserverCallback
      (new OrthancStone::Callable
       <Toto, OrthancStone::SleepOracleCommand::TimeoutMessage>(*this, &Toto::Handle));

    oracleObservable.RegisterObserverCallback
      (new OrthancStone::Callable
       <Toto, OrthancStone::OrthancRestApiCommand::SuccessMessage>(*this, &Toto::Handle));

    oracleObservable.RegisterObserverCallback
      (new OrthancStone::Callable
       <Toto, OrthancStone::GetOrthancImageCommand::SuccessMessage>(*this, &Toto::Handle));

    oracleObservable.RegisterObserverCallback
      (new OrthancStone::Callable
       <Toto, OrthancStone::GetOrthancWebViewerJpegCommand::SuccessMessage>(*this, &Toto::Handle));

    oracleObservable.RegisterObserverCallback
      (new OrthancStone::Callable
       <Toto, OrthancStone::OracleCommandExceptionMessage>(*this, &Toto::Handle));
  }

  void SetReferenceLoader(OrthancStone::IObservable& loader)
  {
    loader.RegisterObserverCallback
      (new OrthancStone::Callable
       <Toto, OrthancStone::DicomVolumeImage::GeometryReadyMessage>(*this, &Toto::Handle));
  }

  void SetVolume1(int depth,
                  const boost::shared_ptr<OrthancStone::IVolumeSlicer>& volume,
                  OrthancStone::ILayerStyleConfigurator* style)
  {
    source1_.reset(new OrthancStone::VolumeSceneLayerSource(scene_, depth, volume));

    if (style != NULL)
    {
      source1_->SetConfigurator(style);
    }
  }

  void SetVolume2(int depth,
                  const boost::shared_ptr<OrthancStone::IVolumeSlicer>& volume,
                  OrthancStone::ILayerStyleConfigurator* style)
  {
    source2_.reset(new OrthancStone::VolumeSceneLayerSource(scene_, depth, volume));

    if (style != NULL)
    {
      source2_->SetConfigurator(style);
    }
  }

  void SetStructureSet(int depth,
                       const boost::shared_ptr<OrthancStone::DicomStructureSetLoader>& volume)
  {
    source3_.reset(new OrthancStone::VolumeSceneLayerSource(scene_, depth, volume));
  }
                       
};


void Run(OrthancStone::NativeApplicationContext& context,
         OrthancStone::ThreadedOracle& oracle)
{
  boost::shared_ptr<OrthancStone::DicomVolumeImage>  ct(new OrthancStone::DicomVolumeImage);
  boost::shared_ptr<OrthancStone::DicomVolumeImage>  dose(new OrthancStone::DicomVolumeImage);

  
  boost::shared_ptr<Toto> toto;
  boost::shared_ptr<OrthancStone::OrthancSeriesVolumeProgressiveLoader> ctLoader;
  boost::shared_ptr<OrthancStone::OrthancMultiframeVolumeLoader> doseLoader;
  boost::shared_ptr<OrthancStone::DicomStructureSetLoader>  rtstructLoader;

  {
    OrthancStone::NativeApplicationContext::WriterLock lock(context);
    toto.reset(new Toto(oracle, lock.GetOracleObservable()));
    ctLoader.reset(new OrthancStone::OrthancSeriesVolumeProgressiveLoader(ct, oracle, lock.GetOracleObservable()));
    doseLoader.reset(new OrthancStone::OrthancMultiframeVolumeLoader(dose, oracle, lock.GetOracleObservable()));
    rtstructLoader.reset(new OrthancStone::DicomStructureSetLoader(oracle, lock.GetOracleObservable()));
  }


  //toto->SetReferenceLoader(*ctLoader);
  toto->SetReferenceLoader(*doseLoader);


#if 1
  toto->SetVolume1(0, ctLoader, new OrthancStone::GrayscaleStyleConfigurator);
#else
  {
    boost::shared_ptr<OrthancStone::IVolumeSlicer> reslicer(new OrthancStone::DicomVolumeImageReslicer(ct));
    toto->SetVolume1(0, reslicer, new OrthancStone::GrayscaleStyleConfigurator);
  }
#endif  
  

  {
    std::auto_ptr<OrthancStone::LookupTableStyleConfigurator> config(new OrthancStone::LookupTableStyleConfigurator);
    config->SetLookupTable(Orthanc::EmbeddedResources::COLORMAP_HOT);

    boost::shared_ptr<OrthancStone::DicomVolumeImageMPRSlicer> tmp(new OrthancStone::DicomVolumeImageMPRSlicer(dose));
    toto->SetVolume2(1, tmp, config.release());
  }

  toto->SetStructureSet(2, rtstructLoader);
  
  oracle.Schedule(*toto, new OrthancStone::SleepOracleCommand(100));

  if (0)
  {
    Json::Value v = Json::objectValue;
    v["Level"] = "Series";
    v["Query"] = Json::objectValue;

    std::auto_ptr<OrthancStone::OrthancRestApiCommand>  command(new OrthancStone::OrthancRestApiCommand);
    command->SetMethod(Orthanc::HttpMethod_Post);
    command->SetUri("/tools/find");
    command->SetBody(v);

    oracle.Schedule(*toto, command.release());
  }
  
  if (0)
  {
    std::auto_ptr<OrthancStone::GetOrthancImageCommand>  command(new OrthancStone::GetOrthancImageCommand);
    command->SetHttpHeader("Accept", std::string(Orthanc::EnumerationToString(Orthanc::MimeType_Jpeg)));
    command->SetUri("/instances/6687cc73-07cae193-52ff29c8-f646cb16-0753ed92/preview");
    oracle.Schedule(*toto, command.release());
  }
  
  if (0)
  {
    std::auto_ptr<OrthancStone::GetOrthancImageCommand>  command(new OrthancStone::GetOrthancImageCommand);
    command->SetHttpHeader("Accept", std::string(Orthanc::EnumerationToString(Orthanc::MimeType_Png)));
    command->SetUri("/instances/6687cc73-07cae193-52ff29c8-f646cb16-0753ed92/preview");
    oracle.Schedule(*toto, command.release());
  }
  
  if (0)
  {
    std::auto_ptr<OrthancStone::GetOrthancImageCommand>  command(new OrthancStone::GetOrthancImageCommand);
    command->SetHttpHeader("Accept", std::string(Orthanc::EnumerationToString(Orthanc::MimeType_Png)));
    command->SetUri("/instances/6687cc73-07cae193-52ff29c8-f646cb16-0753ed92/image-uint16");
    oracle.Schedule(*toto, command.release());
  }
  
  if (0)
  {
    std::auto_ptr<OrthancStone::GetOrthancImageCommand>  command(new OrthancStone::GetOrthancImageCommand);
    command->SetHttpHeader("Accept-Encoding", "gzip");
    command->SetHttpHeader("Accept", std::string(Orthanc::EnumerationToString(Orthanc::MimeType_Pam)));
    command->SetUri("/instances/6687cc73-07cae193-52ff29c8-f646cb16-0753ed92/image-uint16");
    oracle.Schedule(*toto, command.release());
  }
  
  if (0)
  {
    std::auto_ptr<OrthancStone::GetOrthancImageCommand>  command(new OrthancStone::GetOrthancImageCommand);
    command->SetHttpHeader("Accept", std::string(Orthanc::EnumerationToString(Orthanc::MimeType_Pam)));
    command->SetUri("/instances/6687cc73-07cae193-52ff29c8-f646cb16-0753ed92/image-uint16");
    oracle.Schedule(*toto, command.release());
  }

  if (0)
  {
    std::auto_ptr<OrthancStone::GetOrthancWebViewerJpegCommand>  command(new OrthancStone::GetOrthancWebViewerJpegCommand);
    command->SetHttpHeader("Accept-Encoding", "gzip");
    command->SetInstance("e6c7c20b-c9f65d7e-0d76f2e2-830186f2-3e3c600e");
    command->SetQuality(90);
    oracle.Schedule(*toto, command.release());
  }


  if (0)
  {
    for (unsigned int i = 0; i < 10; i++)
    {
      std::auto_ptr<OrthancStone::SleepOracleCommand> command(new OrthancStone::SleepOracleCommand(i * 1000));
      command->SetPayload(new Orthanc::SingleValueObject<unsigned int>(42 * i));
      oracle.Schedule(*toto, command.release());
    }
  }

  
  // 2017-11-17-Anonymized
#if 0
  // BGO data
  ctLoader->LoadSeries("a04ecf01-79b2fc33-58239f7e-ad9db983-28e81afa");  // CT
  doseLoader->LoadInstance("830a69ff-8e4b5ee3-b7f966c8-bccc20fb-d322dceb");  // RT-DOSE
  //rtstructLoader->LoadInstance("54460695-ba3885ee-ddf61ac0-f028e31d-a6e474d9");  // RT-STRUCT
#else
  //ctLoader->LoadSeries("cb3ea4d1-d08f3856-ad7b6314-74d88d77-60b05618");  // CT
  //doseLoader->LoadInstance("41029085-71718346-811efac4-420e2c15-d39f99b6");  // RT-DOSE
  //rtstructLoader->LoadInstance("83d9c0c3-913a7fee-610097d7-cbf0522d-fd75bee6");  // RT-STRUCT

  // 2017-05-16
  ctLoader->LoadSeries("a04ecf01-79b2fc33-58239f7e-ad9db983-28e81afa");  // CT
  doseLoader->LoadInstance("eac822ef-a395f94e-e8121fe0-8411fef8-1f7bffad");  // RT-DOSE
  rtstructLoader->LoadInstance("54460695-ba3885ee-ddf61ac0-f028e31d-a6e474d9");  // RT-STRUCT
#endif
  // 2015-01-28-Multiframe
  //doseLoader->LoadInstance("88f71e2a-5fad1c61-96ed14d6-5b3d3cf7-a5825279");  // Multiframe CT
  
  // Delphine
  //ctLoader->LoadSeries("5990e39c-51e5f201-fe87a54c-31a55943-e59ef80e");  // CT
  //ctLoader->LoadSeries("67f1b334-02c16752-45026e40-a5b60b6b-030ecab5");  // Lung 1/10mm


  {
    LOG(WARNING) << "...Waiting for Ctrl-C...";

    oracle.Start();

    Orthanc::SystemToolbox::ServerBarrier();

    /**
     * WARNING => The oracle must be stopped BEFORE the objects using
     * it are destroyed!!! This forces to wait for the completion of
     * the running callback methods. Otherwise, the callbacks methods
     * might still be running while their parent object is destroyed,
     * resulting in crashes. This is very visible if adding a sleep(),
     * as in (*).
     **/

    oracle.Stop();
  }
}



/**
 * IMPORTANT: The full arguments to "main()" are needed for SDL on
 * Windows. Otherwise, one gets the linking error "undefined reference
 * to `SDL_main'". https://wiki.libsdl.org/FAQWindows
 **/
int main(int argc, char* argv[])
{
  OrthancStone::StoneInitialize();
  //Orthanc::Logging::EnableInfoLevel(true);

  try
  {
    OrthancStone::NativeApplicationContext context;

    OrthancStone::ThreadedOracle oracle(context);
    //oracle.SetThreadsCount(1);

    {
      Orthanc::WebServiceParameters p;
      //p.SetUrl("http://localhost:8043/");
      p.SetCredentials("orthanc", "orthanc");
      oracle.SetOrthancParameters(p);
    }

    //oracle.Start();

    Run(context, oracle);
    
    //oracle.Stop();
  }
  catch (Orthanc::OrthancException& e)
  {
    LOG(ERROR) << "EXCEPTION: " << e.What();
  }

  OrthancStone::StoneFinalize();

  return 0;
}