changeset 1337:b1396be5aa27 broker

Moved the fixed loaders back from the dead
author Benjamin Golinvaux <bgo@osimis.io>
date Fri, 03 Apr 2020 16:13:06 +0200
parents 04055b6b9e2c
children 1b24f6b06408
files Framework/Deprecated/Loaders/DicomStructureSetLoader.cpp Framework/Deprecated/Loaders/DicomStructureSetLoader.h Framework/Deprecated/Loaders/LoaderCache.cpp Framework/Deprecated/Loaders/LoaderCache.h Framework/Deprecated/Loaders/LoaderStateMachine.cpp Framework/Deprecated/Loaders/LoaderStateMachine.h Framework/Deprecated/Loaders/OrthancMultiframeVolumeLoader.cpp Framework/Deprecated/Loaders/OrthancMultiframeVolumeLoader.h Framework/Deprecated/Loaders/OrthancSeriesVolumeProgressiveLoader.cpp Framework/Deprecated/Loaders/OrthancSeriesVolumeProgressiveLoader.h Framework/Loaders/DicomStructureSetLoader.cpp Framework/Loaders/DicomStructureSetLoader.h Framework/Loaders/LoaderCache.cpp Framework/Loaders/LoaderCache.h Framework/Loaders/LoaderStateMachine.cpp Framework/Loaders/LoaderStateMachine.h Framework/Loaders/OrthancMultiframeVolumeLoader.cpp Framework/Loaders/OrthancMultiframeVolumeLoader.h Framework/Loaders/OrthancSeriesVolumeProgressiveLoader.cpp Framework/Loaders/OrthancSeriesVolumeProgressiveLoader.h Resources/CMake/OrthancStoneConfiguration.cmake
diffstat 21 files changed, 2834 insertions(+), 2844 deletions(-) [+]
line wrap: on
line diff
--- a/Framework/Deprecated/Loaders/DicomStructureSetLoader.cpp	Tue Mar 31 11:01:34 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,428 +0,0 @@
-/**
- * Stone of Orthanc
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU Affero General Public License
- * as published by the Free Software Foundation, either version 3 of
- * the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Affero General Public License for more details.
- * 
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#include "DicomStructureSetLoader.h"
-
-#include "../../Scene2D/PolylineSceneLayer.h"
-#include "../../StoneException.h"
-#include "../../Toolbox/GeometryToolbox.h"
-
-#include <Core/Toolbox.h>
-
-#include <algorithm>
-
-#if 0
-bool logbgo233 = false;
-bool logbgo115 = false;
-#endif
-
-namespace Deprecated
-{
-
-#if 0
-  void DumpDicomMap(std::ostream& o, const Orthanc::DicomMap& dicomMap)
-  {
-    using namespace std;
-    //ios_base::fmtflags state = o.flags();
-    //o.flags(ios::right | ios::hex);
-    //o << "(" << setfill('0') << setw(4) << tag.GetGroup()
-    //  << "," << setw(4) << tag.GetElement() << ")";
-    //o.flags(state);
-    Json::Value val;
-    dicomMap.Serialize(val);
-    o << val;
-    //return o;
-  }
-#endif
-
-
-  class DicomStructureSetLoader::AddReferencedInstance : public LoaderStateMachine::State
-  {
-  private:
-    std::string instanceId_;
-      
-  public:
-    AddReferencedInstance(DicomStructureSetLoader& that,
-                          const std::string& instanceId) :
-      State(that),
-      instanceId_(instanceId)
-    {
-    }
-
-    virtual void Handle(const OrthancStone::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_++;
-        loader.SetStructuresReady();
-      }
-    }
-  };
-
-
-  // State that converts a "SOP Instance UID" to an Orthanc identifier
-  class DicomStructureSetLoader::LookupInstance : public LoaderStateMachine::State
-  {
-  private:
-    std::string  sopInstanceUid_;
-      
-  public:
-    LookupInstance(DicomStructureSetLoader& that,
-                   const std::string& sopInstanceUid) :
-      State(that),
-      sopInstanceUid_(sopInstanceUid)
-    {
-    }
-
-    virtual void Handle(const OrthancStone::OrthancRestApiCommand::SuccessMessage& message)
-    {
-#if 0
-      LOG(TRACE) << "DicomStructureSetLoader::LookupInstance::Handle() (SUCCESS)";
-#endif
-      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")
-      {
-        std::stringstream msg;
-        msg << "Unknown resource! message.GetAnswer() = " << message.GetAnswer() << " message.GetAnswerHeaders() = ";
-        for (OrthancStone::OrthancRestApiCommand::HttpHeaders::const_iterator it = message.GetAnswerHeaders().begin();
-             it != message.GetAnswerHeaders().end(); ++it)
-        {
-          msg << "\nkey: \"" << it->first << "\" value: \"" << it->second << "\"\n";
-        }
-        const std::string msgStr = msg.str();
-        LOG(ERROR) << msgStr;
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_UnknownResource);          
-      }
-
-      const std::string instanceId = lookup[0]["ID"].asString();
-
-      {
-        std::unique_ptr<OrthancStone::OrthancRestApiCommand> command(new OrthancStone::OrthancRestApiCommand);
-        command->SetHttpHeader("Accept-Encoding", "gzip");
-        std::string uri = "/instances/" + instanceId + "/tags";
-        command->SetUri(uri);
-        command->AcquirePayload(new AddReferencedInstance(loader, instanceId));
-        Schedule(command.release());
-      }
-    }
-  };
-
-
-  class DicomStructureSetLoader::LoadStructure : public LoaderStateMachine::State
-  {
-  public:
-    LoadStructure(DicomStructureSetLoader& that) :
-    State(that)
-    {
-    }
-    
-    virtual void Handle(const OrthancStone::OrthancRestApiCommand::SuccessMessage& message)
-    {
-#if 0
-      if (logbgo115)
-        LOG(TRACE) << "DicomStructureSetLoader::LoadStructure::Handle() (SUCCESS)";
-#endif
-      DicomStructureSetLoader& loader = GetLoader<DicomStructureSetLoader>();
-        
-      {
-        OrthancPlugins::FullOrthancDataset dicom(message.GetAnswer());
-        loader.content_.reset(new OrthancStone::DicomStructureSet(dicom));
-        size_t structureCount = loader.content_->GetStructuresCount();
-        loader.structureVisibility_.resize(structureCount);
-        bool everythingVisible = false;
-        if ((loader.initiallyVisibleStructures_.size() == 1)
-          && (loader.initiallyVisibleStructures_[0].size() == 1)
-          && (loader.initiallyVisibleStructures_[0][0] == '*'))
-        {
-          everythingVisible = true;
-        }
-
-        for (size_t i = 0; i < structureCount; ++i)
-        {
-          // if a single "*" string is supplied, this means we want everything 
-          // to be visible...
-          if(everythingVisible)
-          {
-            loader.structureVisibility_.at(i) = true;
-          }
-          else
-          {
-            // otherwise, we only enable visibility for those structures whose 
-            // names are mentioned in the initiallyVisibleStructures_ array
-            const std::string& structureName = loader.content_->GetStructureName(i);
-
-            std::vector<std::string>::iterator foundIt =
-              std::find(
-                loader.initiallyVisibleStructures_.begin(),
-                loader.initiallyVisibleStructures_.end(),
-                structureName);
-            std::vector<std::string>::iterator endIt = loader.initiallyVisibleStructures_.end();
-            if (foundIt != endIt)
-              loader.structureVisibility_.at(i) = true;
-            else
-              loader.structureVisibility_.at(i) = false;
-          }
-        }
-      }
-
-      // Some (admittedly invalid) Dicom files have empty values in the 
-      // 0008,1155 tag. We try our best to cope with this.
-      std::set<std::string> instances;
-      std::set<std::string> nonEmptyInstances;
-      loader.content_->GetReferencedInstances(instances);
-      for (std::set<std::string>::const_iterator
-        it = instances.begin(); it != instances.end(); ++it)
-      {
-        std::string instance = Orthanc::Toolbox::StripSpaces(*it);
-        if(instance != "")
-          nonEmptyInstances.insert(instance);
-      }
-
-      loader.countReferencedInstances_ = 
-        static_cast<unsigned int>(nonEmptyInstances.size());
-
-      for (std::set<std::string>::const_iterator
-        it = nonEmptyInstances.begin(); it != nonEmptyInstances.end(); ++it)
-      {
-        std::unique_ptr<OrthancStone::OrthancRestApiCommand> command(new OrthancStone::OrthancRestApiCommand);
-        command->SetUri("/tools/lookup");
-        command->SetMethod(Orthanc::HttpMethod_Post);
-        command->SetBody(*it);
-        command->AcquirePayload(new LookupInstance(loader, *it));
-        Schedule(command.release());
-      }
-    }
-  };
-    
-
-  class DicomStructureSetLoader::Slice : public IExtractedSlice
-  {
-  private:
-    const OrthancStone::DicomStructureSet&  content_;
-    uint64_t                  revision_;
-    bool                      isValid_;
-    std::vector<bool>         visibility_;
-      
-  public:
-    /**
-    The visibility vector must either:
-    - be empty
-    or
-    - contain the same number of items as the number of structures in the 
-      structure set.
-    In the first case (empty vector), all the structures are displayed.
-    In the second case, the visibility of each structure is defined by the 
-    content of the vector at the corresponding index.
-    */
-    Slice(const OrthancStone::DicomStructureSet& content,
-          uint64_t revision,
-          const OrthancStone::CoordinateSystem3D& cuttingPlane,
-          std::vector<bool> visibility = std::vector<bool>()) 
-      : content_(content)
-      , revision_(revision)
-      , visibility_(visibility)
-    {
-      ORTHANC_ASSERT((visibility_.size() == content_.GetStructuresCount())
-        || (visibility_.size() == 0u));
-
-      bool opposite;
-
-      const OrthancStone::Vector normal = content.GetNormal();
-      isValid_ = (
-        OrthancStone::GeometryToolbox::IsParallelOrOpposite(opposite, normal, cuttingPlane.GetNormal()) ||
-        OrthancStone::GeometryToolbox::IsParallelOrOpposite(opposite, normal, cuttingPlane.GetAxisX()) ||
-        OrthancStone::GeometryToolbox::IsParallelOrOpposite(opposite, normal, cuttingPlane.GetAxisY()));
-    }
-      
-    virtual bool IsValid()
-    {
-      return isValid_;
-    }
-
-    virtual uint64_t GetRevision()
-    {
-      return revision_;
-    }
-
-    virtual OrthancStone::ISceneLayer* CreateSceneLayer(
-      const OrthancStone::ILayerStyleConfigurator* configurator,
-      const OrthancStone::CoordinateSystem3D& cuttingPlane)
-    {
-      assert(isValid_);
-
-      std::unique_ptr<OrthancStone::PolylineSceneLayer> layer(new OrthancStone::PolylineSceneLayer);
-      layer->SetThickness(2);
-
-      for (size_t i = 0; i < content_.GetStructuresCount(); i++)
-      {
-        if ((visibility_.size() == 0) || visibility_.at(i))
-        {
-          const OrthancStone::Color& color = content_.GetStructureColor(i);
-
-#ifdef USE_BOOST_UNION_FOR_POLYGONS 
-          std::vector< std::vector<OrthancStone::Point2D> > 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].x, polygons[j][k].y);
-    }
-
-              layer->AddChain(chain, true /* closed */, color);
-  }
-        }
-#else
-          std::vector< std::pair<OrthancStone::Point2D, OrthancStone::Point2D> > segments;
-
-          if (content_.ProjectStructure(segments, i, cuttingPlane))
-          {
-            for (size_t j = 0; j < segments.size(); j++)
-            {
-              OrthancStone::PolylineSceneLayer::Chain chain;
-              chain.resize(2);
-
-              chain[0] = OrthancStone::ScenePoint2D(segments[j].first.x, segments[j].first.y);
-              chain[1] = OrthancStone::ScenePoint2D(segments[j].second.x, segments[j].second.y);
-
-              layer->AddChain(chain, false /* NOT closed */, color);
-            }
-          }
-#endif        
-        }
-      }
-
-      return layer.release();
-    }
-  };
-    
-
-  DicomStructureSetLoader::DicomStructureSetLoader(
-    OrthancStone::ILoadersContext& loadersContext) 
-    : LoaderStateMachine(loadersContext)
-    , loadersContext_(loadersContext)
-    , revision_(0)
-    , countProcessedInstances_(0)
-    , countReferencedInstances_(0)
-    , structuresReady_(false)
-  {
-  }
-   
-    
-  boost::shared_ptr<Deprecated::DicomStructureSetLoader> DicomStructureSetLoader::Create(OrthancStone::ILoadersContext& loadersContext)
-  {
-    boost::shared_ptr<DicomStructureSetLoader> obj(
-      new DicomStructureSetLoader(
-        loadersContext));
-    obj->LoaderStateMachine::PostConstructor();
-    return obj;
-
-  }
-
-  void DicomStructureSetLoader::SetStructureDisplayState(size_t structureIndex, bool display)
-  {
-    structureVisibility_.at(structureIndex) = display;
-    revision_++;
-  }
-
-  DicomStructureSetLoader::~DicomStructureSetLoader()
-  {
-    LOG(TRACE) << "DicomStructureSetLoader::~DicomStructureSetLoader()";
-  }
-
-  void DicomStructureSetLoader::LoadInstance(
-    const std::string& instanceId, 
-    const std::vector<std::string>& initiallyVisibleStructures)
-  {
-    Start();
-      
-    instanceId_ = instanceId;
-    initiallyVisibleStructures_ = initiallyVisibleStructures;
-
-    {
-      std::unique_ptr<OrthancStone::OrthancRestApiCommand> command(new OrthancStone::OrthancRestApiCommand);
-      command->SetHttpHeader("Accept-Encoding", "gzip");
-
-      std::string uri = "/instances/" + instanceId + "/tags?ignore-length=3006-0050";
-
-      command->SetUri(uri);
-      command->AcquirePayload(new LoadStructure(*this));
-      Schedule(command.release());
-    }
-  }
-
-
-  OrthancStone::IVolumeSlicer::IExtractedSlice* DicomStructureSetLoader::ExtractSlice(const OrthancStone::CoordinateSystem3D& cuttingPlane)
-  {
-    if (content_.get() == NULL)
-    {
-      // Geometry is not available yet
-      return new OrthancStone::IVolumeSlicer::InvalidSlice;
-    }
-    else
-    {
-      return new Slice(*content_, revision_, cuttingPlane, structureVisibility_);
-    }
-  }
-
-  void DicomStructureSetLoader::SetStructuresReady()
-  {
-    ORTHANC_ASSERT(!structuresReady_);
-    structuresReady_ = true;
-    BroadcastMessage(DicomStructureSetLoader::StructuresReady(*this));
-  }
-
-  bool DicomStructureSetLoader::AreStructuresReady() const
-  {
-    return structuresReady_;
-  }
-
-}
--- a/Framework/Deprecated/Loaders/DicomStructureSetLoader.h	Tue Mar 31 11:01:34 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,104 +0,0 @@
-/**
- * Stone of Orthanc
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU Affero General Public License
- * as published by the Free Software Foundation, either version 3 of
- * the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Affero General Public License for more details.
- * 
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#pragma once
-
-#include "../../Toolbox/DicomStructureSet.h"
-#include "../../Volumes/IVolumeSlicer.h"
-#include "LoaderStateMachine.h"
-
-#include <vector>
-
-namespace Deprecated
-{
-  class DicomStructureSetLoader :
-    public LoaderStateMachine,
-    public OrthancStone::IVolumeSlicer,
-    public OrthancStone::IObservable
-  {
-  private:
-    class Slice;
-
-    // States of LoaderStateMachine
-    class AddReferencedInstance;   // 3rd state
-    class LookupInstance;          // 2nd state
-    class LoadStructure;           // 1st state
-    
-    OrthancStone::ILoadersContext&                    loadersContext_;
-    std::unique_ptr<OrthancStone::DicomStructureSet>  content_;
-    uint64_t                                          revision_;
-    std::string                                       instanceId_;
-    unsigned int                                      countProcessedInstances_;
-    unsigned int                                      countReferencedInstances_;  
-
-    // will be set to true once the loading is finished
-    bool                                              structuresReady_;
-
-    /**
-    At load time, these strings are used to initialize the structureVisibility_ 
-    vector.
-
-    As a special case, if initiallyVisibleStructures_ contains a single string
-    that is '*', ALL structures will be made visible.
-    */
-    std::vector<std::string> initiallyVisibleStructures_;
-
-    /**
-    Contains the "Should this structure be displayed?" flag for all structures.
-    Only filled when structures are loaded.
-
-    Changing this value directly affects the rendering
-    */
-    std::vector<bool>                  structureVisibility_;
-
-  protected:
-    DicomStructureSetLoader(OrthancStone::ILoadersContext& loadersContext);
-
-  public:
-    ORTHANC_STONE_DEFINE_ORIGIN_MESSAGE(__FILE__, __LINE__, StructuresReady, DicomStructureSetLoader);
-
-    static boost::shared_ptr<DicomStructureSetLoader> Create(
-      OrthancStone::ILoadersContext& loadersContext);
-
-    OrthancStone::DicomStructureSet* GetContent()
-    {
-      return content_.get();
-    }
-
-    void SetStructureDisplayState(size_t structureIndex, bool display);
-    
-    bool GetStructureDisplayState(size_t structureIndex) const
-    {
-      return structureVisibility_.at(structureIndex);
-    }
-
-    ~DicomStructureSetLoader();
-    
-    void LoadInstance(const std::string& instanceId, 
-                      const std::vector<std::string>& initiallyVisibleStructures = std::vector<std::string>());
-
-    virtual IExtractedSlice* ExtractSlice(const OrthancStone::CoordinateSystem3D& cuttingPlane) ORTHANC_OVERRIDE;
-
-    void SetStructuresReady();
-
-    bool AreStructuresReady() const;
-  };
-}
--- a/Framework/Deprecated/Loaders/LoaderCache.cpp	Tue Mar 31 11:01:34 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,384 +0,0 @@
-/**
- * Stone of Orthanc
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 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 "LoaderCache.h"
-
-#include "../../StoneException.h"
-#include "OrthancSeriesVolumeProgressiveLoader.h"
-#include "OrthancMultiframeVolumeLoader.h"
-#include "DicomStructureSetLoader.h"
-
-#include "../../Loaders/ILoadersContext.h"
-
-
-#ifdef BGO_ENABLE_DICOMSTRUCTURESETLOADER2
-#include "DicomStructureSetLoader2.h"
-#endif 
- //BGO_ENABLE_DICOMSTRUCTURESETLOADER2
-
-
-#if ORTHANC_ENABLE_WASM == 1
-# include <unistd.h>
-# include "../../Oracle/WebAssemblyOracle.h"
-#else
-# include "../../Oracle/ThreadedOracle.h"
-#endif
-
-#ifdef BGO_ENABLE_DICOMSTRUCTURESETLOADER2
-#include "../../Toolbox/DicomStructureSet2.h"
-#endif 
-//BGO_ENABLE_DICOMSTRUCTURESETLOADER2
-
-#include "../../Volumes/DicomVolumeImage.h"
-#include "../../Volumes/DicomVolumeImageMPRSlicer.h"
-
-#ifdef BGO_ENABLE_DICOMSTRUCTURESETLOADER2
-#include "../../Volumes/DicomStructureSetSlicer2.h"
-#endif 
-//BGO_ENABLE_DICOMSTRUCTURESETLOADER2
-
-#include <Core/OrthancException.h>
-#include <Core/Toolbox.h>
-
-namespace Deprecated
-{
-  LoaderCache::LoaderCache(OrthancStone::ILoadersContext& loadersContext)
-    : loadersContext_(loadersContext)
-  {
-
-  }
-
-  boost::shared_ptr<OrthancSeriesVolumeProgressiveLoader> 
-    LoaderCache::GetSeriesVolumeProgressiveLoader(std::string seriesUuid)
-  {
-    try
-    {
-      
-      // normalize keys a little
-      seriesUuid = Orthanc::Toolbox::StripSpaces(seriesUuid);
-      Orthanc::Toolbox::ToLowerCase(seriesUuid);
-
-      // find in cache
-      if (seriesVolumeProgressiveLoaders_.find(seriesUuid) == seriesVolumeProgressiveLoaders_.end())
-      {
-        std::unique_ptr<OrthancStone::ILoadersContext::ILock> lock(loadersContext_.Lock());
-
-        boost::shared_ptr<OrthancStone::DicomVolumeImage> volumeImage(new OrthancStone::DicomVolumeImage);
-        boost::shared_ptr<OrthancSeriesVolumeProgressiveLoader> loader;
-      
-        // true means "use progressive quality"
-        // false means "load high quality slices only"
-        loader = OrthancSeriesVolumeProgressiveLoader::Create(loadersContext_, volumeImage, false);
-        loader->LoadSeries(seriesUuid);
-        seriesVolumeProgressiveLoaders_[seriesUuid] = loader;
-      }
-      else
-      {
-//        LOG(TRACE) << "LoaderCache::GetSeriesVolumeProgressiveLoader : returning cached loader for seriesUUid = " << seriesUuid;
-      }
-      return seriesVolumeProgressiveLoaders_[seriesUuid];
-    }
-    catch (const Orthanc::OrthancException& e)
-    {
-      if (e.HasDetails())
-      {
-        LOG(ERROR) << "OrthancException in LoaderCache: " << e.What() << " Details: " << e.GetDetails();
-      }
-      else
-      {
-        LOG(ERROR) << "OrthancException in LoaderCache: " << e.What();
-      }
-      throw;
-    }
-    catch (const std::exception& e)
-    {
-      LOG(ERROR) << "std::exception in LoaderCache: " << e.what();
-      throw;
-    }
-    catch (...)
-    {
-      LOG(ERROR) << "Unknown exception in LoaderCache";
-      throw;
-    }
-  }
-
-  boost::shared_ptr<OrthancMultiframeVolumeLoader> LoaderCache::GetMultiframeVolumeLoader(std::string instanceUuid)
-  {
-    // if the loader is not available, let's trigger its creation
-    if(multiframeVolumeLoaders_.find(instanceUuid) == multiframeVolumeLoaders_.end())
-    {
-      GetMultiframeDicomVolumeImageMPRSlicer(instanceUuid);
-    }
-    ORTHANC_ASSERT(multiframeVolumeLoaders_.find(instanceUuid) != multiframeVolumeLoaders_.end());
-
-    return multiframeVolumeLoaders_[instanceUuid];
-  }
-
-  boost::shared_ptr<OrthancStone::DicomVolumeImageMPRSlicer> LoaderCache::GetMultiframeDicomVolumeImageMPRSlicer(std::string instanceUuid)
-  {
-    try
-    {
-      // normalize keys a little
-      instanceUuid = Orthanc::Toolbox::StripSpaces(instanceUuid);
-      Orthanc::Toolbox::ToLowerCase(instanceUuid);
-
-      // find in cache
-      if (dicomVolumeImageMPRSlicers_.find(instanceUuid) == dicomVolumeImageMPRSlicers_.end())
-      {
-        std::unique_ptr<OrthancStone::ILoadersContext::ILock> lock(loadersContext_.Lock());
-        boost::shared_ptr<OrthancStone::DicomVolumeImage> volumeImage(new OrthancStone::DicomVolumeImage);
-        boost::shared_ptr<OrthancMultiframeVolumeLoader> loader;
-        {
-          loader = OrthancMultiframeVolumeLoader::Create(loadersContext_, volumeImage);
-          loader->LoadInstance(instanceUuid);
-        }
-        multiframeVolumeLoaders_[instanceUuid] = loader;
-        boost::shared_ptr<OrthancStone::DicomVolumeImageMPRSlicer> mprSlicer(new OrthancStone::DicomVolumeImageMPRSlicer(volumeImage));
-        dicomVolumeImageMPRSlicers_[instanceUuid] = mprSlicer;
-      }
-      return dicomVolumeImageMPRSlicers_[instanceUuid];
-    }
-    catch (const Orthanc::OrthancException& e)
-    {
-      if (e.HasDetails())
-      {
-        LOG(ERROR) << "OrthancException in LoaderCache: " << e.What() << " Details: " << e.GetDetails();
-      }
-      else
-      {
-        LOG(ERROR) << "OrthancException in LoaderCache: " << e.What();
-      }
-      throw;
-    }
-    catch (const std::exception& e)
-    {
-      LOG(ERROR) << "std::exception in LoaderCache: " << e.what();
-      throw;
-    }
-    catch (...)
-    {
-      LOG(ERROR) << "Unknown exception in LoaderCache";
-      throw;
-    }
-  }
-  
-#ifdef BGO_ENABLE_DICOMSTRUCTURESETLOADER2
-
-  boost::shared_ptr<DicomStructureSetSlicer2> LoaderCache::GetDicomStructureSetSlicer2(std::string instanceUuid)
-  {
-    // if the loader is not available, let's trigger its creation
-    if (dicomStructureSetSlicers2_.find(instanceUuid) == dicomStructureSetSlicers2_.end())
-    {
-      GetDicomStructureSetLoader2(instanceUuid);
-    }
-    ORTHANC_ASSERT(dicomStructureSetSlicers2_.find(instanceUuid) != dicomStructureSetSlicers2_.end());
-
-    return dicomStructureSetSlicers2_[instanceUuid];
-  }
-#endif
-//BGO_ENABLE_DICOMSTRUCTURESETLOADER2
-
-
-  /**
-  This method allows to convert a list of string into a string by 
-  sorting the strings then joining them
-  */
-  static std::string SortAndJoin(const std::vector<std::string>& stringList)
-  {
-    if (stringList.size() == 0)
-    {
-      return "";
-    } 
-    else
-    {
-      std::vector<std::string> sortedStringList = stringList;
-      std::sort(sortedStringList.begin(), sortedStringList.end());
-      std::stringstream s;
-      s << sortedStringList[0];
-      for (size_t i = 1; i < sortedStringList.size(); ++i)
-      {
-        s << "-" << sortedStringList[i];
-      }
-      return s.str();
-    }
-  }
-  
-  boost::shared_ptr<DicomStructureSetLoader> 
-    LoaderCache::GetDicomStructureSetLoader(
-      std::string inInstanceUuid, 
-      const std::vector<std::string>& initiallyVisibleStructures)
-  {
-    try
-    {
-      // normalize keys a little
-      inInstanceUuid = Orthanc::Toolbox::StripSpaces(inInstanceUuid);
-      Orthanc::Toolbox::ToLowerCase(inInstanceUuid);
-
-      std::string initiallyVisibleStructuresKey = 
-        SortAndJoin(initiallyVisibleStructures);
-
-      std::string entryKey = inInstanceUuid + "_" + initiallyVisibleStructuresKey;
-
-      // find in cache
-      if (dicomStructureSetLoaders_.find(entryKey) == dicomStructureSetLoaders_.end())
-      {
-        std::unique_ptr<OrthancStone::ILoadersContext::ILock> lock(loadersContext_.Lock());
-
-        boost::shared_ptr<DicomStructureSetLoader> loader;
-        {
-          loader = DicomStructureSetLoader::Create(loadersContext_);
-          loader->LoadInstance(inInstanceUuid, initiallyVisibleStructures);
-        }
-        dicomStructureSetLoaders_[entryKey] = loader;
-      }
-      return dicomStructureSetLoaders_[entryKey];
-    }
-    catch (const Orthanc::OrthancException& e)
-    {
-      if (e.HasDetails())
-      {
-        LOG(ERROR) << "OrthancException in LoaderCache: " << e.What() << " Details: " << e.GetDetails();
-      }
-      else
-      {
-        LOG(ERROR) << "OrthancException in LoaderCache: " << e.What();
-      }
-      throw;
-    }
-    catch (const std::exception& e)
-    {
-      LOG(ERROR) << "std::exception in LoaderCache: " << e.what();
-      throw;
-    }
-    catch (...)
-    {
-      LOG(ERROR) << "Unknown exception in LoaderCache";
-      throw;
-    }
-  }
-
-#ifdef BGO_ENABLE_DICOMSTRUCTURESETLOADER2
-
-  boost::shared_ptr<DicomStructureSetLoader2> LoaderCache::GetDicomStructureSetLoader2(std::string instanceUuid)
-  {
-    try
-    {
-      // normalize keys a little
-      instanceUuid = Orthanc::Toolbox::StripSpaces(instanceUuid);
-      Orthanc::Toolbox::ToLowerCase(instanceUuid);
-
-      // find in cache
-      if (dicomStructureSetLoaders2_.find(instanceUuid) == dicomStructureSetLoaders2_.end())
-      {
-        boost::shared_ptr<DicomStructureSetLoader2> loader;
-        boost::shared_ptr<DicomStructureSet2> structureSet(new DicomStructureSet2());
-        boost::shared_ptr<DicomStructureSetSlicer2> rtSlicer(new DicomStructureSetSlicer2(structureSet));
-        dicomStructureSetSlicers2_[instanceUuid] = rtSlicer;
-        dicomStructureSets2_[instanceUuid] = structureSet; // to prevent it from being deleted
-        {
-#if ORTHANC_ENABLE_WASM == 1
-          loader.reset(new DicomStructureSetLoader2(*(structureSet.get()), oracle_, oracle_));
-#else
-          LockingEmitter::WriterLock lock(lockingEmitter_);
-          // TODO: clarify lifetimes... this is DANGEROUS!
-          loader.reset(new DicomStructureSetLoader2(*(structureSet.get()), oracle_, lock.GetOracleObservable()));
-#endif
-          loader->LoadInstance(instanceUuid);
-        }
-        dicomStructureSetLoaders2_[instanceUuid] = loader;
-      }
-      return dicomStructureSetLoaders2_[instanceUuid];
-    }
-    catch (const Orthanc::OrthancException& e)
-    {
-      if (e.HasDetails())
-      {
-        LOG(ERROR) << "OrthancException in GetDicomStructureSetLoader2: " << e.What() << " Details: " << e.GetDetails();
-      }
-      else
-      {
-        LOG(ERROR) << "OrthancException in GetDicomStructureSetLoader2: " << e.What();
-      }
-      throw;
-    }
-    catch (const std::exception& e)
-    {
-      LOG(ERROR) << "std::exception in GetDicomStructureSetLoader2: " << e.what();
-      throw;
-    }
-    catch (...)
-    {
-      LOG(ERROR) << "Unknown exception in GetDicomStructureSetLoader2";
-      throw;
-    }
-  }
-
-#endif
-// BGO_ENABLE_DICOMSTRUCTURESETLOADER2
-
-
-  void LoaderCache::ClearCache()
-  {
-    std::unique_ptr<OrthancStone::ILoadersContext::ILock> lock(loadersContext_.Lock());
-    
-#ifndef NDEBUG
-    // ISO way of checking for debug builds
-    DebugDisplayObjRefCounts();
-#endif
-    seriesVolumeProgressiveLoaders_.clear();
-    multiframeVolumeLoaders_.clear();
-    dicomVolumeImageMPRSlicers_.clear();
-    dicomStructureSetLoaders_.clear();
-
-#ifdef BGO_ENABLE_DICOMSTRUCTURESETLOADER2
-    // order is important!
-    dicomStructureSetLoaders2_.clear();
-    dicomStructureSetSlicers2_.clear();
-    dicomStructureSets2_.clear();
-#endif
-// BGO_ENABLE_DICOMSTRUCTURESETLOADER2
-  }
-
-  template<typename T> void DebugDisplayObjRefCountsInMap(
-    const std::string& name, const std::map<std::string, boost::shared_ptr<T> >& myMap)
-  {
-    LOG(TRACE) << "Map \"" << name << "\" ref counts:";
-    size_t i = 0;
-    for (typename std::map<std::string, boost::shared_ptr<T> >::const_iterator 
-           it = myMap.begin(); it != myMap.end(); ++it)
-    {
-      LOG(TRACE) << "  element #" << i << ": ref count = " << it->second.use_count();
-      i++;
-    }
-  }
-
-  void LoaderCache::DebugDisplayObjRefCounts()
-  {
-    DebugDisplayObjRefCountsInMap("seriesVolumeProgressiveLoaders_", seriesVolumeProgressiveLoaders_);
-    DebugDisplayObjRefCountsInMap("multiframeVolumeLoaders_", multiframeVolumeLoaders_);
-    DebugDisplayObjRefCountsInMap("dicomVolumeImageMPRSlicers_", dicomVolumeImageMPRSlicers_);
-    DebugDisplayObjRefCountsInMap("dicomStructureSetLoaders_", dicomStructureSetLoaders_);
-#ifdef BGO_ENABLE_DICOMSTRUCTURESETLOADER2
-    DebugDisplayObjRefCountsInMap("dicomStructureSetLoaders2_", dicomStructureSetLoaders2_);
-    DebugDisplayObjRefCountsInMap("dicomStructureSetSlicers2_", dicomStructureSetSlicers2_);
-#endif
-//BGO_ENABLE_DICOMSTRUCTURESETLOADER2
-  }
-}
--- a/Framework/Deprecated/Loaders/LoaderCache.h	Tue Mar 31 11:01:34 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,97 +0,0 @@
-/**
- * Stone of Orthanc
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 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/LockingEmitter.h"
-#include "../../Volumes/DicomVolumeImageMPRSlicer.h"
-#include "OrthancSeriesVolumeProgressiveLoader.h"
-#include "OrthancMultiframeVolumeLoader.h"
-#include "DicomStructureSetLoader.h"
-
-#include <boost/shared_ptr.hpp>
-
-#include <map>
-#include <string>
-#include <vector>
-
-namespace OrthancStone
-{
-  class ILoadersContext;
-}
-
-namespace Deprecated
-{
-  class LoaderCache
-  {
-  public:
-    LoaderCache(OrthancStone::ILoadersContext& loadersContext);
-
-    boost::shared_ptr<OrthancSeriesVolumeProgressiveLoader>
-      GetSeriesVolumeProgressiveLoader      (std::string seriesUuid);
-    
-    boost::shared_ptr<OrthancStone::DicomVolumeImageMPRSlicer>
-      GetMultiframeDicomVolumeImageMPRSlicer(std::string instanceUuid);
-
-    boost::shared_ptr<OrthancMultiframeVolumeLoader>
-      GetMultiframeVolumeLoader(std::string instanceUuid);
-
-    boost::shared_ptr<DicomStructureSetLoader>
-      GetDicomStructureSetLoader(
-        std::string instanceUuid,
-        const std::vector<std::string>& initiallyVisibleStructures);
-
-#ifdef BGO_ENABLE_DICOMSTRUCTURESETLOADER2
-    boost::shared_ptr<DicomStructureSetLoader2>
-      GetDicomStructureSetLoader2(std::string instanceUuid);
-
-    boost::shared_ptr<DicomStructureSetSlicer2>
-      GetDicomStructureSetSlicer2(std::string instanceUuid);
-#endif 
-    //BGO_ENABLE_DICOMSTRUCTURESETLOADER2
-
-    void ClearCache();
-
-  private:
-    
-    void DebugDisplayObjRefCounts();
-
-    OrthancStone::ILoadersContext& loadersContext_;
-
-    std::map<std::string, boost::shared_ptr<OrthancSeriesVolumeProgressiveLoader> >
-      seriesVolumeProgressiveLoaders_;
-    std::map<std::string, boost::shared_ptr<OrthancMultiframeVolumeLoader> >
-      multiframeVolumeLoaders_;
-    std::map<std::string, boost::shared_ptr<OrthancStone::DicomVolumeImageMPRSlicer> >
-      dicomVolumeImageMPRSlicers_;
-    std::map<std::string, boost::shared_ptr<DicomStructureSetLoader> >
-      dicomStructureSetLoaders_;
-#ifdef BGO_ENABLE_DICOMSTRUCTURESETLOADER2
-    std::map<std::string, boost::shared_ptr<DicomStructureSetLoader2> >
-      dicomStructureSetLoaders2_;
-    std::map<std::string, boost::shared_ptr<DicomStructureSet2> >
-      dicomStructureSets2_;
-    std::map<std::string, boost::shared_ptr<DicomStructureSetSlicer2> >
-      dicomStructureSetSlicers2_;
-#endif 
-    //BGO_ENABLE_DICOMSTRUCTURESETLOADER2
-  };
-}
-
--- a/Framework/Deprecated/Loaders/LoaderStateMachine.cpp	Tue Mar 31 11:01:34 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,219 +0,0 @@
-/**
- * Stone of Orthanc
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU Affero General Public License
- * as published by the Free Software Foundation, either version 3 of
- * the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Affero General Public License for more details.
- * 
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#include "LoaderStateMachine.h"
-
-#include "../../Loaders/ILoadersContext.h"
-
-#include <Core/OrthancException.h>
-
-namespace Deprecated
-{
-  void LoaderStateMachine::State::Handle(const OrthancStone::OrthancRestApiCommand::SuccessMessage& message)
-  {
-    throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
-  }
-      
-
-  void LoaderStateMachine::State::Handle(const OrthancStone::GetOrthancImageCommand::SuccessMessage& message)
-  {
-    throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
-  }
-
-      
-  void LoaderStateMachine::State::Handle(const OrthancStone::GetOrthancWebViewerJpegCommand::SuccessMessage& message)
-  {
-    throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
-  }
-
-
-  void LoaderStateMachine::Schedule(OrthancStone::OracleCommandBase* command)
-  {
-    LOG(TRACE) << "LoaderStateMachine(" << std::hex << this << std::dec << ")::Schedule()";
-
-    std::unique_ptr<OrthancStone::OracleCommandBase> protection(command);
-
-    if (command == NULL)
-    {
-      throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
-    }
-      
-    if (!command->HasPayload())
-    {
-      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange,
-                                      "The payload must contain the next state");
-    }
-    pendingCommands_.push_back(protection.release());
-
-    Step();
-  }
-
-
-  void LoaderStateMachine::Start()
-  {
-    LOG(TRACE) << "LoaderStateMachine(" << std::hex << this << std::dec << ")::Start()";
-
-    if (active_)
-    {
-      LOG(TRACE) << "LoaderStateMachine::Start() called while active_ is true";
-      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
-    }
-
-    active_ = true;
-
-    for (size_t i = 0; i < simultaneousDownloads_; i++)
-    {
-      Step();
-    }
-  }
-
-
-  void LoaderStateMachine::Step()
-  {
-    if (!pendingCommands_.empty() &&
-        activeCommands_ < simultaneousDownloads_)
-    {
-
-      OrthancStone::IOracleCommand* nextCommand = pendingCommands_.front();
-
-      LOG(TRACE) << "    LoaderStateMachine(" << std::hex << this << std::dec << 
-        ")::Step(): activeCommands_ (" << activeCommands_ << 
-        ") < simultaneousDownloads_ (" << simultaneousDownloads_ << 
-        ") --> will Schedule command addr " << std::hex << nextCommand << std::dec;
-
-      {
-        std::unique_ptr<OrthancStone::ILoadersContext::ILock> lock(loadersContext_.Lock());
-        boost::shared_ptr<IObserver> observer(GetSharedObserver());
-        lock->Schedule(observer, 0, nextCommand); // TODO: priority!
-      }
-      pendingCommands_.pop_front();
-
-      activeCommands_++;
-    }
-    else
-    {
-      LOG(TRACE) << "    LoaderStateMachine(" << std::hex << this << std::dec << 
-        ")::Step(): activeCommands_ (" << activeCommands_ << 
-        ") >= simultaneousDownloads_ (" << simultaneousDownloads_ << 
-        ") --> will NOT Schedule command";
-    }
-  }
-
-
-  void LoaderStateMachine::Clear()
-  {
-    LOG(TRACE) << "LoaderStateMachine(" << std::hex << this << std::dec << ")::Clear()";
-    for (PendingCommands::iterator it = pendingCommands_.begin();
-         it != pendingCommands_.end(); ++it)
-    {
-      delete *it;
-    }
-
-    pendingCommands_.clear();
-  }
-  
-
-  void LoaderStateMachine::HandleExceptionMessage(const OrthancStone::OracleCommandExceptionMessage& message)
-  {
-    LOG(ERROR) << "LoaderStateMachine::HandleExceptionMessage: error in the state machine, stopping all processing";
-    LOG(ERROR) << "Error: " << message.GetException().What() << " Details: " <<
-      message.GetException().GetDetails();
-      Clear();
-  }
-
-  template <typename T>
-  void LoaderStateMachine::HandleSuccessMessage(const T& message)
-  {
-    if (activeCommands_ <= 0) {
-      LOG(ERROR) << "LoaderStateMachine(" << std::hex << this << std::dec << ")::HandleSuccessMessage : activeCommands_ should be > 0 but is: " << activeCommands_;
-    }
-    else {
-      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() << " Details: " << e.GetDetails();
-        Clear();
-      }
-    }
-  }
-
-
-  LoaderStateMachine::LoaderStateMachine(
-    OrthancStone::ILoadersContext& loadersContext)
-    : loadersContext_(loadersContext)
-    , active_(false)
-    , simultaneousDownloads_(4)
-    , activeCommands_(0)
-  {
-    using OrthancStone::ILoadersContext;
-
-    LOG(TRACE) 
-      << "LoaderStateMachine(" << std::hex << this 
-      << std::dec << ")::LoaderStateMachine()";
-  }
-
-  void LoaderStateMachine::PostConstructor()
-  {
-    std::unique_ptr<OrthancStone::ILoadersContext::ILock>
-      lock(loadersContext_.Lock());
-
-    OrthancStone::IObservable& observable = lock->GetOracleObservable();
-
-    // TODO => Move this out of constructor
-    Register<OrthancStone::OrthancRestApiCommand::SuccessMessage>(
-      observable, &LoaderStateMachine::HandleSuccessMessage);
-    Register<OrthancStone::GetOrthancImageCommand::SuccessMessage>(
-      observable, &LoaderStateMachine::HandleSuccessMessage);
-    Register<OrthancStone::GetOrthancWebViewerJpegCommand::SuccessMessage>(
-      observable, &LoaderStateMachine::HandleSuccessMessage);
-    Register<OrthancStone::OracleCommandExceptionMessage>(
-      observable, &LoaderStateMachine::HandleExceptionMessage);
-  }
-
-  LoaderStateMachine::~LoaderStateMachine()
-  {
-    LOG(TRACE) << "LoaderStateMachine(" << std::hex << this << std::dec << ")::~LoaderStateMachine()";
-    Clear();
-  }
-
-  void LoaderStateMachine::SetSimultaneousDownloads(unsigned int count)
-  {
-    if (active_)
-    {
-      LOG(ERROR) << "LoaderStateMachine::SetSimultaneousDownloads called while active_ is true";
-      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
-    }
-    else if (count == 0)
-    {
-      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);        
-    }
-    else
-    {
-      simultaneousDownloads_ = count;
-    }
-  }
-}
--- a/Framework/Deprecated/Loaders/LoaderStateMachine.h	Tue Mar 31 11:01:34 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,129 +0,0 @@
-/**
- * Stone of Orthanc
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 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/ObserverBase.h"
-#include "../../Oracle/GetOrthancImageCommand.h"
-#include "../../Oracle/GetOrthancWebViewerJpegCommand.h"
-#include "../../Oracle/IOracle.h"
-#include "../../Oracle/OracleCommandExceptionMessage.h"
-#include "../../Oracle/OrthancRestApiCommand.h"
-
-#include <Core/IDynamicObject.h>
-
-#include <list>
-
-namespace OrthancStone
-{
-  class ILoadersContext;
-}
-
-namespace Deprecated
-{
-  /**
-     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.
-
-     To use it, you need to create commands that derive from State.
-
-     You need to initialize them with the object that must be called when 
-     an answer is received. 
-  */
-
-  class LoaderStateMachine : public OrthancStone::ObserverBase<LoaderStateMachine>
-  {
-  public:
-    class State : public Orthanc::IDynamicObject
-    {
-    private:
-      LoaderStateMachine&  that_;
-
-    public:
-      State(LoaderStateMachine& that) :
-      that_(that)
-      {
-      }
-
-      State(const State& currentState) :
-      that_(currentState.that_)
-      {
-      }
-
-      void Schedule(OrthancStone::OracleCommandBase* command) const
-      {
-        that_.Schedule(command);
-      }
-
-      template <typename T>
-      T& GetLoader() const
-      {
-        return dynamic_cast<T&>(that_);
-      }
-      
-      virtual void Handle(const OrthancStone::OrthancRestApiCommand::SuccessMessage& message);
-      
-      virtual void Handle(const OrthancStone::GetOrthancImageCommand::SuccessMessage& message);
-      
-      virtual void Handle(const OrthancStone::GetOrthancWebViewerJpegCommand::SuccessMessage& message);
-    };
-
-    void Schedule(OrthancStone::OracleCommandBase* command);
-
-    void Start();
-
-  private:
-    void Step();
-
-    void Clear();
-
-    void HandleExceptionMessage(const OrthancStone::OracleCommandExceptionMessage& message);
-
-    template <typename T>
-    void HandleSuccessMessage(const T& message);
-
-    typedef std::list<OrthancStone::IOracleCommand*>  PendingCommands;
-
-    OrthancStone::ILoadersContext&  loadersContext_;
-    bool                            active_;
-    unsigned int                    simultaneousDownloads_;
-    PendingCommands                 pendingCommands_;
-    unsigned int                    activeCommands_;
-
-
-  public:
-    LoaderStateMachine(OrthancStone::ILoadersContext& loadersContext);
-
-    void PostConstructor();
-
-    virtual ~LoaderStateMachine();
-
-    bool IsActive() const
-    {
-      return active_;
-    }
-
-    void SetSimultaneousDownloads(unsigned int count);  
-  };
-}
--- a/Framework/Deprecated/Loaders/OrthancMultiframeVolumeLoader.cpp	Tue Mar 31 11:01:34 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,593 +0,0 @@
-/**
- * Stone of Orthanc
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU Affero General Public License
- * as published by the Free Software Foundation, either version 3 of
- * the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Affero General Public License for more details.
- * 
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#include "OrthancMultiframeVolumeLoader.h"
-
-#include <Core/Endianness.h>
-#include <Core/Toolbox.h>
-
-namespace Deprecated
-{
-  class OrthancMultiframeVolumeLoader::LoadRTDoseGeometry : public LoaderStateMachine::State
-  {
-  private:
-    std::unique_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 OrthancStone::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.LookupStringValue(s, Orthanc::DICOM_TAG_SOP_CLASS_UID, false))
-    {
-      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat,
-                                      "DICOM file without SOP class UID");
-    }
-    else
-    {
-      return s;
-    }
-  }
-    
-
-  class OrthancMultiframeVolumeLoader::LoadGeometry : public State
-  {
-  public:
-    LoadGeometry(OrthancMultiframeVolumeLoader& that) :
-    State(that)
-    {
-    }
-      
-    virtual void Handle(const OrthancStone::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::unique_ptr<Orthanc::DicomMap> dicom(new Orthanc::DicomMap);
-      dicom->FromDicomAsJson(body);
-
-      if (OrthancStone::StringToSopClassUid(GetSopClassUid(*dicom)) == OrthancStone::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::unique_ptr<OrthancStone::OrthancRestApiCommand> command(new OrthancStone::OrthancRestApiCommand);
-        command->SetUri("/instances/" + loader.GetInstanceId() + "/content/" +
-                        Orthanc::DICOM_TAG_GRID_FRAME_OFFSET_VECTOR.Format());
-        command->AcquirePayload(new LoadRTDoseGeometry(loader, dicom.release()));
-
-        Schedule(command.release());
-      }
-      else
-      {
-        loader.SetGeometry(*dicom);
-      }
-    }
-  };
-
-  class OrthancMultiframeVolumeLoader::LoadTransferSyntax : public State
-  {
-  public:
-    LoadTransferSyntax(OrthancMultiframeVolumeLoader& that) :
-    State(that)
-    {
-    }
-      
-    virtual void Handle(const OrthancStone::OrthancRestApiCommand::SuccessMessage& message)
-    {
-      GetLoader<OrthancMultiframeVolumeLoader>().SetTransferSyntax(message.GetAnswer());
-    }
-  };
-
-  class OrthancMultiframeVolumeLoader::LoadUncompressedPixelData : public State
-  {
-  public:
-    LoadUncompressedPixelData(OrthancMultiframeVolumeLoader& that) :
-    State(that)
-    {
-    }
-      
-    virtual void Handle(const OrthancStone::OrthancRestApiCommand::SuccessMessage& message)
-    {
-      GetLoader<OrthancMultiframeVolumeLoader>().SetUncompressedPixelData(message.GetAnswer());
-    }
-  };
-
-  const std::string& OrthancMultiframeVolumeLoader::GetInstanceId() const
-  {
-    if (IsActive())
-    {
-      return instanceId_;
-    }
-    else
-    {
-      LOG(ERROR) << "OrthancMultiframeVolumeLoader::GetInstanceId(): (!IsActive())";
-      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
-    }
-  }
-
-  void OrthancMultiframeVolumeLoader::ScheduleFrameDownloads()
-  {
-    if (transferSyntaxUid_.empty() ||
-        !volume_->HasGeometry())
-    {
-      return;
-    }
-    /*
-      1.2.840.10008.1.2	Implicit VR Endian: Default Transfer Syntax for DICOM
-      1.2.840.10008.1.2.1	Explicit VR Little Endian
-      1.2.840.10008.1.2.2	Explicit VR Big Endian
-
-      See https://www.dicomlibrary.com/dicom/transfer-syntax/
-    */
-    if (transferSyntaxUid_ == "1.2.840.10008.1.2" ||
-        transferSyntaxUid_ == "1.2.840.10008.1.2.1" ||
-        transferSyntaxUid_ == "1.2.840.10008.1.2.2")
-    {
-      std::unique_ptr<OrthancStone::OrthancRestApiCommand> command(new OrthancStone::OrthancRestApiCommand);
-      command->SetHttpHeader("Accept-Encoding", "gzip");
-      command->SetUri("/instances/" + instanceId_ + "/content/" +
-                      Orthanc::DICOM_TAG_PIXEL_DATA.Format() + "/0");
-      command->AcquirePayload(new LoadUncompressedPixelData(*this));
-      Schedule(command.release());
-    }
-    else
-    {
-      throw Orthanc::OrthancException(
-        Orthanc::ErrorCode_NotImplemented,
-        "No support for multiframe instances with transfer syntax: " + transferSyntaxUid_);
-    }
-  }
-
-  void OrthancMultiframeVolumeLoader::SetTransferSyntax(const std::string& transferSyntax)
-  {
-    transferSyntaxUid_ = Orthanc::Toolbox::StripSpaces(transferSyntax);
-    ScheduleFrameDownloads();
-  }
-
-  void OrthancMultiframeVolumeLoader::SetGeometry(const Orthanc::DicomMap& dicom)
-  {
-    OrthancStone::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 OrthancStone::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();
-
-    {
-      OrthancStone::VolumeImageGeometry geometry;
-      geometry.SetSizeInVoxels(width, height, depth);
-      geometry.SetAxialGeometry(parameters.GetGeometry());
-      geometry.SetVoxelDimensions(parameters.GetPixelSpacingX(),
-                                  parameters.GetPixelSpacingY(), spacingZ);
-      volume_->Initialize(geometry, format, true /* Do compute range */);
-    }
-
-    volume_->GetPixelData().Clear();
-
-    ScheduleFrameDownloads();
-
-
-
-    BroadcastMessage(OrthancStone::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));
-  }
-
-  ORTHANC_FORCE_INLINE
-    static void CopyPixel(uint16_t& target, const void* source)
-  {
-    // TODO - check alignement?
-    target = le16toh(*reinterpret_cast<const uint16_t*>(source));
-  }
-
-  ORTHANC_FORCE_INLINE
-    static void CopyPixel(int16_t& target, const void* source)
-  {
-    // byte swapping is the same for unsigned and signed integers
-    // (the sign bit is always stored with the MSByte)
-    uint16_t* targetUp = reinterpret_cast<uint16_t*>(&target);
-    CopyPixel(*targetUp, source);
-  }
-
-  template <typename T>
-  void OrthancMultiframeVolumeLoader::CopyPixelDataAndComputeDistribution(
-    const std::string& pixelData, std::map<T,uint64_t>& distribution)
-  {
-    OrthancStone::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;
-    }
-
-    // first pass to initialize map
-    {
-      const uint8_t* source = reinterpret_cast<const uint8_t*>(pixelData.c_str());
-
-      for (unsigned int z = 0; z < depth; z++)
-      {
-        for (unsigned int y = 0; y < height; y++)
-        {
-          for (unsigned int x = 0; x < width; x++)
-          {
-            T value;
-            CopyPixel(value, source);
-            distribution[value] = 0;
-            source += bpp;
-          }
-        }
-      }
-    }
-
-    {
-      const uint8_t* source = reinterpret_cast<const uint8_t*>(pixelData.c_str());
-
-      for (unsigned int z = 0; z < depth; z++)
-      {
-        OrthancStone::ImageBuffer3D::SliceWriter writer(target, OrthancStone::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);
-
-            distribution[*target] += 1;
-
-            target++;
-            source += bpp;
-          }
-        }
-      }
-    }
-  }
-
-  template <typename T>
-  void OrthancMultiframeVolumeLoader::ComputeMinMaxWithOutlierRejection(
-    const std::map<T, uint64_t>& distribution)
-  {
-    if (distribution.size() == 0)
-    {
-      LOG(ERROR) << "ComputeMinMaxWithOutlierRejection -- Volume image empty.";
-    }
-    else
-    {
-      OrthancStone::ImageBuffer3D& target = volume_->GetPixelData();
-
-      const uint64_t width = target.GetWidth();
-      const uint64_t height = target.GetHeight();
-      const uint64_t depth = target.GetDepth();
-      const uint64_t voxelCount = width * height * depth;
-
-      // now that we have distribution[pixelValue] == numberOfPixelsWithValue
-      // compute number of values and check (assertion) that it is equal to 
-      // width * height * depth 
-      {
-        typename std::map<T, uint64_t>::const_iterator it = distribution.begin();
-        uint64_t totalCount = 0;
-        distributionRawMin_ = static_cast<float>(it->first);
-
-        while (it != distribution.end())
-        {
-          T pixelValue = it->first;
-          uint64_t count = it->second;
-          totalCount += count;
-          it++;
-          if (it == distribution.end())
-            distributionRawMax_ = static_cast<float>(pixelValue);
-        }
-        LOG(INFO) << "Volume image. First distribution value = " 
-          << static_cast<float>(distributionRawMin_) 
-          << " | Last distribution value = " 
-          << static_cast<float>(distributionRawMax_);
-
-        if (totalCount != voxelCount)
-        {
-          LOG(ERROR) << "Internal error in dose distribution computation. TC (" 
-            << totalCount << ") != VoxC (" << voxelCount;
-          throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
-        }
-      }
-
-      // compute the number of voxels to reject at each end of the distribution
-      uint64_t endRejectionCount = static_cast<uint64_t>(
-        outliersHalfRejectionRate_ * voxelCount);
-
-      if (endRejectionCount > voxelCount)
-      {
-        LOG(ERROR) << "Internal error in dose distribution computation."
-          << " endRejectionCount = " << endRejectionCount
-          << " | voxelCount = " << voxelCount;
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
-      }
-
-      // this will contain the actual distribution minimum after outlier 
-      // rejection
-      T resultMin = 0;
-
-      // then start from start and remove pixel values up to 
-      // endRejectionCount voxels rejected
-      {
-        typename std::map<T, uint64_t>::const_iterator it = distribution.begin();
-        
-        uint64_t currentCount = 0;
-
-        while (it != distribution.end())
-        {
-          T pixelValue = it->first;
-          uint64_t count = it->second;
-
-          // if this pixelValue crosses the rejection threshold, let's set it
-          // and exit the loop
-          if ((currentCount <= endRejectionCount) &&
-              (currentCount + count > endRejectionCount))
-          {
-            resultMin = pixelValue;
-            break;
-          }
-          else
-          {
-            currentCount += count;
-          }
-          // and continue walking along the distribution
-          it++;
-        }
-      }
-
-      // this will contain the actual distribution maximum after outlier 
-      // rejection
-      T resultMax = 0;
-      // now start from END and remove pixel values up to 
-      // endRejectionCount voxels rejected
-      {
-        typename std::map<T, uint64_t>::const_reverse_iterator it = distribution.rbegin();
-
-        uint64_t currentCount = 0;
-
-        while (it != distribution.rend())
-        {
-          T pixelValue = it->first;
-          uint64_t count = it->second;
-
-          if ((currentCount <= endRejectionCount) &&
-              (currentCount + count > endRejectionCount))
-          {
-            resultMax = pixelValue;
-            break;
-          }
-          else
-          {
-            currentCount += count;
-          }
-          // and continue walking along the distribution
-          it++;
-        }
-      }
-      if (resultMin > resultMax)
-      {
-        LOG(ERROR) << "Internal error in dose distribution computation! " << 
-          "resultMin (" << resultMin << ") > resultMax (" << resultMax << ")";
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
-      }
-      computedDistributionMin_ = static_cast<float>(resultMin);
-      computedDistributionMax_ = static_cast<float>(resultMax);
-    }
-  }
-
-  template <typename T>
-  void OrthancMultiframeVolumeLoader::CopyPixelDataAndComputeMinMax(
-    const std::string& pixelData)
-  {
-    std::map<T, uint64_t> distribution;
-    CopyPixelDataAndComputeDistribution(pixelData, distribution);
-    ComputeMinMaxWithOutlierRejection(distribution);
-  }
-
-  void OrthancMultiframeVolumeLoader::SetUncompressedPixelData(const std::string& pixelData)
-  {
-    switch (volume_->GetPixelData().GetFormat())
-    {
-      case Orthanc::PixelFormat_Grayscale32:
-        CopyPixelDataAndComputeMinMax<uint32_t>(pixelData);
-        break;
-      case Orthanc::PixelFormat_Grayscale16:
-        CopyPixelDataAndComputeMinMax<uint16_t>(pixelData);
-        break;
-      case Orthanc::PixelFormat_SignedGrayscale16:
-        CopyPixelDataAndComputeMinMax<int16_t>(pixelData);
-        break;
-      default:
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
-    }
-
-    volume_->IncrementRevision();
-
-    pixelDataLoaded_ = true;
-    BroadcastMessage(OrthancStone::DicomVolumeImage::ContentUpdatedMessage(*volume_));
-  }
-  
-  bool OrthancMultiframeVolumeLoader::HasGeometry() const
-  {
-    return volume_->HasGeometry();
-  }
-
-  const OrthancStone::VolumeImageGeometry& OrthancMultiframeVolumeLoader::GetImageGeometry() const
-  {
-    return volume_->GetGeometry();
-  }
-
-  OrthancMultiframeVolumeLoader::OrthancMultiframeVolumeLoader(
-    OrthancStone::ILoadersContext& loadersContext,
-    boost::shared_ptr<OrthancStone::DicomVolumeImage> volume,
-    float outliersHalfRejectionRate) 
-    : LoaderStateMachine(loadersContext)
-    , volume_(volume)
-    , pixelDataLoaded_(false)
-    , outliersHalfRejectionRate_(outliersHalfRejectionRate)
-    , distributionRawMin_(0)
-    , distributionRawMax_(0)
-    , computedDistributionMin_(0)
-    , computedDistributionMax_(0)
-  {
-    if (volume.get() == NULL)
-    {
-      throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
-    }
-  }
-
-
-  boost::shared_ptr<OrthancMultiframeVolumeLoader>
-    OrthancMultiframeVolumeLoader::Create(
-      OrthancStone::ILoadersContext& loadersContext, 
-      boost::shared_ptr<OrthancStone::DicomVolumeImage> volume, 
-      float outliersHalfRejectionRate /*= 0.0005*/)
-  {
-    boost::shared_ptr<OrthancMultiframeVolumeLoader> obj(
-      new OrthancMultiframeVolumeLoader(
-        loadersContext,
-        volume,
-        outliersHalfRejectionRate));
-    obj->LoaderStateMachine::PostConstructor();
-    return obj;
-  }
-
-  OrthancMultiframeVolumeLoader::~OrthancMultiframeVolumeLoader()
-  {
-    LOG(TRACE) << "OrthancMultiframeVolumeLoader::~OrthancMultiframeVolumeLoader()";
-  }
-  
-  void OrthancMultiframeVolumeLoader::GetDistributionMinMax
-  (float& minValue, float& maxValue) const
-  {
-    if (distributionRawMin_ == 0 && distributionRawMax_ == 0)
-    {
-      LOG(WARNING) << "GetDistributionMinMaxWithOutliersRejection called before computation!";
-    }
-    minValue = distributionRawMin_;
-    maxValue = distributionRawMax_;
-  }
-  
-  void OrthancMultiframeVolumeLoader::GetDistributionMinMaxWithOutliersRejection
-    (float& minValue, float& maxValue) const
-  {
-    if (computedDistributionMin_ == 0 && computedDistributionMax_ == 0)
-    {
-      LOG(WARNING) << "GetDistributionMinMaxWithOutliersRejection called before computation!";
-    }
-    minValue = computedDistributionMin_;
-    maxValue = computedDistributionMax_;
-  }
-
-  void OrthancMultiframeVolumeLoader::LoadInstance(const std::string& instanceId)
-  {
-    Start();
-
-    instanceId_ = instanceId;
-
-    {
-      std::unique_ptr<OrthancStone::OrthancRestApiCommand> command(new OrthancStone::OrthancRestApiCommand);
-      command->SetHttpHeader("Accept-Encoding", "gzip");
-      command->SetUri("/instances/" + instanceId + "/tags");
-      command->AcquirePayload(new LoadGeometry(*this));
-      Schedule(command.release());
-    }
-
-    {
-      std::unique_ptr<OrthancStone::OrthancRestApiCommand> command(new OrthancStone::OrthancRestApiCommand);
-      command->SetUri("/instances/" + instanceId + "/metadata/TransferSyntax");
-      command->AcquirePayload(new LoadTransferSyntax(*this));
-      Schedule(command.release());
-    }
-  }
-}
--- a/Framework/Deprecated/Loaders/OrthancMultiframeVolumeLoader.h	Tue Mar 31 11:01:34 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,123 +0,0 @@
-/**
- * Stone of Orthanc
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU Affero General Public License
- * as published by the Free Software Foundation, either version 3 of
- * the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Affero General Public License for more details.
- * 
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#pragma once
-
-#include "LoaderStateMachine.h"
-#include "../../Volumes/DicomVolumeImage.h"
-#include "../Volumes/IGeometryProvider.h"
-
-#include <boost/shared_ptr.hpp>
-
-namespace Deprecated
-{
-  class OrthancMultiframeVolumeLoader :
-    public LoaderStateMachine,
-    public OrthancStone::IObservable,
-    public IGeometryProvider
-  {
-  private:
-    class LoadRTDoseGeometry;
-    class LoadGeometry;
-    class LoadTransferSyntax;    
-    class LoadUncompressedPixelData;
-
-    boost::shared_ptr<OrthancStone::DicomVolumeImage>  volume_;
-    std::string                          instanceId_;
-    std::string                          transferSyntaxUid_;
-    bool                                 pixelDataLoaded_;
-    float                                outliersHalfRejectionRate_;
-    float                                distributionRawMin_;
-    float                                distributionRawMax_;
-    float                                computedDistributionMin_;
-    float                                computedDistributionMax_;
-
-    const std::string& GetInstanceId() const;
-
-    void ScheduleFrameDownloads();
-
-    void SetTransferSyntax(const std::string& transferSyntax);
-
-    void SetGeometry(const Orthanc::DicomMap& dicom);
-
-
-    /**
-    This method will :
-    
-    - copy the pixel values from the response to the volume image
-    - compute the maximum and minimum value while discarding the
-      outliersHalfRejectionRate_ fraction of the outliers from both the start 
-      and the end of the distribution.
-
-      In English, this means that, if the volume dataset contains a few extreme
-      values very different from the rest (outliers) that we want to get rid of,
-      this method allows to do so.
-
-      If you supply 0.005, for instance, it means 1% of the extreme values will
-      be rejected (0.5% on each side of the distribution)
-    */
-    template <typename T>
-    void CopyPixelDataAndComputeMinMax(const std::string& pixelData);
-      
-    /** Service method for CopyPixelDataAndComputeMinMax*/
-    template <typename T>
-    void CopyPixelDataAndComputeDistribution(
-      const std::string& pixelData, 
-      std::map<T, uint64_t>& distribution);
-
-    /** Service method for CopyPixelDataAndComputeMinMax*/
-    template <typename T>
-    void ComputeMinMaxWithOutlierRejection(
-      const std::map<T, uint64_t>& distribution);
-
-    void SetUncompressedPixelData(const std::string& pixelData);
-
-    bool HasGeometry() const;
-    const OrthancStone::VolumeImageGeometry& GetImageGeometry() const;
-
-  protected:
-    OrthancMultiframeVolumeLoader(
-      OrthancStone::ILoadersContext& loadersContext,
-      boost::shared_ptr<OrthancStone::DicomVolumeImage> volume,
-      float outliersHalfRejectionRate);
-  public:
-
-    static boost::shared_ptr<OrthancMultiframeVolumeLoader> Create(
-      OrthancStone::ILoadersContext& loadersContext,
-      boost::shared_ptr<OrthancStone::DicomVolumeImage> volume,
-      float outliersHalfRejectionRate = 0.0005);
-
-    virtual ~OrthancMultiframeVolumeLoader();
-
-    bool IsPixelDataLoaded() const
-    {
-      return pixelDataLoaded_;
-    }
-
-    void GetDistributionMinMax
-      (float& minValue, float& maxValue) const;
-
-    void GetDistributionMinMaxWithOutliersRejection
-      (float& minValue, float& maxValue) const;
-
-    void LoadInstance(const std::string& instanceId);
-  };
-}
--- a/Framework/Deprecated/Loaders/OrthancSeriesVolumeProgressiveLoader.cpp	Tue Mar 31 11:01:34 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,577 +0,0 @@
-/**
- * Stone of Orthanc
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 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 "../../StoneException.h"
-#include "../../Loaders/ILoadersContext.h"
-#include "../../Loaders/BasicFetchingItemsSorter.h"
-#include "../../Loaders/BasicFetchingStrategy.h"
-#include "../../Toolbox/GeometryToolbox.h"
-#include "../../Volumes/DicomVolumeImageMPRSlicer.h"
-
-#include <Core/Images/ImageProcessing.h>
-#include <Core/OrthancException.h>
-
-namespace Deprecated
-{
-  using OrthancStone::ILoadersContext;
-
-  class OrthancSeriesVolumeProgressiveLoader::ExtractedSlice : public OrthancStone::DicomVolumeImageMPRSlicer::Slice
-  {
-  private:
-    const OrthancSeriesVolumeProgressiveLoader&  that_;
-
-  public:
-    ExtractedSlice(const OrthancSeriesVolumeProgressiveLoader& that,
-                   const OrthancStone::CoordinateSystem3D& plane) :
-      OrthancStone::DicomVolumeImageMPRSlicer::Slice(*that.volume_, plane),
-      that_(that)
-    {
-      if (IsValid())
-      {
-        if (GetProjection() == OrthancStone::VolumeProjection_Axial)
-        {
-          // 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 
-          SetRevision(that_.seriesGeometry_.GetSliceRevision(GetSliceIndex()));
-        }
-      
-        if (that_.strategy_.get() != NULL &&
-            GetProjection() == OrthancStone::VolumeProjection_Axial)
-        {
-          that_.strategy_->SetCurrent(GetSliceIndex());
-        }
-      }
-    }
-  };
-    
-  void OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::CheckSlice(
-    size_t index, const OrthancStone::DicomInstanceParameters& reference) const
-  {
-    const OrthancStone::DicomInstanceParameters& slice = *slices_[index];
-      
-    if (!OrthancStone::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 (!OrthancStone::LinearAlgebra::IsNear(reference.GetPixelSpacingX(), slice.GetPixelSpacingX()) ||
-        !OrthancStone::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 OrthancStone::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())
-    {
-      LOG(ERROR) << "OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::CheckSliceIndex(size_t index): (!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(OrthancStone::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 OrthancStone::VolumeImageGeometry);
-    }
-    else
-    {
-      slices_.reserve(slices.GetSlicesCount());
-      slicesRevision_.resize(slices.GetSlicesCount(), 0);
-
-      for (size_t i = 0; i < slices.GetSlicesCount(); i++)
-      {
-        const OrthancStone::DicomInstanceParameters& slice =
-          dynamic_cast<const OrthancStone::DicomInstanceParameters&>(slices.GetSlicePayload(i));
-        slices_.push_back(new OrthancStone::DicomInstanceParameters(slice));
-      }
-
-      CheckVolume();
-
-      double spacingZ;
-
-      if (slices.ComputeSpacingBetweenSlices(spacingZ))
-      {
-        LOG(TRACE) << "Computed spacing between slices: " << spacingZ << "mm";
-      
-        const OrthancStone::DicomInstanceParameters& parameters = *slices_[0];
-
-        geometry_.reset(new OrthancStone::VolumeImageGeometry);
-        geometry_->SetSizeInVoxels(parameters.GetImageInformation().GetWidth(),
-                                   parameters.GetImageInformation().GetHeight(),
-                                   static_cast<unsigned int>(slices.GetSlicesCount()));
-        geometry_->SetAxialGeometry(slices.GetSliceGeometry(0));
-        geometry_->SetVoxelDimensions(parameters.GetPixelSpacingX(),
-                                      parameters.GetPixelSpacingY(), spacingZ);
-      }
-      else
-      {
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadGeometry,
-                                        "The origins of the slices of a volume image are not regularly spaced");
-     }
-    }
-  }
-
-
-  const OrthancStone::VolumeImageGeometry& OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::GetImageGeometry() const
-  {
-    if (!HasGeometry())
-    {
-      LOG(ERROR) << "OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::GetImageGeometry(): (!HasGeometry())";
-      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
-    }
-    else
-    {
-      assert(slices_.size() == geometry_->GetDepth());
-      return *geometry_;
-    }
-  }
-
-
-  const OrthancStone::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 OrthancStone::OracleCommandBase& command)
-  {
-    assert(command.HasPayload());
-    return dynamic_cast< const Orthanc::SingleValueObject<unsigned int>& >(command.GetPayload()).GetValue();
-  }
-
-
-  void OrthancSeriesVolumeProgressiveLoader::ScheduleNextSliceDownload()
-  {
-    assert(strategy_.get() != NULL);
-      
-    unsigned int sliceIndex = 0, quality = 0;
-      
-    if (strategy_->GetNext(sliceIndex, quality))
-    {
-      if (!progressiveQuality_)
-      {
-        ORTHANC_ASSERT(quality == QUALITY_00, "INTERNAL ERROR. quality != QUALITY_00 in "
-                       << "OrthancSeriesVolumeProgressiveLoader::ScheduleNextSliceDownload");
-      }
-
-      const OrthancStone::DicomInstanceParameters& slice = seriesGeometry_.GetSliceParameters(sliceIndex);
-          
-      const std::string& instance = slice.GetOrthancInstanceIdentifier();
-      if (instance.empty())
-      {
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
-      }
-
-      std::unique_ptr<OrthancStone::OracleCommandBase> command;
-        
-      if (!progressiveQuality_ || quality == QUALITY_02)
-      {
-        std::unique_ptr<OrthancStone::GetOrthancImageCommand> tmp(new OrthancStone::GetOrthancImageCommand);
-        // TODO: review the following comment. 
-        // - Commented out by bgo on 2019-07-19 | reason: Alain has seen cases 
-        //   where gzipping the uint16 image took 11 sec to produce 5mb. 
-        //   The unzipped request was much much faster.
-        // - Re-enabled on 2019-07-30. Reason: in Web Assembly, the browser 
-        //   does not use the Accept-Encoding header and always requests
-        //   compression. Furthermore, NOT 
-        tmp->SetHttpHeader("Accept-Encoding", "gzip");
-        tmp->SetHttpHeader("Accept", std::string(Orthanc::EnumerationToString(Orthanc::MimeType_Pam)));
-        tmp->SetInstanceUri(instance, slice.GetExpectedPixelFormat());
-        tmp->SetExpectedPixelFormat(slice.GetExpectedPixelFormat());
-        //LOG(INFO) 
-        //  << "OrthancSeriesVolumeProgressiveLoader.ScheduleNextSliceDownload()"
-        //  << " sliceIndex = " << sliceIndex << " slice quality = " << quality 
-        //  << " URI = " << tmp->GetUri();
-        command.reset(tmp.release());
-      }
-      else // progressive mode is true AND quality is not final (different from QUALITY_02
-      {
-        std::unique_ptr<OrthancStone::GetOrthancWebViewerJpegCommand> tmp(
-          new OrthancStone::GetOrthancWebViewerJpegCommand);
-
-        // TODO: review the following comment. Commented out by bgo on 2019-07-19
-        // (gzip for jpeg seems overkill)
-        //tmp->SetHttpHeader("Accept-Encoding", "gzip");
-        tmp->SetInstance(instance);
-        tmp->SetQuality((quality == 0 ? 50 : 90)); // QUALITY_00 is Jpeg50 while QUALITY_01 is Jpeg90
-        tmp->SetExpectedPixelFormat(slice.GetExpectedPixelFormat());
-        LOG(TRACE)
-          << "OrthancSeriesVolumeProgressiveLoader.ScheduleNextSliceDownload()"
-          << " sliceIndex = " << sliceIndex << " slice quality = " << quality;
-        command.reset(tmp.release());
-      }
-
-      command->AcquirePayload(new Orthanc::SingleValueObject<unsigned int>(sliceIndex));
-      
-      {
-        std::unique_ptr<OrthancStone::ILoadersContext::ILock> lock(loadersContext_.Lock());
-        boost::shared_ptr<IObserver> observer(GetSharedObserver());
-        lock->Schedule(observer, 0, command.release()); // TODO: priority!
-      }
-    }
-    else
-    {
-      // loading is finished!
-      volumeImageReadyInHighQuality_ = true;
-      BroadcastMessage(OrthancSeriesVolumeProgressiveLoader::VolumeImageReadyInHighQuality(*this));
-    }
-  }
-
-/**
-   This is called in response to GET "/series/XXXXXXXXXXXXX/instances-tags"
-*/
-  void OrthancSeriesVolumeProgressiveLoader::LoadGeometry(const OrthancStone::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();
-
-      OrthancStone::SlicesSorter slices;
-        
-      for (size_t i = 0; i < instances.size(); i++)
-      {
-        Orthanc::DicomMap dicom;
-        dicom.FromDicomAsJson(body[instances[i]]);
-
-        std::unique_ptr<OrthancStone::DicomInstanceParameters> instance(new OrthancStone::DicomInstanceParameters(dicom));
-        instance->SetOrthancInstanceIdentifier(instances[i]);
-
-        // the 3D plane corresponding to the slice
-        OrthancStone::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 OrthancStone::DicomInstanceParameters& parameters = seriesGeometry_.GetSliceParameters(0);
-        
-      volume_->Initialize(seriesGeometry_.GetImageGeometry(), parameters.GetExpectedPixelFormat());
-      volume_->SetDicomParameters(parameters);
-      volume_->GetPixelData().Clear();
-
-      // If we are in progressive mode, the Fetching strategy will first request QUALITY_00, then QUALITY_01, then
-      // QUALITY_02... Otherwise, it's only QUALITY_00
-      unsigned int maxQuality = QUALITY_00;
-      if (progressiveQuality_)
-        maxQuality = QUALITY_02;
-
-      strategy_.reset(new OrthancStone::BasicFetchingStrategy(
-        sorter_->CreateSorter(static_cast<unsigned int>(slicesCount)),
-        maxQuality));
-
-      assert(simultaneousDownloads_ != 0);
-      for (unsigned int i = 0; i < simultaneousDownloads_; i++)
-      {
-        ScheduleNextSliceDownload();
-      }
-    }
-
-    slicesQuality_.resize(slicesCount, 0);
-
-    BroadcastMessage(OrthancStone::DicomVolumeImage::GeometryReadyMessage(*volume_));
-  }
-
-
-  void OrthancSeriesVolumeProgressiveLoader::SetSliceContent(unsigned int sliceIndex,
-                                                             const Orthanc::ImageAccessor& image,
-                                                             unsigned int quality)
-  {
-    ORTHANC_ASSERT(sliceIndex < slicesQuality_.size() &&
-           slicesQuality_.size() == volume_->GetPixelData().GetDepth());
-      
-    if (!progressiveQuality_)
-    {
-      ORTHANC_ASSERT(quality                    == QUALITY_00);
-      ORTHANC_ASSERT(slicesQuality_[sliceIndex] == QUALITY_00);
-    }
-
-    if (quality >= slicesQuality_[sliceIndex])
-    {
-      {
-        OrthancStone::ImageBuffer3D::SliceWriter writer(volume_->GetPixelData(), 
-                                                        OrthancStone::VolumeProjection_Axial, 
-                                                        sliceIndex);
-        
-        Orthanc::ImageProcessing::Copy(writer.GetAccessor(), image);
-      }
-
-      volume_->IncrementRevision();
-      seriesGeometry_.IncrementSliceRevision(sliceIndex);
-      slicesQuality_[sliceIndex] = quality;
-
-      BroadcastMessage(OrthancStone::DicomVolumeImage::ContentUpdatedMessage(*volume_));
-    }
-    LOG(TRACE) << "SetSliceContent sliceIndex = " << sliceIndex << " -- will "
-      << " now call ScheduleNextSliceDownload()";
-    ScheduleNextSliceDownload();
-  }
-
-  void OrthancSeriesVolumeProgressiveLoader::LoadBestQualitySliceContent(
-    const OrthancStone::GetOrthancImageCommand::SuccessMessage& message)
-  {
-    unsigned int quality = QUALITY_00;
-    if (progressiveQuality_)
-      quality = QUALITY_02;
-
-    SetSliceContent(GetSliceIndexPayload(message.GetOrigin()), 
-                                         message.GetImage(),
-                                         quality);
-  }
-
-  void OrthancSeriesVolumeProgressiveLoader::LoadJpegSliceContent(
-    const OrthancStone::GetOrthancWebViewerJpegCommand::SuccessMessage& message)
-  {
-    ORTHANC_ASSERT(progressiveQuality_, "INTERNAL ERROR: OrthancSeriesVolumeProgressiveLoader::LoadJpegSliceContent"
-                   << " called while progressiveQuality_ is false!");
-
-    LOG(TRACE) << "OrthancSeriesVolumeProgressiveLoader::LoadJpegSliceContent";
-    unsigned int quality;
-      
-    switch (dynamic_cast<const OrthancStone::GetOrthancWebViewerJpegCommand&>(message.GetOrigin()).GetQuality())
-    {
-      case 50:
-        quality = QUALITY_00;
-        break;
-
-      case 90:
-        quality = QUALITY_01;
-        break;
-
-      default:
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
-    }
-      
-    SetSliceContent(GetSliceIndexPayload(message.GetOrigin()), message.GetImage(), quality);
-  }
-
-  OrthancSeriesVolumeProgressiveLoader::OrthancSeriesVolumeProgressiveLoader(
-    OrthancStone::ILoadersContext& loadersContext,
-    boost::shared_ptr<OrthancStone::DicomVolumeImage> volume,
-    bool progressiveQuality)
-    : loadersContext_(loadersContext)
-    , active_(false)
-    , progressiveQuality_(progressiveQuality)
-    , simultaneousDownloads_(4)
-    , volume_(volume)
-    , sorter_(new OrthancStone::BasicFetchingItemsSorter::Factory)
-    , volumeImageReadyInHighQuality_(false)
-  {
-  }
-
-  boost::shared_ptr<OrthancSeriesVolumeProgressiveLoader> 
-    OrthancSeriesVolumeProgressiveLoader::Create(
-      OrthancStone::ILoadersContext& loadersContext,
-      boost::shared_ptr<OrthancStone::DicomVolumeImage> volume,
-      bool progressiveQuality)
-  {
-    std::auto_ptr<OrthancStone::ILoadersContext::ILock> lock(loadersContext.Lock());
-
-    boost::shared_ptr<OrthancSeriesVolumeProgressiveLoader> obj(
-        new OrthancSeriesVolumeProgressiveLoader(
-          loadersContext, volume, progressiveQuality));
-
-    obj->Register<OrthancStone::OrthancRestApiCommand::SuccessMessage>(
-      lock->GetOracleObservable(),
-      &OrthancSeriesVolumeProgressiveLoader::LoadGeometry);
-
-    obj->Register<OrthancStone::GetOrthancImageCommand::SuccessMessage>(
-      lock->GetOracleObservable(),
-      &OrthancSeriesVolumeProgressiveLoader::LoadBestQualitySliceContent);
-
-    obj->Register<OrthancStone::GetOrthancWebViewerJpegCommand::SuccessMessage>(
-      lock->GetOracleObservable(),
-      &OrthancSeriesVolumeProgressiveLoader::LoadJpegSliceContent);
-
-    return obj;
-  }
-
-
-  OrthancSeriesVolumeProgressiveLoader::~OrthancSeriesVolumeProgressiveLoader()
-  {
-    LOG(TRACE) << "OrthancSeriesVolumeProgressiveLoader::~OrthancSeriesVolumeProgressiveLoader()";
-  }
-
-  void OrthancSeriesVolumeProgressiveLoader::SetSimultaneousDownloads(unsigned int count)
-  {
-    if (active_)
-    {
-      LOG(ERROR) << "OrthancSeriesVolumeProgressiveLoader::SetSimultaneousDownloads(): (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_)
-    {
-      LOG(ERROR) << "OrthancSeriesVolumeProgressiveLoader::LoadSeries(const std::string& seriesId): (active_)";
-      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
-    }
-    else
-    {
-      active_ = true;
-
-      std::unique_ptr<OrthancStone::OrthancRestApiCommand> command(new OrthancStone::OrthancRestApiCommand);
-      command->SetUri("/series/" + seriesId + "/instances-tags");
-      {
-        std::unique_ptr<OrthancStone::ILoadersContext::ILock> lock(loadersContext_.Lock());
-        boost::shared_ptr<IObserver> observer(GetSharedObserver());
-        lock->Schedule(observer, 0, command.release()); //TODO: priority!
-      }
-    }
-  }
-  
-
-  OrthancStone::IVolumeSlicer::IExtractedSlice* 
-  OrthancSeriesVolumeProgressiveLoader::ExtractSlice(const OrthancStone::CoordinateSystem3D& cuttingPlane)
-  {
-    if (volume_->HasGeometry())
-    {
-      return new ExtractedSlice(*this, cuttingPlane);
-    }
-    else
-    {
-      return new IVolumeSlicer::InvalidSlice;
-    }
-  }
-}
--- a/Framework/Deprecated/Loaders/OrthancSeriesVolumeProgressiveLoader.h	Tue Mar 31 11:01:34 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,180 +0,0 @@
-/**
- * Stone of Orthanc
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 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 "../../Loaders/IFetchingItemsSorter.h"
-#include "../../Loaders/IFetchingStrategy.h"
-#include "../../Messages/IObservable.h"
-#include "../../Messages/ObserverBase.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 "../Volumes/IGeometryProvider.h"
-
-
-#include <boost/shared_ptr.hpp>
-
-namespace OrthancStone
-{
-  class ILoadersContext;
-}
-
-namespace Deprecated
-{
-  /**
-    This class is used to manage the progressive loading of a volume that
-    is stored in a Dicom series.
-  */
-  class OrthancSeriesVolumeProgressiveLoader : 
-    public OrthancStone::ObserverBase<OrthancSeriesVolumeProgressiveLoader>,
-    public OrthancStone::IObservable,
-    public OrthancStone::IVolumeSlicer,
-    public IGeometryProvider
-  {
-  private:
-    static const unsigned int QUALITY_00 = 0;
-    static const unsigned int QUALITY_01 = 1;
-    static const unsigned int QUALITY_02 = 2;
-        
-    class ExtractedSlice;
-    
-    /** Helper class internal to OrthancSeriesVolumeProgressiveLoader */
-    class SeriesGeometry : public boost::noncopyable
-    {
-    private:
-      void CheckSlice(size_t index,
-                      const OrthancStone::DicomInstanceParameters& reference) const;
-    
-      void CheckVolume() const;
-
-      void Clear();
-
-      void CheckSliceIndex(size_t index) const;
-
-      std::unique_ptr<OrthancStone::VolumeImageGeometry>     geometry_;
-      std::vector<OrthancStone::DicomInstanceParameters*>  slices_;
-      std::vector<uint64_t>                  slicesRevision_;
-
-    public:
-      ~SeriesGeometry()
-      {
-        Clear();
-      }
-
-      void ComputeGeometry(OrthancStone::SlicesSorter& slices);
-
-      virtual bool HasGeometry() const
-      {
-        return geometry_.get() != NULL;
-      }
-
-      virtual const OrthancStone::VolumeImageGeometry& GetImageGeometry() const;
-
-      const OrthancStone::DicomInstanceParameters& GetSliceParameters(size_t index) const;
-
-      uint64_t GetSliceRevision(size_t index) const;
-
-      void IncrementSliceRevision(size_t index);
-    };
-
-    void ScheduleNextSliceDownload();
-
-    void LoadGeometry(const OrthancStone::OrthancRestApiCommand::SuccessMessage& message);
-
-    void SetSliceContent(unsigned int sliceIndex,
-                         const Orthanc::ImageAccessor& image,
-                         unsigned int quality);
-
-    void LoadBestQualitySliceContent(const OrthancStone::GetOrthancImageCommand::SuccessMessage& message);
-
-    void LoadJpegSliceContent(const OrthancStone::GetOrthancWebViewerJpegCommand::SuccessMessage& message);
-
-    OrthancStone::ILoadersContext&  loadersContext_;
-    bool                                          active_;
-    bool                                          progressiveQuality_;
-    unsigned int                                  simultaneousDownloads_;
-    SeriesGeometry                                seriesGeometry_;
-    boost::shared_ptr<OrthancStone::DicomVolumeImage> volume_;
-    std::unique_ptr<OrthancStone::IFetchingItemsSorter::IFactory> sorter_;
-    std::unique_ptr<OrthancStone::IFetchingStrategy> strategy_;
-    std::vector<unsigned int>     slicesQuality_;
-    bool                          volumeImageReadyInHighQuality_;
-    
-    OrthancSeriesVolumeProgressiveLoader(
-      OrthancStone::ILoadersContext& loadersContext,
-      boost::shared_ptr<OrthancStone::DicomVolumeImage> volume,
-      bool progressiveQuality);
-  
-  public:
-    ORTHANC_STONE_DEFINE_ORIGIN_MESSAGE(__FILE__, __LINE__, VolumeImageReadyInHighQuality, OrthancSeriesVolumeProgressiveLoader);
-
-    /**
-    See doc for the progressiveQuality_ field
-    */
-    static boost::shared_ptr<OrthancSeriesVolumeProgressiveLoader> Create(
-      OrthancStone::ILoadersContext& context,
-      boost::shared_ptr<OrthancStone::DicomVolumeImage> volume,
-      bool progressiveQuality = false);
-
-    virtual ~OrthancSeriesVolumeProgressiveLoader();
-
-    void SetSimultaneousDownloads(unsigned int count);
-
-    bool IsVolumeImageReadyInHighQuality() const
-    {
-      return volumeImageReadyInHighQuality_;
-    }
-
-    void LoadSeries(const std::string& seriesId);
-
-    /**
-    This getter is used by clients that do not receive the geometry through
-    subscribing, for instance if they are created or listening only AFTER the
-    "geometry loaded" message is broadcast 
-    */
-    bool HasGeometry() const ORTHANC_OVERRIDE
-    {
-      return seriesGeometry_.HasGeometry();
-    }
-
-    /**
-    Same remark as HasGeometry
-    */
-    const OrthancStone::VolumeImageGeometry& GetImageGeometry() const ORTHANC_OVERRIDE
-    {
-      return seriesGeometry_.GetImageGeometry();
-    }
-
-    /**
-    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 OrthancStone::CoordinateSystem3D& cuttingPlane) ORTHANC_OVERRIDE;
-  };
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Loaders/DicomStructureSetLoader.cpp	Fri Apr 03 16:13:06 2020 +0200
@@ -0,0 +1,428 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "DicomStructureSetLoader.h"
+
+#include "../../Scene2D/PolylineSceneLayer.h"
+#include "../../StoneException.h"
+#include "../../Toolbox/GeometryToolbox.h"
+
+#include <Core/Toolbox.h>
+
+#include <algorithm>
+
+#if 0
+bool logbgo233 = false;
+bool logbgo115 = false;
+#endif
+
+namespace OrthancStone
+{
+
+#if 0
+  void DumpDicomMap(std::ostream& o, const Orthanc::DicomMap& dicomMap)
+  {
+    using namespace std;
+    //ios_base::fmtflags state = o.flags();
+    //o.flags(ios::right | ios::hex);
+    //o << "(" << setfill('0') << setw(4) << tag.GetGroup()
+    //  << "," << setw(4) << tag.GetElement() << ")";
+    //o.flags(state);
+    Json::Value val;
+    dicomMap.Serialize(val);
+    o << val;
+    //return o;
+  }
+#endif
+
+
+  class DicomStructureSetLoader::AddReferencedInstance : public LoaderStateMachine::State
+  {
+  private:
+    std::string instanceId_;
+      
+  public:
+    AddReferencedInstance(DicomStructureSetLoader& that,
+                          const std::string& instanceId) :
+      State(that),
+      instanceId_(instanceId)
+    {
+    }
+
+    virtual void Handle(const OrthancStone::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_++;
+        loader.SetStructuresReady();
+      }
+    }
+  };
+
+
+  // State that converts a "SOP Instance UID" to an Orthanc identifier
+  class DicomStructureSetLoader::LookupInstance : public LoaderStateMachine::State
+  {
+  private:
+    std::string  sopInstanceUid_;
+      
+  public:
+    LookupInstance(DicomStructureSetLoader& that,
+                   const std::string& sopInstanceUid) :
+      State(that),
+      sopInstanceUid_(sopInstanceUid)
+    {
+    }
+
+    virtual void Handle(const OrthancStone::OrthancRestApiCommand::SuccessMessage& message)
+    {
+#if 0
+      LOG(TRACE) << "DicomStructureSetLoader::LookupInstance::Handle() (SUCCESS)";
+#endif
+      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")
+      {
+        std::stringstream msg;
+        msg << "Unknown resource! message.GetAnswer() = " << message.GetAnswer() << " message.GetAnswerHeaders() = ";
+        for (OrthancStone::OrthancRestApiCommand::HttpHeaders::const_iterator it = message.GetAnswerHeaders().begin();
+             it != message.GetAnswerHeaders().end(); ++it)
+        {
+          msg << "\nkey: \"" << it->first << "\" value: \"" << it->second << "\"\n";
+        }
+        const std::string msgStr = msg.str();
+        LOG(ERROR) << msgStr;
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_UnknownResource);          
+      }
+
+      const std::string instanceId = lookup[0]["ID"].asString();
+
+      {
+        std::unique_ptr<OrthancStone::OrthancRestApiCommand> command(new OrthancStone::OrthancRestApiCommand);
+        command->SetHttpHeader("Accept-Encoding", "gzip");
+        std::string uri = "/instances/" + instanceId + "/tags";
+        command->SetUri(uri);
+        command->AcquirePayload(new AddReferencedInstance(loader, instanceId));
+        Schedule(command.release());
+      }
+    }
+  };
+
+
+  class DicomStructureSetLoader::LoadStructure : public LoaderStateMachine::State
+  {
+  public:
+    LoadStructure(DicomStructureSetLoader& that) :
+    State(that)
+    {
+    }
+    
+    virtual void Handle(const OrthancStone::OrthancRestApiCommand::SuccessMessage& message)
+    {
+#if 0
+      if (logbgo115)
+        LOG(TRACE) << "DicomStructureSetLoader::LoadStructure::Handle() (SUCCESS)";
+#endif
+      DicomStructureSetLoader& loader = GetLoader<DicomStructureSetLoader>();
+        
+      {
+        OrthancPlugins::FullOrthancDataset dicom(message.GetAnswer());
+        loader.content_.reset(new OrthancStone::DicomStructureSet(dicom));
+        size_t structureCount = loader.content_->GetStructuresCount();
+        loader.structureVisibility_.resize(structureCount);
+        bool everythingVisible = false;
+        if ((loader.initiallyVisibleStructures_.size() == 1)
+          && (loader.initiallyVisibleStructures_[0].size() == 1)
+          && (loader.initiallyVisibleStructures_[0][0] == '*'))
+        {
+          everythingVisible = true;
+        }
+
+        for (size_t i = 0; i < structureCount; ++i)
+        {
+          // if a single "*" string is supplied, this means we want everything 
+          // to be visible...
+          if(everythingVisible)
+          {
+            loader.structureVisibility_.at(i) = true;
+          }
+          else
+          {
+            // otherwise, we only enable visibility for those structures whose 
+            // names are mentioned in the initiallyVisibleStructures_ array
+            const std::string& structureName = loader.content_->GetStructureName(i);
+
+            std::vector<std::string>::iterator foundIt =
+              std::find(
+                loader.initiallyVisibleStructures_.begin(),
+                loader.initiallyVisibleStructures_.end(),
+                structureName);
+            std::vector<std::string>::iterator endIt = loader.initiallyVisibleStructures_.end();
+            if (foundIt != endIt)
+              loader.structureVisibility_.at(i) = true;
+            else
+              loader.structureVisibility_.at(i) = false;
+          }
+        }
+      }
+
+      // Some (admittedly invalid) Dicom files have empty values in the 
+      // 0008,1155 tag. We try our best to cope with this.
+      std::set<std::string> instances;
+      std::set<std::string> nonEmptyInstances;
+      loader.content_->GetReferencedInstances(instances);
+      for (std::set<std::string>::const_iterator
+        it = instances.begin(); it != instances.end(); ++it)
+      {
+        std::string instance = Orthanc::Toolbox::StripSpaces(*it);
+        if(instance != "")
+          nonEmptyInstances.insert(instance);
+      }
+
+      loader.countReferencedInstances_ = 
+        static_cast<unsigned int>(nonEmptyInstances.size());
+
+      for (std::set<std::string>::const_iterator
+        it = nonEmptyInstances.begin(); it != nonEmptyInstances.end(); ++it)
+      {
+        std::unique_ptr<OrthancStone::OrthancRestApiCommand> command(new OrthancStone::OrthancRestApiCommand);
+        command->SetUri("/tools/lookup");
+        command->SetMethod(Orthanc::HttpMethod_Post);
+        command->SetBody(*it);
+        command->AcquirePayload(new LookupInstance(loader, *it));
+        Schedule(command.release());
+      }
+    }
+  };
+    
+
+  class DicomStructureSetLoader::Slice : public IExtractedSlice
+  {
+  private:
+    const OrthancStone::DicomStructureSet&  content_;
+    uint64_t                  revision_;
+    bool                      isValid_;
+    std::vector<bool>         visibility_;
+      
+  public:
+    /**
+    The visibility vector must either:
+    - be empty
+    or
+    - contain the same number of items as the number of structures in the 
+      structure set.
+    In the first case (empty vector), all the structures are displayed.
+    In the second case, the visibility of each structure is defined by the 
+    content of the vector at the corresponding index.
+    */
+    Slice(const OrthancStone::DicomStructureSet& content,
+          uint64_t revision,
+          const OrthancStone::CoordinateSystem3D& cuttingPlane,
+          std::vector<bool> visibility = std::vector<bool>()) 
+      : content_(content)
+      , revision_(revision)
+      , visibility_(visibility)
+    {
+      ORTHANC_ASSERT((visibility_.size() == content_.GetStructuresCount())
+        || (visibility_.size() == 0u));
+
+      bool opposite;
+
+      const OrthancStone::Vector normal = content.GetNormal();
+      isValid_ = (
+        OrthancStone::GeometryToolbox::IsParallelOrOpposite(opposite, normal, cuttingPlane.GetNormal()) ||
+        OrthancStone::GeometryToolbox::IsParallelOrOpposite(opposite, normal, cuttingPlane.GetAxisX()) ||
+        OrthancStone::GeometryToolbox::IsParallelOrOpposite(opposite, normal, cuttingPlane.GetAxisY()));
+    }
+      
+    virtual bool IsValid()
+    {
+      return isValid_;
+    }
+
+    virtual uint64_t GetRevision()
+    {
+      return revision_;
+    }
+
+    virtual OrthancStone::ISceneLayer* CreateSceneLayer(
+      const OrthancStone::ILayerStyleConfigurator* configurator,
+      const OrthancStone::CoordinateSystem3D& cuttingPlane)
+    {
+      assert(isValid_);
+
+      std::unique_ptr<OrthancStone::PolylineSceneLayer> layer(new OrthancStone::PolylineSceneLayer);
+      layer->SetThickness(2);
+
+      for (size_t i = 0; i < content_.GetStructuresCount(); i++)
+      {
+        if ((visibility_.size() == 0) || visibility_.at(i))
+        {
+          const OrthancStone::Color& color = content_.GetStructureColor(i);
+
+#ifdef USE_BOOST_UNION_FOR_POLYGONS 
+          std::vector< std::vector<OrthancStone::Point2D> > 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].x, polygons[j][k].y);
+    }
+
+              layer->AddChain(chain, true /* closed */, color);
+  }
+        }
+#else
+          std::vector< std::pair<OrthancStone::Point2D, OrthancStone::Point2D> > segments;
+
+          if (content_.ProjectStructure(segments, i, cuttingPlane))
+          {
+            for (size_t j = 0; j < segments.size(); j++)
+            {
+              OrthancStone::PolylineSceneLayer::Chain chain;
+              chain.resize(2);
+
+              chain[0] = OrthancStone::ScenePoint2D(segments[j].first.x, segments[j].first.y);
+              chain[1] = OrthancStone::ScenePoint2D(segments[j].second.x, segments[j].second.y);
+
+              layer->AddChain(chain, false /* NOT closed */, color);
+            }
+          }
+#endif        
+        }
+      }
+
+      return layer.release();
+    }
+  };
+    
+
+  DicomStructureSetLoader::DicomStructureSetLoader(
+    OrthancStone::ILoadersContext& loadersContext) 
+    : LoaderStateMachine(loadersContext)
+    , loadersContext_(loadersContext)
+    , revision_(0)
+    , countProcessedInstances_(0)
+    , countReferencedInstances_(0)
+    , structuresReady_(false)
+  {
+  }
+   
+    
+  boost::shared_ptr<Deprecated::DicomStructureSetLoader> DicomStructureSetLoader::Create(OrthancStone::ILoadersContext& loadersContext)
+  {
+    boost::shared_ptr<DicomStructureSetLoader> obj(
+      new DicomStructureSetLoader(
+        loadersContext));
+    obj->LoaderStateMachine::PostConstructor();
+    return obj;
+
+  }
+
+  void DicomStructureSetLoader::SetStructureDisplayState(size_t structureIndex, bool display)
+  {
+    structureVisibility_.at(structureIndex) = display;
+    revision_++;
+  }
+
+  DicomStructureSetLoader::~DicomStructureSetLoader()
+  {
+    LOG(TRACE) << "DicomStructureSetLoader::~DicomStructureSetLoader()";
+  }
+
+  void DicomStructureSetLoader::LoadInstance(
+    const std::string& instanceId, 
+    const std::vector<std::string>& initiallyVisibleStructures)
+  {
+    Start();
+      
+    instanceId_ = instanceId;
+    initiallyVisibleStructures_ = initiallyVisibleStructures;
+
+    {
+      std::unique_ptr<OrthancStone::OrthancRestApiCommand> command(new OrthancStone::OrthancRestApiCommand);
+      command->SetHttpHeader("Accept-Encoding", "gzip");
+
+      std::string uri = "/instances/" + instanceId + "/tags?ignore-length=3006-0050";
+
+      command->SetUri(uri);
+      command->AcquirePayload(new LoadStructure(*this));
+      Schedule(command.release());
+    }
+  }
+
+
+  OrthancStone::IVolumeSlicer::IExtractedSlice* DicomStructureSetLoader::ExtractSlice(const OrthancStone::CoordinateSystem3D& cuttingPlane)
+  {
+    if (content_.get() == NULL)
+    {
+      // Geometry is not available yet
+      return new OrthancStone::IVolumeSlicer::InvalidSlice;
+    }
+    else
+    {
+      return new Slice(*content_, revision_, cuttingPlane, structureVisibility_);
+    }
+  }
+
+  void DicomStructureSetLoader::SetStructuresReady()
+  {
+    ORTHANC_ASSERT(!structuresReady_);
+    structuresReady_ = true;
+    BroadcastMessage(DicomStructureSetLoader::StructuresReady(*this));
+  }
+
+  bool DicomStructureSetLoader::AreStructuresReady() const
+  {
+    return structuresReady_;
+  }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Loaders/DicomStructureSetLoader.h	Fri Apr 03 16:13:06 2020 +0200
@@ -0,0 +1,104 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../../Toolbox/DicomStructureSet.h"
+#include "../../Volumes/IVolumeSlicer.h"
+#include "LoaderStateMachine.h"
+
+#include <vector>
+
+namespace OrthancStone
+{
+  class DicomStructureSetLoader :
+    public LoaderStateMachine,
+    public OrthancStone::IVolumeSlicer,
+    public OrthancStone::IObservable
+  {
+  private:
+    class Slice;
+
+    // States of LoaderStateMachine
+    class AddReferencedInstance;   // 3rd state
+    class LookupInstance;          // 2nd state
+    class LoadStructure;           // 1st state
+    
+    OrthancStone::ILoadersContext&                    loadersContext_;
+    std::unique_ptr<OrthancStone::DicomStructureSet>  content_;
+    uint64_t                                          revision_;
+    std::string                                       instanceId_;
+    unsigned int                                      countProcessedInstances_;
+    unsigned int                                      countReferencedInstances_;  
+
+    // will be set to true once the loading is finished
+    bool                                              structuresReady_;
+
+    /**
+    At load time, these strings are used to initialize the structureVisibility_ 
+    vector.
+
+    As a special case, if initiallyVisibleStructures_ contains a single string
+    that is '*', ALL structures will be made visible.
+    */
+    std::vector<std::string> initiallyVisibleStructures_;
+
+    /**
+    Contains the "Should this structure be displayed?" flag for all structures.
+    Only filled when structures are loaded.
+
+    Changing this value directly affects the rendering
+    */
+    std::vector<bool>                  structureVisibility_;
+
+  protected:
+    DicomStructureSetLoader(OrthancStone::ILoadersContext& loadersContext);
+
+  public:
+    ORTHANC_STONE_DEFINE_ORIGIN_MESSAGE(__FILE__, __LINE__, StructuresReady, DicomStructureSetLoader);
+
+    static boost::shared_ptr<DicomStructureSetLoader> Create(
+      OrthancStone::ILoadersContext& loadersContext);
+
+    OrthancStone::DicomStructureSet* GetContent()
+    {
+      return content_.get();
+    }
+
+    void SetStructureDisplayState(size_t structureIndex, bool display);
+    
+    bool GetStructureDisplayState(size_t structureIndex) const
+    {
+      return structureVisibility_.at(structureIndex);
+    }
+
+    ~DicomStructureSetLoader();
+    
+    void LoadInstance(const std::string& instanceId, 
+                      const std::vector<std::string>& initiallyVisibleStructures = std::vector<std::string>());
+
+    virtual IExtractedSlice* ExtractSlice(const OrthancStone::CoordinateSystem3D& cuttingPlane) ORTHANC_OVERRIDE;
+
+    void SetStructuresReady();
+
+    bool AreStructuresReady() const;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Loaders/LoaderCache.cpp	Fri Apr 03 16:13:06 2020 +0200
@@ -0,0 +1,384 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 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 "LoaderCache.h"
+
+#include "../../StoneException.h"
+#include "OrthancSeriesVolumeProgressiveLoader.h"
+#include "OrthancMultiframeVolumeLoader.h"
+#include "DicomStructureSetLoader.h"
+
+#include "../../Loaders/ILoadersContext.h"
+
+
+#ifdef BGO_ENABLE_DICOMSTRUCTURESETLOADER2
+#include "DicomStructureSetLoader2.h"
+#endif 
+ //BGO_ENABLE_DICOMSTRUCTURESETLOADER2
+
+
+#if ORTHANC_ENABLE_WASM == 1
+# include <unistd.h>
+# include "../../Oracle/WebAssemblyOracle.h"
+#else
+# include "../../Oracle/ThreadedOracle.h"
+#endif
+
+#ifdef BGO_ENABLE_DICOMSTRUCTURESETLOADER2
+#include "../../Toolbox/DicomStructureSet2.h"
+#endif 
+//BGO_ENABLE_DICOMSTRUCTURESETLOADER2
+
+#include "../../Volumes/DicomVolumeImage.h"
+#include "../../Volumes/DicomVolumeImageMPRSlicer.h"
+
+#ifdef BGO_ENABLE_DICOMSTRUCTURESETLOADER2
+#include "../../Volumes/DicomStructureSetSlicer2.h"
+#endif 
+//BGO_ENABLE_DICOMSTRUCTURESETLOADER2
+
+#include <Core/OrthancException.h>
+#include <Core/Toolbox.h>
+
+namespace OrthancStone
+{
+  LoaderCache::LoaderCache(OrthancStone::ILoadersContext& loadersContext)
+    : loadersContext_(loadersContext)
+  {
+
+  }
+
+  boost::shared_ptr<OrthancSeriesVolumeProgressiveLoader> 
+    LoaderCache::GetSeriesVolumeProgressiveLoader(std::string seriesUuid)
+  {
+    try
+    {
+      
+      // normalize keys a little
+      seriesUuid = Orthanc::Toolbox::StripSpaces(seriesUuid);
+      Orthanc::Toolbox::ToLowerCase(seriesUuid);
+
+      // find in cache
+      if (seriesVolumeProgressiveLoaders_.find(seriesUuid) == seriesVolumeProgressiveLoaders_.end())
+      {
+        std::unique_ptr<OrthancStone::ILoadersContext::ILock> lock(loadersContext_.Lock());
+
+        boost::shared_ptr<OrthancStone::DicomVolumeImage> volumeImage(new OrthancStone::DicomVolumeImage);
+        boost::shared_ptr<OrthancSeriesVolumeProgressiveLoader> loader;
+      
+        // true means "use progressive quality"
+        // false means "load high quality slices only"
+        loader = OrthancSeriesVolumeProgressiveLoader::Create(loadersContext_, volumeImage, false);
+        loader->LoadSeries(seriesUuid);
+        seriesVolumeProgressiveLoaders_[seriesUuid] = loader;
+      }
+      else
+      {
+//        LOG(TRACE) << "LoaderCache::GetSeriesVolumeProgressiveLoader : returning cached loader for seriesUUid = " << seriesUuid;
+      }
+      return seriesVolumeProgressiveLoaders_[seriesUuid];
+    }
+    catch (const Orthanc::OrthancException& e)
+    {
+      if (e.HasDetails())
+      {
+        LOG(ERROR) << "OrthancException in LoaderCache: " << e.What() << " Details: " << e.GetDetails();
+      }
+      else
+      {
+        LOG(ERROR) << "OrthancException in LoaderCache: " << e.What();
+      }
+      throw;
+    }
+    catch (const std::exception& e)
+    {
+      LOG(ERROR) << "std::exception in LoaderCache: " << e.what();
+      throw;
+    }
+    catch (...)
+    {
+      LOG(ERROR) << "Unknown exception in LoaderCache";
+      throw;
+    }
+  }
+
+  boost::shared_ptr<OrthancMultiframeVolumeLoader> LoaderCache::GetMultiframeVolumeLoader(std::string instanceUuid)
+  {
+    // if the loader is not available, let's trigger its creation
+    if(multiframeVolumeLoaders_.find(instanceUuid) == multiframeVolumeLoaders_.end())
+    {
+      GetMultiframeDicomVolumeImageMPRSlicer(instanceUuid);
+    }
+    ORTHANC_ASSERT(multiframeVolumeLoaders_.find(instanceUuid) != multiframeVolumeLoaders_.end());
+
+    return multiframeVolumeLoaders_[instanceUuid];
+  }
+
+  boost::shared_ptr<OrthancStone::DicomVolumeImageMPRSlicer> LoaderCache::GetMultiframeDicomVolumeImageMPRSlicer(std::string instanceUuid)
+  {
+    try
+    {
+      // normalize keys a little
+      instanceUuid = Orthanc::Toolbox::StripSpaces(instanceUuid);
+      Orthanc::Toolbox::ToLowerCase(instanceUuid);
+
+      // find in cache
+      if (dicomVolumeImageMPRSlicers_.find(instanceUuid) == dicomVolumeImageMPRSlicers_.end())
+      {
+        std::unique_ptr<OrthancStone::ILoadersContext::ILock> lock(loadersContext_.Lock());
+        boost::shared_ptr<OrthancStone::DicomVolumeImage> volumeImage(new OrthancStone::DicomVolumeImage);
+        boost::shared_ptr<OrthancMultiframeVolumeLoader> loader;
+        {
+          loader = OrthancMultiframeVolumeLoader::Create(loadersContext_, volumeImage);
+          loader->LoadInstance(instanceUuid);
+        }
+        multiframeVolumeLoaders_[instanceUuid] = loader;
+        boost::shared_ptr<OrthancStone::DicomVolumeImageMPRSlicer> mprSlicer(new OrthancStone::DicomVolumeImageMPRSlicer(volumeImage));
+        dicomVolumeImageMPRSlicers_[instanceUuid] = mprSlicer;
+      }
+      return dicomVolumeImageMPRSlicers_[instanceUuid];
+    }
+    catch (const Orthanc::OrthancException& e)
+    {
+      if (e.HasDetails())
+      {
+        LOG(ERROR) << "OrthancException in LoaderCache: " << e.What() << " Details: " << e.GetDetails();
+      }
+      else
+      {
+        LOG(ERROR) << "OrthancException in LoaderCache: " << e.What();
+      }
+      throw;
+    }
+    catch (const std::exception& e)
+    {
+      LOG(ERROR) << "std::exception in LoaderCache: " << e.what();
+      throw;
+    }
+    catch (...)
+    {
+      LOG(ERROR) << "Unknown exception in LoaderCache";
+      throw;
+    }
+  }
+  
+#ifdef BGO_ENABLE_DICOMSTRUCTURESETLOADER2
+
+  boost::shared_ptr<DicomStructureSetSlicer2> LoaderCache::GetDicomStructureSetSlicer2(std::string instanceUuid)
+  {
+    // if the loader is not available, let's trigger its creation
+    if (dicomStructureSetSlicers2_.find(instanceUuid) == dicomStructureSetSlicers2_.end())
+    {
+      GetDicomStructureSetLoader2(instanceUuid);
+    }
+    ORTHANC_ASSERT(dicomStructureSetSlicers2_.find(instanceUuid) != dicomStructureSetSlicers2_.end());
+
+    return dicomStructureSetSlicers2_[instanceUuid];
+  }
+#endif
+//BGO_ENABLE_DICOMSTRUCTURESETLOADER2
+
+
+  /**
+  This method allows to convert a list of string into a string by 
+  sorting the strings then joining them
+  */
+  static std::string SortAndJoin(const std::vector<std::string>& stringList)
+  {
+    if (stringList.size() == 0)
+    {
+      return "";
+    } 
+    else
+    {
+      std::vector<std::string> sortedStringList = stringList;
+      std::sort(sortedStringList.begin(), sortedStringList.end());
+      std::stringstream s;
+      s << sortedStringList[0];
+      for (size_t i = 1; i < sortedStringList.size(); ++i)
+      {
+        s << "-" << sortedStringList[i];
+      }
+      return s.str();
+    }
+  }
+  
+  boost::shared_ptr<DicomStructureSetLoader> 
+    LoaderCache::GetDicomStructureSetLoader(
+      std::string inInstanceUuid, 
+      const std::vector<std::string>& initiallyVisibleStructures)
+  {
+    try
+    {
+      // normalize keys a little
+      inInstanceUuid = Orthanc::Toolbox::StripSpaces(inInstanceUuid);
+      Orthanc::Toolbox::ToLowerCase(inInstanceUuid);
+
+      std::string initiallyVisibleStructuresKey = 
+        SortAndJoin(initiallyVisibleStructures);
+
+      std::string entryKey = inInstanceUuid + "_" + initiallyVisibleStructuresKey;
+
+      // find in cache
+      if (dicomStructureSetLoaders_.find(entryKey) == dicomStructureSetLoaders_.end())
+      {
+        std::unique_ptr<OrthancStone::ILoadersContext::ILock> lock(loadersContext_.Lock());
+
+        boost::shared_ptr<DicomStructureSetLoader> loader;
+        {
+          loader = DicomStructureSetLoader::Create(loadersContext_);
+          loader->LoadInstance(inInstanceUuid, initiallyVisibleStructures);
+        }
+        dicomStructureSetLoaders_[entryKey] = loader;
+      }
+      return dicomStructureSetLoaders_[entryKey];
+    }
+    catch (const Orthanc::OrthancException& e)
+    {
+      if (e.HasDetails())
+      {
+        LOG(ERROR) << "OrthancException in LoaderCache: " << e.What() << " Details: " << e.GetDetails();
+      }
+      else
+      {
+        LOG(ERROR) << "OrthancException in LoaderCache: " << e.What();
+      }
+      throw;
+    }
+    catch (const std::exception& e)
+    {
+      LOG(ERROR) << "std::exception in LoaderCache: " << e.what();
+      throw;
+    }
+    catch (...)
+    {
+      LOG(ERROR) << "Unknown exception in LoaderCache";
+      throw;
+    }
+  }
+
+#ifdef BGO_ENABLE_DICOMSTRUCTURESETLOADER2
+
+  boost::shared_ptr<DicomStructureSetLoader2> LoaderCache::GetDicomStructureSetLoader2(std::string instanceUuid)
+  {
+    try
+    {
+      // normalize keys a little
+      instanceUuid = Orthanc::Toolbox::StripSpaces(instanceUuid);
+      Orthanc::Toolbox::ToLowerCase(instanceUuid);
+
+      // find in cache
+      if (dicomStructureSetLoaders2_.find(instanceUuid) == dicomStructureSetLoaders2_.end())
+      {
+        boost::shared_ptr<DicomStructureSetLoader2> loader;
+        boost::shared_ptr<DicomStructureSet2> structureSet(new DicomStructureSet2());
+        boost::shared_ptr<DicomStructureSetSlicer2> rtSlicer(new DicomStructureSetSlicer2(structureSet));
+        dicomStructureSetSlicers2_[instanceUuid] = rtSlicer;
+        dicomStructureSets2_[instanceUuid] = structureSet; // to prevent it from being deleted
+        {
+#if ORTHANC_ENABLE_WASM == 1
+          loader.reset(new DicomStructureSetLoader2(*(structureSet.get()), oracle_, oracle_));
+#else
+          LockingEmitter::WriterLock lock(lockingEmitter_);
+          // TODO: clarify lifetimes... this is DANGEROUS!
+          loader.reset(new DicomStructureSetLoader2(*(structureSet.get()), oracle_, lock.GetOracleObservable()));
+#endif
+          loader->LoadInstance(instanceUuid);
+        }
+        dicomStructureSetLoaders2_[instanceUuid] = loader;
+      }
+      return dicomStructureSetLoaders2_[instanceUuid];
+    }
+    catch (const Orthanc::OrthancException& e)
+    {
+      if (e.HasDetails())
+      {
+        LOG(ERROR) << "OrthancException in GetDicomStructureSetLoader2: " << e.What() << " Details: " << e.GetDetails();
+      }
+      else
+      {
+        LOG(ERROR) << "OrthancException in GetDicomStructureSetLoader2: " << e.What();
+      }
+      throw;
+    }
+    catch (const std::exception& e)
+    {
+      LOG(ERROR) << "std::exception in GetDicomStructureSetLoader2: " << e.what();
+      throw;
+    }
+    catch (...)
+    {
+      LOG(ERROR) << "Unknown exception in GetDicomStructureSetLoader2";
+      throw;
+    }
+  }
+
+#endif
+// BGO_ENABLE_DICOMSTRUCTURESETLOADER2
+
+
+  void LoaderCache::ClearCache()
+  {
+    std::unique_ptr<OrthancStone::ILoadersContext::ILock> lock(loadersContext_.Lock());
+    
+#ifndef NDEBUG
+    // ISO way of checking for debug builds
+    DebugDisplayObjRefCounts();
+#endif
+    seriesVolumeProgressiveLoaders_.clear();
+    multiframeVolumeLoaders_.clear();
+    dicomVolumeImageMPRSlicers_.clear();
+    dicomStructureSetLoaders_.clear();
+
+#ifdef BGO_ENABLE_DICOMSTRUCTURESETLOADER2
+    // order is important!
+    dicomStructureSetLoaders2_.clear();
+    dicomStructureSetSlicers2_.clear();
+    dicomStructureSets2_.clear();
+#endif
+// BGO_ENABLE_DICOMSTRUCTURESETLOADER2
+  }
+
+  template<typename T> void DebugDisplayObjRefCountsInMap(
+    const std::string& name, const std::map<std::string, boost::shared_ptr<T> >& myMap)
+  {
+    LOG(TRACE) << "Map \"" << name << "\" ref counts:";
+    size_t i = 0;
+    for (typename std::map<std::string, boost::shared_ptr<T> >::const_iterator 
+           it = myMap.begin(); it != myMap.end(); ++it)
+    {
+      LOG(TRACE) << "  element #" << i << ": ref count = " << it->second.use_count();
+      i++;
+    }
+  }
+
+  void LoaderCache::DebugDisplayObjRefCounts()
+  {
+    DebugDisplayObjRefCountsInMap("seriesVolumeProgressiveLoaders_", seriesVolumeProgressiveLoaders_);
+    DebugDisplayObjRefCountsInMap("multiframeVolumeLoaders_", multiframeVolumeLoaders_);
+    DebugDisplayObjRefCountsInMap("dicomVolumeImageMPRSlicers_", dicomVolumeImageMPRSlicers_);
+    DebugDisplayObjRefCountsInMap("dicomStructureSetLoaders_", dicomStructureSetLoaders_);
+#ifdef BGO_ENABLE_DICOMSTRUCTURESETLOADER2
+    DebugDisplayObjRefCountsInMap("dicomStructureSetLoaders2_", dicomStructureSetLoaders2_);
+    DebugDisplayObjRefCountsInMap("dicomStructureSetSlicers2_", dicomStructureSetSlicers2_);
+#endif
+//BGO_ENABLE_DICOMSTRUCTURESETLOADER2
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Loaders/LoaderCache.h	Fri Apr 03 16:13:06 2020 +0200
@@ -0,0 +1,97 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 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/LockingEmitter.h"
+#include "../../Volumes/DicomVolumeImageMPRSlicer.h"
+#include "OrthancSeriesVolumeProgressiveLoader.h"
+#include "OrthancMultiframeVolumeLoader.h"
+#include "DicomStructureSetLoader.h"
+
+#include <boost/shared_ptr.hpp>
+
+#include <map>
+#include <string>
+#include <vector>
+
+namespace OrthancStone
+{
+  class ILoadersContext;
+}
+
+namespace OrthancStone
+{
+  class LoaderCache
+  {
+  public:
+    LoaderCache(OrthancStone::ILoadersContext& loadersContext);
+
+    boost::shared_ptr<OrthancSeriesVolumeProgressiveLoader>
+      GetSeriesVolumeProgressiveLoader      (std::string seriesUuid);
+    
+    boost::shared_ptr<OrthancStone::DicomVolumeImageMPRSlicer>
+      GetMultiframeDicomVolumeImageMPRSlicer(std::string instanceUuid);
+
+    boost::shared_ptr<OrthancMultiframeVolumeLoader>
+      GetMultiframeVolumeLoader(std::string instanceUuid);
+
+    boost::shared_ptr<DicomStructureSetLoader>
+      GetDicomStructureSetLoader(
+        std::string instanceUuid,
+        const std::vector<std::string>& initiallyVisibleStructures);
+
+#ifdef BGO_ENABLE_DICOMSTRUCTURESETLOADER2
+    boost::shared_ptr<DicomStructureSetLoader2>
+      GetDicomStructureSetLoader2(std::string instanceUuid);
+
+    boost::shared_ptr<DicomStructureSetSlicer2>
+      GetDicomStructureSetSlicer2(std::string instanceUuid);
+#endif 
+    //BGO_ENABLE_DICOMSTRUCTURESETLOADER2
+
+    void ClearCache();
+
+  private:
+    
+    void DebugDisplayObjRefCounts();
+
+    OrthancStone::ILoadersContext& loadersContext_;
+
+    std::map<std::string, boost::shared_ptr<OrthancSeriesVolumeProgressiveLoader> >
+      seriesVolumeProgressiveLoaders_;
+    std::map<std::string, boost::shared_ptr<OrthancMultiframeVolumeLoader> >
+      multiframeVolumeLoaders_;
+    std::map<std::string, boost::shared_ptr<OrthancStone::DicomVolumeImageMPRSlicer> >
+      dicomVolumeImageMPRSlicers_;
+    std::map<std::string, boost::shared_ptr<DicomStructureSetLoader> >
+      dicomStructureSetLoaders_;
+#ifdef BGO_ENABLE_DICOMSTRUCTURESETLOADER2
+    std::map<std::string, boost::shared_ptr<DicomStructureSetLoader2> >
+      dicomStructureSetLoaders2_;
+    std::map<std::string, boost::shared_ptr<DicomStructureSet2> >
+      dicomStructureSets2_;
+    std::map<std::string, boost::shared_ptr<DicomStructureSetSlicer2> >
+      dicomStructureSetSlicers2_;
+#endif 
+    //BGO_ENABLE_DICOMSTRUCTURESETLOADER2
+  };
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Loaders/LoaderStateMachine.cpp	Fri Apr 03 16:13:06 2020 +0200
@@ -0,0 +1,219 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "LoaderStateMachine.h"
+
+#include "../../Loaders/ILoadersContext.h"
+
+#include <Core/OrthancException.h>
+
+namespace OrthancStone
+{
+  void LoaderStateMachine::State::Handle(const OrthancStone::OrthancRestApiCommand::SuccessMessage& message)
+  {
+    throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+  }
+      
+
+  void LoaderStateMachine::State::Handle(const OrthancStone::GetOrthancImageCommand::SuccessMessage& message)
+  {
+    throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+  }
+
+      
+  void LoaderStateMachine::State::Handle(const OrthancStone::GetOrthancWebViewerJpegCommand::SuccessMessage& message)
+  {
+    throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+  }
+
+
+  void LoaderStateMachine::Schedule(OrthancStone::OracleCommandBase* command)
+  {
+    LOG(TRACE) << "LoaderStateMachine(" << std::hex << this << std::dec << ")::Schedule()";
+
+    std::unique_ptr<OrthancStone::OracleCommandBase> protection(command);
+
+    if (command == NULL)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
+    }
+      
+    if (!command->HasPayload())
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange,
+                                      "The payload must contain the next state");
+    }
+    pendingCommands_.push_back(protection.release());
+
+    Step();
+  }
+
+
+  void LoaderStateMachine::Start()
+  {
+    LOG(TRACE) << "LoaderStateMachine(" << std::hex << this << std::dec << ")::Start()";
+
+    if (active_)
+    {
+      LOG(TRACE) << "LoaderStateMachine::Start() called while active_ is true";
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+    }
+
+    active_ = true;
+
+    for (size_t i = 0; i < simultaneousDownloads_; i++)
+    {
+      Step();
+    }
+  }
+
+
+  void LoaderStateMachine::Step()
+  {
+    if (!pendingCommands_.empty() &&
+        activeCommands_ < simultaneousDownloads_)
+    {
+
+      OrthancStone::IOracleCommand* nextCommand = pendingCommands_.front();
+
+      LOG(TRACE) << "    LoaderStateMachine(" << std::hex << this << std::dec << 
+        ")::Step(): activeCommands_ (" << activeCommands_ << 
+        ") < simultaneousDownloads_ (" << simultaneousDownloads_ << 
+        ") --> will Schedule command addr " << std::hex << nextCommand << std::dec;
+
+      {
+        std::unique_ptr<OrthancStone::ILoadersContext::ILock> lock(loadersContext_.Lock());
+        boost::shared_ptr<IObserver> observer(GetSharedObserver());
+        lock->Schedule(observer, 0, nextCommand); // TODO: priority!
+      }
+      pendingCommands_.pop_front();
+
+      activeCommands_++;
+    }
+    else
+    {
+      LOG(TRACE) << "    LoaderStateMachine(" << std::hex << this << std::dec << 
+        ")::Step(): activeCommands_ (" << activeCommands_ << 
+        ") >= simultaneousDownloads_ (" << simultaneousDownloads_ << 
+        ") --> will NOT Schedule command";
+    }
+  }
+
+
+  void LoaderStateMachine::Clear()
+  {
+    LOG(TRACE) << "LoaderStateMachine(" << std::hex << this << std::dec << ")::Clear()";
+    for (PendingCommands::iterator it = pendingCommands_.begin();
+         it != pendingCommands_.end(); ++it)
+    {
+      delete *it;
+    }
+
+    pendingCommands_.clear();
+  }
+  
+
+  void LoaderStateMachine::HandleExceptionMessage(const OrthancStone::OracleCommandExceptionMessage& message)
+  {
+    LOG(ERROR) << "LoaderStateMachine::HandleExceptionMessage: error in the state machine, stopping all processing";
+    LOG(ERROR) << "Error: " << message.GetException().What() << " Details: " <<
+      message.GetException().GetDetails();
+      Clear();
+  }
+
+  template <typename T>
+  void LoaderStateMachine::HandleSuccessMessage(const T& message)
+  {
+    if (activeCommands_ <= 0) {
+      LOG(ERROR) << "LoaderStateMachine(" << std::hex << this << std::dec << ")::HandleSuccessMessage : activeCommands_ should be > 0 but is: " << activeCommands_;
+    }
+    else {
+      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() << " Details: " << e.GetDetails();
+        Clear();
+      }
+    }
+  }
+
+
+  LoaderStateMachine::LoaderStateMachine(
+    OrthancStone::ILoadersContext& loadersContext)
+    : loadersContext_(loadersContext)
+    , active_(false)
+    , simultaneousDownloads_(4)
+    , activeCommands_(0)
+  {
+    using OrthancStone::ILoadersContext;
+
+    LOG(TRACE) 
+      << "LoaderStateMachine(" << std::hex << this 
+      << std::dec << ")::LoaderStateMachine()";
+  }
+
+  void LoaderStateMachine::PostConstructor()
+  {
+    std::unique_ptr<OrthancStone::ILoadersContext::ILock>
+      lock(loadersContext_.Lock());
+
+    OrthancStone::IObservable& observable = lock->GetOracleObservable();
+
+    // TODO => Move this out of constructor
+    Register<OrthancStone::OrthancRestApiCommand::SuccessMessage>(
+      observable, &LoaderStateMachine::HandleSuccessMessage);
+    Register<OrthancStone::GetOrthancImageCommand::SuccessMessage>(
+      observable, &LoaderStateMachine::HandleSuccessMessage);
+    Register<OrthancStone::GetOrthancWebViewerJpegCommand::SuccessMessage>(
+      observable, &LoaderStateMachine::HandleSuccessMessage);
+    Register<OrthancStone::OracleCommandExceptionMessage>(
+      observable, &LoaderStateMachine::HandleExceptionMessage);
+  }
+
+  LoaderStateMachine::~LoaderStateMachine()
+  {
+    LOG(TRACE) << "LoaderStateMachine(" << std::hex << this << std::dec << ")::~LoaderStateMachine()";
+    Clear();
+  }
+
+  void LoaderStateMachine::SetSimultaneousDownloads(unsigned int count)
+  {
+    if (active_)
+    {
+      LOG(ERROR) << "LoaderStateMachine::SetSimultaneousDownloads called while active_ is true";
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+    }
+    else if (count == 0)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);        
+    }
+    else
+    {
+      simultaneousDownloads_ = count;
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Loaders/LoaderStateMachine.h	Fri Apr 03 16:13:06 2020 +0200
@@ -0,0 +1,124 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 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/ObserverBase.h"
+#include "../../Oracle/GetOrthancImageCommand.h"
+#include "../../Oracle/GetOrthancWebViewerJpegCommand.h"
+#include "../../Oracle/IOracle.h"
+#include "../../Oracle/OracleCommandExceptionMessage.h"
+#include "../../Oracle/OrthancRestApiCommand.h"
+
+#include <Core/IDynamicObject.h>
+
+#include <list>
+
+namespace OrthancStone
+{
+  /**
+     This class is supplied with Oracle commands and will schedule up to 
+     simultaneousDownloads_ of them at the same time, then will schedule the 
+     rest once slots become available. It is used, a.o., by the 
+     OrtancMultiframeVolumeLoader class.
+
+     To use it, you need to create commands that derive from State.
+
+     You need to initialize them with the object that must be called when 
+     an answer is received. 
+  */
+
+  class LoaderStateMachine : public OrthancStone::ObserverBase<LoaderStateMachine>
+  {
+  public:
+    class State : public Orthanc::IDynamicObject
+    {
+    private:
+      LoaderStateMachine&  that_;
+
+    public:
+      State(LoaderStateMachine& that) :
+      that_(that)
+      {
+      }
+
+      State(const State& currentState) :
+      that_(currentState.that_)
+      {
+      }
+
+      void Schedule(OrthancStone::OracleCommandBase* command) const
+      {
+        that_.Schedule(command);
+      }
+
+      template <typename T>
+      T& GetLoader() const
+      {
+        return dynamic_cast<T&>(that_);
+      }
+      
+      virtual void Handle(const OrthancStone::OrthancRestApiCommand::SuccessMessage& message);
+      
+      virtual void Handle(const OrthancStone::GetOrthancImageCommand::SuccessMessage& message);
+      
+      virtual void Handle(const OrthancStone::GetOrthancWebViewerJpegCommand::SuccessMessage& message);
+    };
+
+    void Schedule(OrthancStone::OracleCommandBase* command);
+
+    void Start();
+
+  private:
+    void Step();
+
+    void Clear();
+
+    void HandleExceptionMessage(const OrthancStone::OracleCommandExceptionMessage& message);
+
+    template <typename T>
+    void HandleSuccessMessage(const T& message);
+
+    typedef std::list<OrthancStone::IOracleCommand*>  PendingCommands;
+
+    OrthancStone::ILoadersContext&  loadersContext_;
+    bool                            active_;
+    unsigned int                    simultaneousDownloads_;
+    PendingCommands                 pendingCommands_;
+    unsigned int                    activeCommands_;
+
+
+  public:
+    LoaderStateMachine(OrthancStone::ILoadersContext& loadersContext);
+
+    void PostConstructor();
+
+    virtual ~LoaderStateMachine();
+
+    bool IsActive() const
+    {
+      return active_;
+    }
+
+    void SetSimultaneousDownloads(unsigned int count);  
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Loaders/OrthancMultiframeVolumeLoader.cpp	Fri Apr 03 16:13:06 2020 +0200
@@ -0,0 +1,593 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "OrthancMultiframeVolumeLoader.h"
+
+#include <Core/Endianness.h>
+#include <Core/Toolbox.h>
+
+namespace OrthancStone
+{
+  class OrthancMultiframeVolumeLoader::LoadRTDoseGeometry : public LoaderStateMachine::State
+  {
+  private:
+    std::unique_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 OrthancStone::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.LookupStringValue(s, Orthanc::DICOM_TAG_SOP_CLASS_UID, false))
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat,
+                                      "DICOM file without SOP class UID");
+    }
+    else
+    {
+      return s;
+    }
+  }
+    
+
+  class OrthancMultiframeVolumeLoader::LoadGeometry : public State
+  {
+  public:
+    LoadGeometry(OrthancMultiframeVolumeLoader& that) :
+    State(that)
+    {
+    }
+      
+    virtual void Handle(const OrthancStone::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::unique_ptr<Orthanc::DicomMap> dicom(new Orthanc::DicomMap);
+      dicom->FromDicomAsJson(body);
+
+      if (OrthancStone::StringToSopClassUid(GetSopClassUid(*dicom)) == OrthancStone::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::unique_ptr<OrthancStone::OrthancRestApiCommand> command(new OrthancStone::OrthancRestApiCommand);
+        command->SetUri("/instances/" + loader.GetInstanceId() + "/content/" +
+                        Orthanc::DICOM_TAG_GRID_FRAME_OFFSET_VECTOR.Format());
+        command->AcquirePayload(new LoadRTDoseGeometry(loader, dicom.release()));
+
+        Schedule(command.release());
+      }
+      else
+      {
+        loader.SetGeometry(*dicom);
+      }
+    }
+  };
+
+  class OrthancMultiframeVolumeLoader::LoadTransferSyntax : public State
+  {
+  public:
+    LoadTransferSyntax(OrthancMultiframeVolumeLoader& that) :
+    State(that)
+    {
+    }
+      
+    virtual void Handle(const OrthancStone::OrthancRestApiCommand::SuccessMessage& message)
+    {
+      GetLoader<OrthancMultiframeVolumeLoader>().SetTransferSyntax(message.GetAnswer());
+    }
+  };
+
+  class OrthancMultiframeVolumeLoader::LoadUncompressedPixelData : public State
+  {
+  public:
+    LoadUncompressedPixelData(OrthancMultiframeVolumeLoader& that) :
+    State(that)
+    {
+    }
+      
+    virtual void Handle(const OrthancStone::OrthancRestApiCommand::SuccessMessage& message)
+    {
+      GetLoader<OrthancMultiframeVolumeLoader>().SetUncompressedPixelData(message.GetAnswer());
+    }
+  };
+
+  const std::string& OrthancMultiframeVolumeLoader::GetInstanceId() const
+  {
+    if (IsActive())
+    {
+      return instanceId_;
+    }
+    else
+    {
+      LOG(ERROR) << "OrthancMultiframeVolumeLoader::GetInstanceId(): (!IsActive())";
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+    }
+  }
+
+  void OrthancMultiframeVolumeLoader::ScheduleFrameDownloads()
+  {
+    if (transferSyntaxUid_.empty() ||
+        !volume_->HasGeometry())
+    {
+      return;
+    }
+    /*
+      1.2.840.10008.1.2	Implicit VR Endian: Default Transfer Syntax for DICOM
+      1.2.840.10008.1.2.1	Explicit VR Little Endian
+      1.2.840.10008.1.2.2	Explicit VR Big Endian
+
+      See https://www.dicomlibrary.com/dicom/transfer-syntax/
+    */
+    if (transferSyntaxUid_ == "1.2.840.10008.1.2" ||
+        transferSyntaxUid_ == "1.2.840.10008.1.2.1" ||
+        transferSyntaxUid_ == "1.2.840.10008.1.2.2")
+    {
+      std::unique_ptr<OrthancStone::OrthancRestApiCommand> command(new OrthancStone::OrthancRestApiCommand);
+      command->SetHttpHeader("Accept-Encoding", "gzip");
+      command->SetUri("/instances/" + instanceId_ + "/content/" +
+                      Orthanc::DICOM_TAG_PIXEL_DATA.Format() + "/0");
+      command->AcquirePayload(new LoadUncompressedPixelData(*this));
+      Schedule(command.release());
+    }
+    else
+    {
+      throw Orthanc::OrthancException(
+        Orthanc::ErrorCode_NotImplemented,
+        "No support for multiframe instances with transfer syntax: " + transferSyntaxUid_);
+    }
+  }
+
+  void OrthancMultiframeVolumeLoader::SetTransferSyntax(const std::string& transferSyntax)
+  {
+    transferSyntaxUid_ = Orthanc::Toolbox::StripSpaces(transferSyntax);
+    ScheduleFrameDownloads();
+  }
+
+  void OrthancMultiframeVolumeLoader::SetGeometry(const Orthanc::DicomMap& dicom)
+  {
+    OrthancStone::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 OrthancStone::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();
+
+    {
+      OrthancStone::VolumeImageGeometry geometry;
+      geometry.SetSizeInVoxels(width, height, depth);
+      geometry.SetAxialGeometry(parameters.GetGeometry());
+      geometry.SetVoxelDimensions(parameters.GetPixelSpacingX(),
+                                  parameters.GetPixelSpacingY(), spacingZ);
+      volume_->Initialize(geometry, format, true /* Do compute range */);
+    }
+
+    volume_->GetPixelData().Clear();
+
+    ScheduleFrameDownloads();
+
+
+
+    BroadcastMessage(OrthancStone::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));
+  }
+
+  ORTHANC_FORCE_INLINE
+    static void CopyPixel(uint16_t& target, const void* source)
+  {
+    // TODO - check alignement?
+    target = le16toh(*reinterpret_cast<const uint16_t*>(source));
+  }
+
+  ORTHANC_FORCE_INLINE
+    static void CopyPixel(int16_t& target, const void* source)
+  {
+    // byte swapping is the same for unsigned and signed integers
+    // (the sign bit is always stored with the MSByte)
+    uint16_t* targetUp = reinterpret_cast<uint16_t*>(&target);
+    CopyPixel(*targetUp, source);
+  }
+
+  template <typename T>
+  void OrthancMultiframeVolumeLoader::CopyPixelDataAndComputeDistribution(
+    const std::string& pixelData, std::map<T,uint64_t>& distribution)
+  {
+    OrthancStone::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;
+    }
+
+    // first pass to initialize map
+    {
+      const uint8_t* source = reinterpret_cast<const uint8_t*>(pixelData.c_str());
+
+      for (unsigned int z = 0; z < depth; z++)
+      {
+        for (unsigned int y = 0; y < height; y++)
+        {
+          for (unsigned int x = 0; x < width; x++)
+          {
+            T value;
+            CopyPixel(value, source);
+            distribution[value] = 0;
+            source += bpp;
+          }
+        }
+      }
+    }
+
+    {
+      const uint8_t* source = reinterpret_cast<const uint8_t*>(pixelData.c_str());
+
+      for (unsigned int z = 0; z < depth; z++)
+      {
+        OrthancStone::ImageBuffer3D::SliceWriter writer(target, OrthancStone::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);
+
+            distribution[*target] += 1;
+
+            target++;
+            source += bpp;
+          }
+        }
+      }
+    }
+  }
+
+  template <typename T>
+  void OrthancMultiframeVolumeLoader::ComputeMinMaxWithOutlierRejection(
+    const std::map<T, uint64_t>& distribution)
+  {
+    if (distribution.size() == 0)
+    {
+      LOG(ERROR) << "ComputeMinMaxWithOutlierRejection -- Volume image empty.";
+    }
+    else
+    {
+      OrthancStone::ImageBuffer3D& target = volume_->GetPixelData();
+
+      const uint64_t width = target.GetWidth();
+      const uint64_t height = target.GetHeight();
+      const uint64_t depth = target.GetDepth();
+      const uint64_t voxelCount = width * height * depth;
+
+      // now that we have distribution[pixelValue] == numberOfPixelsWithValue
+      // compute number of values and check (assertion) that it is equal to 
+      // width * height * depth 
+      {
+        typename std::map<T, uint64_t>::const_iterator it = distribution.begin();
+        uint64_t totalCount = 0;
+        distributionRawMin_ = static_cast<float>(it->first);
+
+        while (it != distribution.end())
+        {
+          T pixelValue = it->first;
+          uint64_t count = it->second;
+          totalCount += count;
+          it++;
+          if (it == distribution.end())
+            distributionRawMax_ = static_cast<float>(pixelValue);
+        }
+        LOG(INFO) << "Volume image. First distribution value = " 
+          << static_cast<float>(distributionRawMin_) 
+          << " | Last distribution value = " 
+          << static_cast<float>(distributionRawMax_);
+
+        if (totalCount != voxelCount)
+        {
+          LOG(ERROR) << "Internal error in dose distribution computation. TC (" 
+            << totalCount << ") != VoxC (" << voxelCount;
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+        }
+      }
+
+      // compute the number of voxels to reject at each end of the distribution
+      uint64_t endRejectionCount = static_cast<uint64_t>(
+        outliersHalfRejectionRate_ * voxelCount);
+
+      if (endRejectionCount > voxelCount)
+      {
+        LOG(ERROR) << "Internal error in dose distribution computation."
+          << " endRejectionCount = " << endRejectionCount
+          << " | voxelCount = " << voxelCount;
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+      }
+
+      // this will contain the actual distribution minimum after outlier 
+      // rejection
+      T resultMin = 0;
+
+      // then start from start and remove pixel values up to 
+      // endRejectionCount voxels rejected
+      {
+        typename std::map<T, uint64_t>::const_iterator it = distribution.begin();
+        
+        uint64_t currentCount = 0;
+
+        while (it != distribution.end())
+        {
+          T pixelValue = it->first;
+          uint64_t count = it->second;
+
+          // if this pixelValue crosses the rejection threshold, let's set it
+          // and exit the loop
+          if ((currentCount <= endRejectionCount) &&
+              (currentCount + count > endRejectionCount))
+          {
+            resultMin = pixelValue;
+            break;
+          }
+          else
+          {
+            currentCount += count;
+          }
+          // and continue walking along the distribution
+          it++;
+        }
+      }
+
+      // this will contain the actual distribution maximum after outlier 
+      // rejection
+      T resultMax = 0;
+      // now start from END and remove pixel values up to 
+      // endRejectionCount voxels rejected
+      {
+        typename std::map<T, uint64_t>::const_reverse_iterator it = distribution.rbegin();
+
+        uint64_t currentCount = 0;
+
+        while (it != distribution.rend())
+        {
+          T pixelValue = it->first;
+          uint64_t count = it->second;
+
+          if ((currentCount <= endRejectionCount) &&
+              (currentCount + count > endRejectionCount))
+          {
+            resultMax = pixelValue;
+            break;
+          }
+          else
+          {
+            currentCount += count;
+          }
+          // and continue walking along the distribution
+          it++;
+        }
+      }
+      if (resultMin > resultMax)
+      {
+        LOG(ERROR) << "Internal error in dose distribution computation! " << 
+          "resultMin (" << resultMin << ") > resultMax (" << resultMax << ")";
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+      }
+      computedDistributionMin_ = static_cast<float>(resultMin);
+      computedDistributionMax_ = static_cast<float>(resultMax);
+    }
+  }
+
+  template <typename T>
+  void OrthancMultiframeVolumeLoader::CopyPixelDataAndComputeMinMax(
+    const std::string& pixelData)
+  {
+    std::map<T, uint64_t> distribution;
+    CopyPixelDataAndComputeDistribution(pixelData, distribution);
+    ComputeMinMaxWithOutlierRejection(distribution);
+  }
+
+  void OrthancMultiframeVolumeLoader::SetUncompressedPixelData(const std::string& pixelData)
+  {
+    switch (volume_->GetPixelData().GetFormat())
+    {
+      case Orthanc::PixelFormat_Grayscale32:
+        CopyPixelDataAndComputeMinMax<uint32_t>(pixelData);
+        break;
+      case Orthanc::PixelFormat_Grayscale16:
+        CopyPixelDataAndComputeMinMax<uint16_t>(pixelData);
+        break;
+      case Orthanc::PixelFormat_SignedGrayscale16:
+        CopyPixelDataAndComputeMinMax<int16_t>(pixelData);
+        break;
+      default:
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+    }
+
+    volume_->IncrementRevision();
+
+    pixelDataLoaded_ = true;
+    BroadcastMessage(OrthancStone::DicomVolumeImage::ContentUpdatedMessage(*volume_));
+  }
+  
+  bool OrthancMultiframeVolumeLoader::HasGeometry() const
+  {
+    return volume_->HasGeometry();
+  }
+
+  const OrthancStone::VolumeImageGeometry& OrthancMultiframeVolumeLoader::GetImageGeometry() const
+  {
+    return volume_->GetGeometry();
+  }
+
+  OrthancMultiframeVolumeLoader::OrthancMultiframeVolumeLoader(
+    OrthancStone::ILoadersContext& loadersContext,
+    boost::shared_ptr<OrthancStone::DicomVolumeImage> volume,
+    float outliersHalfRejectionRate) 
+    : LoaderStateMachine(loadersContext)
+    , volume_(volume)
+    , pixelDataLoaded_(false)
+    , outliersHalfRejectionRate_(outliersHalfRejectionRate)
+    , distributionRawMin_(0)
+    , distributionRawMax_(0)
+    , computedDistributionMin_(0)
+    , computedDistributionMax_(0)
+  {
+    if (volume.get() == NULL)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
+    }
+  }
+
+
+  boost::shared_ptr<OrthancMultiframeVolumeLoader>
+    OrthancMultiframeVolumeLoader::Create(
+      OrthancStone::ILoadersContext& loadersContext, 
+      boost::shared_ptr<OrthancStone::DicomVolumeImage> volume, 
+      float outliersHalfRejectionRate /*= 0.0005*/)
+  {
+    boost::shared_ptr<OrthancMultiframeVolumeLoader> obj(
+      new OrthancMultiframeVolumeLoader(
+        loadersContext,
+        volume,
+        outliersHalfRejectionRate));
+    obj->LoaderStateMachine::PostConstructor();
+    return obj;
+  }
+
+  OrthancMultiframeVolumeLoader::~OrthancMultiframeVolumeLoader()
+  {
+    LOG(TRACE) << "OrthancMultiframeVolumeLoader::~OrthancMultiframeVolumeLoader()";
+  }
+  
+  void OrthancMultiframeVolumeLoader::GetDistributionMinMax
+  (float& minValue, float& maxValue) const
+  {
+    if (distributionRawMin_ == 0 && distributionRawMax_ == 0)
+    {
+      LOG(WARNING) << "GetDistributionMinMaxWithOutliersRejection called before computation!";
+    }
+    minValue = distributionRawMin_;
+    maxValue = distributionRawMax_;
+  }
+  
+  void OrthancMultiframeVolumeLoader::GetDistributionMinMaxWithOutliersRejection
+    (float& minValue, float& maxValue) const
+  {
+    if (computedDistributionMin_ == 0 && computedDistributionMax_ == 0)
+    {
+      LOG(WARNING) << "GetDistributionMinMaxWithOutliersRejection called before computation!";
+    }
+    minValue = computedDistributionMin_;
+    maxValue = computedDistributionMax_;
+  }
+
+  void OrthancMultiframeVolumeLoader::LoadInstance(const std::string& instanceId)
+  {
+    Start();
+
+    instanceId_ = instanceId;
+
+    {
+      std::unique_ptr<OrthancStone::OrthancRestApiCommand> command(new OrthancStone::OrthancRestApiCommand);
+      command->SetHttpHeader("Accept-Encoding", "gzip");
+      command->SetUri("/instances/" + instanceId + "/tags");
+      command->AcquirePayload(new LoadGeometry(*this));
+      Schedule(command.release());
+    }
+
+    {
+      std::unique_ptr<OrthancStone::OrthancRestApiCommand> command(new OrthancStone::OrthancRestApiCommand);
+      command->SetUri("/instances/" + instanceId + "/metadata/TransferSyntax");
+      command->AcquirePayload(new LoadTransferSyntax(*this));
+      Schedule(command.release());
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Loaders/OrthancMultiframeVolumeLoader.h	Fri Apr 03 16:13:06 2020 +0200
@@ -0,0 +1,123 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "LoaderStateMachine.h"
+#include "../../Volumes/DicomVolumeImage.h"
+#include "../Volumes/IGeometryProvider.h"
+
+#include <boost/shared_ptr.hpp>
+
+namespace OrthancStone
+{
+  class OrthancMultiframeVolumeLoader :
+    public LoaderStateMachine,
+    public OrthancStone::IObservable,
+    public IGeometryProvider
+  {
+  private:
+    class LoadRTDoseGeometry;
+    class LoadGeometry;
+    class LoadTransferSyntax;    
+    class LoadUncompressedPixelData;
+
+    boost::shared_ptr<OrthancStone::DicomVolumeImage>  volume_;
+    std::string                          instanceId_;
+    std::string                          transferSyntaxUid_;
+    bool                                 pixelDataLoaded_;
+    float                                outliersHalfRejectionRate_;
+    float                                distributionRawMin_;
+    float                                distributionRawMax_;
+    float                                computedDistributionMin_;
+    float                                computedDistributionMax_;
+
+    const std::string& GetInstanceId() const;
+
+    void ScheduleFrameDownloads();
+
+    void SetTransferSyntax(const std::string& transferSyntax);
+
+    void SetGeometry(const Orthanc::DicomMap& dicom);
+
+
+    /**
+    This method will :
+    
+    - copy the pixel values from the response to the volume image
+    - compute the maximum and minimum value while discarding the
+      outliersHalfRejectionRate_ fraction of the outliers from both the start 
+      and the end of the distribution.
+
+      In English, this means that, if the volume dataset contains a few extreme
+      values very different from the rest (outliers) that we want to get rid of,
+      this method allows to do so.
+
+      If you supply 0.005, for instance, it means 1% of the extreme values will
+      be rejected (0.5% on each side of the distribution)
+    */
+    template <typename T>
+    void CopyPixelDataAndComputeMinMax(const std::string& pixelData);
+      
+    /** Service method for CopyPixelDataAndComputeMinMax*/
+    template <typename T>
+    void CopyPixelDataAndComputeDistribution(
+      const std::string& pixelData, 
+      std::map<T, uint64_t>& distribution);
+
+    /** Service method for CopyPixelDataAndComputeMinMax*/
+    template <typename T>
+    void ComputeMinMaxWithOutlierRejection(
+      const std::map<T, uint64_t>& distribution);
+
+    void SetUncompressedPixelData(const std::string& pixelData);
+
+    bool HasGeometry() const;
+    const OrthancStone::VolumeImageGeometry& GetImageGeometry() const;
+
+  protected:
+    OrthancMultiframeVolumeLoader(
+      OrthancStone::ILoadersContext& loadersContext,
+      boost::shared_ptr<OrthancStone::DicomVolumeImage> volume,
+      float outliersHalfRejectionRate);
+  public:
+
+    static boost::shared_ptr<OrthancMultiframeVolumeLoader> Create(
+      OrthancStone::ILoadersContext& loadersContext,
+      boost::shared_ptr<OrthancStone::DicomVolumeImage> volume,
+      float outliersHalfRejectionRate = 0.0005);
+
+    virtual ~OrthancMultiframeVolumeLoader();
+
+    bool IsPixelDataLoaded() const
+    {
+      return pixelDataLoaded_;
+    }
+
+    void GetDistributionMinMax
+      (float& minValue, float& maxValue) const;
+
+    void GetDistributionMinMaxWithOutliersRejection
+      (float& minValue, float& maxValue) const;
+
+    void LoadInstance(const std::string& instanceId);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Loaders/OrthancSeriesVolumeProgressiveLoader.cpp	Fri Apr 03 16:13:06 2020 +0200
@@ -0,0 +1,577 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 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 "../../StoneException.h"
+#include "../../Loaders/ILoadersContext.h"
+#include "../../Loaders/BasicFetchingItemsSorter.h"
+#include "../../Loaders/BasicFetchingStrategy.h"
+#include "../../Toolbox/GeometryToolbox.h"
+#include "../../Volumes/DicomVolumeImageMPRSlicer.h"
+
+#include <Core/Images/ImageProcessing.h>
+#include <Core/OrthancException.h>
+
+namespace OrthancStone
+{
+  using OrthancStone::ILoadersContext;
+
+  class OrthancSeriesVolumeProgressiveLoader::ExtractedSlice : public OrthancStone::DicomVolumeImageMPRSlicer::Slice
+  {
+  private:
+    const OrthancSeriesVolumeProgressiveLoader&  that_;
+
+  public:
+    ExtractedSlice(const OrthancSeriesVolumeProgressiveLoader& that,
+                   const OrthancStone::CoordinateSystem3D& plane) :
+      OrthancStone::DicomVolumeImageMPRSlicer::Slice(*that.volume_, plane),
+      that_(that)
+    {
+      if (IsValid())
+      {
+        if (GetProjection() == OrthancStone::VolumeProjection_Axial)
+        {
+          // 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 
+          SetRevision(that_.seriesGeometry_.GetSliceRevision(GetSliceIndex()));
+        }
+      
+        if (that_.strategy_.get() != NULL &&
+            GetProjection() == OrthancStone::VolumeProjection_Axial)
+        {
+          that_.strategy_->SetCurrent(GetSliceIndex());
+        }
+      }
+    }
+  };
+    
+  void OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::CheckSlice(
+    size_t index, const OrthancStone::DicomInstanceParameters& reference) const
+  {
+    const OrthancStone::DicomInstanceParameters& slice = *slices_[index];
+      
+    if (!OrthancStone::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 (!OrthancStone::LinearAlgebra::IsNear(reference.GetPixelSpacingX(), slice.GetPixelSpacingX()) ||
+        !OrthancStone::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 OrthancStone::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())
+    {
+      LOG(ERROR) << "OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::CheckSliceIndex(size_t index): (!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(OrthancStone::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 OrthancStone::VolumeImageGeometry);
+    }
+    else
+    {
+      slices_.reserve(slices.GetSlicesCount());
+      slicesRevision_.resize(slices.GetSlicesCount(), 0);
+
+      for (size_t i = 0; i < slices.GetSlicesCount(); i++)
+      {
+        const OrthancStone::DicomInstanceParameters& slice =
+          dynamic_cast<const OrthancStone::DicomInstanceParameters&>(slices.GetSlicePayload(i));
+        slices_.push_back(new OrthancStone::DicomInstanceParameters(slice));
+      }
+
+      CheckVolume();
+
+      double spacingZ;
+
+      if (slices.ComputeSpacingBetweenSlices(spacingZ))
+      {
+        LOG(TRACE) << "Computed spacing between slices: " << spacingZ << "mm";
+      
+        const OrthancStone::DicomInstanceParameters& parameters = *slices_[0];
+
+        geometry_.reset(new OrthancStone::VolumeImageGeometry);
+        geometry_->SetSizeInVoxels(parameters.GetImageInformation().GetWidth(),
+                                   parameters.GetImageInformation().GetHeight(),
+                                   static_cast<unsigned int>(slices.GetSlicesCount()));
+        geometry_->SetAxialGeometry(slices.GetSliceGeometry(0));
+        geometry_->SetVoxelDimensions(parameters.GetPixelSpacingX(),
+                                      parameters.GetPixelSpacingY(), spacingZ);
+      }
+      else
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadGeometry,
+                                        "The origins of the slices of a volume image are not regularly spaced");
+     }
+    }
+  }
+
+
+  const OrthancStone::VolumeImageGeometry& OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::GetImageGeometry() const
+  {
+    if (!HasGeometry())
+    {
+      LOG(ERROR) << "OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::GetImageGeometry(): (!HasGeometry())";
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      assert(slices_.size() == geometry_->GetDepth());
+      return *geometry_;
+    }
+  }
+
+
+  const OrthancStone::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 OrthancStone::OracleCommandBase& command)
+  {
+    assert(command.HasPayload());
+    return dynamic_cast< const Orthanc::SingleValueObject<unsigned int>& >(command.GetPayload()).GetValue();
+  }
+
+
+  void OrthancSeriesVolumeProgressiveLoader::ScheduleNextSliceDownload()
+  {
+    assert(strategy_.get() != NULL);
+      
+    unsigned int sliceIndex = 0, quality = 0;
+      
+    if (strategy_->GetNext(sliceIndex, quality))
+    {
+      if (!progressiveQuality_)
+      {
+        ORTHANC_ASSERT(quality == QUALITY_00, "INTERNAL ERROR. quality != QUALITY_00 in "
+                       << "OrthancSeriesVolumeProgressiveLoader::ScheduleNextSliceDownload");
+      }
+
+      const OrthancStone::DicomInstanceParameters& slice = seriesGeometry_.GetSliceParameters(sliceIndex);
+          
+      const std::string& instance = slice.GetOrthancInstanceIdentifier();
+      if (instance.empty())
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+      }
+
+      std::unique_ptr<OrthancStone::OracleCommandBase> command;
+        
+      if (!progressiveQuality_ || quality == QUALITY_02)
+      {
+        std::unique_ptr<OrthancStone::GetOrthancImageCommand> tmp(new OrthancStone::GetOrthancImageCommand);
+        // TODO: review the following comment. 
+        // - Commented out by bgo on 2019-07-19 | reason: Alain has seen cases 
+        //   where gzipping the uint16 image took 11 sec to produce 5mb. 
+        //   The unzipped request was much much faster.
+        // - Re-enabled on 2019-07-30. Reason: in Web Assembly, the browser 
+        //   does not use the Accept-Encoding header and always requests
+        //   compression. Furthermore, NOT 
+        tmp->SetHttpHeader("Accept-Encoding", "gzip");
+        tmp->SetHttpHeader("Accept", std::string(Orthanc::EnumerationToString(Orthanc::MimeType_Pam)));
+        tmp->SetInstanceUri(instance, slice.GetExpectedPixelFormat());
+        tmp->SetExpectedPixelFormat(slice.GetExpectedPixelFormat());
+        //LOG(INFO) 
+        //  << "OrthancSeriesVolumeProgressiveLoader.ScheduleNextSliceDownload()"
+        //  << " sliceIndex = " << sliceIndex << " slice quality = " << quality 
+        //  << " URI = " << tmp->GetUri();
+        command.reset(tmp.release());
+      }
+      else // progressive mode is true AND quality is not final (different from QUALITY_02
+      {
+        std::unique_ptr<OrthancStone::GetOrthancWebViewerJpegCommand> tmp(
+          new OrthancStone::GetOrthancWebViewerJpegCommand);
+
+        // TODO: review the following comment. Commented out by bgo on 2019-07-19
+        // (gzip for jpeg seems overkill)
+        //tmp->SetHttpHeader("Accept-Encoding", "gzip");
+        tmp->SetInstance(instance);
+        tmp->SetQuality((quality == 0 ? 50 : 90)); // QUALITY_00 is Jpeg50 while QUALITY_01 is Jpeg90
+        tmp->SetExpectedPixelFormat(slice.GetExpectedPixelFormat());
+        LOG(TRACE)
+          << "OrthancSeriesVolumeProgressiveLoader.ScheduleNextSliceDownload()"
+          << " sliceIndex = " << sliceIndex << " slice quality = " << quality;
+        command.reset(tmp.release());
+      }
+
+      command->AcquirePayload(new Orthanc::SingleValueObject<unsigned int>(sliceIndex));
+      
+      {
+        std::unique_ptr<OrthancStone::ILoadersContext::ILock> lock(loadersContext_.Lock());
+        boost::shared_ptr<IObserver> observer(GetSharedObserver());
+        lock->Schedule(observer, 0, command.release()); // TODO: priority!
+      }
+    }
+    else
+    {
+      // loading is finished!
+      volumeImageReadyInHighQuality_ = true;
+      BroadcastMessage(OrthancSeriesVolumeProgressiveLoader::VolumeImageReadyInHighQuality(*this));
+    }
+  }
+
+/**
+   This is called in response to GET "/series/XXXXXXXXXXXXX/instances-tags"
+*/
+  void OrthancSeriesVolumeProgressiveLoader::LoadGeometry(const OrthancStone::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();
+
+      OrthancStone::SlicesSorter slices;
+        
+      for (size_t i = 0; i < instances.size(); i++)
+      {
+        Orthanc::DicomMap dicom;
+        dicom.FromDicomAsJson(body[instances[i]]);
+
+        std::unique_ptr<OrthancStone::DicomInstanceParameters> instance(new OrthancStone::DicomInstanceParameters(dicom));
+        instance->SetOrthancInstanceIdentifier(instances[i]);
+
+        // the 3D plane corresponding to the slice
+        OrthancStone::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 OrthancStone::DicomInstanceParameters& parameters = seriesGeometry_.GetSliceParameters(0);
+        
+      volume_->Initialize(seriesGeometry_.GetImageGeometry(), parameters.GetExpectedPixelFormat());
+      volume_->SetDicomParameters(parameters);
+      volume_->GetPixelData().Clear();
+
+      // If we are in progressive mode, the Fetching strategy will first request QUALITY_00, then QUALITY_01, then
+      // QUALITY_02... Otherwise, it's only QUALITY_00
+      unsigned int maxQuality = QUALITY_00;
+      if (progressiveQuality_)
+        maxQuality = QUALITY_02;
+
+      strategy_.reset(new OrthancStone::BasicFetchingStrategy(
+        sorter_->CreateSorter(static_cast<unsigned int>(slicesCount)),
+        maxQuality));
+
+      assert(simultaneousDownloads_ != 0);
+      for (unsigned int i = 0; i < simultaneousDownloads_; i++)
+      {
+        ScheduleNextSliceDownload();
+      }
+    }
+
+    slicesQuality_.resize(slicesCount, 0);
+
+    BroadcastMessage(OrthancStone::DicomVolumeImage::GeometryReadyMessage(*volume_));
+  }
+
+
+  void OrthancSeriesVolumeProgressiveLoader::SetSliceContent(unsigned int sliceIndex,
+                                                             const Orthanc::ImageAccessor& image,
+                                                             unsigned int quality)
+  {
+    ORTHANC_ASSERT(sliceIndex < slicesQuality_.size() &&
+           slicesQuality_.size() == volume_->GetPixelData().GetDepth());
+      
+    if (!progressiveQuality_)
+    {
+      ORTHANC_ASSERT(quality                    == QUALITY_00);
+      ORTHANC_ASSERT(slicesQuality_[sliceIndex] == QUALITY_00);
+    }
+
+    if (quality >= slicesQuality_[sliceIndex])
+    {
+      {
+        OrthancStone::ImageBuffer3D::SliceWriter writer(volume_->GetPixelData(), 
+                                                        OrthancStone::VolumeProjection_Axial, 
+                                                        sliceIndex);
+        
+        Orthanc::ImageProcessing::Copy(writer.GetAccessor(), image);
+      }
+
+      volume_->IncrementRevision();
+      seriesGeometry_.IncrementSliceRevision(sliceIndex);
+      slicesQuality_[sliceIndex] = quality;
+
+      BroadcastMessage(OrthancStone::DicomVolumeImage::ContentUpdatedMessage(*volume_));
+    }
+    LOG(TRACE) << "SetSliceContent sliceIndex = " << sliceIndex << " -- will "
+      << " now call ScheduleNextSliceDownload()";
+    ScheduleNextSliceDownload();
+  }
+
+  void OrthancSeriesVolumeProgressiveLoader::LoadBestQualitySliceContent(
+    const OrthancStone::GetOrthancImageCommand::SuccessMessage& message)
+  {
+    unsigned int quality = QUALITY_00;
+    if (progressiveQuality_)
+      quality = QUALITY_02;
+
+    SetSliceContent(GetSliceIndexPayload(message.GetOrigin()), 
+                                         message.GetImage(),
+                                         quality);
+  }
+
+  void OrthancSeriesVolumeProgressiveLoader::LoadJpegSliceContent(
+    const OrthancStone::GetOrthancWebViewerJpegCommand::SuccessMessage& message)
+  {
+    ORTHANC_ASSERT(progressiveQuality_, "INTERNAL ERROR: OrthancSeriesVolumeProgressiveLoader::LoadJpegSliceContent"
+                   << " called while progressiveQuality_ is false!");
+
+    LOG(TRACE) << "OrthancSeriesVolumeProgressiveLoader::LoadJpegSliceContent";
+    unsigned int quality;
+      
+    switch (dynamic_cast<const OrthancStone::GetOrthancWebViewerJpegCommand&>(message.GetOrigin()).GetQuality())
+    {
+      case 50:
+        quality = QUALITY_00;
+        break;
+
+      case 90:
+        quality = QUALITY_01;
+        break;
+
+      default:
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+    }
+      
+    SetSliceContent(GetSliceIndexPayload(message.GetOrigin()), message.GetImage(), quality);
+  }
+
+  OrthancSeriesVolumeProgressiveLoader::OrthancSeriesVolumeProgressiveLoader(
+    OrthancStone::ILoadersContext& loadersContext,
+    boost::shared_ptr<OrthancStone::DicomVolumeImage> volume,
+    bool progressiveQuality)
+    : loadersContext_(loadersContext)
+    , active_(false)
+    , progressiveQuality_(progressiveQuality)
+    , simultaneousDownloads_(4)
+    , volume_(volume)
+    , sorter_(new OrthancStone::BasicFetchingItemsSorter::Factory)
+    , volumeImageReadyInHighQuality_(false)
+  {
+  }
+
+  boost::shared_ptr<OrthancSeriesVolumeProgressiveLoader> 
+    OrthancSeriesVolumeProgressiveLoader::Create(
+      OrthancStone::ILoadersContext& loadersContext,
+      boost::shared_ptr<OrthancStone::DicomVolumeImage> volume,
+      bool progressiveQuality)
+  {
+    std::auto_ptr<OrthancStone::ILoadersContext::ILock> lock(loadersContext.Lock());
+
+    boost::shared_ptr<OrthancSeriesVolumeProgressiveLoader> obj(
+        new OrthancSeriesVolumeProgressiveLoader(
+          loadersContext, volume, progressiveQuality));
+
+    obj->Register<OrthancStone::OrthancRestApiCommand::SuccessMessage>(
+      lock->GetOracleObservable(),
+      &OrthancSeriesVolumeProgressiveLoader::LoadGeometry);
+
+    obj->Register<OrthancStone::GetOrthancImageCommand::SuccessMessage>(
+      lock->GetOracleObservable(),
+      &OrthancSeriesVolumeProgressiveLoader::LoadBestQualitySliceContent);
+
+    obj->Register<OrthancStone::GetOrthancWebViewerJpegCommand::SuccessMessage>(
+      lock->GetOracleObservable(),
+      &OrthancSeriesVolumeProgressiveLoader::LoadJpegSliceContent);
+
+    return obj;
+  }
+
+
+  OrthancSeriesVolumeProgressiveLoader::~OrthancSeriesVolumeProgressiveLoader()
+  {
+    LOG(TRACE) << "OrthancSeriesVolumeProgressiveLoader::~OrthancSeriesVolumeProgressiveLoader()";
+  }
+
+  void OrthancSeriesVolumeProgressiveLoader::SetSimultaneousDownloads(unsigned int count)
+  {
+    if (active_)
+    {
+      LOG(ERROR) << "OrthancSeriesVolumeProgressiveLoader::SetSimultaneousDownloads(): (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_)
+    {
+      LOG(ERROR) << "OrthancSeriesVolumeProgressiveLoader::LoadSeries(const std::string& seriesId): (active_)";
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      active_ = true;
+
+      std::unique_ptr<OrthancStone::OrthancRestApiCommand> command(new OrthancStone::OrthancRestApiCommand);
+      command->SetUri("/series/" + seriesId + "/instances-tags");
+      {
+        std::unique_ptr<OrthancStone::ILoadersContext::ILock> lock(loadersContext_.Lock());
+        boost::shared_ptr<IObserver> observer(GetSharedObserver());
+        lock->Schedule(observer, 0, command.release()); //TODO: priority!
+      }
+    }
+  }
+  
+
+  OrthancStone::IVolumeSlicer::IExtractedSlice* 
+  OrthancSeriesVolumeProgressiveLoader::ExtractSlice(const OrthancStone::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	Fri Apr 03 16:13:06 2020 +0200
@@ -0,0 +1,175 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 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 "../../Loaders/IFetchingItemsSorter.h"
+#include "../../Loaders/IFetchingStrategy.h"
+#include "../../Messages/IObservable.h"
+#include "../../Messages/ObserverBase.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 "../Volumes/IGeometryProvider.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 OrthancStone::ObserverBase<OrthancSeriesVolumeProgressiveLoader>,
+    public OrthancStone::IObservable,
+    public OrthancStone::IVolumeSlicer,
+    public IGeometryProvider
+  {
+  private:
+    static const unsigned int QUALITY_00 = 0;
+    static const unsigned int QUALITY_01 = 1;
+    static const unsigned int QUALITY_02 = 2;
+        
+    class ExtractedSlice;
+    
+    /** Helper class internal to OrthancSeriesVolumeProgressiveLoader */
+    class SeriesGeometry : public boost::noncopyable
+    {
+    private:
+      void CheckSlice(size_t index,
+                      const OrthancStone::DicomInstanceParameters& reference) const;
+    
+      void CheckVolume() const;
+
+      void Clear();
+
+      void CheckSliceIndex(size_t index) const;
+
+      std::unique_ptr<OrthancStone::VolumeImageGeometry>     geometry_;
+      std::vector<OrthancStone::DicomInstanceParameters*>  slices_;
+      std::vector<uint64_t>                  slicesRevision_;
+
+    public:
+      ~SeriesGeometry()
+      {
+        Clear();
+      }
+
+      void ComputeGeometry(OrthancStone::SlicesSorter& slices);
+
+      virtual bool HasGeometry() const
+      {
+        return geometry_.get() != NULL;
+      }
+
+      virtual const OrthancStone::VolumeImageGeometry& GetImageGeometry() const;
+
+      const OrthancStone::DicomInstanceParameters& GetSliceParameters(size_t index) const;
+
+      uint64_t GetSliceRevision(size_t index) const;
+
+      void IncrementSliceRevision(size_t index);
+    };
+
+    void ScheduleNextSliceDownload();
+
+    void LoadGeometry(const OrthancStone::OrthancRestApiCommand::SuccessMessage& message);
+
+    void SetSliceContent(unsigned int sliceIndex,
+                         const Orthanc::ImageAccessor& image,
+                         unsigned int quality);
+
+    void LoadBestQualitySliceContent(const OrthancStone::GetOrthancImageCommand::SuccessMessage& message);
+
+    void LoadJpegSliceContent(const OrthancStone::GetOrthancWebViewerJpegCommand::SuccessMessage& message);
+
+    OrthancStone::ILoadersContext&  loadersContext_;
+    bool                                          active_;
+    bool                                          progressiveQuality_;
+    unsigned int                                  simultaneousDownloads_;
+    SeriesGeometry                                seriesGeometry_;
+    boost::shared_ptr<OrthancStone::DicomVolumeImage> volume_;
+    std::unique_ptr<OrthancStone::IFetchingItemsSorter::IFactory> sorter_;
+    std::unique_ptr<OrthancStone::IFetchingStrategy> strategy_;
+    std::vector<unsigned int>     slicesQuality_;
+    bool                          volumeImageReadyInHighQuality_;
+    
+    OrthancSeriesVolumeProgressiveLoader(
+      OrthancStone::ILoadersContext& loadersContext,
+      boost::shared_ptr<OrthancStone::DicomVolumeImage> volume,
+      bool progressiveQuality);
+  
+  public:
+    ORTHANC_STONE_DEFINE_ORIGIN_MESSAGE(__FILE__, __LINE__, VolumeImageReadyInHighQuality, OrthancSeriesVolumeProgressiveLoader);
+
+    /**
+    See doc for the progressiveQuality_ field
+    */
+    static boost::shared_ptr<OrthancSeriesVolumeProgressiveLoader> Create(
+      OrthancStone::ILoadersContext& context,
+      boost::shared_ptr<OrthancStone::DicomVolumeImage> volume,
+      bool progressiveQuality = false);
+
+    virtual ~OrthancSeriesVolumeProgressiveLoader();
+
+    void SetSimultaneousDownloads(unsigned int count);
+
+    bool IsVolumeImageReadyInHighQuality() const
+    {
+      return volumeImageReadyInHighQuality_;
+    }
+
+    void LoadSeries(const std::string& seriesId);
+
+    /**
+    This getter is used by clients that do not receive the geometry through
+    subscribing, for instance if they are created or listening only AFTER the
+    "geometry loaded" message is broadcast 
+    */
+    bool HasGeometry() const ORTHANC_OVERRIDE
+    {
+      return seriesGeometry_.HasGeometry();
+    }
+
+    /**
+    Same remark as HasGeometry
+    */
+    const OrthancStone::VolumeImageGeometry& GetImageGeometry() const ORTHANC_OVERRIDE
+    {
+      return seriesGeometry_.GetImageGeometry();
+    }
+
+    /**
+    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 OrthancStone::CoordinateSystem3D& cuttingPlane) ORTHANC_OVERRIDE;
+  };
+}
--- a/Resources/CMake/OrthancStoneConfiguration.cmake	Tue Mar 31 11:01:34 2020 +0200
+++ b/Resources/CMake/OrthancStoneConfiguration.cmake	Fri Apr 03 16:13:06 2020 +0200
@@ -395,18 +395,8 @@
     # ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Layers/SingleFrameRendererFactory.h
     ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Layers/SliceOutlineRenderer.cpp
     ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Layers/SliceOutlineRenderer.h
-    ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Loaders/DicomStructureSetLoader.cpp
-    ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Loaders/DicomStructureSetLoader.h
     ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Loaders/DicomStructureSetLoader2.cpp
     ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Loaders/DicomStructureSetLoader2.h
-    ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Loaders/LoaderCache.cpp
-    ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Loaders/LoaderCache.h
-    ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Loaders/LoaderStateMachine.cpp
-    ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Loaders/LoaderStateMachine.h
-    ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Loaders/OrthancMultiframeVolumeLoader.cpp
-    ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Loaders/OrthancMultiframeVolumeLoader.h
-    ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Loaders/OrthancSeriesVolumeProgressiveLoader.cpp
-    ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Loaders/OrthancSeriesVolumeProgressiveLoader.h
     ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Toolbox/BaseWebService.cpp
     ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Toolbox/BaseWebService.h
     ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Toolbox/DicomFrameConverter.cpp
@@ -537,11 +527,21 @@
   ${ORTHANC_STONE_ROOT}/Framework/Loaders/BasicFetchingStrategy.h
   ${ORTHANC_STONE_ROOT}/Framework/Loaders/DicomResourcesLoader.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Loaders/DicomSource.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Loaders/DicomStructureSetLoader.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Loaders/DicomStructureSetLoader.h
   ${ORTHANC_STONE_ROOT}/Framework/Loaders/DicomVolumeLoader.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Loaders/IFetchingItemsSorter.h
   ${ORTHANC_STONE_ROOT}/Framework/Loaders/IFetchingStrategy.h
   ${ORTHANC_STONE_ROOT}/Framework/Loaders/LoadedDicomResources.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Loaders/LoaderCache.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Loaders/LoaderCache.h
+  ${ORTHANC_STONE_ROOT}/Framework/Loaders/LoaderStateMachine.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Loaders/LoaderStateMachine.h
+  ${ORTHANC_STONE_ROOT}/Framework/Loaders/OrthancMultiframeVolumeLoader.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Loaders/OrthancMultiframeVolumeLoader.h
   ${ORTHANC_STONE_ROOT}/Framework/Loaders/OracleScheduler.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Loaders/OrthancSeriesVolumeProgressiveLoader.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Loaders/OrthancSeriesVolumeProgressiveLoader.h
   ${ORTHANC_STONE_ROOT}/Framework/Loaders/SeriesFramesLoader.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Loaders/SeriesMetadataLoader.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Loaders/SeriesOrderedFrames.cpp