changeset 1228:c471a0aa137b broker

adding the next generation of loaders
author Sebastien Jodogne <s.jodogne@gmail.com>
date Mon, 09 Dec 2019 13:58:37 +0100
parents a1c0c9c9f9af
children b9f2a111c5b9
files Framework/Loaders/DicomResourcesLoader.cpp Framework/Loaders/DicomResourcesLoader.h Framework/Loaders/DicomSource.cpp Framework/Loaders/DicomSource.h Framework/Loaders/DicomVolumeLoader.cpp Framework/Loaders/DicomVolumeLoader.h Framework/Loaders/GenericLoadersContext.cpp Framework/Loaders/GenericLoadersContext.h Framework/Loaders/ILoaderFactory.h Framework/Loaders/ILoadersContext.h Framework/Loaders/LoadedDicomResources.cpp Framework/Loaders/LoadedDicomResources.h Framework/Loaders/OracleScheduler.cpp Framework/Loaders/OracleScheduler.h Framework/Loaders/SeriesFramesLoader.cpp Framework/Loaders/SeriesFramesLoader.h Framework/Loaders/SeriesMetadataLoader.cpp Framework/Loaders/SeriesMetadataLoader.h Framework/Loaders/SeriesOrderedFrames.cpp Framework/Loaders/SeriesOrderedFrames.h Framework/Loaders/SeriesThumbnailsLoader.cpp Framework/Loaders/SeriesThumbnailsLoader.h Resources/CMake/OrthancStoneConfiguration.cmake
diffstat 23 files changed, 5830 insertions(+), 4 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Loaders/DicomResourcesLoader.cpp	Mon Dec 09 13:58:37 2019 +0100
@@ -0,0 +1,905 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "DicomResourcesLoader.h"
+
+#if !defined(ORTHANC_ENABLE_DCMTK)
+#  error The macro ORTHANC_ENABLE_DCMTK must be defined
+#endif
+
+#if ORTHANC_ENABLE_DCMTK == 1
+#  include "../Oracle/ParseDicomFromFileCommand.h"
+#  include <Core/DicomParsing/ParsedDicomFile.h>
+#endif
+
+#include <boost/filesystem/path.hpp>
+
+namespace OrthancStone
+{
+  static std::string GetUri(Orthanc::ResourceType level)
+  {
+    switch (level)
+    {
+      case Orthanc::ResourceType_Patient:
+        return "patients";
+        
+      case Orthanc::ResourceType_Study:
+        return "studies";
+        
+      case Orthanc::ResourceType_Series:
+        return "series";
+        
+      case Orthanc::ResourceType_Instance:
+        return "instances";
+
+      default:
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  class DicomResourcesLoader::Handler : public Orthanc::IDynamicObject
+  {
+  private:
+    boost::shared_ptr<DicomResourcesLoader>     loader_;
+    boost::shared_ptr<LoadedDicomResources>     target_;
+    int                                         priority_;
+    DicomSource                                 source_;
+    boost::shared_ptr<Orthanc::IDynamicObject>  userPayload_;
+
+  public:
+    Handler(boost::shared_ptr<DicomResourcesLoader> loader,
+            boost::shared_ptr<LoadedDicomResources> target,
+            int priority,
+            const DicomSource& source,
+            boost::shared_ptr<Orthanc::IDynamicObject> userPayload) :
+      loader_(loader),
+      target_(target),
+      priority_(priority),
+      source_(source),
+      userPayload_(userPayload)
+    {
+      if (!loader ||
+          !target)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
+      }
+    }
+      
+    virtual ~Handler()
+    {
+    }
+
+    void BroadcastSuccess()
+    {
+      SuccessMessage message(*loader_, target_, priority_, source_, userPayload_.get());
+      loader_->BroadcastMessage(message);
+    }
+
+    boost::shared_ptr<DicomResourcesLoader> GetLoader()
+    {
+      assert(loader_);
+      return loader_;
+    }
+
+    boost::shared_ptr<LoadedDicomResources> GetTarget()
+    {
+      assert(target_);
+      return target_;
+    }
+
+    int GetPriority() const
+    {
+      return priority_;
+    }
+
+    const DicomSource& GetSource() const
+    {
+      return source_;
+    }
+
+    const boost::shared_ptr<Orthanc::IDynamicObject> GetUserPayload() const
+    {
+      return userPayload_;
+    }
+  };
+
+
+  class DicomResourcesLoader::StringHandler : public DicomResourcesLoader::Handler
+  {
+  public:
+    StringHandler(boost::shared_ptr<DicomResourcesLoader> loader,
+                  boost::shared_ptr<LoadedDicomResources> target,
+                  int priority,
+                  const DicomSource& source,
+                  boost::shared_ptr<Orthanc::IDynamicObject> userPayload) :
+      Handler(loader, target, priority, source, userPayload)
+    {
+    }
+
+    virtual void HandleJson(const Json::Value& body) = 0;
+      
+    virtual void HandleString(const std::string& body)
+    {
+      Json::Reader reader;
+      Json::Value value;
+      if (reader.parse(body, value))
+      {
+        HandleJson(value);
+      }
+      else
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol);
+      }
+    }
+  };
+
+
+  class DicomResourcesLoader::DicomWebHandler : public StringHandler
+  {
+  public:
+    DicomWebHandler(boost::shared_ptr<DicomResourcesLoader> loader,
+                    boost::shared_ptr<LoadedDicomResources> target,
+                    int priority,
+                    const DicomSource& source,
+                    boost::shared_ptr<Orthanc::IDynamicObject> userPayload) :
+      StringHandler(loader, target, priority, source, userPayload)
+    {
+    }
+
+    virtual void HandleJson(const Json::Value& body)
+    {
+      GetTarget()->AddFromDicomWeb(body);
+      BroadcastSuccess();
+    }
+  };
+
+
+  class DicomResourcesLoader::OrthancHandler : public StringHandler
+  {
+  private:
+    boost::shared_ptr<unsigned int>  remainingCommands_;
+
+  protected:
+    void CloseCommand()
+    {
+      assert(remainingCommands_);
+        
+      if (*remainingCommands_ == 0)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+      }
+
+      (*remainingCommands_) --;
+
+      if (*remainingCommands_ == 0)
+      {
+        BroadcastSuccess();
+      }
+    }
+      
+  public:
+    OrthancHandler(boost::shared_ptr<DicomResourcesLoader> loader,
+                   boost::shared_ptr<LoadedDicomResources> target,
+                   int priority,
+                   const DicomSource& source,
+                   boost::shared_ptr<unsigned int> remainingCommands,
+                   boost::shared_ptr<Orthanc::IDynamicObject> userPayload) :
+      StringHandler(loader, target, priority, source, userPayload),
+      remainingCommands_(remainingCommands)
+    {
+      if (!remainingCommands)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
+      }
+
+      (*remainingCommands) ++;
+    }
+
+    boost::shared_ptr<unsigned int> GetRemainingCommands()
+    {
+      assert(remainingCommands_);
+      return remainingCommands_;
+    }
+  };
+
+    
+  class DicomResourcesLoader::OrthancInstanceTagsHandler : public OrthancHandler
+  {
+  public:
+    OrthancInstanceTagsHandler(boost::shared_ptr<DicomResourcesLoader> loader,
+                               boost::shared_ptr<LoadedDicomResources> target,
+                               int priority,
+                               const DicomSource& source,
+                               boost::shared_ptr<unsigned int> remainingCommands,
+                               boost::shared_ptr<Orthanc::IDynamicObject> userPayload) :
+      OrthancHandler(loader, target, priority, source, remainingCommands, userPayload)
+    {
+    }
+
+    virtual void HandleJson(const Json::Value& body)
+    {
+      GetTarget()->AddFromOrthanc(body);
+      CloseCommand();
+    }
+  };
+
+    
+  class DicomResourcesLoader::OrthancOneChildInstanceHandler : public OrthancHandler
+  {
+  public:
+    OrthancOneChildInstanceHandler(boost::shared_ptr<DicomResourcesLoader> loader,
+                                   boost::shared_ptr<LoadedDicomResources> target,
+                                   int  priority,
+                                   const DicomSource& source,
+                                   boost::shared_ptr<unsigned int> remainingCommands,
+                                   boost::shared_ptr<Orthanc::IDynamicObject> userPayload) :
+      OrthancHandler(loader, target, priority, source, remainingCommands, userPayload)
+    {
+    }
+
+    virtual void HandleJson(const Json::Value& body)
+    {
+      static const char* const ID = "ID";
+      
+      if (body.type() == Json::arrayValue)
+      {
+        if (body.size() > 0)
+        {
+          if (body[0].type() == Json::objectValue &&
+              body[0].isMember(ID) &&
+              body[0][ID].type() == Json::stringValue)
+          {
+            GetLoader()->ScheduleLoadOrthancInstanceTags
+              (GetTarget(), GetPriority(), GetSource(), body[0][ID].asString(), GetRemainingCommands(), GetUserPayload());
+            CloseCommand();
+          }
+          else
+          {
+            throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol);
+          }
+        }
+      }
+      else
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol);
+      }
+    }
+  };
+
+
+  class DicomResourcesLoader::OrthancAllChildrenInstancesHandler : public OrthancHandler
+  {
+  private:
+    Orthanc::ResourceType  bottomLevel_;
+
+  public:
+    OrthancAllChildrenInstancesHandler(boost::shared_ptr<DicomResourcesLoader> loader,
+                                       boost::shared_ptr<LoadedDicomResources> target,
+                                       int  priority,
+                                       const DicomSource& source,
+                                       boost::shared_ptr<unsigned int> remainingCommands,
+                                       Orthanc::ResourceType bottomLevel,
+                                       boost::shared_ptr<Orthanc::IDynamicObject> userPayload) :
+      OrthancHandler(loader, target, priority, source, remainingCommands, userPayload),
+      bottomLevel_(bottomLevel)
+    {
+    }
+
+    virtual void HandleJson(const Json::Value& body)
+    {
+      static const char* const ID = "ID";
+      static const char* const INSTANCES = "Instances";
+
+      if (body.type() == Json::arrayValue)
+      {
+        for (Json::Value::ArrayIndex i = 0; i < body.size(); i++)
+        {
+          switch (bottomLevel_)
+          {
+            case Orthanc::ResourceType_Patient:
+            case Orthanc::ResourceType_Study:
+              if (body[i].type() == Json::objectValue &&
+                  body[i].isMember(ID) &&
+                  body[i][ID].type() == Json::stringValue)
+              {
+                GetLoader()->ScheduleLoadOrthancOneChildInstance
+                  (GetTarget(), GetPriority(), GetSource(), bottomLevel_,
+                   body[i][ID].asString(), GetRemainingCommands(), GetUserPayload());
+              }
+              else
+              {
+                throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol);
+              }
+              
+              break;
+            
+            case Orthanc::ResourceType_Series:
+              // At the series level, avoid a call to
+              // "/series/.../instances", as we already have this
+              // information in the JSON
+              if (body[i].type() == Json::objectValue &&
+                  body[i].isMember(INSTANCES) &&
+                  body[i][INSTANCES].type() == Json::arrayValue)
+              {
+                if (body[i][INSTANCES].size() > 0)
+                {
+                  if (body[i][INSTANCES][0].type() == Json::stringValue)
+                  {
+                    GetLoader()->ScheduleLoadOrthancInstanceTags
+                      (GetTarget(), GetPriority(), GetSource(),
+                       body[i][INSTANCES][0].asString(), GetRemainingCommands(), GetUserPayload());
+                  }
+                  else
+                  {
+                    throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol);
+                  }
+                }
+              }
+
+              break;
+
+            case Orthanc::ResourceType_Instance:
+              if (body[i].type() == Json::objectValue &&
+                  body[i].isMember(ID) &&
+                  body[i][ID].type() == Json::stringValue)
+              {
+                GetLoader()->ScheduleLoadOrthancInstanceTags
+                  (GetTarget(), GetPriority(), GetSource(),
+                   body[i][ID].asString(), GetRemainingCommands(), GetUserPayload());
+              }
+              else
+              {
+                throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol);
+              }
+
+              break;
+
+            default:
+              throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+          }
+        }
+      }
+      
+      CloseCommand();
+    }
+  };
+
+
+#if ORTHANC_ENABLE_DCMTK == 1
+  static void ExploreDicomDir(OrthancStone::LoadedDicomResources& instances,
+                              const Orthanc::ParsedDicomDir& dicomDir,
+                              Orthanc::ResourceType level,
+                              size_t index,
+                              const Orthanc::DicomMap& parent)
+  {
+    std::string expectedType;
+
+    switch (level)
+    {
+      case Orthanc::ResourceType_Patient:
+        expectedType = "PATIENT";
+        break;
+
+      case Orthanc::ResourceType_Study:
+        expectedType = "STUDY";
+        break;
+
+      case Orthanc::ResourceType_Series:
+        expectedType = "SERIES";
+        break;
+
+      case Orthanc::ResourceType_Instance:
+        expectedType = "IMAGE";
+        break;
+
+      default:
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+
+    for (;;)
+    {
+      std::auto_ptr<Orthanc::DicomMap> current(dicomDir.GetItem(index).Clone());
+      current->RemoveBinaryTags();
+      current->Merge(parent);
+
+      std::string type;
+      if (!current->LookupStringValue(type, Orthanc::DICOM_TAG_DIRECTORY_RECORD_TYPE, false))
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+      }
+
+      if (type == expectedType)
+      {
+        if (level == Orthanc::ResourceType_Instance)
+        {
+          instances.AddResource(*current);
+        }
+        else
+        {
+          size_t lower;
+          if (dicomDir.LookupLower(lower, index))
+          {
+            ExploreDicomDir(instances, dicomDir, Orthanc::GetChildResourceType(level), lower, *current);
+          }
+        }
+      }
+
+      size_t next;
+      if (dicomDir.LookupNext(next, index))
+      {
+        index = next;
+      }
+      else
+      {
+        return;
+      }
+    }
+  }
+#endif
+
+
+#if ORTHANC_ENABLE_DCMTK == 1
+  void DicomResourcesLoader::GetDicomDirInstances(LoadedDicomResources& target,
+                                                  const Orthanc::ParsedDicomDir& dicomDir)
+  {
+    Orthanc::DicomMap parent;
+    ExploreDicomDir(target, dicomDir, Orthanc::ResourceType_Patient, 0, parent);
+  }
+#endif
+
+
+#if ORTHANC_ENABLE_DCMTK == 1
+  class DicomResourcesLoader::DicomDirHandler : public StringHandler
+  {
+  public:
+    DicomDirHandler(boost::shared_ptr<DicomResourcesLoader> loader,
+                    boost::shared_ptr<LoadedDicomResources> target,
+                    int priority,
+                    const DicomSource& source,
+                    boost::shared_ptr<Orthanc::IDynamicObject> userPayload) :
+      StringHandler(loader, target, priority, source, userPayload)
+    {
+    }
+
+    virtual void HandleJson(const Json::Value& body)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+    }
+      
+    virtual void HandleString(const std::string& body)
+    {
+      Orthanc::ParsedDicomDir dicomDir(body);
+      GetDicomDirInstances(*GetTarget(), dicomDir);
+      BroadcastSuccess();
+    }
+  };
+#endif
+  
+    
+  void DicomResourcesLoader::Handle(const HttpCommand::SuccessMessage& message)
+  {
+    if (message.GetOrigin().HasPayload())
+    {
+      dynamic_cast<StringHandler&>(message.GetOrigin().GetPayload()).HandleString(message.GetAnswer());
+    }
+  }
+
+
+  void DicomResourcesLoader::Handle(const OrthancRestApiCommand::SuccessMessage& message)
+  {
+    if (message.GetOrigin().HasPayload())
+    {
+      dynamic_cast<StringHandler&>(message.GetOrigin().GetPayload()).HandleString(message.GetAnswer());
+    }
+  }
+
+
+  void DicomResourcesLoader::Handle(const ReadFileCommand::SuccessMessage& message)
+  {
+    if (message.GetOrigin().HasPayload())
+    {
+      dynamic_cast<StringHandler&>(message.GetOrigin().GetPayload()).HandleString(message.GetContent());
+    }
+  }
+
+
+#if ORTHANC_ENABLE_DCMTK == 1
+  void DicomResourcesLoader::Handle(const ParseDicomSuccessMessage& message)
+  {
+    if (message.GetOrigin().HasPayload())
+    {
+      Handler& handler = dynamic_cast<Handler&>(message.GetOrigin().GetPayload());
+
+      std::set<Orthanc::DicomTag> ignoreTagLength;
+      ignoreTagLength.insert(Orthanc::DICOM_TAG_GRID_FRAME_OFFSET_VECTOR);  // Needed for RT-DOSE
+
+      Orthanc::DicomMap summary;
+      message.GetDicom().ExtractDicomSummary(summary, ignoreTagLength);
+      handler.GetTarget()->AddResource(summary);
+
+      handler.BroadcastSuccess();
+    }
+  }
+#endif
+
+
+  void DicomResourcesLoader::Handle(const OracleCommandExceptionMessage& message)
+  {
+    // TODO
+    LOG(ERROR) << "Exception: " << message.GetException().What();
+  }
+    
+
+  void DicomResourcesLoader::ScheduleLoadOrthancInstanceTags(boost::shared_ptr<LoadedDicomResources> target,
+                                                             int priority,
+                                                             const DicomSource& source,
+                                                             const std::string& instanceId,
+                                                             boost::shared_ptr<unsigned int> remainingCommands,
+                                                             boost::shared_ptr<Orthanc::IDynamicObject> userPayload)
+  {
+    std::auto_ptr<OrthancRestApiCommand> command(new OrthancRestApiCommand);
+    command->SetUri("/instances/" + instanceId + "/tags");
+    command->AcquirePayload(new OrthancInstanceTagsHandler(shared_from_this(), target, priority,
+                                                           source, remainingCommands, userPayload));
+
+    {
+      std::auto_ptr<ILoadersContext::ILock> lock(context_.Lock());
+      lock->Schedule(GetSharedObserver(), priority, command.release());
+    }
+  }
+
+
+  void DicomResourcesLoader::ScheduleLoadOrthancOneChildInstance(boost::shared_ptr<LoadedDicomResources> target,
+                                                                 int priority,
+                                                                 const DicomSource& source,
+                                                                 Orthanc::ResourceType level,
+                                                                 const std::string& id,
+                                                                 boost::shared_ptr<unsigned int> remainingCommands,
+                                                                 boost::shared_ptr<Orthanc::IDynamicObject> userPayload)
+  {
+    std::auto_ptr<OrthancRestApiCommand> command(new OrthancRestApiCommand);
+    command->SetUri("/" + GetUri(level) + "/" + id + "/instances");
+    command->AcquirePayload(new OrthancOneChildInstanceHandler(shared_from_this(), target, priority,
+                                                               source, remainingCommands, userPayload));
+
+    {
+      std::auto_ptr<ILoadersContext::ILock> lock(context_.Lock());
+      lock->Schedule(GetSharedObserver(), priority, command.release());
+    }
+  }
+    
+    
+
+  const Orthanc::IDynamicObject& DicomResourcesLoader::SuccessMessage::GetUserPayload() const
+  {
+    if (userPayload_ == NULL)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      return *userPayload_;
+    }
+  }
+
+
+  boost::shared_ptr<IObserver> DicomResourcesLoader::Factory::Create(ILoadersContext::ILock& stone)
+  {
+    boost::shared_ptr<DicomResourcesLoader> result(new DicomResourcesLoader(stone.GetContext()));
+    result->Register<HttpCommand::SuccessMessage>(stone.GetOracleObservable(), &DicomResourcesLoader::Handle);
+    result->Register<OracleCommandExceptionMessage>(stone.GetOracleObservable(), &DicomResourcesLoader::Handle);
+    result->Register<OrthancRestApiCommand::SuccessMessage>(stone.GetOracleObservable(), &DicomResourcesLoader::Handle);
+    result->Register<ReadFileCommand::SuccessMessage>(stone.GetOracleObservable(), &DicomResourcesLoader::Handle);
+
+#if ORTHANC_ENABLE_DCMTK == 1
+    result->Register<ParseDicomSuccessMessage>(stone.GetOracleObservable(), &DicomResourcesLoader::Handle);
+#endif
+    
+    return boost::shared_ptr<IObserver>(result);
+  }
+
+
+  static void SetIncludeTags(std::map<std::string, std::string>& arguments,
+                             const std::set<Orthanc::DicomTag>& includeTags)
+  {
+    if (!includeTags.empty())
+    {
+      std::string s;
+      bool first = true;
+
+      for (std::set<Orthanc::DicomTag>::const_iterator
+             it = includeTags.begin(); it != includeTags.end(); ++it)
+      {
+        if (first)
+        {
+          first = false;
+        }
+        else
+        {
+          s += ",";
+        }
+
+        char buf[16];
+        sprintf(buf, "%04X%04X", it->GetGroup(), it->GetElement());
+        s += std::string(buf);
+      }
+
+      arguments["includefield"] = s;
+    }    
+  }
+  
+  
+  void DicomResourcesLoader::ScheduleWado(boost::shared_ptr<LoadedDicomResources> target,
+                                          int priority,
+                                          const DicomSource& source,
+                                          const std::string& uri,
+                                          const std::set<Orthanc::DicomTag>& includeTags,
+                                          Orthanc::IDynamicObject* userPayload)
+  {
+    boost::shared_ptr<Orthanc::IDynamicObject> protection(userPayload);
+    
+    if (!source.IsDicomWeb())
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls, "Not a DICOMweb source");
+    }
+
+    std::map<std::string, std::string> arguments, headers;
+    SetIncludeTags(arguments, includeTags);
+  
+    std::auto_ptr<IOracleCommand> command(
+      source.CreateDicomWebCommand(uri, arguments, headers, 
+                                   new DicomWebHandler(shared_from_this(), target, priority, source, protection)));
+      
+    {
+      std::auto_ptr<ILoadersContext::ILock> lock(context_.Lock());
+      lock->Schedule(GetSharedObserver(), priority, command.release());
+    }
+  }
+  
+
+  void DicomResourcesLoader::ScheduleQido(boost::shared_ptr<LoadedDicomResources> target,
+                                          int priority,
+                                          const DicomSource& source,
+                                          Orthanc::ResourceType level,
+                                          const Orthanc::DicomMap& filter,
+                                          const std::set<Orthanc::DicomTag>& includeTags,
+                                          Orthanc::IDynamicObject* userPayload)
+  {
+    boost::shared_ptr<Orthanc::IDynamicObject> protection(userPayload);
+    
+    if (!source.IsDicomWeb())
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls, "Not a DICOMweb source");
+    }
+
+    std::string uri;
+    switch (level)
+    {
+      case Orthanc::ResourceType_Study:
+        uri = "/studies";
+        break;
+
+      case Orthanc::ResourceType_Series:
+        uri = "/series";
+        break;
+
+      case Orthanc::ResourceType_Instance:
+        uri = "/instances";
+        break;
+
+      default:
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+
+    std::set<Orthanc::DicomTag> tags;
+    filter.GetTags(tags);
+
+    std::map<std::string, std::string> arguments, headers;
+
+    for (std::set<Orthanc::DicomTag>::const_iterator it = tags.begin(); it != tags.end(); ++it)
+    {
+      std::string s;
+      if (filter.LookupStringValue(s, *it, false /* no binary */))
+      {
+        char buf[16];
+        sprintf(buf, "%04X%04X", it->GetGroup(), it->GetElement());
+        arguments[buf] = s;
+      }
+    }
+
+    SetIncludeTags(arguments, includeTags);
+
+    std::auto_ptr<IOracleCommand> command(
+      source.CreateDicomWebCommand(uri, arguments, headers, 
+                                   new DicomWebHandler(shared_from_this(), target, priority, source, protection)));
+
+
+    {
+      std::auto_ptr<ILoadersContext::ILock> lock(context_.Lock());
+      lock->Schedule(GetSharedObserver(), priority, command.release());
+    }
+  }
+
+    
+  void DicomResourcesLoader::ScheduleLoadOrthancResources(boost::shared_ptr<LoadedDicomResources> target,
+                                                          int priority,
+                                                          const DicomSource& source,
+                                                          Orthanc::ResourceType topLevel,
+                                                          const std::string& topId,
+                                                          Orthanc::ResourceType bottomLevel,
+                                                          Orthanc::IDynamicObject* userPayload)
+  {
+    boost::shared_ptr<Orthanc::IDynamicObject> protection(userPayload);
+    
+    if (!source.IsOrthanc())
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls, "Not an Orthanc source");
+    }
+
+    bool ok = false;
+
+    switch (topLevel)
+    {
+      case Orthanc::ResourceType_Patient:
+        ok = (bottomLevel == Orthanc::ResourceType_Patient ||
+              bottomLevel == Orthanc::ResourceType_Study ||
+              bottomLevel == Orthanc::ResourceType_Series ||
+              bottomLevel == Orthanc::ResourceType_Instance);
+        break;
+              
+      case Orthanc::ResourceType_Study:
+        ok = (bottomLevel == Orthanc::ResourceType_Study ||
+              bottomLevel == Orthanc::ResourceType_Series ||
+              bottomLevel == Orthanc::ResourceType_Instance);
+        break;
+              
+      case Orthanc::ResourceType_Series:
+        ok = (bottomLevel == Orthanc::ResourceType_Series ||
+              bottomLevel == Orthanc::ResourceType_Instance);
+        break;
+              
+      case Orthanc::ResourceType_Instance:
+        ok = (bottomLevel == Orthanc::ResourceType_Instance);
+        break;
+
+      default:
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+
+    if (!ok)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+
+    boost::shared_ptr<unsigned int> remainingCommands(new unsigned int(0));
+
+    if (topLevel == Orthanc::ResourceType_Instance)
+    {
+      ScheduleLoadOrthancInstanceTags(target, priority, source, topId, remainingCommands, protection);
+    }
+    else if (topLevel == bottomLevel)
+    {
+      ScheduleLoadOrthancOneChildInstance(target, priority, source, topLevel, topId, remainingCommands, protection);
+    }
+    else 
+    {
+      std::auto_ptr<OrthancRestApiCommand> command(new OrthancRestApiCommand);
+      command->SetUri("/" + GetUri(topLevel) + "/" + topId + "/" + GetUri(bottomLevel));
+      command->AcquirePayload(new OrthancAllChildrenInstancesHandler
+                              (shared_from_this(), target, priority, source,
+                               remainingCommands, bottomLevel, protection));
+
+      {
+        std::auto_ptr<ILoadersContext::ILock> lock(context_.Lock());
+        lock->Schedule(GetSharedObserver(), priority, command.release());
+      }
+    }
+  }
+
+
+  void DicomResourcesLoader::ScheduleLoadDicomDir(boost::shared_ptr<LoadedDicomResources> target,
+                                                  int priority,
+                                                  const DicomSource& source,
+                                                  const std::string& path,
+                                                  Orthanc::IDynamicObject* userPayload)
+  {
+    boost::shared_ptr<Orthanc::IDynamicObject> protection(userPayload);
+    
+    if (!source.IsDicomDir())
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls, "Not a DICOMDIR source");
+    }
+
+    if (target->GetIndexedTag() == Orthanc::DICOM_TAG_SOP_INSTANCE_UID)
+    {
+      LOG(WARNING) << "If loading DICOMDIR, it is advised to index tag "
+                   << "ReferencedSopInstanceUidInFile (0004,1511)";
+    }
+
+#if ORTHANC_ENABLE_DCMTK == 1
+    std::auto_ptr<ReadFileCommand> command(new ReadFileCommand(path));
+    command->AcquirePayload(new DicomDirHandler(shared_from_this(), target, priority, source, protection));
+
+    {
+      std::auto_ptr<ILoadersContext::ILock> lock(context_.Lock());      
+      lock->Schedule(GetSharedObserver(), priority, command.release());
+    }
+#else
+    throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError,
+                                    "DCMTK is disabled, cannot load DICOMDIR");
+#endif
+  }
+
+
+  void DicomResourcesLoader::ScheduleLoadDicomFile(boost::shared_ptr<LoadedDicomResources> target,
+                                                   int priority,
+                                                   const DicomSource& source,
+                                                   const std::string& path,
+                                                   bool includePixelData,
+                                                   Orthanc::IDynamicObject* userPayload)
+  {
+    boost::shared_ptr<Orthanc::IDynamicObject> protection(userPayload);
+    
+#if ORTHANC_ENABLE_DCMTK == 1
+    std::auto_ptr<ParseDicomFromFileCommand> command(new ParseDicomFromFileCommand(path));
+    command->SetPixelDataIncluded(includePixelData);
+    command->AcquirePayload(new Handler(shared_from_this(), target, priority, source, protection));
+
+    {
+      std::auto_ptr<ILoadersContext::ILock> lock(context_.Lock());
+      lock->Schedule(GetSharedObserver(), priority, command.release());
+    }
+#else
+    throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError,
+                                    "DCMTK is disabled, cannot load DICOM files");
+#endif
+  }
+
+
+  bool DicomResourcesLoader::ScheduleLoadDicomFile(boost::shared_ptr<LoadedDicomResources> target,
+                                                   int priority,
+                                                   const DicomSource& source,
+                                                   const std::string& dicomDirPath,
+                                                   const Orthanc::DicomMap& dicomDirEntry,
+                                                   bool includePixelData,
+                                                   Orthanc::IDynamicObject* userPayload)
+  {
+    std::auto_ptr<Orthanc::IDynamicObject> protection(userPayload);
+    
+#if ORTHANC_ENABLE_DCMTK == 1
+    std::string file;
+    if (dicomDirEntry.LookupStringValue(file, Orthanc::DICOM_TAG_REFERENCED_FILE_ID, false))
+    {
+      ScheduleLoadDicomFile(target, priority, source, ParseDicomFromFileCommand::GetDicomDirPath(dicomDirPath, file),
+                            includePixelData, protection.release());
+      return true;
+    }
+    else
+    {
+      return false;
+    }
+#else
+    throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError,
+                                    "DCMTK is disabled, cannot load DICOM files");
+#endif
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Loaders/DicomResourcesLoader.h	Mon Dec 09 13:58:37 2019 +0100
@@ -0,0 +1,220 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#if !defined(ORTHANC_ENABLE_DCMTK)
+#  error The macro ORTHANC_ENABLE_DCMTK must be defined
+#endif
+
+#if ORTHANC_ENABLE_DCMTK == 1
+#  include "../Oracle/ParseDicomFromFileCommand.h"
+#  include <Core/DicomParsing/ParsedDicomDir.h>
+#endif
+
+#include "../Oracle/HttpCommand.h"
+#include "../Oracle/OracleCommandExceptionMessage.h"
+#include "../Oracle/OrthancRestApiCommand.h"
+#include "../Oracle/ReadFileCommand.h"
+#include "DicomSource.h"
+#include "ILoaderFactory.h"
+#include "LoadedDicomResources.h"
+#include "OracleScheduler.h"
+
+namespace OrthancStone
+{
+  class DicomResourcesLoader :
+    public ObserverBase<DicomResourcesLoader>,
+    public IObservable
+  {
+  private:
+    class Handler;
+    class StringHandler;
+    class DicomWebHandler;
+    class OrthancHandler;
+    class OrthancInstanceTagsHandler;    
+    class OrthancOneChildInstanceHandler;
+    class OrthancAllChildrenInstancesHandler;
+
+#if ORTHANC_ENABLE_DCMTK == 1
+    class DicomDirHandler;
+#endif
+
+    void Handle(const HttpCommand::SuccessMessage& message);
+
+    void Handle(const OrthancRestApiCommand::SuccessMessage& message);
+
+    void Handle(const ReadFileCommand::SuccessMessage& message);
+
+    void Handle(const OracleCommandExceptionMessage& message);
+
+#if ORTHANC_ENABLE_DCMTK == 1
+    void Handle(const ParseDicomSuccessMessage& message);
+#endif
+
+    void ScheduleLoadOrthancInstanceTags(boost::shared_ptr<LoadedDicomResources> target,
+                                         int priority,
+                                         const DicomSource& source,
+                                         const std::string& instanceId,
+                                         boost::shared_ptr<unsigned int> remainingCommands,
+                                         boost::shared_ptr<Orthanc::IDynamicObject> userPayload);
+
+    void ScheduleLoadOrthancOneChildInstance(boost::shared_ptr<LoadedDicomResources> target,
+                                             int priority,
+                                             const DicomSource& source,
+                                             Orthanc::ResourceType level,
+                                             const std::string& id,
+                                             boost::shared_ptr<unsigned int> remainingCommands,
+                                             boost::shared_ptr<Orthanc::IDynamicObject> userPayload);
+    
+    DicomResourcesLoader(ILoadersContext& context) :
+      context_(context)
+    {
+    }
+
+    ILoadersContext&  context_;
+
+
+  public:
+    class SuccessMessage : public OrthancStone::OriginMessage<DicomResourcesLoader>
+    {
+      ORTHANC_STONE_MESSAGE(__FILE__, __LINE__);
+      
+    private:
+      boost::shared_ptr<LoadedDicomResources>  resources_;
+      int                                      priority_;
+      const DicomSource&                       source_;
+      const Orthanc::IDynamicObject*           userPayload_;
+      
+    public:
+      SuccessMessage(const DicomResourcesLoader& origin,
+                     boost::shared_ptr<LoadedDicomResources> resources,
+                     int priority,
+                     const DicomSource& source,
+                     const Orthanc::IDynamicObject* userPayload) :
+        OriginMessage(origin),
+        resources_(resources),
+        priority_(priority),
+        source_(source),
+        userPayload_(userPayload)
+      {
+      }
+
+      int GetPriority() const
+      {
+        return priority_;
+      }
+
+      const boost::shared_ptr<LoadedDicomResources> GetResources() const
+      {
+        return resources_;
+      }
+
+      const DicomSource& GetDicomSource() const
+      {
+        return source_;
+      }
+
+      bool HasUserPayload() const
+      {
+        return userPayload_ != NULL;
+      }
+
+      const Orthanc::IDynamicObject& GetUserPayload() const;
+    };
+
+
+    class Factory : public ILoaderFactory
+    {
+    public:
+      virtual boost::shared_ptr<IObserver> Create(ILoadersContext::ILock& stone);
+    };
+
+    void ScheduleWado(boost::shared_ptr<LoadedDicomResources> target,
+                      int priority,
+                      const DicomSource& source,
+                      const std::string& uri,
+                      const std::set<Orthanc::DicomTag>& includeTags,
+                      Orthanc::IDynamicObject* userPayload);
+
+    void ScheduleWado(boost::shared_ptr<LoadedDicomResources> target,
+                      int priority,
+                      const DicomSource& source,
+                      const std::string& uri,
+                      Orthanc::IDynamicObject* userPayload)
+    {
+      std::set<Orthanc::DicomTag> includeTags;
+      ScheduleWado(target, priority, source, uri, includeTags, userPayload);
+    }        
+
+    void ScheduleQido(boost::shared_ptr<LoadedDicomResources> target,
+                      int priority,
+                      const DicomSource& source,
+                      Orthanc::ResourceType level,
+                      const Orthanc::DicomMap& filter,
+                      const std::set<Orthanc::DicomTag>& includeTags,
+                      Orthanc::IDynamicObject* userPayload);
+
+    void ScheduleLoadOrthancResources(boost::shared_ptr<LoadedDicomResources> target,
+                                      int priority,
+                                      const DicomSource& source,
+                                      Orthanc::ResourceType topLevel,
+                                      const std::string& topId,
+                                      Orthanc::ResourceType bottomLevel,
+                                      Orthanc::IDynamicObject* userPayload);
+
+    void ScheduleLoadOrthancResource(boost::shared_ptr<LoadedDicomResources> target,
+                                     int priority,
+                                     const DicomSource& source,
+                                     Orthanc::ResourceType level,
+                                     const std::string& id,
+                                     Orthanc::IDynamicObject* userPayload)
+    {
+      ScheduleLoadOrthancResources(target, priority, source, level, id, level, userPayload);
+    }
+
+#if ORTHANC_ENABLE_DCMTK == 1
+    static void GetDicomDirInstances(LoadedDicomResources& target,
+                                     const Orthanc::ParsedDicomDir& dicomDir);
+#endif
+
+    void ScheduleLoadDicomDir(boost::shared_ptr<LoadedDicomResources> target,
+                              int priority,
+                              const DicomSource& source,
+                              const std::string& path,
+                              Orthanc::IDynamicObject* userPayload);
+    
+    void ScheduleLoadDicomFile(boost::shared_ptr<LoadedDicomResources> target,
+                               int priority,
+                               const DicomSource& source,
+                               const std::string& path,
+                               bool includePixelData,
+                               Orthanc::IDynamicObject* userPayload);
+
+    bool ScheduleLoadDicomFile(boost::shared_ptr<LoadedDicomResources> target,
+                               int priority,
+                               const DicomSource& source,
+                               const std::string& dicomDirPath,
+                               const Orthanc::DicomMap& dicomDirEntry,
+                               bool includePixelData,
+                               Orthanc::IDynamicObject* userPayload);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Loaders/DicomSource.cpp	Mon Dec 09 13:58:37 2019 +0100
@@ -0,0 +1,356 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "DicomSource.h"
+
+#include "../Oracle/HttpCommand.h"
+#include "../Oracle/OrthancRestApiCommand.h"
+
+#include <Core/OrthancException.h>
+
+#include <boost/algorithm/string/predicate.hpp>
+
+namespace OrthancStone
+{
+  static std::string EncodeGetArguments(const std::string& uri,
+                                        const std::map<std::string, std::string>& arguments)
+  {
+    std::string s = uri;
+    bool first = true;
+
+    for (std::map<std::string, std::string>::const_iterator
+           it = arguments.begin(); it != arguments.end(); ++it)
+    {
+      if (first)
+      {
+        s += "?";
+        first = false;
+      }
+      else
+      {
+        s += "&";
+      }
+
+      s += it->first + "=" + it->second;
+    }
+
+    // TODO: Call Orthanc::Toolbox::UriEncode() ?
+
+    return s;
+  }
+
+
+  void DicomSource::SetOrthancSource(const Orthanc::WebServiceParameters& parameters)
+  {
+    type_ = DicomSourceType_Orthanc;
+    webService_ = parameters;
+    hasOrthancWebViewer1_ = false;
+    hasOrthancAdvancedPreview_ = false;
+  }
+
+
+  void DicomSource::SetOrthancSource()
+  {
+    Orthanc::WebServiceParameters parameters;
+    parameters.SetUrl("http://localhost:8042/");
+    SetOrthancSource(parameters);
+  }
+
+
+  const Orthanc::WebServiceParameters& DicomSource::GetOrthancParameters() const
+  {
+    if (type_ == DicomSourceType_Orthanc ||
+        type_ == DicomSourceType_DicomWebThroughOrthanc)
+    {
+      return webService_;
+    }
+    else
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+    }
+  }
+
+
+  void DicomSource::SetDicomDirSource()
+  {
+    type_ = DicomSourceType_DicomDir;
+  }
+
+
+  void DicomSource::SetDicomWebSource(const std::string& baseUrl)
+  {
+    type_ = DicomSourceType_DicomWeb;
+    webService_.SetUrl(baseUrl);
+    webService_.ClearCredentials();
+  }
+
+  
+  void DicomSource::SetDicomWebSource(const std::string& baseUrl,
+                                      const std::string& username,
+                                      const std::string& password)
+  {
+    type_ = DicomSourceType_DicomWeb;
+    webService_.SetUrl(baseUrl);
+    webService_.SetCredentials(username, password);
+  }
+
+  
+  void DicomSource::SetDicomWebThroughOrthancSource(const Orthanc::WebServiceParameters& orthancParameters,
+                                                    const std::string& dicomWebRoot,
+                                                    const std::string& serverName)
+  {
+    type_ = DicomSourceType_DicomWebThroughOrthanc;
+    webService_ = orthancParameters;
+    orthancDicomWebRoot_ = dicomWebRoot;
+    serverName_ = serverName;
+  }
+
+  
+  void DicomSource::SetDicomWebThroughOrthancSource(const std::string& serverName)
+  {
+    Orthanc::WebServiceParameters orthanc;
+    orthanc.SetUrl("http://localhost:8042/");
+    SetDicomWebThroughOrthancSource(orthanc, "/dicom-web/", serverName);
+  }
+
+  
+  bool DicomSource::IsDicomWeb() const
+  {
+    return (type_ == DicomSourceType_DicomWeb ||
+            type_ == DicomSourceType_DicomWebThroughOrthanc);
+  }
+
+
+  IOracleCommand* DicomSource::CreateDicomWebCommand(const std::string& uri,
+                                                     const std::map<std::string, std::string>& arguments,
+                                                     const std::map<std::string, std::string>& headers,
+                                                     Orthanc::IDynamicObject* payload) const
+  {
+    std::auto_ptr<Orthanc::IDynamicObject> protection(payload);
+
+    switch (type_)
+    {
+      case DicomSourceType_DicomWeb:
+      {
+        std::auto_ptr<HttpCommand> command(new HttpCommand);
+        
+        command->SetMethod(Orthanc::HttpMethod_Get);
+        command->SetUrl(webService_.GetUrl() + "/" + EncodeGetArguments(uri, arguments));
+        command->SetHttpHeaders(webService_.GetHttpHeaders());
+
+        for (std::map<std::string, std::string>::const_iterator
+               it = headers.begin(); it != headers.end(); ++it)
+        {
+          command->SetHttpHeader(it->first, it->second);
+        }
+      
+        if (!webService_.GetUsername().empty())
+        {
+          command->SetCredentials(webService_.GetUsername(), webService_.GetPassword());
+        }         
+
+        if (protection.get())
+        {
+          command->AcquirePayload(protection.release());
+        }
+        
+        return command.release();
+      }
+
+      case DicomSourceType_DicomWebThroughOrthanc:
+      {
+        Json::Value args = Json::objectValue;
+        for (std::map<std::string, std::string>::const_iterator
+               it = arguments.begin(); it != arguments.end(); ++it)
+        {
+          args[it->first] = it->second;
+        }
+          
+        Json::Value h = Json::objectValue;
+        for (std::map<std::string, std::string>::const_iterator
+               it = headers.begin(); it != headers.end(); ++it)
+        {
+          h[it->first] = it->second;
+        }
+          
+        Json::Value body = Json::objectValue;
+        body["Uri"] = uri;
+        body["Arguments"] = args;
+        body["Headers"] = h;
+
+        std::auto_ptr<OrthancRestApiCommand> command(new OrthancRestApiCommand);
+        command->SetMethod(Orthanc::HttpMethod_Post);
+        command->SetUri(orthancDicomWebRoot_ + "/servers/" + serverName_ + "/get");
+        command->SetBody(body);
+
+        if (protection.get())
+        {
+          command->AcquirePayload(protection.release());
+        }
+        
+        return command.release();
+      }
+
+      default:
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+    }
+  }
+
+
+  void DicomSource::AutodetectOrthancFeatures(const std::string& system,
+                                              const std::string& plugins)
+  {
+    static const char* const REST_API_VERSION = "ApiVersion";
+
+    if (IsDicomWeb())
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+    }
+
+    Json::Value a, b;
+    Json::Reader reader;
+    if (reader.parse(system, a) &&
+        reader.parse(plugins, b) &&
+        a.type() == Json::objectValue &&
+        b.type() == Json::arrayValue &&
+        a.isMember(REST_API_VERSION) &&
+        a[REST_API_VERSION].type() == Json::intValue)
+    {
+      SetOrthancAdvancedPreview(a[REST_API_VERSION].asInt() >= 5);
+
+      hasOrthancWebViewer1_ = false;
+
+      for (Json::Value::ArrayIndex i = 0; i < b.size(); i++)
+      {
+        if (b[i].type() != Json::stringValue)
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+        }
+
+        if (boost::iequals(b[i].asString(), "web-viewer"))
+        {
+          hasOrthancWebViewer1_ = true;
+        }
+      }
+    }
+    else
+    {
+      printf("[%s] [%s]\n", system.c_str(), plugins.c_str());
+
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+    }
+  }
+
+
+  void DicomSource::SetOrthancWebViewer1(bool hasPlugin)
+  {
+    if (IsOrthanc())
+    {
+      hasOrthancWebViewer1_ = hasPlugin;
+    }
+    else
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+    }
+  }
+
+  bool DicomSource::HasOrthancWebViewer1() const
+  {
+    if (IsOrthanc())
+    {
+      return hasOrthancWebViewer1_;
+    }
+    else
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+    }
+  }
+
+  void DicomSource::SetOrthancAdvancedPreview(bool hasFeature)
+  {
+    if (IsOrthanc())
+    {
+      hasOrthancAdvancedPreview_ = hasFeature;
+    }
+    else
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+    }
+  }
+
+  bool DicomSource::HasOrthancAdvancedPreview() const
+  {
+    if (IsOrthanc())
+    {
+      return hasOrthancAdvancedPreview_;
+    }
+    else
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+    }
+  }
+
+  void DicomSource::SetDicomWebRendered(bool hasFeature)
+  {
+    if (IsDicomWeb())
+    {
+      hasDicomWebRendered_ = hasFeature;
+    }
+    else
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+    }
+  }
+
+  bool DicomSource::HasDicomWebRendered() const
+  {
+    if (IsDicomWeb())
+    {
+      return hasDicomWebRendered_;
+    }
+    else
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+    }
+  }
+
+
+  unsigned int DicomSource::GetQualityCount() const
+  {
+    if (IsDicomWeb())
+    {
+      return (HasDicomWebRendered() ? 2 : 1);
+    }
+    else if (IsOrthanc())
+    {
+      return (HasOrthancWebViewer1() || 
+              HasOrthancAdvancedPreview() ? 2 : 1);
+    }
+    else if (IsDicomDir())
+    {
+      return 1;
+    }
+    else
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Loaders/DicomSource.h	Mon Dec 09 13:58:37 2019 +0100
@@ -0,0 +1,118 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../Oracle/IOracleCommand.h"
+
+#include <Core/WebServiceParameters.h>
+
+namespace OrthancStone
+{
+  enum DicomSourceType
+  {
+    DicomSourceType_Orthanc,
+    DicomSourceType_DicomWeb,
+    DicomSourceType_DicomWebThroughOrthanc,
+    DicomSourceType_DicomDir
+  };
+
+
+  class DicomSource
+  {
+  private:
+    DicomSourceType                type_;
+    Orthanc::WebServiceParameters  webService_;
+    std::string                    orthancDicomWebRoot_;
+    std::string                    serverName_;
+    bool                           hasOrthancWebViewer1_;
+    bool                           hasOrthancAdvancedPreview_;
+    bool                           hasDicomWebRendered_;
+
+  public:
+    DicomSource() :
+      hasOrthancWebViewer1_(false),
+      hasOrthancAdvancedPreview_(false),
+      hasDicomWebRendered_(false)
+    {
+      SetOrthancSource();
+    }
+
+    DicomSourceType GetType() const
+    {
+      return type_;
+    }
+
+    void SetOrthancSource();
+
+    void SetOrthancSource(const Orthanc::WebServiceParameters& parameters);
+
+    const Orthanc::WebServiceParameters& GetOrthancParameters() const;
+
+    void SetDicomDirSource();
+
+    void SetDicomWebSource(const std::string& baseUrl);
+
+    void SetDicomWebSource(const std::string& baseUrl,
+                           const std::string& username,
+                           const std::string& password);
+
+    void SetDicomWebThroughOrthancSource(const Orthanc::WebServiceParameters& orthancParameters,
+                                         const std::string& dicomWebRoot,
+                                         const std::string& serverName);
+    
+    void SetDicomWebThroughOrthancSource(const std::string& serverName);
+    
+    bool IsDicomWeb() const;
+
+    bool IsOrthanc() const
+    {
+      return type_ == DicomSourceType_Orthanc;
+    }
+
+    bool IsDicomDir() const
+    {
+      return type_ == DicomSourceType_DicomDir;
+    }
+
+    IOracleCommand* CreateDicomWebCommand(const std::string& uri,
+                                          const std::map<std::string, std::string>& arguments,
+                                          const std::map<std::string, std::string>& headers,
+                                          Orthanc::IDynamicObject* payload /* takes ownership */) const;
+    
+    void AutodetectOrthancFeatures(const std::string& system,
+                                   const std::string& plugins);
+
+    void SetOrthancWebViewer1(bool hasPlugin);
+
+    bool HasOrthancWebViewer1() const;
+
+    void SetOrthancAdvancedPreview(bool hasFeature);
+
+    bool HasOrthancAdvancedPreview() const;
+
+    void SetDicomWebRendered(bool hasFeature);
+
+    bool HasDicomWebRendered() const;
+
+    unsigned int GetQualityCount() const;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Loaders/DicomVolumeLoader.cpp	Mon Dec 09 13:58:37 2019 +0100
@@ -0,0 +1,182 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "DicomVolumeLoader.h"
+
+#include <Core/Images/ImageProcessing.h>
+
+namespace OrthancStone
+{
+  DicomVolumeLoader::DicomVolumeLoader(boost::shared_ptr<SeriesFramesLoader>& framesLoader,
+                                       bool computeRange) :
+    framesLoader_(framesLoader),
+    isValid_(false),
+    started_(false),
+    remaining_(0)
+  {
+    volume_.reset(new OrthancStone::DicomVolumeImage);
+
+    const SeriesOrderedFrames& frames = framesLoader_->GetOrderedFrames();
+
+    if (frames.IsRegular3DVolume() &&
+        frames.GetFramesCount() > 0)
+    {
+      // TODO - Is "0" the good choice for the reference frame?
+      // Shouldn't we use "count - 1" depending on the direction
+      // of the normal?
+      const OrthancStone::DicomInstanceParameters& parameters = frames.GetInstanceParameters(0);
+
+      OrthancStone::CoordinateSystem3D plane(frames.GetInstance(0));
+
+      OrthancStone::VolumeImageGeometry geometry;
+      geometry.SetSizeInVoxels(parameters.GetImageInformation().GetWidth(),
+                               parameters.GetImageInformation().GetHeight(),
+                               static_cast<unsigned int>(frames.GetFramesCount()));
+      geometry.SetAxialGeometry(plane);
+
+      double spacing;
+      if (parameters.GetSopClassUid() == SopClassUid_RTDose)
+      {
+        if (!parameters.ComputeRegularSpacing(spacing))
+        {
+          LOG(WARNING) << "Unable to compute the spacing in a RT-DOSE instance";
+          spacing = frames.GetSpacingBetweenSlices();
+        }
+      }
+      else
+      {
+        spacing = frames.GetSpacingBetweenSlices();
+      }
+
+      geometry.SetVoxelDimensions(parameters.GetPixelSpacingX(),
+                                  parameters.GetPixelSpacingY(), spacing);
+      volume_->Initialize(geometry, parameters.GetExpectedPixelFormat(), computeRange);
+      volume_->GetPixelData().Clear();
+      volume_->SetDicomParameters(parameters);
+
+      remaining_ = frames.GetFramesCount();
+      isValid_ = true;
+    }
+    else
+    {
+      LOG(WARNING) << "Not a regular 3D volume";
+    }
+  }
+
+
+  void DicomVolumeLoader::Handle(const OrthancStone::SeriesFramesLoader::FrameLoadedMessage& message)
+  {
+    if (remaining_ == 0 ||
+        !message.HasUserPayload())
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+    }
+
+    if (message.GetImage().GetWidth() != volume_->GetPixelData().GetWidth() ||
+        message.GetImage().GetHeight() != volume_->GetPixelData().GetHeight())
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageSize);
+    }
+
+    if (message.GetImage().GetFormat() != volume_->GetPixelData().GetFormat())
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageFormat);
+    }
+
+    if (message.GetFrameIndex() >= volume_->GetPixelData().GetDepth())
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+
+    size_t frameIndex = dynamic_cast<const Orthanc::SingleValueObject<size_t>&>(message.GetUserPayload()).GetValue();
+
+    {
+      ImageBuffer3D::SliceWriter writer(volume_->GetPixelData(), VolumeProjection_Axial, frameIndex);
+      Orthanc::ImageProcessing::Copy(writer.GetAccessor(), message.GetImage());
+    }
+
+    volume_->IncrementRevision();
+
+    {
+      VolumeUpdatedMessage updated(*this, frameIndex);
+      BroadcastMessage(updated);
+    }
+
+    remaining_--;
+
+    if (remaining_ == 0)
+    {
+      VolumeReadyMessage ready(*this);
+      BroadcastMessage(ready);
+    }
+  }
+
+
+  DicomVolumeLoader::Factory::Factory(LoadedDicomResources& instances) :
+    framesFactory_(instances),
+    computeRange_(false)
+  {
+  }
+
+  DicomVolumeLoader::Factory::Factory(const SeriesMetadataLoader::SeriesLoadedMessage& metadata) :
+    framesFactory_(metadata.GetInstances()),
+    computeRange_(false)
+  {
+    SetDicomDir(metadata.GetDicomDirPath(), metadata.GetDicomDir());  // Only useful for DICOMDIR sources
+  }
+
+
+  boost::shared_ptr<IObserver> DicomVolumeLoader::Factory::Create(ILoadersContext::ILock& context)
+  { 
+    boost::shared_ptr<SeriesFramesLoader> frames =
+      boost::dynamic_pointer_cast<SeriesFramesLoader>(framesFactory_.Create(context));
+
+    boost::shared_ptr<DicomVolumeLoader> volume(new DicomVolumeLoader(frames, computeRange_));
+    volume->Register<SeriesFramesLoader::FrameLoadedMessage>(*frames, &DicomVolumeLoader::Handle);
+
+    return volume;
+  }
+
+  void DicomVolumeLoader::Start(int priority,
+                                const DicomSource& source)
+  {
+    if (started_)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+    }
+
+    started_ = true;
+
+    if (IsValid())
+    {
+      for (size_t i = 0; i < GetOrderedFrames().GetFramesCount(); i++)
+      {
+        framesLoader_->ScheduleLoadFrame(priority, source, i, source.GetQualityCount() - 1,
+                                         new Orthanc::SingleValueObject<size_t>(i));
+      }
+    }
+    else
+    {
+      VolumeReadyMessage ready(*this);
+      BroadcastMessage(ready);
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Loaders/DicomVolumeLoader.h	Mon Dec 09 13:58:37 2019 +0100
@@ -0,0 +1,141 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../Volumes/DicomVolumeImage.h"
+#include "SeriesFramesLoader.h"
+#include "SeriesMetadataLoader.h"
+
+namespace OrthancStone
+{
+  class DicomVolumeLoader : 
+    public ObserverBase<DicomVolumeLoader>,
+    public IObservable
+  {
+  private:
+    boost::shared_ptr<SeriesFramesLoader>  framesLoader_;
+    boost::shared_ptr<DicomVolumeImage>    volume_;
+    bool                                   isValid_;
+    bool                                   started_;
+    size_t                                 remaining_;
+
+    DicomVolumeLoader(boost::shared_ptr<SeriesFramesLoader>& framesLoader,
+                      bool computeRange);
+
+    void Handle(const OrthancStone::SeriesFramesLoader::FrameLoadedMessage& message);
+
+  public:
+    class VolumeReadyMessage : public OriginMessage<DicomVolumeLoader>
+    {
+      ORTHANC_STONE_MESSAGE(__FILE__, __LINE__);
+
+    public:
+      VolumeReadyMessage(const DicomVolumeLoader& loader) :
+        OriginMessage(loader)
+      {
+      }
+
+      const DicomVolumeImage& GetVolume() const
+      {
+        assert(GetOrigin().GetVolume());
+        return *GetOrigin().GetVolume();
+      }
+    };
+
+
+    class VolumeUpdatedMessage : public OriginMessage<DicomVolumeLoader>
+    {
+      ORTHANC_STONE_MESSAGE(__FILE__, __LINE__);
+
+    private:
+      unsigned int   axial_;
+
+    public:
+      VolumeUpdatedMessage(const DicomVolumeLoader& loader,
+                           unsigned int axial) :
+        OriginMessage(loader),
+        axial_(axial)
+      {
+      }
+
+      unsigned int GetAxialIndex() const
+      {
+        return axial_;
+      }
+
+      const DicomVolumeImage& GetVolume() const
+      {
+        assert(GetOrigin().GetVolume());
+        return *GetOrigin().GetVolume();
+      }
+    };
+
+
+    class Factory : public ILoaderFactory
+    {
+    private:
+      SeriesFramesLoader::Factory  framesFactory_;
+      bool                         computeRange_;
+
+    public:
+      Factory(LoadedDicomResources& instances);
+
+      Factory(const SeriesMetadataLoader::SeriesLoadedMessage& metadata);
+
+      void SetComputeRange(bool computeRange)
+      {
+        computeRange_ = computeRange;
+      }
+
+      void SetDicomDir(const std::string& dicomDirPath,
+                       boost::shared_ptr<LoadedDicomResources> dicomDir)
+      {
+        framesFactory_.SetDicomDir(dicomDirPath, dicomDir);
+      }
+
+      virtual boost::shared_ptr<IObserver> Create(ILoadersContext::ILock& context) ORTHANC_OVERRIDE;
+    };
+
+    bool IsValid() const
+    {
+      return isValid_;
+    }
+
+    bool IsFullyLoaded() const
+    {
+      return remaining_ == 0;
+    }
+
+    boost::shared_ptr<DicomVolumeImage> GetVolume() const
+    {
+      return volume_;
+    }
+
+    const SeriesOrderedFrames& GetOrderedFrames() const
+    {
+      return framesLoader_->GetOrderedFrames();
+    }
+
+    void Start(int priority,
+               const DicomSource& source);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Loaders/GenericLoadersContext.cpp	Mon Dec 09 13:58:37 2019 +0100
@@ -0,0 +1,185 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "GenericLoadersContext.h"
+
+namespace OrthancStone
+{
+  class GenericLoadersContext::Locker : public ILoadersContext::ILock
+  {
+  private:
+    GenericLoadersContext& that_;
+    boost::recursive_mutex::scoped_lock lock_;
+
+  public:
+    Locker(GenericLoadersContext& that) :
+      that_(that),
+      lock_(that.mutex_)
+    {
+    }
+      
+    virtual ILoadersContext& GetContext() const ORTHANC_OVERRIDE
+    {
+      return that_;
+    };
+
+    virtual void AddLoader(boost::shared_ptr<IObserver> loader) ORTHANC_OVERRIDE
+    {
+      that_.loaders_.push_back(loader);
+    }
+
+    virtual IObservable& GetOracleObservable() const ORTHANC_OVERRIDE
+    {
+      return that_.oracleObservable_;
+    }
+
+    virtual void Schedule(boost::shared_ptr<IObserver> receiver,
+                          int priority,
+                          IOracleCommand* command /* Takes ownership */) ORTHANC_OVERRIDE
+    {
+      that_.scheduler_->Schedule(receiver, priority, command);
+    };
+
+    virtual void CancelRequests(boost::shared_ptr<IObserver> receiver) ORTHANC_OVERRIDE
+    {
+      that_.scheduler_->CancelRequests(receiver);
+    }
+
+    virtual void CancelAllRequests() ORTHANC_OVERRIDE
+    {
+      that_.scheduler_->CancelAllRequests();
+    }
+  };
+
+
+  void GenericLoadersContext::EmitMessage(boost::weak_ptr<IObserver> observer,
+                                          const IMessage& message)
+  {
+    boost::recursive_mutex::scoped_lock lock(mutex_);
+    //LOG(INFO) << "  inside emit lock: " << message.GetIdentifier().AsString();
+    oracleObservable_.EmitMessage(observer, message);
+    //LOG(INFO) << "  outside emit lock";
+  }
+
+
+  GenericLoadersContext::GenericLoadersContext(unsigned int maxHighPriority,
+                                               unsigned int maxStandardPriority,
+                                               unsigned int maxLowPriority)
+  {
+    oracle_.reset(new ThreadedOracle(*this));
+    scheduler_ = OracleScheduler::Create(*oracle_, oracleObservable_, *this,
+                                         maxHighPriority, maxStandardPriority, maxLowPriority);
+  }
+
+
+  GenericLoadersContext::~GenericLoadersContext()
+  {
+    LOG(WARNING) << "scheduled commands: " << scheduler_->GetTotalScheduled()
+                 << ", processed commands: " << scheduler_->GetTotalProcessed();
+    scheduler_.reset();
+    //LOG(INFO) << "counter: " << scheduler_.use_count();
+  }
+
+  
+  void GenericLoadersContext::SetOrthancParameters(const Orthanc::WebServiceParameters& parameters)
+  {
+    boost::recursive_mutex::scoped_lock lock(mutex_);
+    oracle_->SetOrthancParameters(parameters);
+  }
+
+  
+  void GenericLoadersContext::SetRootDirectory(const std::string& root)
+  {
+    boost::recursive_mutex::scoped_lock lock(mutex_);
+    oracle_->SetRootDirectory(root);
+  }
+
+  
+  void GenericLoadersContext::SetDicomCacheSize(size_t size)
+  {
+    boost::recursive_mutex::scoped_lock lock(mutex_);
+    oracle_->SetDicomCacheSize(size);
+  }
+
+  
+  void GenericLoadersContext::StartOracle()
+  {
+    boost::recursive_mutex::scoped_lock lock(mutex_);
+    oracle_->Start();
+    //LOG(INFO) << "STARTED ORACLE";
+  }
+
+  
+  void GenericLoadersContext::StopOracle()
+  {
+    /**
+     * DON'T lock "mutex_" here, otherwise Stone won't be able to
+     * stop if one command being executed by the oracle has to emit
+     * a message (method "EmitMessage()" would have to lock the
+     * mutex too).
+     **/
+      
+    //LOG(INFO) << "STOPPING ORACLE";
+    oracle_->Stop();
+    //LOG(INFO) << "STOPPED ORACLE";
+  }
+
+  
+  void GenericLoadersContext::WaitUntilComplete()
+  {
+    for (;;)
+    {
+      {
+        boost::recursive_mutex::scoped_lock lock(mutex_);
+        if (scheduler_ &&
+            scheduler_->GetTotalScheduled() == scheduler_->GetTotalProcessed())
+        {
+          return;
+        }
+      }
+
+      boost::this_thread::sleep(boost::posix_time::milliseconds(100));
+    }
+  }
+   
+
+  ILoadersContext::ILock* GenericLoadersContext::Lock()
+  {
+    return new Locker(*this);
+  }
+
+  
+  void GenericLoadersContext::GetStatistics(uint64_t& scheduledCommands,
+                                            uint64_t& processedCommands)
+  {
+    boost::recursive_mutex::scoped_lock lock(mutex_);
+    if (scheduler_)
+    {
+      scheduledCommands = scheduler_->GetTotalScheduled();
+      processedCommands = scheduler_->GetTotalProcessed();
+    }
+    else
+    {
+      scheduledCommands = 0;
+      processedCommands = 0;
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Loaders/GenericLoadersContext.h	Mon Dec 09 13:58:37 2019 +0100
@@ -0,0 +1,85 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../Messages/IMessageEmitter.h"
+#include "../Oracle/ThreadedOracle.h"
+#include "DicomSource.h"
+#include "ILoaderFactory.h"
+#include "OracleScheduler.h"
+
+#include <Core/HttpClient.h>
+#include <Core/Toolbox.h>
+
+#include <boost/thread/recursive_mutex.hpp>
+
+namespace OrthancStone
+{
+  class GenericLoadersContext : 
+    public ILoadersContext,
+    private IMessageEmitter
+  {
+  private:
+    class Locker;
+
+    // "Recursive mutex" is necessary, to be able to run
+    // "ILoaderFactory" from a message handler triggered by
+    // "EmitMessage()"
+    boost::recursive_mutex  mutex_;
+
+    IObservable                         oracleObservable_;
+    std::auto_ptr<ThreadedOracle>       oracle_;
+    boost::shared_ptr<OracleScheduler>  scheduler_;
+
+    // Necessary to keep the loaders persistent (including global
+    // function promises), after the function that created them is
+    // left. This avoids creating one global variable for each loader.
+    std::list< boost::shared_ptr<IObserver> >  loaders_; 
+
+    virtual void EmitMessage(boost::weak_ptr<IObserver> observer,
+                             const IMessage& message);
+
+  public:
+    GenericLoadersContext(unsigned int maxHighPriority,
+                        unsigned int maxStandardPriority,
+                          unsigned int maxLowPriority);
+
+    virtual ~GenericLoadersContext();
+   
+    virtual ILock* Lock() ORTHANC_OVERRIDE;
+
+    virtual void GetStatistics(uint64_t& scheduledCommands,
+                               uint64_t& processedCommands) ORTHANC_OVERRIDE;
+
+    void SetOrthancParameters(const Orthanc::WebServiceParameters& parameters);
+
+    void SetRootDirectory(const std::string& root);
+    
+    void SetDicomCacheSize(size_t size);
+
+    void StartOracle();
+
+    void StopOracle();
+
+    void WaitUntilComplete();
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Loaders/ILoaderFactory.h	Mon Dec 09 13:58:37 2019 +0100
@@ -0,0 +1,41 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "ILoadersContext.h"
+
+namespace OrthancStone
+{
+  class ILoaderFactory : public boost::noncopyable
+  {
+  public:
+    virtual ~ILoaderFactory()
+    {
+    }
+
+    /**
+     * Factory function that creates a new loader, to be used by the
+     * Stone loaders context.
+     **/
+    virtual boost::shared_ptr<IObserver> Create(ILoadersContext::ILock& context) = 0;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Loaders/ILoadersContext.h	Mon Dec 09 13:58:37 2019 +0100
@@ -0,0 +1,122 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../Messages/IObserver.h"
+#include "../Messages/IObservable.h"
+#include "../Oracle/IOracleCommand.h"
+
+#include <boost/shared_ptr.hpp>
+
+namespace OrthancStone
+{
+  class ILoadersContext : public boost::noncopyable
+  {
+  public:
+    class ILock : public boost::noncopyable
+    {
+    public:
+      virtual ~ILock()
+      {
+      }
+
+      /**
+       * This method is useful for loaders that must be able to
+       * re-lock the Stone loaders context in the future (for instance
+       * to schedule new commands once some command is processed).
+       **/
+      virtual ILoadersContext& GetContext() const = 0;
+
+      /**
+       * Get a reference to the observable against which a loader must
+       * listen to be informed of messages issued by the oracle once
+       * some command is processed.
+       **/
+      virtual IObservable& GetOracleObservable() const = 0;
+
+      /**
+       * Schedule a new command for further processing by the
+       * oracle. The "receiver" argument indicates to which object the
+       * notification messages are sent by the oracle upon completion
+       * of the command. The command is possibly not directly sent to
+       * the oracle: Instead, an internal "OracleScheduler" object is
+       * often used as a priority queue to rule the order in which
+       * commands are actually sent to the oracle. Hence the
+       * "priority" argument (commands with lower value are executed
+       * first).
+       **/
+      virtual void Schedule(boost::shared_ptr<IObserver> receiver,
+                            int priority,
+                            IOracleCommand* command /* Takes ownership */) = 0;
+
+      /**
+       * Cancel all the commands that are waiting in the
+       * "OracleScheduler" queue and that are linked to the given
+       * receiver (i.e. the observer that was specified at the time
+       * method "Schedule()" was called). This is useful for real-time
+       * processing, as it allows to replace commands that were
+       * scheduled in the past by more urgent commands.
+       *
+       * Note that this call does not affect commands that would have
+       * already be sent to the oracle. As a consequence, the receiver
+       * might still receive messages that were sent to the oracle
+       * before the cancellation (be prepared to handle such
+       * messages).
+       **/
+      virtual void CancelRequests(boost::shared_ptr<IObserver> receiver) = 0;
+
+      /**
+       * Same as "CancelRequests()", but targets all the receivers.
+       **/
+      virtual void CancelAllRequests() = 0;
+
+      /**
+       * Add a reference to the given observer in the Stone loaders
+       * context. This can be used to match the lifetime of a loader
+       * with the lifetime of the Stone context: This is useful if
+       * your Stone application does not keep a reference to the
+       * loader by itself (typically in global promises), which would
+       * make the loader disappear as soon as the scope of the
+       * variable is left.
+       **/
+      virtual void AddLoader(boost::shared_ptr<IObserver> loader) = 0;
+    };
+
+    /**
+     * Locks the Stone loaders context, to give access to its
+     * underlying features. This is important for Stone applications
+     * running in a multi-threaded environment, for which a global
+     * mutex is locked.
+     **/
+    virtual ILock* Lock() = 0;
+
+    /**
+     * Returns the number of commands that were scheduled and
+     * processed using the "ILock::Schedule()" method. By "processed"
+     * commands, we refer to the number of commands that were either
+     * executed by the oracle, or canceled by the user. So the
+     * counting sequences are monotonically increasing over time.
+     **/
+    virtual void GetStatistics(uint64_t& scheduledCommands,
+                               uint64_t& processedCommands) = 0;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Loaders/LoadedDicomResources.cpp	Mon Dec 09 13:58:37 2019 +0100
@@ -0,0 +1,216 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "LoadedDicomResources.h"
+
+#include <Core/OrthancException.h>
+
+#include <cassert>
+
+
+namespace OrthancStone
+{
+  void LoadedDicomResources::Flatten()
+  {
+    // Lazy generation of a "std::vector" from the "std::map"
+    if (flattened_.empty())
+    {
+      flattened_.resize(resources_.size());
+
+      size_t pos = 0;
+      for (Resources::const_iterator it = resources_.begin(); it != resources_.end(); ++it)
+      {
+        assert(it->second != NULL);
+        flattened_[pos++] = it->second;
+      }
+    }
+    else
+    {
+      // No need to flatten
+      assert(flattened_.size() == resources_.size());
+    }
+  }
+
+
+  void LoadedDicomResources::AddFromDicomWebInternal(const Json::Value& dicomweb)
+  {
+    assert(dicomweb.type() == Json::objectValue);
+    Orthanc::DicomMap dicom;
+    dicom.FromDicomWeb(dicomweb);
+    AddResource(dicom);
+  }
+
+  
+  LoadedDicomResources::LoadedDicomResources(const LoadedDicomResources& other,
+                                             const Orthanc::DicomTag& indexedTag) :
+    indexedTag_(indexedTag)
+  {
+    for (Resources::const_iterator it = other.resources_.begin();
+         it != other.resources_.end(); ++it)
+    {
+      assert(it->second != NULL);
+      AddResource(*it->second);
+    }
+  }
+
+  void LoadedDicomResources::Clear()
+  {
+    for (Resources::iterator it = resources_.begin(); it != resources_.end(); ++it)
+    {
+      assert(it->second != NULL);
+      delete it->second;
+    }
+
+    resources_.clear();
+    flattened_.clear();
+  }
+
+
+  Orthanc::DicomMap& LoadedDicomResources::GetResource(size_t index)
+  {
+    Flatten();
+
+    if (index >= flattened_.size())
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+    else
+    {
+      assert(flattened_[index] != NULL);
+      return *flattened_[index];
+    }
+  }
+
+
+  bool LoadedDicomResources::LookupStringValue(std::string& target,
+                                               const std::string& id,
+                                               const Orthanc::DicomTag& tag) const
+  {
+    Resources::const_iterator found = resources_.find(id);
+
+    if (found == resources_.end())
+    {
+      return false;
+    }
+    else
+    {
+      assert(found->second != NULL);
+      return found->second->LookupStringValue(target, tag, false);
+    }  
+  }
+
+  
+  void LoadedDicomResources::AddResource(const Orthanc::DicomMap& dicom)
+  {
+    std::string id;
+    
+    if (dicom.LookupStringValue(id, indexedTag_, false /* no binary value */) &&
+        resources_.find(id) == resources_.end() /* Don't index twice the same resource */)
+    {
+      resources_[id] = dicom.Clone();
+      flattened_.clear();   // Invalidate the flattened version 
+    }
+  }
+
+
+  void LoadedDicomResources::AddFromOrthanc(const Json::Value& tags)
+  {
+    Orthanc::DicomMap dicom;
+    dicom.FromDicomAsJson(tags);
+    AddResource(dicom);
+  }
+
+
+  void LoadedDicomResources::AddFromDicomWeb(const Json::Value& dicomweb)
+  {
+    if (dicomweb.type() == Json::objectValue)
+    {
+      AddFromDicomWebInternal(dicomweb);
+    }
+    else if (dicomweb.type() == Json::arrayValue)
+    {
+      for (Json::Value::ArrayIndex i = 0; i < dicomweb.size(); i++)
+      {
+        if (dicomweb[i].type() == Json::objectValue)
+        {
+          AddFromDicomWebInternal(dicomweb[i]);
+        }
+        else
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol);
+        }
+      }
+    }
+    else
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol);
+    }
+  }
+
+
+  bool LoadedDicomResources::LookupTagValueConsensus(std::string& target,
+                                                     const Orthanc::DicomTag& tag) const
+  {
+    typedef std::map<std::string, unsigned int>  Counter;
+
+    Counter counter;
+
+    for (Resources::const_iterator it = resources_.begin(); it != resources_.end(); ++it)
+    {
+      assert(it->second != NULL);
+      
+      std::string value;
+      if (it->second->LookupStringValue(value, tag, false))
+      {
+        Counter::iterator found = counter.find(value);
+        if (found == counter.end())
+        {
+          counter[value] = 1;
+        }
+        else
+        {
+          found->second ++;
+        }
+      }
+    }
+
+    Counter::const_iterator best = counter.end();
+    
+    for (Counter::const_iterator it = counter.begin(); it != counter.end(); ++it)
+    {
+      if (best == counter.end() ||
+          best->second < it->second)
+      {
+        best = it;
+      }
+    }
+
+    if (best == counter.end())
+    {
+      return false;
+    }
+    else
+    {
+      target = best->first;
+      return true;
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Loaders/LoadedDicomResources.h	Mon Dec 09 13:58:37 2019 +0100
@@ -0,0 +1,89 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include <Core/DicomFormat/DicomMap.h>
+
+
+namespace OrthancStone
+{
+  class LoadedDicomResources : public boost::noncopyable
+  {
+  private:
+    typedef std::map<std::string, Orthanc::DicomMap*>  Resources;
+
+    Orthanc::DicomTag                indexedTag_;
+    Resources                        resources_;
+    std::vector<Orthanc::DicomMap*>  flattened_;
+
+    void Flatten();
+
+    void AddFromDicomWebInternal(const Json::Value& dicomweb);
+
+  public:
+    LoadedDicomResources(const Orthanc::DicomTag& indexedTag) :
+      indexedTag_(indexedTag)
+    {
+    }
+
+    // Re-index another set of resources using another tag
+    LoadedDicomResources(const LoadedDicomResources& other,
+                         const Orthanc::DicomTag& indexedTag);
+
+    ~LoadedDicomResources()
+    {
+      Clear();
+    }
+
+    const Orthanc::DicomTag& GetIndexedTag() const
+    {
+      return indexedTag_;
+    }
+  
+    void Clear();
+
+    size_t GetSize() const
+    {
+      return resources_.size();
+    }
+
+    Orthanc::DicomMap& GetResource(size_t index);
+
+    bool HasResource(const std::string& id) const
+    {
+      return resources_.find(id) != resources_.end();
+    }
+  
+    bool LookupStringValue(std::string& target,
+                           const std::string& id,
+                           const Orthanc::DicomTag& tag) const;
+
+    void AddResource(const Orthanc::DicomMap& dicom);
+
+    void AddFromOrthanc(const Json::Value& tags);
+  
+    void AddFromDicomWeb(const Json::Value& dicomweb);
+
+    bool LookupTagValueConsensus(std::string& target,
+                                 const Orthanc::DicomTag& tag) const;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Loaders/OracleScheduler.cpp	Mon Dec 09 13:58:37 2019 +0100
@@ -0,0 +1,557 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "OracleScheduler.h"
+
+#include "../Oracle/ParseDicomFromFileCommand.h"
+
+namespace OrthancStone
+{
+  class OracleScheduler::ReceiverPayload : public Orthanc::IDynamicObject
+  {
+  private:
+    Priority   priority_;
+    boost::weak_ptr<IObserver>  receiver_;
+    std::auto_ptr<IOracleCommand>  command_;
+
+  public:
+    ReceiverPayload(Priority priority,
+                    boost::weak_ptr<IObserver> receiver,
+                    IOracleCommand* command) :
+      priority_(priority),
+      receiver_(receiver),
+      command_(command)
+    {
+      if (command == NULL)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
+      }
+    }
+
+    Priority GetActivePriority() const
+    {
+      return priority_;
+    }
+
+    boost::weak_ptr<IObserver> GetOriginalReceiver() const
+    {
+      return receiver_;
+    }
+
+    const IOracleCommand& GetOriginalCommand() const
+    {
+      assert(command_.get() != NULL);
+      return *command_;
+    }
+  }; 
+
+
+  class OracleScheduler::ScheduledCommand : public boost::noncopyable
+  {
+  private:
+    boost::weak_ptr<IObserver>     receiver_;
+    std::auto_ptr<IOracleCommand>  command_;
+
+  public:
+    ScheduledCommand(boost::shared_ptr<IObserver> receiver,
+                     IOracleCommand* command) :
+      receiver_(receiver),
+      command_(command)
+    {
+      if (command == NULL)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
+      }
+    }
+
+    boost::weak_ptr<IObserver> GetReceiver() 
+    {
+      return receiver_;
+    }
+  
+    bool IsSameReceiver(boost::shared_ptr<OrthancStone::IObserver> receiver) const
+    {
+      boost::shared_ptr<IObserver> lock(receiver_.lock());
+
+      return (lock &&
+              lock.get() == receiver.get());
+    }
+
+    IOracleCommand* WrapCommand(Priority priority)
+    {
+      if (command_.get() == NULL)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+      }
+      else
+      {
+        std::auto_ptr<IOracleCommand> wrapped(command_->Clone());
+        dynamic_cast<OracleCommandBase&>(*wrapped).AcquirePayload(new ReceiverPayload(priority, receiver_, command_.release()));
+        return wrapped.release();
+      }
+    }
+  };
+
+
+
+  void OracleScheduler::ClearQueue(Queue& queue)
+  {
+    for (Queue::iterator it = queue.begin(); it != queue.end(); ++it)
+    {
+      assert(it->second != NULL);
+      delete it->second;
+
+      totalProcessed_ ++;
+    }
+
+    queue.clear();
+  }
+
+  
+  void OracleScheduler::RemoveReceiverFromQueue(Queue& queue,
+                                                boost::shared_ptr<IObserver> receiver)
+  {
+    if (!receiver)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
+    }
+    
+    Queue tmp;
+  
+    for (Queue::iterator it = queue.begin(); it != queue.end(); ++it)
+    {
+      assert(it->second != NULL);
+
+      if (!(it->second->IsSameReceiver(receiver)))
+      {
+        // This promise is still active
+        tmp.insert(std::make_pair(it->first, it->second));
+      }
+      else
+      {
+        delete it->second;
+        
+        totalProcessed_ ++;
+      }
+    }
+
+    queue = tmp;
+  }
+
+  
+  void OracleScheduler::CheckInvariants() const
+  {
+#ifndef NDEBUG
+    /*char buf[1024];
+      sprintf(buf, "active: %d %d %d ; pending: %lu %lu %lu", 
+      activeHighPriorityCommands_, activeStandardPriorityCommands_, activeLowPriorityCommands_,
+      highPriorityQueue_.size(), standardPriorityQueue_.size(), lowPriorityQueue_.size());
+      LOG(INFO) << buf;*/
+  
+    assert(activeHighPriorityCommands_ <= maxHighPriorityCommands_);
+    assert(activeStandardPriorityCommands_ <= maxStandardPriorityCommands_);
+    assert(activeLowPriorityCommands_ <= maxLowPriorityCommands_);
+    assert(totalProcessed_ <= totalScheduled_);
+    
+    for (Queue::const_iterator it = standardPriorityQueue_.begin(); it != standardPriorityQueue_.end(); ++it)
+    {
+      assert(it->first > PRIORITY_HIGH &&
+             it->first < PRIORITY_LOW);
+    }
+
+    for (Queue::const_iterator it = highPriorityQueue_.begin(); it != highPriorityQueue_.end(); ++it)
+    {
+      assert(it->first <= PRIORITY_HIGH);
+    }
+
+    for (Queue::const_iterator it = lowPriorityQueue_.begin(); it != lowPriorityQueue_.end(); ++it)
+    {
+      assert(it->first >= PRIORITY_LOW);
+    }
+#endif
+  }
+
+  
+  void OracleScheduler::SpawnFromQueue(Queue& queue,
+                                       Priority priority)
+  {
+    CheckInvariants();
+
+    Queue::iterator item = queue.begin();
+    assert(item != queue.end());
+
+    std::auto_ptr<ScheduledCommand> command(dynamic_cast<ScheduledCommand*>(item->second));
+    queue.erase(item);
+
+    if (command.get() != NULL)
+    {
+      /**
+       * Only schedule the command for execution in the oracle, if its
+       * receiver has not been destroyed yet.
+       **/
+      boost::shared_ptr<IObserver> observer(command->GetReceiver().lock());
+      if (observer)
+      {
+        if (oracle_.Schedule(GetSharedObserver(), command->WrapCommand(priority)))
+        {
+          /**
+           * Executing this code if "Schedule()" returned "false"
+           * above, will result in a memory leak within
+           * "OracleScheduler", as the scheduler believes that some
+           * command is still active (i.e. pending to be executed by
+           * the oracle), hereby stalling the scheduler during its
+           * destruction, and not freeing the
+           * "shared_ptr<OracleScheduler>" of the Stone context (check
+           * out "sjo-playground/WebViewer/Backend/Leak")
+           **/
+
+          switch (priority)
+          {
+            case Priority_High:
+              activeHighPriorityCommands_ ++;
+              break;
+
+            case Priority_Standard:
+              activeStandardPriorityCommands_ ++;
+              break;
+
+            case Priority_Low:
+              activeLowPriorityCommands_ ++;
+              break;
+
+            default:
+              throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+          }
+        }
+        else
+        {
+          totalProcessed_ ++;
+        }
+      }
+    }
+    else
+    {
+      LOG(ERROR) << "NULL command, should never happen";
+    }
+
+    CheckInvariants();
+  }
+
+  
+  void OracleScheduler::SpawnCommands()
+  {
+    // Send as many commands as possible to the oracle
+    while (!highPriorityQueue_.empty())
+    {
+      if (activeHighPriorityCommands_ < maxHighPriorityCommands_)
+      {
+        // First fill the high-priority lane
+        SpawnFromQueue(highPriorityQueue_, Priority_High);
+      }
+      else if (activeStandardPriorityCommands_ < maxStandardPriorityCommands_)
+      {
+        // There remain too many high-priority commands for the
+        // high-priority lane, schedule them to the standard-priority lanes
+        SpawnFromQueue(highPriorityQueue_, Priority_Standard);
+      }
+      else if (activeLowPriorityCommands_ < maxLowPriorityCommands_)
+      {
+        SpawnFromQueue(highPriorityQueue_, Priority_Low);
+      }
+      else
+      {
+        return;   // No slot available
+      }
+    }
+  
+    while (!standardPriorityQueue_.empty())
+    {
+      if (activeStandardPriorityCommands_ < maxStandardPriorityCommands_)
+      {
+        SpawnFromQueue(standardPriorityQueue_, Priority_Standard);
+      }
+      else if (activeLowPriorityCommands_ < maxLowPriorityCommands_)
+      {
+        SpawnFromQueue(standardPriorityQueue_, Priority_Low);
+      }
+      else
+      {
+        return;
+      }
+    }
+  
+    while (!lowPriorityQueue_.empty())
+    {
+      if (activeLowPriorityCommands_ < maxLowPriorityCommands_)
+      {
+        SpawnFromQueue(lowPriorityQueue_, Priority_Low);
+      }
+      else
+      {
+        return;
+      }
+    }  
+  }
+  
+
+  void OracleScheduler::RemoveActiveCommand(const ReceiverPayload& payload)
+  {
+    CheckInvariants();
+
+    totalProcessed_ ++;
+
+    switch (payload.GetActivePriority())
+    {
+      case Priority_High:
+        assert(activeHighPriorityCommands_ > 0);
+        activeHighPriorityCommands_ --;
+        break;
+
+      case Priority_Standard:
+        assert(activeStandardPriorityCommands_ > 0);
+        activeStandardPriorityCommands_ --;
+        break;
+
+      case Priority_Low:
+        assert(activeLowPriorityCommands_ > 0);
+        activeLowPriorityCommands_ --;
+        break;
+
+      default:
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+    }
+
+    SpawnCommands();
+
+    CheckInvariants();
+  }
+
+  
+  void OracleScheduler::Handle(const GetOrthancImageCommand::SuccessMessage& message)
+  {
+    assert(message.GetOrigin().HasPayload());
+    const ReceiverPayload& payload = dynamic_cast<const ReceiverPayload&>(message.GetOrigin().GetPayload());
+    
+    RemoveActiveCommand(payload);
+
+    GetOrthancImageCommand::SuccessMessage bis(
+      dynamic_cast<const GetOrthancImageCommand&>(payload.GetOriginalCommand()),
+      message.GetImage(), message.GetMimeType());
+    emitter_.EmitMessage(payload.GetOriginalReceiver(), bis);
+  }
+  
+
+  void OracleScheduler::Handle(const GetOrthancWebViewerJpegCommand::SuccessMessage& message)
+  {
+    assert(message.GetOrigin().HasPayload());
+    const ReceiverPayload& payload = dynamic_cast<const ReceiverPayload&>(message.GetOrigin().GetPayload());
+    
+    RemoveActiveCommand(payload);
+
+    GetOrthancWebViewerJpegCommand::SuccessMessage bis(
+      dynamic_cast<const GetOrthancWebViewerJpegCommand&>(payload.GetOriginalCommand()),
+      message.GetImage());
+    emitter_.EmitMessage(payload.GetOriginalReceiver(), bis);
+  }
+
+  
+  void OracleScheduler::Handle(const HttpCommand::SuccessMessage& message)
+  {
+    assert(message.GetOrigin().HasPayload());
+    const ReceiverPayload& payload = dynamic_cast<const ReceiverPayload&>(message.GetOrigin().GetPayload());
+    
+    RemoveActiveCommand(payload);
+
+    HttpCommand::SuccessMessage bis(
+      dynamic_cast<const HttpCommand&>(payload.GetOriginalCommand()),
+      message.GetAnswerHeaders(), message.GetAnswer());
+    emitter_.EmitMessage(payload.GetOriginalReceiver(), bis);
+  }
+
+  
+  void OracleScheduler::Handle(const OrthancRestApiCommand::SuccessMessage& message)
+  {
+    assert(message.GetOrigin().HasPayload());
+    const ReceiverPayload& payload = dynamic_cast<const ReceiverPayload&>(message.GetOrigin().GetPayload());
+    
+    RemoveActiveCommand(payload);
+
+    OrthancRestApiCommand::SuccessMessage bis(
+      dynamic_cast<const OrthancRestApiCommand&>(payload.GetOriginalCommand()),
+      message.GetAnswerHeaders(), message.GetAnswer());
+    emitter_.EmitMessage(payload.GetOriginalReceiver(), bis);
+  }
+
+  
+#if ORTHANC_ENABLE_DCMTK == 1
+  void OracleScheduler::Handle(const ParseDicomSuccessMessage& message)
+  {
+    assert(message.GetOrigin().HasPayload());
+    const ReceiverPayload& payload = dynamic_cast<const ReceiverPayload&>(message.GetOrigin().GetPayload());
+    
+    RemoveActiveCommand(payload);
+
+    ParseDicomSuccessMessage bis(
+      dynamic_cast<const OracleCommandBase&>(payload.GetOriginalCommand()),
+      message.GetDicom(), message.GetFileSize(), message.HasPixelData());
+    emitter_.EmitMessage(payload.GetOriginalReceiver(), bis);
+  }
+#endif
+  
+
+  void OracleScheduler::Handle(const ReadFileCommand::SuccessMessage& message)
+  {
+    assert(message.GetOrigin().HasPayload());
+    const ReceiverPayload& payload = dynamic_cast<const ReceiverPayload&>(message.GetOrigin().GetPayload());
+    
+    RemoveActiveCommand(payload);
+
+    ReadFileCommand::SuccessMessage bis(
+      dynamic_cast<const ReadFileCommand&>(payload.GetOriginalCommand()),
+      message.GetContent());
+    emitter_.EmitMessage(payload.GetOriginalReceiver(), bis);
+  }
+  
+
+  void OracleScheduler::Handle(const OracleCommandExceptionMessage& message)
+  {
+    const OracleCommandBase& command = dynamic_cast<const OracleCommandBase&>(message.GetOrigin());
+    
+    assert(command.HasPayload());
+    const ReceiverPayload& payload = dynamic_cast<const ReceiverPayload&>(command.GetPayload());
+    
+    RemoveActiveCommand(payload);
+
+    OracleCommandExceptionMessage bis(payload.GetOriginalCommand(), message.GetException());
+    emitter_.EmitMessage(payload.GetOriginalReceiver(), bis);
+  }  
+
+  
+  OracleScheduler::OracleScheduler(IOracle& oracle,
+                                   IMessageEmitter& emitter,
+                                   unsigned int maxHighPriority,
+                                   unsigned int maxStandardPriority,
+                                   unsigned int maxLowPriority) :
+    oracle_(oracle),
+    emitter_(emitter),
+    maxHighPriorityCommands_(maxHighPriority),
+    maxStandardPriorityCommands_(maxStandardPriority),
+    maxLowPriorityCommands_(maxLowPriority),
+    activeHighPriorityCommands_(0),
+    activeStandardPriorityCommands_(0),
+    activeLowPriorityCommands_(0),
+    totalScheduled_(0),
+    totalProcessed_(0)
+  {
+    assert(PRIORITY_HIGH < 0 &&
+           PRIORITY_LOW > 0);
+    
+    if (maxLowPriority <= 0)
+    {
+      // There must be at least 1 lane available to deal with low-priority commands
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+    
+  boost::shared_ptr<OracleScheduler> OracleScheduler::Create(IOracle& oracle,
+                                                             IObservable& oracleObservable,
+                                                             IMessageEmitter& emitter,
+                                                             unsigned int maxHighPriority,
+                                                             unsigned int maxStandardPriority,
+                                                             unsigned int maxLowPriority)
+  {
+    boost::shared_ptr<OracleScheduler> scheduler
+      (new OracleScheduler(oracle, emitter, maxHighPriority, maxStandardPriority, maxLowPriority));
+    scheduler->Register<GetOrthancImageCommand::SuccessMessage>(oracleObservable, &OracleScheduler::Handle);
+    scheduler->Register<GetOrthancWebViewerJpegCommand::SuccessMessage>(oracleObservable, &OracleScheduler::Handle);
+    scheduler->Register<HttpCommand::SuccessMessage>(oracleObservable, &OracleScheduler::Handle);
+    scheduler->Register<OrthancRestApiCommand::SuccessMessage>(oracleObservable, &OracleScheduler::Handle);
+    scheduler->Register<ReadFileCommand::SuccessMessage>(oracleObservable, &OracleScheduler::Handle);
+    scheduler->Register<OracleCommandExceptionMessage>(oracleObservable, &OracleScheduler::Handle);
+
+#if ORTHANC_ENABLE_DCMTK == 1
+    scheduler->Register<ParseDicomSuccessMessage>(oracleObservable, &OracleScheduler::Handle);
+#endif
+
+    return scheduler;
+  }
+    
+
+  OracleScheduler::~OracleScheduler()
+  {      
+    CancelAllRequests();
+  }
+
+
+  void OracleScheduler::CancelRequests(boost::shared_ptr<IObserver> receiver)
+  {
+    RemoveReceiverFromQueue(standardPriorityQueue_, receiver);
+    RemoveReceiverFromQueue(highPriorityQueue_, receiver);
+    RemoveReceiverFromQueue(lowPriorityQueue_, receiver);
+  }
+
+  
+  void OracleScheduler::CancelAllRequests()
+  {      
+    ClearQueue(standardPriorityQueue_);
+    ClearQueue(highPriorityQueue_);
+    ClearQueue(lowPriorityQueue_);
+  }
+
+
+  void OracleScheduler::Schedule(boost::shared_ptr<IObserver> receiver,
+                                 int priority,
+                                 IOracleCommand* command /* Takes ownership */)
+  {
+    std::auto_ptr<ScheduledCommand> pending(new ScheduledCommand(receiver, dynamic_cast<IOracleCommand*>(command)));
+
+    /**
+     * Safeguard to remember that a new "Handle()" method and a call
+     * to "scheduler->Register()" must be implemented for each
+     * possible oracle command.
+     **/
+    assert(command->GetType() == IOracleCommand::Type_GetOrthancImage ||
+           command->GetType() == IOracleCommand::Type_GetOrthancWebViewerJpeg ||
+           command->GetType() == IOracleCommand::Type_Http ||
+           command->GetType() == IOracleCommand::Type_OrthancRestApi ||
+           command->GetType() == IOracleCommand::Type_ParseDicomFromFile ||
+           command->GetType() == IOracleCommand::Type_ParseDicomFromWado ||
+           command->GetType() == IOracleCommand::Type_ReadFile);
+
+    if (priority <= PRIORITY_HIGH)
+    {
+      highPriorityQueue_.insert(std::make_pair(priority, pending.release()));
+    }
+    else if (priority >= PRIORITY_LOW)
+    {
+      lowPriorityQueue_.insert(std::make_pair(priority, pending.release()));
+    }
+    else
+    {
+      standardPriorityQueue_.insert(std::make_pair(priority, pending.release()));
+    }
+
+    totalScheduled_ ++;
+
+    SpawnCommands();
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Loaders/OracleScheduler.h	Mon Dec 09 13:58:37 2019 +0100
@@ -0,0 +1,167 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#if !defined(ORTHANC_ENABLE_DCMTK)
+#  error The macro ORTHANC_ENABLE_DCMTK must be defined
+#endif
+
+#include "../Messages/IMessageEmitter.h"
+#include "../Messages/ObserverBase.h"
+#include "../Oracle/GetOrthancImageCommand.h"
+#include "../Oracle/GetOrthancWebViewerJpegCommand.h"
+#include "../Oracle/HttpCommand.h"
+#include "../Oracle/IOracle.h"
+#include "../Oracle/OracleCommandExceptionMessage.h"
+#include "../Oracle/OrthancRestApiCommand.h"
+#include "../Oracle/ReadFileCommand.h"
+
+#if ORTHANC_ENABLE_DCMTK == 1
+#  include "../Oracle/ParseDicomSuccessMessage.h"
+#endif
+
+namespace OrthancStone
+{
+  class OracleScheduler : public ObserverBase<OracleScheduler>
+  {
+  public:
+    static const int PRIORITY_HIGH = -1;
+    static const int PRIORITY_LOW = 100;
+  
+  private:
+    enum Priority
+    {
+      Priority_Low,
+      Priority_Standard,
+      Priority_High
+    };
+
+    class ReceiverPayload;
+    class ScheduledCommand;
+
+    typedef std::multimap<int, ScheduledCommand*>  Queue;
+
+    IOracle&  oracle_;
+    IMessageEmitter&  emitter_;
+    Queue          standardPriorityQueue_;
+    Queue          highPriorityQueue_;
+    Queue          lowPriorityQueue_;
+    unsigned int   maxHighPriorityCommands_;  // Used if priority <= PRIORITY_HIGH
+    unsigned int   maxStandardPriorityCommands_;
+    unsigned int   maxLowPriorityCommands_;  // Used if priority >= PRIORITY_LOW
+    unsigned int   activeHighPriorityCommands_;
+    unsigned int   activeStandardPriorityCommands_;
+    unsigned int   activeLowPriorityCommands_;
+    uint64_t       totalScheduled_;
+    uint64_t       totalProcessed_;
+
+    void ClearQueue(Queue& queue);
+
+    void RemoveReceiverFromQueue(Queue& queue,
+                                 boost::shared_ptr<IObserver> receiver);
+
+    void CheckInvariants() const;
+
+    void SpawnFromQueue(Queue& queue,
+                        Priority priority);
+
+    void SpawnCommands();
+
+    void RemoveActiveCommand(const ReceiverPayload& payload);
+
+    void Handle(const GetOrthancImageCommand::SuccessMessage& message);
+
+    void Handle(const GetOrthancWebViewerJpegCommand::SuccessMessage& message);
+
+    void Handle(const HttpCommand::SuccessMessage& message);
+
+    void Handle(const OrthancRestApiCommand::SuccessMessage& message);
+
+#if ORTHANC_ENABLE_DCMTK == 1
+    void Handle(const ParseDicomSuccessMessage& message);
+#endif
+
+    void Handle(const ReadFileCommand::SuccessMessage& message);
+
+    void Handle(const OracleCommandExceptionMessage& message);
+
+    OracleScheduler(IOracle& oracle,
+                    IMessageEmitter& emitter,
+                    unsigned int maxHighPriority,
+                    unsigned int maxStandardPriority,
+                    unsigned int maxLowPriority);
+    
+  public:
+    static boost::shared_ptr<OracleScheduler> Create(IOracle& oracle,
+                                                     IObservable& oracleObservable,
+                                                     IMessageEmitter& emitter)
+    {
+      return Create(oracle, oracleObservable, emitter, 1, 4, 1);
+    }
+
+    static boost::shared_ptr<OracleScheduler> Create(IOracle& oracle,
+                                                     IObservable& oracleObservable,
+                                                     IMessageEmitter& emitter,
+                                                     unsigned int maxHighPriority,
+                                                     unsigned int maxStandardPriority,
+                                                     unsigned int maxLowPriority);
+
+    ~OracleScheduler();
+
+    unsigned int GetMaxHighPriorityCommands() const
+    {
+      return maxHighPriorityCommands_;
+    }
+
+    unsigned int GetMaxStandardPriorityCommands() const
+    {
+      return maxStandardPriorityCommands_;
+    }
+
+    unsigned int GetMaxLowPriorityCommands() const
+    {
+      return maxLowPriorityCommands_;
+    }
+
+    uint64_t GetTotalScheduled() const
+    {
+      return totalScheduled_;
+    }
+
+    uint64_t GetTotalProcessed() const
+    {
+      return totalProcessed_;
+    }
+
+    // Cancel the HTTP requests that are still pending in the queues,
+    // and that are associated with the given receiver. Note that the
+    // receiver might still receive answers to HTTP requests that were
+    // already submitted to the oracle.
+    void CancelRequests(boost::shared_ptr<IObserver> receiver);
+
+    void CancelAllRequests();
+
+    void Schedule(boost::shared_ptr<IObserver> receiver,
+                  int priority,
+                  IOracleCommand* command /* Takes ownership */);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Loaders/SeriesFramesLoader.cpp	Mon Dec 09 13:58:37 2019 +0100
@@ -0,0 +1,548 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "SeriesFramesLoader.h"
+
+#include "../Oracle/ParseDicomFromFileCommand.h"
+#include "../Oracle/ParseDicomFromWadoCommand.h"
+
+#if ORTHANC_ENABLE_DCMTK == 1
+#  include <Core/DicomParsing/Internals/DicomImageDecoder.h>
+#endif
+
+#include <Core/DicomFormat/DicomInstanceHasher.h>
+#include <Core/Images/Image.h>
+#include <Core/Images/ImageProcessing.h>
+#include <Core/Images/JpegReader.h>
+
+#include <boost/algorithm/string/predicate.hpp>
+
+namespace OrthancStone
+{  
+  class SeriesFramesLoader::Payload : public Orthanc::IDynamicObject
+  {
+  private:
+    DicomSource   source_;
+    size_t        seriesIndex_;
+    std::string   sopInstanceUid_;  // Only used for debug purpose
+    unsigned int  quality_;
+    bool          hasWindowing_;
+    float         windowingCenter_;
+    float         windowingWidth_;
+    std::auto_ptr<Orthanc::IDynamicObject>  userPayload_;
+
+  public:
+    Payload(const DicomSource& source,
+            size_t seriesIndex,
+            const std::string& sopInstanceUid,
+            unsigned int quality,
+            Orthanc::IDynamicObject* userPayload) :
+      source_(source),
+      seriesIndex_(seriesIndex),
+      sopInstanceUid_(sopInstanceUid),
+      quality_(quality),
+      hasWindowing_(false),
+      userPayload_(userPayload)
+    {
+    }
+
+    size_t GetSeriesIndex() const
+    {
+      return seriesIndex_;
+    }
+
+    const std::string& GetSopInstanceUid() const
+    {
+      return sopInstanceUid_;
+    }
+
+    unsigned int GetQuality() const
+    {
+      return quality_;
+    }
+
+    void SetWindowing(float center,
+                      float width)
+    {
+      hasWindowing_ = true;
+      windowingCenter_ = center;
+      windowingWidth_ = width;
+    }
+
+    bool HasWindowing() const
+    {
+      return hasWindowing_;
+    }
+
+    float GetWindowingCenter() const
+    {
+      if (hasWindowing_)
+      {
+        return windowingCenter_;
+      }
+      else
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+      }
+    }
+
+    float GetWindowingWidth() const
+    {
+      if (hasWindowing_)
+      {
+        return windowingWidth_;
+      }
+      else
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+      }
+    }
+
+    const DicomSource& GetSource() const
+    {
+      return source_;
+    }
+
+    Orthanc::IDynamicObject* GetUserPayload() const
+    {
+      return userPayload_.get();
+    }
+  };
+    
+
+  SeriesFramesLoader::SeriesFramesLoader(ILoadersContext& context,
+                                         LoadedDicomResources& instances,
+                                         const std::string& dicomDirPath,
+                                         boost::shared_ptr<LoadedDicomResources> dicomDir) :
+    context_(context),
+    frames_(instances),
+    dicomDirPath_(dicomDirPath),
+    dicomDir_(dicomDir)
+  {
+  }
+
+
+  void SeriesFramesLoader::EmitMessage(const Payload& payload,
+                                       const Orthanc::ImageAccessor& image)
+  {
+    const DicomInstanceParameters& parameters = frames_.GetInstanceParameters(payload.GetSeriesIndex());
+    const Orthanc::DicomMap& instance = frames_.GetInstance(payload.GetSeriesIndex());
+    size_t frameIndex = frames_.GetFrameIndex(payload.GetSeriesIndex());
+
+    if (frameIndex >= parameters.GetImageInformation().GetNumberOfFrames() ||
+        payload.GetSopInstanceUid() != parameters.GetSopInstanceUid())
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+    }      
+
+    LOG(TRACE) << "Decoded instance " << payload.GetSopInstanceUid() << ", frame "
+               << frameIndex << ": " << image.GetWidth() << "x"
+               << image.GetHeight() << ", " << Orthanc::EnumerationToString(image.GetFormat())
+               << ", quality " << payload.GetQuality();
+      
+    FrameLoadedMessage message(*this, frameIndex, payload.GetQuality(), image, instance, parameters, payload.GetUserPayload());
+    BroadcastMessage(message);
+  }
+
+
+#if ORTHANC_ENABLE_DCMTK == 1
+  void SeriesFramesLoader::HandleDicom(const Payload& payload,
+                                       Orthanc::ParsedDicomFile& dicom)
+  {     
+    size_t frameIndex = frames_.GetFrameIndex(payload.GetSeriesIndex());
+
+    std::auto_ptr<Orthanc::ImageAccessor> decoded;
+    decoded.reset(Orthanc::DicomImageDecoder::Decode(dicom, frameIndex));
+
+    if (decoded.get() == NULL)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
+    }
+
+    EmitMessage(payload, *decoded);
+  }
+#endif
+
+    
+  void SeriesFramesLoader::HandleDicomWebRendered(const Payload& payload,
+                                                  const std::string& body,
+                                                  const std::map<std::string, std::string>& headers)
+  {
+    assert(payload.GetSource().IsDicomWeb() &&
+           payload.HasWindowing());
+
+    bool ok = false;
+    for (std::map<std::string, std::string>::const_iterator it = headers.begin();
+         it != headers.end(); ++it)
+    {
+      if (boost::iequals("content-type", it->first) &&
+          boost::iequals(Orthanc::MIME_JPEG, it->second))
+      {
+        ok = true;
+        break;
+      }
+    }
+
+    if (!ok)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol,
+                                      "The WADO-RS server has not generated a JPEG image on /rendered");
+    }
+
+    Orthanc::JpegReader reader;
+    reader.ReadFromMemory(body);
+
+    switch (reader.GetFormat())
+    {
+      case Orthanc::PixelFormat_RGB24:
+        EmitMessage(payload, reader);
+        break;
+
+      case Orthanc::PixelFormat_Grayscale8:
+      {
+        const DicomInstanceParameters& parameters = frames_.GetInstanceParameters(payload.GetSeriesIndex());
+
+        Orthanc::Image scaled(parameters.GetExpectedPixelFormat(), reader.GetWidth(), reader.GetHeight(), false);
+        Orthanc::ImageProcessing::Convert(scaled, reader);
+          
+        float w = payload.GetWindowingWidth();
+        if (w <= 0.01f)
+        {
+          w = 0.01;  // Prevent division by zero
+        }
+
+        const float c = payload.GetWindowingCenter();
+        const float scaling = w / 255.0f;
+        const float offset = (c - w / 2.0f) / scaling;
+
+        Orthanc::ImageProcessing::ShiftScale(scaled, offset, scaling, false /* truncation to speed up */);
+        EmitMessage(payload, scaled);
+        break;
+      }
+
+      default:
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+    }
+  }
+
+
+#if ORTHANC_ENABLE_DCMTK == 1
+  void SeriesFramesLoader::Handle(const ParseDicomSuccessMessage& message)
+  {
+    assert(message.GetOrigin().HasPayload());
+
+    const Payload& payload = dynamic_cast<const Payload&>(message.GetOrigin().GetPayload());
+    if ((payload.GetSource().IsDicomDir() ||
+         payload.GetSource().IsDicomWeb()) &&
+        message.HasPixelData())
+    {
+      HandleDicom(dynamic_cast<const Payload&>(message.GetOrigin().GetPayload()), message.GetDicom());
+    }
+    else
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+    }
+  }
+#endif
+
+
+  void SeriesFramesLoader::Handle(const GetOrthancImageCommand::SuccessMessage& message)
+  {
+    assert(message.GetOrigin().HasPayload());
+
+    const Payload& payload = dynamic_cast<const Payload&>(message.GetOrigin().GetPayload());
+    assert(payload.GetSource().IsOrthanc());
+
+    EmitMessage(payload, message.GetImage());
+  }
+
+
+  void SeriesFramesLoader::Handle(const GetOrthancWebViewerJpegCommand::SuccessMessage& message)
+  {
+    assert(message.GetOrigin().HasPayload());
+
+    const Payload& payload = dynamic_cast<const Payload&>(message.GetOrigin().GetPayload());
+    assert(payload.GetSource().IsOrthanc());
+
+    EmitMessage(payload, message.GetImage());
+  }
+
+
+  void SeriesFramesLoader::Handle(const OrthancRestApiCommand::SuccessMessage& message)
+  {
+    // This is to handle "/rendered" in DICOMweb
+    assert(message.GetOrigin().HasPayload());
+    HandleDicomWebRendered(dynamic_cast<const Payload&>(message.GetOrigin().GetPayload()),
+                           message.GetAnswer(), message.GetAnswerHeaders());
+  }
+
+
+  void SeriesFramesLoader::Handle(const HttpCommand::SuccessMessage& message)
+  {
+    // This is to handle "/rendered" in DICOMweb
+    assert(message.GetOrigin().HasPayload());
+    HandleDicomWebRendered(dynamic_cast<const Payload&>(message.GetOrigin().GetPayload()),
+                           message.GetAnswer(), message.GetAnswerHeaders());
+  }
+
+
+  void SeriesFramesLoader::GetPreviewWindowing(float& center,
+                                               float& width,
+                                               size_t index) const
+  {
+    const Orthanc::DicomMap& instance = frames_.GetInstance(index);
+    const DicomInstanceParameters& parameters = frames_.GetInstanceParameters(index);
+
+    if (parameters.HasDefaultWindowing())
+    {
+      // TODO - Handle multiple presets (take the largest width)
+      center = parameters.GetDefaultWindowingCenter();
+      width = parameters.GetDefaultWindowingWidth();
+    }
+    else
+    {
+      float a, b;
+      if (instance.ParseFloat(a, Orthanc::DICOM_TAG_SMALLEST_IMAGE_PIXEL_VALUE) &&
+          instance.ParseFloat(b, Orthanc::DICOM_TAG_LARGEST_IMAGE_PIXEL_VALUE) &&
+          a < b)
+      {
+        center = (a + b) / 2.0f;
+        width = (b - a);
+      }
+      else
+      {
+        // Cannot infer a suitable windowing from the available tags
+        center = 128.0f;
+        width = 256.0f;
+      }
+    }
+  }
+
+  
+  Orthanc::IDynamicObject& SeriesFramesLoader::FrameLoadedMessage::GetUserPayload() const
+  {
+    if (userPayload_)
+    {
+      return *userPayload_;
+    }
+    else
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+    }
+  }
+
+
+  void SeriesFramesLoader::Factory::SetDicomDir(const std::string& dicomDirPath,
+                                                boost::shared_ptr<LoadedDicomResources> dicomDir)
+  {
+    dicomDirPath_ = dicomDirPath;
+    dicomDir_ = dicomDir;
+  }
+
+
+  boost::shared_ptr<IObserver> SeriesFramesLoader::Factory::Create(ILoadersContext::ILock& stone)
+  {
+    boost::shared_ptr<SeriesFramesLoader> loader(
+      new SeriesFramesLoader(stone.GetContext(), instances_, dicomDirPath_, dicomDir_));
+    loader->Register<GetOrthancImageCommand::SuccessMessage>(stone.GetOracleObservable(), &SeriesFramesLoader::Handle);
+    loader->Register<GetOrthancWebViewerJpegCommand::SuccessMessage>(stone.GetOracleObservable(), &SeriesFramesLoader::Handle);
+    loader->Register<HttpCommand::SuccessMessage>(stone.GetOracleObservable(), &SeriesFramesLoader::Handle);
+    loader->Register<OrthancRestApiCommand::SuccessMessage>(stone.GetOracleObservable(), &SeriesFramesLoader::Handle);
+
+#if ORTHANC_ENABLE_DCMTK == 1
+    loader->Register<ParseDicomSuccessMessage>(stone.GetOracleObservable(), &SeriesFramesLoader::Handle);
+#endif
+
+    return loader;
+  }
+
+
+  void SeriesFramesLoader::ScheduleLoadFrame(int priority,
+                                             const DicomSource& source,
+                                             size_t index,
+                                             unsigned int quality,
+                                             Orthanc::IDynamicObject* userPayload)
+  {
+    std::auto_ptr<Orthanc::IDynamicObject> protection(userPayload);
+    
+    if (index >= frames_.GetFramesCount() ||
+        quality >= source.GetQualityCount())
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+
+    const Orthanc::DicomMap& instance = frames_.GetInstance(index);
+
+    std::string sopInstanceUid;
+    if (!instance.LookupStringValue(sopInstanceUid, Orthanc::DICOM_TAG_SOP_INSTANCE_UID, false))
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat,
+                                      "Missing SOPInstanceUID in a DICOM instance");
+    }
+      
+    if (source.IsDicomDir())
+    {
+      if (dicomDir_.get() == NULL)
+      {
+        // Should have been set in the factory
+        throw Orthanc::OrthancException(
+          Orthanc::ErrorCode_BadSequenceOfCalls,
+          "SeriesFramesLoader::Factory::SetDicomDir() should have been called");
+      }
+        
+      assert(quality == 0);
+        
+      std::string file;
+      if (dicomDir_->LookupStringValue(file, sopInstanceUid, Orthanc::DICOM_TAG_REFERENCED_FILE_ID))
+      {
+        std::auto_ptr<ParseDicomFromFileCommand> command(new ParseDicomFromFileCommand(dicomDirPath_, file));
+        command->SetPixelDataIncluded(true);
+        command->AcquirePayload(new Payload(source, index, sopInstanceUid, quality, protection.release()));
+
+        {
+          std::auto_ptr<ILoadersContext::ILock> lock(context_.Lock());
+          lock->Schedule(GetSharedObserver(), priority, command.release());
+        }
+      }
+      else
+      {
+        LOG(WARNING) << "Missing tag ReferencedFileID in a DICOMDIR entry";
+      }
+    }
+    else if (source.IsDicomWeb())
+    {
+      std::string studyInstanceUid, seriesInstanceUid;
+      if (!instance.LookupStringValue(studyInstanceUid, Orthanc::DICOM_TAG_STUDY_INSTANCE_UID, false) ||
+          !instance.LookupStringValue(seriesInstanceUid, Orthanc::DICOM_TAG_SERIES_INSTANCE_UID, false))
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat,
+                                        "Missing StudyInstanceUID or SeriesInstanceUID in a DICOM instance");
+      }
+
+      const std::string uri = ("/studies/" + studyInstanceUid +
+                               "/series/" + seriesInstanceUid +
+                               "/instances/" + sopInstanceUid);
+
+      if (source.HasDicomWebRendered() &&
+          quality == 0)
+      {
+        float c, w;
+        GetPreviewWindowing(c, w, index);
+
+        std::map<std::string, std::string> arguments, headers;
+        arguments["window"] = (boost::lexical_cast<std::string>(c) + "," +
+                               boost::lexical_cast<std::string>(w) + ",linear");
+        headers["Accept"] = "image/jpeg";
+
+        std::auto_ptr<Payload> payload(new Payload(source, index, sopInstanceUid, quality, protection.release()));
+        payload->SetWindowing(c, w);
+
+        {
+          std::auto_ptr<ILoadersContext::ILock> lock(context_.Lock());
+          lock->Schedule(GetSharedObserver(), priority,
+                         source.CreateDicomWebCommand(uri + "/rendered", arguments, headers, payload.release()));
+        }
+      }
+      else
+      {
+        assert((source.HasDicomWebRendered() && quality == 1) ||
+               (!source.HasDicomWebRendered() && quality == 0));
+
+#if ORTHANC_ENABLE_DCMTK == 1
+        std::auto_ptr<Payload> payload(new Payload(source, index, sopInstanceUid, quality, protection.release()));
+
+        const std::map<std::string, std::string> empty;
+
+        std::auto_ptr<ParseDicomFromWadoCommand> command(
+          new ParseDicomFromWadoCommand(sopInstanceUid, source.CreateDicomWebCommand(uri, empty, empty, NULL)));
+        command->AcquirePayload(payload.release());
+
+        {
+          std::auto_ptr<ILoadersContext::ILock> lock(context_.Lock());
+          lock->Schedule(GetSharedObserver(), priority, command.release());
+        }
+#else
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented,
+                                        "DCMTK is not enabled, cannot parse a DICOM instance");
+#endif
+      }
+    }
+    else if (source.IsOrthanc())
+    {
+      std::string orthancId;
+
+      {
+        std::string patientId, studyInstanceUid, seriesInstanceUid;
+        if (!instance.LookupStringValue(patientId, Orthanc::DICOM_TAG_PATIENT_ID, false) ||
+            !instance.LookupStringValue(studyInstanceUid, Orthanc::DICOM_TAG_STUDY_INSTANCE_UID, false) ||
+            !instance.LookupStringValue(seriesInstanceUid, Orthanc::DICOM_TAG_SERIES_INSTANCE_UID, false))
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat,
+                                          "Missing StudyInstanceUID or SeriesInstanceUID in a DICOM instance");
+        }
+
+        Orthanc::DicomInstanceHasher hasher(patientId, studyInstanceUid, seriesInstanceUid, sopInstanceUid);
+        orthancId = hasher.HashInstance();
+      }
+
+      const DicomInstanceParameters& parameters = frames_.GetInstanceParameters(index);
+
+      if (quality == 0 && source.HasOrthancWebViewer1())
+      {
+        std::auto_ptr<GetOrthancWebViewerJpegCommand> command(new GetOrthancWebViewerJpegCommand);
+        command->SetInstance(orthancId);
+        command->SetExpectedPixelFormat(parameters.GetExpectedPixelFormat());
+        command->AcquirePayload(new Payload(source, index, sopInstanceUid, quality, protection.release()));
+
+        {
+          std::auto_ptr<ILoadersContext::ILock> lock(context_.Lock());
+          lock->Schedule(GetSharedObserver(), priority, command.release());
+        }
+      }
+      else if (quality == 0 && source.HasOrthancAdvancedPreview())
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+      }
+      else
+      {
+        assert(quality <= 1);
+        assert(quality == 0 || 
+               source.HasOrthancWebViewer1() || 
+               source.HasOrthancAdvancedPreview());
+
+        std::auto_ptr<GetOrthancImageCommand> command(new GetOrthancImageCommand);
+        command->SetFrameUri(orthancId, frames_.GetFrameIndex(index), parameters.GetExpectedPixelFormat());
+        command->SetExpectedPixelFormat(parameters.GetExpectedPixelFormat());
+        command->SetHttpHeader("Accept", Orthanc::MIME_PAM);
+        command->AcquirePayload(new Payload(source, index, sopInstanceUid, quality, protection.release()));
+
+        {
+          std::auto_ptr<ILoadersContext::ILock> lock(context_.Lock());
+          lock->Schedule(GetSharedObserver(), priority, command.release());
+        }
+      }
+    }
+    else
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Loaders/SeriesFramesLoader.h	Mon Dec 09 13:58:37 2019 +0100
@@ -0,0 +1,176 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#if !defined(ORTHANC_ENABLE_DCMTK)
+#  error The macro ORTHANC_ENABLE_DCMTK must be defined
+#endif
+
+#include "OracleScheduler.h"
+#include "DicomSource.h"
+#include "SeriesOrderedFrames.h"
+#include "ILoaderFactory.h"
+
+namespace OrthancStone
+{  
+  class SeriesFramesLoader : 
+    public ObserverBase<SeriesFramesLoader>,
+    public IObservable
+  {
+  private:
+    class Payload;
+
+    ILoadersContext&                           context_;
+    SeriesOrderedFrames                      frames_;
+    std::string                              dicomDirPath_;
+    boost::shared_ptr<LoadedDicomResources>  dicomDir_;
+
+    SeriesFramesLoader(ILoadersContext& context,
+                       LoadedDicomResources& instances,
+                       const std::string& dicomDirPath,
+                       boost::shared_ptr<LoadedDicomResources> dicomDir);
+
+    void EmitMessage(const Payload& payload,
+                     const Orthanc::ImageAccessor& image);
+
+#if ORTHANC_ENABLE_DCMTK == 1
+    void HandleDicom(const Payload& payload,
+                     Orthanc::ParsedDicomFile& dicom);
+#endif
+    
+    void HandleDicomWebRendered(const Payload& payload,
+                                const std::string& body,
+                                const std::map<std::string, std::string>& headers);
+
+#if ORTHANC_ENABLE_DCMTK == 1
+    void Handle(const ParseDicomSuccessMessage& message);
+#endif
+
+    void Handle(const GetOrthancImageCommand::SuccessMessage& message);
+
+    void Handle(const GetOrthancWebViewerJpegCommand::SuccessMessage& message);
+
+    void Handle(const OrthancRestApiCommand::SuccessMessage& message);
+
+    void Handle(const HttpCommand::SuccessMessage& message);
+
+    void GetPreviewWindowing(float& center,
+                             float& width,
+                             size_t index) const;
+
+  public:
+    class FrameLoadedMessage : public OriginMessage<SeriesFramesLoader>
+    {
+      ORTHANC_STONE_MESSAGE(__FILE__, __LINE__);
+
+    private:
+      size_t                          frameIndex_;
+      unsigned int                    quality_;
+      const Orthanc::ImageAccessor&   image_;
+      const Orthanc::DicomMap&        instance_;
+      const DicomInstanceParameters&  parameters_;
+      Orthanc::IDynamicObject*        userPayload_; // Ownership is maintained by the caller
+
+    public:
+      FrameLoadedMessage(const SeriesFramesLoader& loader,
+                         size_t frameIndex,
+                         unsigned int quality,
+                         const Orthanc::ImageAccessor& image,
+                         const Orthanc::DicomMap& instance,
+                         const DicomInstanceParameters&  parameters,
+                         Orthanc::IDynamicObject* userPayload) :
+        OriginMessage(loader),
+        frameIndex_(frameIndex),
+        quality_(quality),
+        image_(image),
+        instance_(instance),
+        parameters_(parameters),
+        userPayload_(userPayload)
+      {
+      }
+
+      size_t GetFrameIndex() const
+      {
+        return frameIndex_;
+      }
+
+      unsigned int GetQuality() const
+      {
+        return quality_;
+      }
+
+      const Orthanc::ImageAccessor& GetImage() const
+      {
+        return image_;
+      }
+
+      const Orthanc::DicomMap& GetInstance() const
+      {
+        return instance_;
+      }
+
+      const DicomInstanceParameters& GetInstanceParameters() const
+      {
+        return parameters_;
+      }
+
+      bool HasUserPayload() const
+      {
+        return userPayload_ != NULL;
+      }
+
+      Orthanc::IDynamicObject& GetUserPayload() const;
+    };
+
+
+    class Factory : public ILoaderFactory
+    {
+    private:
+      LoadedDicomResources&                    instances_;
+      std::string                              dicomDirPath_;
+      boost::shared_ptr<LoadedDicomResources>  dicomDir_;
+
+    public:
+      // No "const" because "LoadedDicomResources::GetResource()" will call "Flatten()"
+      Factory(LoadedDicomResources& instances) :
+      instances_(instances)
+      {
+      }
+
+      void SetDicomDir(const std::string& dicomDirPath,
+                       boost::shared_ptr<LoadedDicomResources> dicomDir);
+
+      virtual boost::shared_ptr<IObserver> Create(ILoadersContext::ILock& context);
+    };
+
+    const SeriesOrderedFrames& GetOrderedFrames() const
+    {
+      return frames_;
+    }
+
+    void ScheduleLoadFrame(int priority,
+                           const DicomSource& source,
+                           size_t index,
+                           unsigned int quality,
+                           Orthanc::IDynamicObject* userPayload /* transfer ownership */);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Loaders/SeriesMetadataLoader.cpp	Mon Dec 09 13:58:37 2019 +0100
@@ -0,0 +1,347 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "SeriesMetadataLoader.h"
+
+#include <Core/DicomFormat/DicomInstanceHasher.h>
+
+namespace OrthancStone
+{
+  SeriesMetadataLoader::SeriesMetadataLoader(boost::shared_ptr<DicomResourcesLoader>& loader) :
+    loader_(loader),
+    state_(State_Setup)
+  {
+  }
+
+
+  bool SeriesMetadataLoader::IsScheduledWithHigherPriority(const std::string& seriesInstanceUid,
+                                                           int priority) const
+  {
+    if (series_.find(seriesInstanceUid) != series_.end())
+    {
+      // This series is readily available
+      return true;
+    }
+    else
+    {
+      std::map<std::string, int>::const_iterator found = scheduled_.find(seriesInstanceUid);
+
+      return (found != scheduled_.end() &&
+              found->second < priority);
+    }
+  }
+
+
+  void SeriesMetadataLoader::Handle(const DicomResourcesLoader::SuccessMessage& message)
+  {
+    assert(message.GetResources());
+
+    switch (state_)
+    {
+      case State_Setup:
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+
+      case State_Default:
+      {
+        std::string studyInstanceUid;
+        std::string seriesInstanceUid;
+
+        if (message.GetResources()->LookupTagValueConsensus(studyInstanceUid, Orthanc::DICOM_TAG_STUDY_INSTANCE_UID) &&
+            message.GetResources()->LookupTagValueConsensus(seriesInstanceUid, Orthanc::DICOM_TAG_SERIES_INSTANCE_UID))
+        {
+          series_[seriesInstanceUid] = message.GetResources();
+
+          SeriesLoadedMessage loadedMessage(*this, message.GetDicomSource(), studyInstanceUid,
+                                            seriesInstanceUid, *message.GetResources());
+          BroadcastMessage(loadedMessage);
+        }
+
+        break;
+      }
+
+      case State_DicomDir:
+      {
+        assert(!dicomDir_);
+        assert(seriesSize_.empty());
+
+        dicomDir_ = message.GetResources();
+            
+        for (size_t i = 0; i < message.GetResources()->GetSize(); i++)
+        {
+          std::string seriesInstanceUid;
+          if (message.GetResources()->GetResource(i).LookupStringValue
+              (seriesInstanceUid, Orthanc::DICOM_TAG_SERIES_INSTANCE_UID, false))
+          {
+            boost::shared_ptr<OrthancStone::LoadedDicomResources> target
+              (new OrthancStone::LoadedDicomResources(Orthanc::DICOM_TAG_SOP_INSTANCE_UID));
+
+            if (loader_->ScheduleLoadDicomFile(target, message.GetPriority(), message.GetDicomSource(), dicomDirPath_, 
+                                               message.GetResources()->GetResource(i), false /* no need for pixel data */,
+                                               NULL /* TODO PAYLOAD */))
+            {
+              std::map<std::string, unsigned int>::iterator found = seriesSize_.find(seriesInstanceUid);
+              if (found == seriesSize_.end())
+              {
+                series_[seriesInstanceUid].reset
+                  (new OrthancStone::LoadedDicomResources(Orthanc::DICOM_TAG_SOP_INSTANCE_UID));
+                seriesSize_[seriesInstanceUid] = 1;
+              }
+              else
+              {
+                found->second ++;
+              }
+            }
+          }
+        }
+
+        LOG(INFO) << "Read a DICOMDIR containing " << seriesSize_.size() << " series";            
+
+        state_ = State_DicomFile;
+        break;
+      }
+
+      case State_DicomFile:
+      {
+        assert(dicomDir_);
+        assert(message.GetResources()->GetSize() <= 1);  // Could be zero if corrupted DICOM instance
+
+        if (message.GetResources()->GetSize() == 1)
+        {
+          const Orthanc::DicomMap& instance = message.GetResources()->GetResource(0);
+
+          std::string studyInstanceUid;
+          std::string seriesInstanceUid;
+          if (instance.LookupStringValue(studyInstanceUid, Orthanc::DICOM_TAG_STUDY_INSTANCE_UID, false) &&
+              instance.LookupStringValue(seriesInstanceUid, Orthanc::DICOM_TAG_SERIES_INSTANCE_UID, false))
+          {
+            Series::const_iterator series = series_.find(seriesInstanceUid);
+            std::map<std::string, unsigned int>::const_iterator size = seriesSize_.find(seriesInstanceUid);
+
+            if (series == series_.end() ||
+                size == seriesSize_.end())
+            {
+              throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+            }
+            else
+            {
+              series->second->AddResource(instance);
+
+              if (series->second->GetSize() > size->second)
+              {
+                throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+              }
+              else if (series->second->GetSize() == size->second)
+              {
+                // The series is complete
+                SeriesLoadedMessage loadedMessage(
+                  *this, message.GetDicomSource(),
+                  studyInstanceUid, seriesInstanceUid, *series->second);
+                loadedMessage.SetDicomDir(dicomDirPath_, dicomDir_);
+                BroadcastMessage(loadedMessage);
+              }
+            }
+          }
+        }
+
+        break;
+      }
+
+      default:
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+    }
+  }
+
+
+  SeriesMetadataLoader::SeriesLoadedMessage::SeriesLoadedMessage(
+    const SeriesMetadataLoader& loader,
+    const DicomSource& source,
+    const std::string& studyInstanceUid,
+    const std::string& seriesInstanceUid,
+    LoadedDicomResources& instances) :
+    OriginMessage(loader),
+    source_(source),
+    studyInstanceUid_(studyInstanceUid),
+    seriesInstanceUid_(seriesInstanceUid),
+    instances_(instances)
+  {
+    LOG(INFO) << "Loaded series " << seriesInstanceUid
+              << ", number of instances: " << instances_.GetSize();
+  }
+
+
+  boost::shared_ptr<IObserver> SeriesMetadataLoader::Factory::Create(ILoadersContext::ILock& context)
+  {
+    DicomResourcesLoader::Factory factory;
+    boost::shared_ptr<DicomResourcesLoader> loader
+      (boost::dynamic_pointer_cast<DicomResourcesLoader>(factory.Create(context)));
+      
+    boost::shared_ptr<SeriesMetadataLoader> obj(new SeriesMetadataLoader(loader));
+    obj->Register<DicomResourcesLoader::SuccessMessage>(*loader, &SeriesMetadataLoader::Handle);
+    return obj;
+  }
+
+
+  SeriesMetadataLoader::Accessor::Accessor(SeriesMetadataLoader& that,
+                                           const std::string& seriesInstanceUid)
+  {
+    Series::const_iterator found = that.series_.find(seriesInstanceUid);
+    if (found != that.series_.end())
+    {
+      assert(found->second != NULL);
+      series_ = found->second;
+    }
+  }
+
+
+  size_t SeriesMetadataLoader::Accessor::GetInstancesCount() const
+  {
+    if (IsComplete())
+    {
+      return series_->GetSize();
+    }
+    else
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+    }
+  }
+
+
+  const Orthanc::DicomMap& SeriesMetadataLoader::Accessor::GetInstance(size_t index) const
+  {
+    if (IsComplete())
+    {
+      return series_->GetResource(index);
+    }
+    else
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+    }     
+  }
+
+
+  void SeriesMetadataLoader::ScheduleLoadSeries(int priority,
+                                                const DicomSource& source,
+                                                const std::string& studyInstanceUid,
+                                                const std::string& seriesInstanceUid)
+  {
+    if (state_ != State_Setup &&
+        state_ != State_Default)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls,
+                                      "The loader is working in DICOMDIR state");
+    }
+
+    state_ = State_Default;
+
+    // Only re-schedule the loading if the previous loading was with lower priority
+    if (!IsScheduledWithHigherPriority(seriesInstanceUid, priority))
+    {
+      if (source.IsDicomWeb())
+      {
+        boost::shared_ptr<LoadedDicomResources> target
+          (new LoadedDicomResources(Orthanc::DICOM_TAG_SOP_INSTANCE_UID));
+        loader_->ScheduleWado(target, priority, source,
+                              "/studies/" + studyInstanceUid + "/series/" + seriesInstanceUid + "/metadata",
+                              NULL /* TODO PAYLOAD */);
+
+        scheduled_[seriesInstanceUid] = priority;
+      }
+      else if (source.IsOrthanc())
+      {
+        // This flavor of the method is only available with DICOMweb, as
+        // Orthanc requires the "PatientID" to be known
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls,
+                                        "The PatientID must be provided on Orthanc sources");
+      }
+      else
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+      }
+    }
+  }
+
+
+  void SeriesMetadataLoader::ScheduleLoadSeries(int priority,
+                                                const DicomSource& source,
+                                                const std::string& patientId,
+                                                const std::string& studyInstanceUid,
+                                                const std::string& seriesInstanceUid)
+  {
+    if (state_ != State_Setup &&
+        state_ != State_Default)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls,
+                                      "The loader is working in DICOMDIR state");
+    }
+
+    state_ = State_Default;
+
+    if (source.IsDicomWeb())
+    {
+      ScheduleLoadSeries(priority, source, studyInstanceUid, seriesInstanceUid);
+    }
+    else if (!IsScheduledWithHigherPriority(seriesInstanceUid, priority))
+    {
+      if (source.IsOrthanc())
+      {
+        // Dummy SOP Instance UID, as we are working at the "series" level
+        Orthanc::DicomInstanceHasher hasher(patientId, studyInstanceUid, seriesInstanceUid, "dummy");
+
+        boost::shared_ptr<LoadedDicomResources> target
+          (new LoadedDicomResources(Orthanc::DICOM_TAG_SOP_INSTANCE_UID));
+        
+        loader_->ScheduleLoadOrthancResources(target, priority, source, Orthanc::ResourceType_Series,
+                                              hasher.HashSeries(), Orthanc::ResourceType_Instance,
+                                              NULL /* TODO PAYLOAD */);
+
+        scheduled_[seriesInstanceUid] = priority;
+      }
+      else
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+      }
+    }
+  }
+
+
+  void SeriesMetadataLoader::ScheduleLoadDicomDir(int priority,
+                                                  const DicomSource& source,
+                                                  const std::string& path)
+  {
+    if (!source.IsDicomDir())
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+    }
+
+    if (state_ != State_Setup)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls,
+                                      "The loader cannot load two different DICOMDIR");
+    }
+
+    state_ = State_DicomDir;
+    dicomDirPath_ = path;
+    boost::shared_ptr<LoadedDicomResources> dicomDir
+      (new LoadedDicomResources(Orthanc::DICOM_TAG_REFERENCED_SOP_INSTANCE_UID_IN_FILE));
+    loader_->ScheduleLoadDicomDir(dicomDir, priority, source, path,
+                                  NULL /* TODO PAYLOAD */);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Loaders/SeriesMetadataLoader.h	Mon Dec 09 13:58:37 2019 +0100
@@ -0,0 +1,170 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "DicomResourcesLoader.h"
+
+namespace OrthancStone
+{
+  class SeriesMetadataLoader :
+    public ObserverBase<SeriesMetadataLoader>,
+    public IObservable
+  {
+  private:
+    enum State
+    {
+      State_Setup,
+      State_Default,
+      State_DicomDir,
+      State_DicomFile
+    };
+
+    typedef std::map<std::string, boost::shared_ptr<LoadedDicomResources> >  Series;
+
+    boost::shared_ptr<DicomResourcesLoader>  loader_;
+    State                                    state_;
+    std::map<std::string, int>               scheduled_;   // Maps a "SeriesInstanceUID" to a priority
+    Series                                   series_;
+    boost::shared_ptr<LoadedDicomResources>  dicomDir_;
+    std::string                              dicomDirPath_;
+    std::map<std::string, unsigned int>      seriesSize_;
+
+    SeriesMetadataLoader(boost::shared_ptr<DicomResourcesLoader>& loader);
+
+    bool IsScheduledWithHigherPriority(const std::string& seriesInstanceUid,
+                                       int priority) const;
+
+    void Handle(const DicomResourcesLoader::SuccessMessage& message);
+
+  public:
+    class SeriesLoadedMessage : public OriginMessage<SeriesMetadataLoader>
+    {
+      ORTHANC_STONE_MESSAGE(__FILE__, __LINE__);
+
+    private:
+      const DicomSource&     source_;
+      const std::string&     studyInstanceUid_;
+      const std::string&     seriesInstanceUid_;
+      LoadedDicomResources&  instances_;
+      std::string            dicomDirPath_;
+      boost::shared_ptr<LoadedDicomResources>  dicomDir_;
+
+    public:
+      SeriesLoadedMessage(const SeriesMetadataLoader& loader,
+                          const DicomSource& source,
+                          const std::string& studyInstanceUid,
+                          const std::string& seriesInstanceUid,
+                          LoadedDicomResources& instances);
+
+      const DicomSource& GetDicomSource() const
+      {
+        return source_;
+      }
+      
+      const std::string& GetStudyInstanceUid() const
+      {
+        return studyInstanceUid_;
+      }
+
+      const std::string& GetSeriesInstanceUid() const
+      {
+        return seriesInstanceUid_;
+      }
+
+      size_t GetInstancesCount() const
+      {
+        return instances_.GetSize();
+      }
+
+      const Orthanc::DicomMap& GetInstance(size_t index) const
+      {
+        return instances_.GetResource(index);
+      }
+
+      LoadedDicomResources& GetInstances() const
+      {
+        return instances_;
+      }
+
+      void SetDicomDir(const std::string& dicomDirPath,
+                       boost::shared_ptr<LoadedDicomResources> dicomDir)
+      {
+        dicomDirPath_ = dicomDirPath;
+        dicomDir_ = dicomDir;
+      }
+
+      const std::string& GetDicomDirPath() const
+      {
+        return dicomDirPath_;
+      }
+
+      // Will be NULL on non-DICOMDIR sources
+      boost::shared_ptr<LoadedDicomResources> GetDicomDir() const
+      {
+        return dicomDir_;
+      }
+    };
+
+  
+    class Factory : public ILoaderFactory
+    {
+    public:
+      virtual boost::shared_ptr<IObserver> Create(ILoadersContext::ILock& context);
+    };
+
+  
+    class Accessor : public boost::noncopyable
+    {
+    private:
+      boost::shared_ptr<LoadedDicomResources>  series_;
+
+    public:
+      Accessor(SeriesMetadataLoader& that,
+               const std::string& seriesInstanceUid);
+
+      bool IsComplete() const
+      {
+        return series_ != NULL;
+      }
+
+      size_t GetInstancesCount() const;
+
+      const Orthanc::DicomMap& GetInstance(size_t index) const;
+    };
+
+
+    void ScheduleLoadSeries(int priority,
+                            const DicomSource& source,
+                            const std::string& studyInstanceUid,
+                            const std::string& seriesInstanceUid);
+
+    void ScheduleLoadSeries(int priority,
+                            const DicomSource& source,
+                            const std::string& patientId,
+                            const std::string& studyInstanceUid,
+                            const std::string& seriesInstanceUid);
+
+    void ScheduleLoadDicomDir(int priority,
+                              const DicomSource& source,
+                              const std::string& path);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Loaders/SeriesOrderedFrames.cpp	Mon Dec 09 13:58:37 2019 +0100
@@ -0,0 +1,343 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "../Toolbox/SlicesSorter.h"
+#include "SeriesOrderedFrames.h"
+
+#include <Core/OrthancException.h>
+
+namespace OrthancStone
+{
+  class SeriesOrderedFrames::Instance : public boost::noncopyable
+  {
+  private:
+    std::auto_ptr<Orthanc::DicomMap>  dicom_;
+    DicomInstanceParameters           parameters_;
+
+  public:
+    Instance(const Orthanc::DicomMap& dicom) :
+      dicom_(dicom.Clone()),
+      parameters_(dicom)
+    {
+    }
+    
+    const Orthanc::DicomMap& GetInstance() const
+    {
+      return *dicom_;
+    }
+    
+    const DicomInstanceParameters& GetInstanceParameters() const
+    {
+      return parameters_;
+    }
+
+    bool Lookup3DGeometry(CoordinateSystem3D& target) const
+    {
+      try
+      {
+        std::string imagePositionPatient, imageOrientationPatient;
+        if (dicom_->LookupStringValue(imagePositionPatient, Orthanc::DICOM_TAG_IMAGE_POSITION_PATIENT, false) &&
+            dicom_->LookupStringValue(imageOrientationPatient, Orthanc::DICOM_TAG_IMAGE_ORIENTATION_PATIENT, false))
+        {
+          target = CoordinateSystem3D(imagePositionPatient, imageOrientationPatient);
+          return true;
+        }
+      }
+      catch (Orthanc::OrthancException&)
+      {
+      }
+
+      return false;
+    }
+
+    bool LookupIndexInSeries(int& target) const
+    {
+      std::string value;
+
+      if (dicom_->LookupStringValue(value, Orthanc::DICOM_TAG_INSTANCE_NUMBER, false) ||
+          dicom_->LookupStringValue(value, Orthanc::DICOM_TAG_IMAGE_INDEX, false))
+      {
+        try
+        {
+          target = boost::lexical_cast<int>(value);
+          return true;
+        }
+        catch (boost::bad_lexical_cast&)
+        {
+        }
+      }
+
+      return false;
+    }
+  };
+
+
+  class SeriesOrderedFrames::Frame : public boost::noncopyable
+  {
+  private:
+    const Instance*  instance_;
+    unsigned int     frameIndex_;
+
+  public:
+    Frame(const Instance& instance,
+          unsigned int frameIndex) :
+      instance_(&instance),
+      frameIndex_(frameIndex)
+    {
+      if (frameIndex_ >= instance.GetInstanceParameters().GetImageInformation().GetNumberOfFrames())
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+      }
+    }
+
+    const Orthanc::DicomMap& GetInstance() const
+    {
+      assert(instance_ != NULL);
+      return instance_->GetInstance();
+    }
+    
+    const DicomInstanceParameters& GetInstanceParameters() const
+    {
+      assert(instance_ != NULL);
+      return instance_->GetInstanceParameters();
+    }
+
+    unsigned int GetFrameIndex() const
+    {
+      return frameIndex_;
+    }
+  };
+
+
+  class SeriesOrderedFrames::InstanceWithIndexInSeries
+  {
+  private:
+    const Instance* instance_;  // Don't use a reference to make "std::sort()" happy
+    int             index_;
+
+  public:
+    InstanceWithIndexInSeries(const Instance& instance) :
+    instance_(&instance)
+    {
+      if (!instance_->LookupIndexInSeries(index_))
+      {
+        index_ = std::numeric_limits<int>::max();
+      }
+    }
+
+    const Instance& GetInstance() const
+    {
+      return *instance_;
+    }
+
+    int GetIndexInSeries() const
+    {
+      return index_;
+    }
+
+    bool operator< (const InstanceWithIndexInSeries& other) const
+    {
+      return (index_ < other.index_);
+    }
+  };
+
+
+  void SeriesOrderedFrames::Clear()
+  {
+    for (size_t i = 0; i < instances_.size(); i++)
+    {
+      assert(instances_[i] != NULL);
+      delete instances_[i];
+    }
+
+    for (size_t i = 0; i < orderedFrames_.size(); i++)
+    {
+      assert(orderedFrames_[i] != NULL);
+      delete orderedFrames_[i];
+    }
+
+    instances_.clear();
+    orderedFrames_.clear();
+  }
+
+
+  bool SeriesOrderedFrames::Sort3DVolume()
+  {
+    SlicesSorter sorter;
+    sorter.Reserve(instances_.size());
+
+    for (size_t i = 0; i < instances_.size(); i++)
+    {
+      CoordinateSystem3D geometry;
+      if (instances_[i]->Lookup3DGeometry(geometry))
+      {
+        sorter.AddSlice(geometry, new Orthanc::SingleValueObject<Instance*>(instances_[i]));
+      }
+      else
+      {
+        return false;   // Not a 3D volume
+      }
+    }
+
+    if (!sorter.Sort() ||
+        sorter.GetSlicesCount() != instances_.size() ||
+        !sorter.AreAllSlicesDistinct())
+    {
+      return false;
+    }
+    else
+    {
+      for (size_t i = 0; i < sorter.GetSlicesCount(); i++)
+      {
+        assert(sorter.HasSlicePayload(i));
+
+        const Orthanc::SingleValueObject<Instance*>& payload =
+          dynamic_cast<const Orthanc::SingleValueObject<Instance*>&>(sorter.GetSlicePayload(i));
+              
+        assert(payload.GetValue() != NULL);
+              
+        for (size_t j = 0; j < payload.GetValue()->GetInstanceParameters().GetImageInformation().GetNumberOfFrames(); j++)
+        {
+          orderedFrames_.push_back(new Frame(*payload.GetValue(), j));
+        }
+      }
+
+      isRegular_ = sorter.ComputeSpacingBetweenSlices(spacingBetweenSlices_);
+      return true;
+    }
+  }
+
+
+  void SeriesOrderedFrames::SortIndexInSeries()
+  {
+    std::vector<InstanceWithIndexInSeries> tmp;
+    tmp.reserve(instances_.size());      
+        
+    for (size_t i = 0; i < instances_.size(); i++)
+    {
+      assert(instances_[i] != NULL);
+      tmp.push_back(InstanceWithIndexInSeries(*instances_[i]));
+    }
+
+    std::sort(tmp.begin(), tmp.end());
+
+    for (size_t i = 0; i < tmp.size(); i++)
+    {
+      for (size_t j = 0; j < tmp[i].GetInstance().GetInstanceParameters().GetImageInformation().GetNumberOfFrames(); j++)
+      {
+        orderedFrames_.push_back(new Frame(tmp[i].GetInstance(), j));
+      }
+    }
+  }
+
+
+  const SeriesOrderedFrames::Frame& SeriesOrderedFrames::GetFrame(size_t seriesIndex) const
+  {
+    if (seriesIndex >= orderedFrames_.size())
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+    else
+    {
+      assert(orderedFrames_[seriesIndex] != NULL);
+      return *(orderedFrames_[seriesIndex]);
+    }
+  }
+  
+
+  SeriesOrderedFrames::SeriesOrderedFrames(LoadedDicomResources& instances) :
+    isVolume_(false),
+    isRegular_(false),
+    spacingBetweenSlices_(0)
+  {
+    instances_.reserve(instances.GetSize());
+
+    size_t numberOfFrames = 0;
+      
+    for (size_t i = 0; i < instances.GetSize(); i++)
+    {
+      try
+      {
+        std::auto_ptr<Instance> instance(new Instance(instances.GetResource(i)));
+        numberOfFrames += instance->GetInstanceParameters().GetImageInformation().GetNumberOfFrames();
+        instances_.push_back(instance.release());
+      }
+      catch (Orthanc::OrthancException&)
+      {
+        // The instance has not all the required DICOM tags, skip it
+      }
+    }
+
+    orderedFrames_.reserve(numberOfFrames);
+      
+    if (Sort3DVolume())
+    {
+      isVolume_ = true;
+
+      if (isRegular_)
+      {
+        LOG(INFO) << "Regular 3D volume detected";
+      }
+      else
+      {
+        LOG(INFO) << "Non-regular 3D volume detected";
+      }
+    }
+    else
+    {
+      LOG(INFO) << "Series is not a 3D volume, sorting by index";
+      SortIndexInSeries();
+    }
+
+    LOG(INFO) << "Number of frames: " << orderedFrames_.size();
+  }
+
+
+  unsigned int SeriesOrderedFrames::GetFrameIndex(size_t seriesIndex) const
+  {
+    return GetFrame(seriesIndex).GetFrameIndex();
+  }
+
+
+  const Orthanc::DicomMap& SeriesOrderedFrames::GetInstance(size_t seriesIndex) const
+  {
+    return GetFrame(seriesIndex).GetInstance();
+  }
+
+
+  const DicomInstanceParameters& SeriesOrderedFrames::GetInstanceParameters(size_t seriesIndex) const
+  {
+    return GetFrame(seriesIndex).GetInstanceParameters();
+  }
+
+
+  double SeriesOrderedFrames::GetSpacingBetweenSlices() const
+  {
+    if (IsRegular3DVolume())
+    {
+      return spacingBetweenSlices_;
+    }
+    else
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Loaders/SeriesOrderedFrames.h	Mon Dec 09 13:58:37 2019 +0100
@@ -0,0 +1,85 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "LoadedDicomResources.h"
+
+#include "../Toolbox/DicomInstanceParameters.h"
+
+namespace OrthancStone
+{
+  class SeriesOrderedFrames : public boost::noncopyable
+  {
+  private:
+    class Instance;
+    class Frame;
+    class InstanceWithIndexInSeries;
+
+    std::vector<Instance*>  instances_;
+    std::vector<Frame*>     orderedFrames_;
+    bool                    isVolume_;
+    bool                    isRegular_;
+    double                  spacingBetweenSlices_;
+
+    void Clear();
+
+    bool Sort3DVolume();
+
+    void SortIndexInSeries();
+
+    const Frame& GetFrame(size_t seriesIndex) const;
+
+  public:
+    SeriesOrderedFrames(LoadedDicomResources& instances);
+
+    ~SeriesOrderedFrames()
+    {
+      Clear();
+    }
+
+    size_t GetFramesCount() const
+    {
+      return orderedFrames_.size();
+    }
+
+    unsigned int GetFrameIndex(size_t seriesIndex) const;
+
+    const Orthanc::DicomMap& GetInstance(size_t seriesIndex) const;
+
+    const DicomInstanceParameters& GetInstanceParameters(size_t seriesIndex) const;
+
+    // Are all frames parallel and aligned?
+    bool Is3DVolume() const
+    {
+      return isVolume_;
+    }
+
+    // Are all frames parallel, aligned and evenly spaced?
+    bool IsRegular3DVolume() const
+    {
+      return isRegular_;
+    }
+
+    // Only available on regular 3D volumes
+    double GetSpacingBetweenSlices() const;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Loaders/SeriesThumbnailsLoader.cpp	Mon Dec 09 13:58:37 2019 +0100
@@ -0,0 +1,554 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "SeriesThumbnailsLoader.h"
+
+#include <Core/DicomFormat/DicomMap.h>
+#include <Core/DicomFormat/DicomInstanceHasher.h>
+#include <Core/Images/ImageProcessing.h>
+#include <Core/Images/JpegWriter.h>
+#include <Core/OrthancException.h>
+
+#include <boost/algorithm/string/predicate.hpp>
+
+static const unsigned int JPEG_QUALITY = 70;  // Only used for Orthanc source
+
+namespace OrthancStone
+{
+  static SeriesThumbnailType ExtractSopClassUid(const std::string& sopClassUid)
+  {
+    if (sopClassUid == "1.2.840.10008.5.1.4.1.1.104.1")  // Encapsulated PDF Storage
+    {
+      return SeriesThumbnailType_Pdf;
+    }
+    else if (sopClassUid == "1.2.840.10008.5.1.4.1.1.77.1.1.1" ||  // Video Endoscopic Image Storage
+             sopClassUid == "1.2.840.10008.5.1.4.1.1.77.1.2.1" ||  // Video Microscopic Image Storage
+             sopClassUid == "1.2.840.10008.5.1.4.1.1.77.1.4.1")    // Video Photographic Image Storage
+    {
+      return SeriesThumbnailType_Video;
+    }
+    else
+    {
+      return SeriesThumbnailType_Unknown;
+    }
+  }
+
+
+  SeriesThumbnailsLoader::Thumbnail::Thumbnail(const std::string& image,
+                                               const std::string& mime) :
+    type_(SeriesThumbnailType_Image),
+    image_(image),
+    mime_(mime)
+  {
+  }
+
+
+  SeriesThumbnailsLoader::Thumbnail::Thumbnail(SeriesThumbnailType type) :
+    type_(type)
+  {
+    if (type == SeriesThumbnailType_Image)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  void SeriesThumbnailsLoader::AcquireThumbnail(const DicomSource& source,
+                                                const std::string& studyInstanceUid,
+                                                const std::string& seriesInstanceUid,
+                                                SeriesThumbnailsLoader::Thumbnail* thumbnail)
+  {
+    assert(thumbnail != NULL);
+  
+    std::auto_ptr<Thumbnail> protection(thumbnail);
+  
+    Thumbnails::iterator found = thumbnails_.find(seriesInstanceUid);
+    if (found == thumbnails_.end())
+    {
+      thumbnails_[seriesInstanceUid] = protection.release();
+    }
+    else
+    {
+      assert(found->second != NULL);
+      delete found->second;
+      found->second = protection.release();
+    }
+
+    ThumbnailLoadedMessage message(*this, source, studyInstanceUid, seriesInstanceUid, *thumbnail);
+    BroadcastMessage(message);
+  }
+
+
+  class SeriesThumbnailsLoader::Handler : public Orthanc::IDynamicObject
+  {
+  private:
+    boost::shared_ptr<SeriesThumbnailsLoader>  loader_;
+    DicomSource                                source_;
+    std::string                                studyInstanceUid_;
+    std::string                                seriesInstanceUid_;
+
+  public:
+    Handler(boost::shared_ptr<SeriesThumbnailsLoader> loader,
+            const DicomSource& source,
+            const std::string& studyInstanceUid,
+            const std::string& seriesInstanceUid) :
+      loader_(loader),
+      source_(source),
+      studyInstanceUid_(studyInstanceUid),
+      seriesInstanceUid_(seriesInstanceUid)
+    {
+      if (!loader)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
+      }
+    }
+
+    boost::shared_ptr<SeriesThumbnailsLoader> GetLoader()
+    {
+      return loader_;
+    }
+
+    const DicomSource& GetSource() const
+    {
+      return source_;
+    }
+
+    const std::string& GetStudyInstanceUid() const
+    {
+      return studyInstanceUid_;
+    }
+
+    const std::string& GetSeriesInstanceUid() const
+    {
+      return seriesInstanceUid_;
+    }
+
+    virtual void HandleSuccess(const std::string& body,
+                               const std::map<std::string, std::string>& headers) = 0;
+
+    virtual void HandleError()
+    {
+      LOG(INFO) << "Cannot generate thumbnail for SeriesInstanceUID: " << seriesInstanceUid_;
+    }
+  };
+
+
+  class SeriesThumbnailsLoader::DicomWebSopClassHandler : public SeriesThumbnailsLoader::Handler
+  {
+  private:
+    static bool GetSopClassUid(std::string& sopClassUid,
+                               const Json::Value& json)
+    {
+      Orthanc::DicomMap dicom;
+      dicom.FromDicomWeb(json);
+
+      return dicom.LookupStringValue(sopClassUid, Orthanc::DICOM_TAG_SOP_CLASS_UID, false);
+    }
+  
+  public:
+    DicomWebSopClassHandler(boost::shared_ptr<SeriesThumbnailsLoader> loader,
+                            const DicomSource& source,
+                            const std::string& studyInstanceUid,
+                            const std::string& seriesInstanceUid) :
+      Handler(loader, source, studyInstanceUid, seriesInstanceUid)
+    {
+    }
+
+    virtual void HandleSuccess(const std::string& body,
+                               const std::map<std::string, std::string>& headers)
+    {
+      Json::Reader reader;
+      Json::Value value;
+
+      if (!reader.parse(body, value) ||
+          value.type() != Json::arrayValue)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol);
+      }
+      else
+      {
+        SeriesThumbnailType type = SeriesThumbnailType_Unknown;
+
+        std::string sopClassUid;
+        if (value.size() > 0 &&
+            GetSopClassUid(sopClassUid, value[0]))
+        {
+          bool ok = true;
+        
+          for (Json::Value::ArrayIndex i = 1; i < value.size() && ok; i++)
+          {
+            std::string s;
+            if (!GetSopClassUid(s, value[i]) ||
+                s != sopClassUid)
+            {
+              ok = false;
+            }
+          }
+
+          if (ok)
+          {
+            type = ExtractSopClassUid(sopClassUid);
+          }
+        }
+      
+        GetLoader()->AcquireThumbnail(GetSource(), GetStudyInstanceUid(),
+                                      GetSeriesInstanceUid(), new Thumbnail(type));
+      }
+    }
+  };
+
+
+  class SeriesThumbnailsLoader::DicomWebThumbnailHandler : public SeriesThumbnailsLoader::Handler
+  {
+  public:
+    DicomWebThumbnailHandler(boost::shared_ptr<SeriesThumbnailsLoader> loader,
+                             const DicomSource& source,
+                             const std::string& studyInstanceUid,
+                             const std::string& seriesInstanceUid) :
+      Handler(loader, source, studyInstanceUid, seriesInstanceUid)
+    {
+    }
+
+    virtual void HandleSuccess(const std::string& body,
+                               const std::map<std::string, std::string>& headers)
+    {
+      std::string mime = Orthanc::MIME_JPEG;
+      for (std::map<std::string, std::string>::const_iterator
+             it = headers.begin(); it != headers.end(); ++it)
+      {
+        if (boost::iequals(it->first, "content-type"))
+        {
+          mime = it->second;
+        }
+      }
+
+      GetLoader()->AcquireThumbnail(GetSource(), GetStudyInstanceUid(),
+                                    GetSeriesInstanceUid(), new Thumbnail(body, mime));
+    }
+
+    virtual void HandleError()
+    {
+      // The DICOMweb wasn't able to generate a thumbnail, try to
+      // retrieve the SopClassUID tag using QIDO-RS
+
+      std::map<std::string, std::string> arguments, headers;
+      arguments["0020000D"] = GetStudyInstanceUid();
+      arguments["0020000E"] = GetSeriesInstanceUid();
+      arguments["includefield"] = "00080016";
+
+      std::auto_ptr<IOracleCommand> command(
+        GetSource().CreateDicomWebCommand(
+          "/instances", arguments, headers, new DicomWebSopClassHandler(
+            GetLoader(), GetSource(), GetStudyInstanceUid(), GetSeriesInstanceUid())));
+      GetLoader()->Schedule(command.release());
+    }
+  };
+
+
+  class SeriesThumbnailsLoader::ThumbnailInformation : public Orthanc::IDynamicObject
+  {
+  private:
+    DicomSource  source_;
+    std::string  studyInstanceUid_;
+    std::string  seriesInstanceUid_;
+
+  public:
+    ThumbnailInformation(const DicomSource& source,
+                         const std::string& studyInstanceUid,
+                         const std::string& seriesInstanceUid) :
+      source_(source),
+      studyInstanceUid_(studyInstanceUid),
+      seriesInstanceUid_(seriesInstanceUid)
+    {
+    }
+
+    const DicomSource& GetDicomSource() const
+    {
+      return source_;
+    }
+
+    const std::string& GetStudyInstanceUid() const
+    {
+      return studyInstanceUid_;
+    }
+
+    const std::string& GetSeriesInstanceUid() const
+    {
+      return seriesInstanceUid_;
+    }
+  };
+
+
+  class SeriesThumbnailsLoader::OrthancSopClassHandler : public SeriesThumbnailsLoader::Handler
+  {
+  private:
+    std::string instanceId_;
+      
+  public:
+    OrthancSopClassHandler(boost::shared_ptr<SeriesThumbnailsLoader> loader,
+                           const DicomSource& source,
+                           const std::string& studyInstanceUid,
+                           const std::string& seriesInstanceUid,
+                           const std::string& instanceId) :
+      Handler(loader, source, studyInstanceUid, seriesInstanceUid),
+      instanceId_(instanceId)
+    {
+    }
+
+    virtual void HandleSuccess(const std::string& body,
+                               const std::map<std::string, std::string>& headers)
+    {
+      SeriesThumbnailType type = ExtractSopClassUid(body);
+
+      if (type == SeriesThumbnailType_Pdf ||
+          type == SeriesThumbnailType_Video)
+      {
+        GetLoader()->AcquireThumbnail(GetSource(), GetStudyInstanceUid(),
+                                      GetSeriesInstanceUid(), new Thumbnail(type));
+      }
+      else
+      {
+        std::auto_ptr<GetOrthancImageCommand> command(new GetOrthancImageCommand);
+        command->SetUri("/instances/" + instanceId_ + "/preview");
+        command->SetHttpHeader("Accept", Orthanc::MIME_JPEG);
+        command->AcquirePayload(new ThumbnailInformation(
+                                  GetSource(), GetStudyInstanceUid(), GetSeriesInstanceUid()));
+        GetLoader()->Schedule(command.release());
+      }
+    }
+  };
+
+
+  class SeriesThumbnailsLoader::SelectOrthancInstanceHandler : public SeriesThumbnailsLoader::Handler
+  {
+  public:
+    SelectOrthancInstanceHandler(boost::shared_ptr<SeriesThumbnailsLoader> loader,
+                                 const DicomSource& source,
+                                 const std::string& studyInstanceUid,
+                                 const std::string& seriesInstanceUid) :
+      Handler(loader, source, studyInstanceUid, seriesInstanceUid)
+    {
+    }
+
+    virtual void HandleSuccess(const std::string& body,
+                               const std::map<std::string, std::string>& headers)
+    {
+      static const char* const INSTANCES = "Instances";
+      
+      Json::Value json;
+      Json::Reader reader;
+      if (!reader.parse(body, json) ||
+          json.type() != Json::objectValue)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol);
+      }
+
+      if (json.isMember(INSTANCES) &&
+          json[INSTANCES].type() == Json::arrayValue &&
+          json[INSTANCES].size() > 0)
+      {
+        // Select one instance of the series to generate the thumbnail
+        Json::Value::ArrayIndex index = json[INSTANCES].size() / 2;
+        if (json[INSTANCES][index].type() == Json::stringValue)
+        {
+          std::map<std::string, std::string> arguments, headers;
+          arguments["quality"] = boost::lexical_cast<std::string>(JPEG_QUALITY);
+          headers["Accept"] = Orthanc::MIME_JPEG;
+
+          const std::string instance = json[INSTANCES][index].asString();
+
+          std::auto_ptr<OrthancRestApiCommand> command(new OrthancRestApiCommand);
+          command->SetUri("/instances/" + instance + "/metadata/SopClassUid");
+          command->AcquirePayload(
+            new OrthancSopClassHandler(
+              GetLoader(), GetSource(), GetStudyInstanceUid(), GetSeriesInstanceUid(), instance));
+          GetLoader()->Schedule(command.release());
+        }
+      }
+    }
+  };
+
+    
+  void SeriesThumbnailsLoader::Schedule(IOracleCommand* command)
+  {
+    std::auto_ptr<ILoadersContext::ILock> lock(context_.Lock());
+    lock->Schedule(GetSharedObserver(), priority_, command);
+  }    
+
+  
+  void SeriesThumbnailsLoader::Handle(const HttpCommand::SuccessMessage& message)
+  {
+    assert(message.GetOrigin().HasPayload());
+    dynamic_cast<Handler&>(message.GetOrigin().GetPayload()).HandleSuccess(message.GetAnswer(), message.GetAnswerHeaders());
+  }
+
+
+  void SeriesThumbnailsLoader::Handle(const OrthancRestApiCommand::SuccessMessage& message)
+  {
+    assert(message.GetOrigin().HasPayload());
+    dynamic_cast<Handler&>(message.GetOrigin().GetPayload()).HandleSuccess(message.GetAnswer(), message.GetAnswerHeaders());
+  }
+
+
+  void SeriesThumbnailsLoader::Handle(const GetOrthancImageCommand::SuccessMessage& message)
+  {
+    assert(message.GetOrigin().HasPayload());
+
+    std::auto_ptr<Orthanc::ImageAccessor> resized(Orthanc::ImageProcessing::FitSize(message.GetImage(), width_, height_));
+
+    std::string jpeg;
+    Orthanc::JpegWriter writer;
+    writer.SetQuality(JPEG_QUALITY);
+    writer.WriteToMemory(jpeg, *resized);
+
+    const ThumbnailInformation& info = dynamic_cast<ThumbnailInformation&>(message.GetOrigin().GetPayload());
+    AcquireThumbnail(info.GetDicomSource(), info.GetStudyInstanceUid(),
+                     info.GetSeriesInstanceUid(), new Thumbnail(jpeg, Orthanc::MIME_JPEG));      
+  }
+
+
+  void SeriesThumbnailsLoader::Handle(const OracleCommandExceptionMessage& message)
+  {
+    const OracleCommandBase& command = dynamic_cast<const OracleCommandBase&>(message.GetOrigin());
+    assert(command.HasPayload());
+    dynamic_cast<Handler&>(command.GetPayload()).HandleError();
+  }
+
+
+  SeriesThumbnailsLoader::SeriesThumbnailsLoader(ILoadersContext& context,
+                                                 int priority) :
+    context_(context),
+    priority_(priority),
+    width_(128),
+    height_(128)
+  {
+  }
+    
+  
+  boost::shared_ptr<IObserver> SeriesThumbnailsLoader::Factory::Create(ILoadersContext::ILock& stone)
+  {
+    boost::shared_ptr<SeriesThumbnailsLoader> result(new SeriesThumbnailsLoader(stone.GetContext(), priority_));
+    result->Register<GetOrthancImageCommand::SuccessMessage>(stone.GetOracleObservable(), &SeriesThumbnailsLoader::Handle);
+    result->Register<HttpCommand::SuccessMessage>(stone.GetOracleObservable(), &SeriesThumbnailsLoader::Handle);
+    result->Register<OracleCommandExceptionMessage>(stone.GetOracleObservable(), &SeriesThumbnailsLoader::Handle);
+    result->Register<OrthancRestApiCommand::SuccessMessage>(stone.GetOracleObservable(), &SeriesThumbnailsLoader::Handle);
+    return result;
+  }
+
+
+  void SeriesThumbnailsLoader::SetThumbnailSize(unsigned int width,
+                                                unsigned int height)
+  {
+    if (width <= 0 ||
+        height <= 0)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+    else
+    {
+      width_ = width;
+      height_ = height;
+    }
+  }
+
+    
+  void SeriesThumbnailsLoader::Clear()
+  {
+    for (Thumbnails::iterator it = thumbnails_.begin(); it != thumbnails_.end(); ++it)
+    {
+      assert(it->second != NULL);
+      delete it->second;
+    }
+
+    thumbnails_.clear();
+  }
+
+    
+  SeriesThumbnailType SeriesThumbnailsLoader::GetSeriesThumbnail(std::string& image,
+                                                                 std::string& mime,
+                                                                 const std::string& seriesInstanceUid) const
+  {
+    Thumbnails::const_iterator found = thumbnails_.find(seriesInstanceUid);
+
+    if (found == thumbnails_.end())
+    {
+      return SeriesThumbnailType_Unknown;
+    }
+    else
+    {
+      assert(found->second != NULL);
+      image.assign(found->second->GetImage());
+      mime.assign(found->second->GetMime());
+      return found->second->GetType();
+    }
+  }
+
+
+  void SeriesThumbnailsLoader::ScheduleLoadThumbnail(const DicomSource& source,
+                                                     const std::string& patientId,
+                                                     const std::string& studyInstanceUid,
+                                                     const std::string& seriesInstanceUid)
+  {
+    if (source.IsDicomWeb())
+    {
+      if (!source.HasDicomWebRendered())
+      {
+        // TODO - Could use DCMTK here
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol,
+                                        "DICOMweb server is not able to generate renderings of DICOM series");
+      }
+      
+      const std::string uri = ("/studies/" + studyInstanceUid +
+                               "/series/" + seriesInstanceUid + "/rendered");
+
+      std::map<std::string, std::string> arguments, headers;
+      arguments["viewport"] = (boost::lexical_cast<std::string>(width_) + "," +
+                               boost::lexical_cast<std::string>(height_));
+
+      // Needed to set this header explicitly, as long as emscripten
+      // does not include macro "EMSCRIPTEN_FETCH_RESPONSE_HEADERS"
+      // https://github.com/emscripten-core/emscripten/pull/8486
+      headers["Accept"] = Orthanc::MIME_JPEG;
+
+      std::auto_ptr<IOracleCommand> command(
+        source.CreateDicomWebCommand(
+          uri, arguments, headers, new DicomWebThumbnailHandler(
+            shared_from_this(), source, studyInstanceUid, seriesInstanceUid)));
+      Schedule(command.release());
+    }
+    else if (source.IsOrthanc())
+    {
+      // Dummy SOP Instance UID, as we are working at the "series" level
+      Orthanc::DicomInstanceHasher hasher(patientId, studyInstanceUid, seriesInstanceUid, "dummy");
+
+      std::auto_ptr<OrthancRestApiCommand> command(new OrthancRestApiCommand);
+      command->SetUri("/series/" + hasher.HashSeries());
+      command->AcquirePayload(new SelectOrthancInstanceHandler(
+                                shared_from_this(), source, studyInstanceUid, seriesInstanceUid));
+      Schedule(command.release());
+    }
+    else
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented,
+                                      "Can only load thumbnails from Orthanc or DICOMweb");
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Loaders/SeriesThumbnailsLoader.h	Mon Dec 09 13:58:37 2019 +0100
@@ -0,0 +1,209 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../Oracle/GetOrthancImageCommand.h"
+#include "../Oracle/HttpCommand.h"
+#include "../Oracle/OracleCommandExceptionMessage.h"
+#include "../Oracle/OrthancRestApiCommand.h"
+#include "DicomSource.h"
+#include "ILoaderFactory.h"
+#include "OracleScheduler.h"
+
+
+namespace OrthancStone
+{
+  enum SeriesThumbnailType
+  {
+    SeriesThumbnailType_Unknown = 1,
+    SeriesThumbnailType_Pdf = 2,
+    SeriesThumbnailType_Video = 3,
+    SeriesThumbnailType_Image = 4
+  };
+  
+
+  class SeriesThumbnailsLoader :
+    public IObservable,
+    public ObserverBase<SeriesThumbnailsLoader>
+  {
+  private:
+    class Thumbnail : public boost::noncopyable
+    {
+    private:
+      SeriesThumbnailType  type_;
+      std::string          image_;
+      std::string          mime_;
+
+    public:
+      Thumbnail(const std::string& image,
+                const std::string& mime);
+
+      Thumbnail(SeriesThumbnailType type);
+
+      SeriesThumbnailType GetType() const
+      {
+        return type_;
+      }
+
+      const std::string& GetImage() const
+      {
+        return image_;
+      }
+
+      const std::string& GetMime() const
+      {
+        return mime_;
+      }
+    };
+
+  public:
+    class ThumbnailLoadedMessage : public OriginMessage<SeriesThumbnailsLoader>
+    {
+      ORTHANC_STONE_MESSAGE(__FILE__, __LINE__);
+      
+    private:
+      const DicomSource&   source_;
+      const std::string&   studyInstanceUid_;
+      const std::string&   seriesInstanceUid_;
+      const Thumbnail&     thumbnail_;
+
+    public:
+      ThumbnailLoadedMessage(const SeriesThumbnailsLoader& origin,
+                             const DicomSource& source,
+                             const std::string& studyInstanceUid,
+                             const std::string& seriesInstanceUid,
+                             const Thumbnail& thumbnail) :
+        OriginMessage(origin),
+        source_(source),
+        studyInstanceUid_(studyInstanceUid),
+        seriesInstanceUid_(seriesInstanceUid),
+        thumbnail_(thumbnail)
+      {
+      }
+
+      const DicomSource& GetDicomSource() const
+      {
+        return source_;
+      }
+
+      SeriesThumbnailType GetType() const
+      {
+        return thumbnail_.GetType();
+      }
+
+      const std::string& GetStudyInstanceUid() const
+      {
+        return studyInstanceUid_;
+      }
+
+      const std::string& GetSeriesInstanceUid() const
+      {
+        return seriesInstanceUid_;
+      }
+
+      const std::string& GetEncodedImage() const
+      {
+        return thumbnail_.GetImage();
+      }
+
+      const std::string& GetMime() const
+      {
+        return thumbnail_.GetMime();
+      }
+    };
+
+  private:
+    class Handler;
+    class DicomWebSopClassHandler;
+    class DicomWebThumbnailHandler;
+    class ThumbnailInformation;
+    class OrthancSopClassHandler;
+    class SelectOrthancInstanceHandler;
+    
+    // Maps a "Series Instance UID" to a thumbnail
+    typedef std::map<std::string, Thumbnail*>  Thumbnails;
+
+    ILoadersContext&  context_;
+    Thumbnails      thumbnails_;
+    int             priority_;
+    unsigned int    width_;
+    unsigned int    height_;
+
+    void AcquireThumbnail(const DicomSource& source,
+                          const std::string& studyInstanceUid,
+                          const std::string& seriesInstanceUid,
+                          Thumbnail* thumbnail /* takes ownership */);
+
+    void Schedule(IOracleCommand* command);
+  
+    void Handle(const HttpCommand::SuccessMessage& message);
+
+    void Handle(const OrthancRestApiCommand::SuccessMessage& message);
+
+    void Handle(const GetOrthancImageCommand::SuccessMessage& message);
+
+    void Handle(const OracleCommandExceptionMessage& message);
+
+    SeriesThumbnailsLoader(ILoadersContext& context,
+                           int priority);
+    
+  public:
+    class Factory : public ILoaderFactory
+    {
+    private:
+      int priority_;
+
+    public:
+      Factory() :
+        priority_(0)
+      {
+      }
+
+      void SetPriority(int priority)
+      {
+        priority_ = priority;
+      }
+
+      virtual boost::shared_ptr<IObserver> Create(ILoadersContext::ILock& context);
+    };
+
+
+    virtual ~SeriesThumbnailsLoader()
+    {
+      Clear();
+    }
+
+    void SetThumbnailSize(unsigned int width,
+                          unsigned int height);
+    
+    void Clear();
+    
+    SeriesThumbnailType GetSeriesThumbnail(std::string& image,
+                                           std::string& mime,
+                                           const std::string& seriesInstanceUid) const;
+
+    void ScheduleLoadThumbnail(const DicomSource& source,
+                               const std::string& patientId,
+                               const std::string& studyInstanceUid,
+                               const std::string& seriesInstanceUid);
+  };
+}
--- a/Resources/CMake/OrthancStoneConfiguration.cmake	Sun Dec 08 12:06:44 2019 +0100
+++ b/Resources/CMake/OrthancStoneConfiguration.cmake	Mon Dec 09 13:58:37 2019 +0100
@@ -252,13 +252,14 @@
 
 if (NOT ORTHANC_SANDBOXED)
   set(PLATFORM_SOURCES
-    ${ORTHANC_STONE_ROOT}/Platforms/Generic/WebServiceCommandBase.cpp
-    ${ORTHANC_STONE_ROOT}/Platforms/Generic/WebServiceGetCommand.cpp
-    ${ORTHANC_STONE_ROOT}/Platforms/Generic/WebServicePostCommand.cpp
-    ${ORTHANC_STONE_ROOT}/Platforms/Generic/WebServiceDeleteCommand.cpp
+    ${ORTHANC_STONE_ROOT}/Framework/Loaders/GenericLoadersContext.cpp
     ${ORTHANC_STONE_ROOT}/Platforms/Generic/DelayedCallCommand.cpp
     ${ORTHANC_STONE_ROOT}/Platforms/Generic/Oracle.cpp
     ${ORTHANC_STONE_ROOT}/Platforms/Generic/OracleDelayedCallExecutor.h
+    ${ORTHANC_STONE_ROOT}/Platforms/Generic/WebServiceCommandBase.cpp
+    ${ORTHANC_STONE_ROOT}/Platforms/Generic/WebServiceDeleteCommand.cpp
+    ${ORTHANC_STONE_ROOT}/Platforms/Generic/WebServiceGetCommand.cpp
+    ${ORTHANC_STONE_ROOT}/Platforms/Generic/WebServicePostCommand.cpp
     )
 
   if (ENABLE_SDL)
@@ -469,8 +470,17 @@
   ${ORTHANC_STONE_ROOT}/Framework/Loaders/BasicFetchingItemsSorter.h
   ${ORTHANC_STONE_ROOT}/Framework/Loaders/BasicFetchingStrategy.cpp
   ${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/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/OracleScheduler.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Loaders/SeriesFramesLoader.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Loaders/SeriesMetadataLoader.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Loaders/SeriesOrderedFrames.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Loaders/SeriesThumbnailsLoader.cpp
   
   ${ORTHANC_STONE_ROOT}/Framework/Messages/ICallable.h
   ${ORTHANC_STONE_ROOT}/Framework/Messages/IMessage.h