changeset 130:1982d6c1d2ff wasm

IVolumeLoader
author Sebastien Jodogne <s.jodogne@gmail.com>
date Thu, 16 Nov 2017 13:46:03 +0100
parents a823122db53d
children 3e6163a53b16
files Applications/BasicApplicationContext.cpp Applications/BasicApplicationContext.h Applications/Samples/SingleVolumeApplication.h Framework/Layers/DicomStructureSetRendererFactory.cpp Framework/Layers/DicomStructureSetRendererFactory.h Framework/Volumes/IVolumeLoader.h Framework/Volumes/StructureSetLoader.cpp Framework/Volumes/StructureSetLoader.h Framework/Volumes/VolumeLoaderBase.cpp Framework/Volumes/VolumeLoaderBase.h Resources/CMake/OrthancStoneConfiguration.cmake
diffstat 11 files changed, 441 insertions(+), 188 deletions(-) [+]
line wrap: on
line diff
--- a/Applications/BasicApplicationContext.cpp	Thu Nov 16 12:50:22 2017 +0100
+++ b/Applications/BasicApplicationContext.cpp	Thu Nov 16 13:46:03 2017 +0100
@@ -56,13 +56,13 @@
       delete *it;
     }
 
-    for (Volumes::iterator it = volumes_.begin(); it != volumes_.end(); ++it)
+    for (SlicedVolumes::iterator it = slicedVolumes_.begin(); it != slicedVolumes_.end(); ++it)
     {
       assert(*it != NULL);
       delete *it;
     }
 
-    for (StructureSets::iterator it = structureSets_.begin(); it != structureSets_.end(); ++it)
+    for (VolumeLoaders::iterator it = volumeLoaders_.begin(); it != volumeLoaders_.end(); ++it)
     {
       assert(*it != NULL);
       delete *it;
@@ -77,28 +77,31 @@
   }
 
 
-  ISlicedVolume& BasicApplicationContext::AddVolume(ISlicedVolume* volume)
+  ISlicedVolume& BasicApplicationContext::AddSlicedVolume(ISlicedVolume* volume)
   {
     if (volume == NULL)
     {
       throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
     }
-
-    volumes_.push_back(volume);
-    return *volume;
+    else
+    {
+      slicedVolumes_.push_back(volume);
+      return *volume;
+    }
   }
 
-  DicomStructureSet& BasicApplicationContext::AddStructureSet(const std::string& instance)
+
+  IVolumeLoader& BasicApplicationContext::AddVolumeLoader(IVolumeLoader* loader)
   {
-    /*std::auto_ptr<DicomStructureSet> structureSet
-      (new DicomStructureSet(GetWebService().GetConnection(), instance));
-
-    DicomStructureSet& result = *structureSet;
-    structureSets_.push_back(structureSet.release());
-
-    return result;*/
-
-    throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+    if (loader == NULL)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
+    }
+    else
+    {
+      volumeLoaders_.push_back(loader);
+      return *loader;
+    }
   }
 
 
--- a/Applications/BasicApplicationContext.h	Thu Nov 16 12:50:22 2017 +0100
+++ b/Applications/BasicApplicationContext.h	Thu Nov 16 13:46:03 2017 +0100
@@ -21,10 +21,10 @@
 
 #pragma once
 
+#include "../../Framework/Viewport/WidgetViewport.h"
 #include "../../Framework/Volumes/ISlicedVolume.h"
-#include "../../Framework/Viewport/WidgetViewport.h"
+#include "../../Framework/Volumes/IVolumeLoader.h"
 #include "../../Framework/Widgets/IWorldSceneInteractor.h"
-#include "../../Framework/Toolbox/DicomStructureSet.h"
 #include "../../Platforms/Generic/OracleWebService.h"
 
 #include <list>
@@ -35,9 +35,9 @@
   class BasicApplicationContext : public boost::noncopyable
   {
   private:
-    typedef std::list<ISlicedVolume*>          Volumes;
+    typedef std::list<ISlicedVolume*>          SlicedVolumes;
+    typedef std::list<IVolumeLoader*>          VolumeLoaders;
     typedef std::list<IWorldSceneInteractor*>  Interactors;
-    typedef std::list<DicomStructureSet*>      StructureSets;
 
     static void UpdateThread(BasicApplicationContext* that);
 
@@ -45,9 +45,9 @@
     OracleWebService    webService_;
     boost::mutex        viewportMutex_;
     WidgetViewport      viewport_;
-    Volumes             volumes_;
+    SlicedVolumes       slicedVolumes_;
+    VolumeLoaders       volumeLoaders_;
     Interactors         interactors_;
-    StructureSets       structureSets_;
     boost::thread       updateThread_;
     bool                stopped_;
     unsigned int        updateDelay_;
@@ -84,9 +84,9 @@
       return webService_;
     }
     
-    ISlicedVolume& AddVolume(ISlicedVolume* volume);
+    ISlicedVolume& AddSlicedVolume(ISlicedVolume* volume);
 
-    DicomStructureSet& AddStructureSet(const std::string& instance);
+    IVolumeLoader& AddVolumeLoader(IVolumeLoader* loader);
 
     IWorldSceneInteractor& AddInteractor(IWorldSceneInteractor* interactor);
 
--- a/Applications/Samples/SingleVolumeApplication.h	Thu Nov 16 12:50:22 2017 +0100
+++ b/Applications/Samples/SingleVolumeApplication.h	Thu Nov 16 13:46:03 2017 +0100
@@ -188,7 +188,7 @@
         widget->AddLayer(new VolumeImageSource(*volume));
 
         context.AddInteractor(new Interactor(*volume, *widget, projection, 0));
-        context.AddVolume(volume.release());
+        context.AddSlicedVolume(volume.release());
 
         {
           RenderStyle s;
@@ -211,19 +211,20 @@
         //pet->ScheduleLoadInstance("830a69ff-8e4b5ee3-b7f966c8-bccc20fb-d322dceb");  // IBA 3
         //pet->ScheduleLoadInstance("269f26f4-0c83eeeb-2e67abbd-5467a40f-f1bec90c");  // 0522c0001 TCIA
 
-        std::auto_ptr<DicomStructureSetRendererFactory> rtStruct(new DicomStructureSetRendererFactory(context.GetWebService()));
+        std::auto_ptr<StructureSetLoader> rtStruct(new StructureSetLoader(context.GetWebService()));
         rtStruct->ScheduleLoadInstance("54460695-ba3885ee-ddf61ac0-f028e31d-a6e474d9");  // IBA
         //rtStruct->ScheduleLoadInstance("17cd032b-ad92a438-ca05f06a-f9e96668-7e3e9e20");  // 0522c0001 TCIA
         
         widget->AddLayer(new VolumeImageSource(*ct));
         widget->AddLayer(new VolumeImageSource(*pet));
-        widget->AddLayer(rtStruct.release());
+        widget->AddLayer(new DicomStructureSetRendererFactory(*rtStruct));
         
         context.AddInteractor(new Interactor(*pet, *widget, projection, 1));
         //context.AddInteractor(new VolumeImageInteractor(*ct, *widget, projection));
 
-        context.AddVolume(ct.release());
-        context.AddVolume(pet.release());
+        context.AddSlicedVolume(ct.release());
+        context.AddSlicedVolume(pet.release());
+        context.AddVolumeLoader(rtStruct.release());
 
         {
           RenderStyle s;
--- a/Framework/Layers/DicomStructureSetRendererFactory.cpp	Thu Nov 16 12:50:22 2017 +0100
+++ b/Framework/Layers/DicomStructureSetRendererFactory.cpp	Thu Nov 16 13:46:03 2017 +0100
@@ -21,10 +21,6 @@
 
 #include "DicomStructureSetRendererFactory.h"
 
-#include "../Toolbox/MessagingToolbox.h"
-
-#include <Core/OrthancException.h>
-
 namespace OrthancStone
 {
   class DicomStructureSetRendererFactory::Renderer : public ILayerRenderer
@@ -103,147 +99,11 @@
   };
 
 
-  class DicomStructureSetRendererFactory::Operation : public Orthanc::IDynamicObject
-  {
-  public:
-    enum Type
-    {
-      Type_LoadStructureSet,
-      Type_LookupSopInstanceUid,
-      Type_LoadReferencedSlice
-    };
-    
-  private:
-    Type         type_;
-    std::string  value_;
-
-  public:
-    Operation(Type type,
-              const std::string& value) :
-      type_(type),
-      value_(value)
-    {
-    }
-
-    Type GetType() const
-    {
-      return type_;
-    }
-
-    const std::string& GetIdentifier() const
-    {
-      return value_;
-    }
-  };
-
-
-  void DicomStructureSetRendererFactory::NotifyError(const std::string& uri,
-                                                     Orthanc::IDynamicObject* payload)
-  {
-    // TODO
-  }
-
-  
-  void DicomStructureSetRendererFactory::NotifySuccess(const std::string& uri,
-                                                       const void* answer,
-                                                       size_t answerSize,
-                                                       Orthanc::IDynamicObject* payload)
-  {
-    std::auto_ptr<Operation> op(dynamic_cast<Operation*>(payload));
-
-    switch (op->GetType())
-    {
-      case Operation::Type_LoadStructureSet:
-      {
-        OrthancPlugins::FullOrthancDataset dataset(answer, answerSize);
-        structureSet_.reset(new DicomStructureSet(dataset));
-
-        std::set<std::string> instances;
-        structureSet_->GetReferencedInstances(instances);
-
-        for (std::set<std::string>::const_iterator it = instances.begin();
-             it != instances.end(); ++it)
-        {
-          orthanc_.SchedulePostRequest(*this, "/tools/lookup", *it,
-                                       new Operation(Operation::Type_LookupSopInstanceUid, *it));
-        }
-        
-        break;
-      }
-        
-      case Operation::Type_LookupSopInstanceUid:
-      {
-        Json::Value lookup;
-        
-        if (MessagingToolbox::ParseJson(lookup, answer, answerSize))
-        {
-          if (lookup.type() != Json::arrayValue ||
-              lookup.size() != 1 ||
-              !lookup[0].isMember("Type") ||
-              !lookup[0].isMember("Path") ||
-              lookup[0]["Type"].type() != Json::stringValue ||
-              lookup[0]["ID"].type() != Json::stringValue ||
-              lookup[0]["Type"].asString() != "Instance")
-          {
-            throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol);          
-          }
-
-          const std::string& instance = lookup[0]["ID"].asString();
-          orthanc_.ScheduleGetRequest(*this, "/instances/" + instance + "/tags",
-                                      new Operation(Operation::Type_LoadReferencedSlice, instance));
-        }
-        else
-        {
-          // TODO
-        }
-        
-        break;
-      }
-
-      case Operation::Type_LoadReferencedSlice:
-      {
-        OrthancPlugins::FullOrthancDataset dataset(answer, answerSize);
-
-        Orthanc::DicomMap slice;
-        MessagingToolbox::ConvertDataset(slice, dataset);
-        structureSet_->AddReferencedSlice(slice);
-
-        LayerSourceBase::NotifyContentChange();
-
-        break;
-      }
-      
-      default:
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
-    }
-  } 
-
-  
-  DicomStructureSetRendererFactory::DicomStructureSetRendererFactory(IWebService& orthanc) :
-    orthanc_(orthanc)
-  {
-  }
-  
-
-  void DicomStructureSetRendererFactory::ScheduleLoadInstance(const std::string& instance)
-  {
-    if (structureSet_.get() != NULL)
-    {
-      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
-    }
-    else
-    {
-      const std::string uri = "/instances/" + instance + "/tags?ignore-length=3006-0050";
-      orthanc_.ScheduleGetRequest(*this, uri, new Operation(Operation::Type_LoadStructureSet, instance));
-    }
-  }
-  
-
   void DicomStructureSetRendererFactory::ScheduleLayerCreation(const CoordinateSystem3D& viewportSlice)
   {
-    if (structureSet_.get() != NULL)
+    if (loader_.HasStructureSet())
     {
-      NotifyLayerReady(new Renderer(*structureSet_, viewportSlice), viewportSlice, false);
+      NotifyLayerReady(new Renderer(loader_.GetStructureSet(), viewportSlice), viewportSlice, false);
     }
   }
 }
--- a/Framework/Layers/DicomStructureSetRendererFactory.h	Thu Nov 16 12:50:22 2017 +0100
+++ b/Framework/Layers/DicomStructureSetRendererFactory.h	Thu Nov 16 13:46:03 2017 +0100
@@ -21,35 +21,41 @@
 
 #pragma once
 
-#include "../Toolbox/DicomStructureSet.h"
-#include "../Toolbox/IWebService.h"
 #include "LayerSourceBase.h"
+#include "../Volumes/StructureSetLoader.h"
 
 namespace OrthancStone
 {
   class DicomStructureSetRendererFactory :
     public LayerSourceBase,
-    private IWebService::ICallback
+    private IVolumeLoader::IObserver
   {
   private:
     class Renderer;
-    class Operation;
+
+    virtual void NotifyGeometryReady(const IVolumeLoader& loader)
+    {
+      LayerSourceBase::NotifyGeometryReady();
+    }
+      
+    virtual void NotifyGeometryError(const IVolumeLoader& loader)
+    {
+      LayerSourceBase::NotifyGeometryError();
+    }
+      
+    virtual void NotifyContentChange(const IVolumeLoader& loader)
+    {
+      LayerSourceBase::NotifyContentChange();
+    }
     
-    virtual void NotifyError(const std::string& uri,
-                             Orthanc::IDynamicObject* payload);
-
-    virtual void NotifySuccess(const std::string& uri,
-                               const void* answer,
-                               size_t answerSize,
-                               Orthanc::IDynamicObject* payload);
-
-    IWebService&                      orthanc_;
-    std::auto_ptr<DicomStructureSet>  structureSet_;
+    StructureSetLoader& loader_;
 
   public:
-    DicomStructureSetRendererFactory(IWebService& orthanc);
-
-    void ScheduleLoadInstance(const std::string& instance);
+    DicomStructureSetRendererFactory(StructureSetLoader& loader) :
+      loader_(loader)
+    {
+      loader_.Register(*this);
+    }
 
     virtual bool GetExtent(std::vector<Vector>& points,
                            const CoordinateSystem3D& viewportSlice)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Volumes/IVolumeLoader.h	Thu Nov 16 13:46:03 2017 +0100
@@ -0,0 +1,53 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017 Osimis, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include <boost/noncopyable.hpp>
+
+namespace OrthancStone
+{
+  class IVolumeLoader : public boost::noncopyable
+  {
+  public:
+    class IObserver : public boost::noncopyable
+    {
+    public:
+      virtual ~IObserver()
+      {
+      }
+      
+      virtual void NotifyGeometryReady(const IVolumeLoader& loader) = 0;
+      
+      virtual void NotifyGeometryError(const IVolumeLoader& loader) = 0;
+      
+      // Triggered if the content of several slices in the loader has
+      // changed
+      virtual void NotifyContentChange(const IVolumeLoader& loader) = 0;
+    };
+    
+    virtual ~IVolumeLoader()
+    {
+    }
+
+    virtual void Register(IObserver& observer) = 0;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Volumes/StructureSetLoader.cpp	Thu Nov 16 13:46:03 2017 +0100
@@ -0,0 +1,179 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017 Osimis, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "StructureSetLoader.h"
+
+#include "../Toolbox/MessagingToolbox.h"
+
+#include <Core/OrthancException.h>
+
+namespace OrthancStone
+{
+  class StructureSetLoader::Operation : public Orthanc::IDynamicObject
+  {
+  public:
+    enum Type
+    {
+      Type_LoadStructureSet,
+      Type_LookupSopInstanceUid,
+      Type_LoadReferencedSlice
+    };
+    
+  private:
+    Type         type_;
+    std::string  value_;
+
+  public:
+    Operation(Type type,
+              const std::string& value) :
+      type_(type),
+      value_(value)
+    {
+    }
+
+    Type GetType() const
+    {
+      return type_;
+    }
+
+    const std::string& GetIdentifier() const
+    {
+      return value_;
+    }
+  };
+
+
+  void StructureSetLoader::NotifyError(const std::string& uri,
+                                       Orthanc::IDynamicObject* payload)
+  {
+    // TODO
+  }
+
+  
+  void StructureSetLoader::NotifySuccess(const std::string& uri,
+                                         const void* answer,
+                                         size_t answerSize,
+                                         Orthanc::IDynamicObject* payload)
+  {
+    std::auto_ptr<Operation> op(dynamic_cast<Operation*>(payload));
+
+    switch (op->GetType())
+    {
+      case Operation::Type_LoadStructureSet:
+      {
+        OrthancPlugins::FullOrthancDataset dataset(answer, answerSize);
+        structureSet_.reset(new DicomStructureSet(dataset));
+
+        std::set<std::string> instances;
+        structureSet_->GetReferencedInstances(instances);
+
+        for (std::set<std::string>::const_iterator it = instances.begin();
+             it != instances.end(); ++it)
+        {
+          orthanc_.SchedulePostRequest(*this, "/tools/lookup", *it,
+                                       new Operation(Operation::Type_LookupSopInstanceUid, *it));
+        }
+        
+        VolumeLoaderBase::NotifyGeometryReady();
+
+        break;
+      }
+        
+      case Operation::Type_LookupSopInstanceUid:
+      {
+        Json::Value lookup;
+        
+        if (MessagingToolbox::ParseJson(lookup, answer, answerSize))
+        {
+          if (lookup.type() != Json::arrayValue ||
+              lookup.size() != 1 ||
+              !lookup[0].isMember("Type") ||
+              !lookup[0].isMember("Path") ||
+              lookup[0]["Type"].type() != Json::stringValue ||
+              lookup[0]["ID"].type() != Json::stringValue ||
+              lookup[0]["Type"].asString() != "Instance")
+          {
+            throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol);          
+          }
+
+          const std::string& instance = lookup[0]["ID"].asString();
+          orthanc_.ScheduleGetRequest(*this, "/instances/" + instance + "/tags",
+                                      new Operation(Operation::Type_LoadReferencedSlice, instance));
+        }
+        else
+        {
+          // TODO
+        }
+        
+        break;
+      }
+
+      case Operation::Type_LoadReferencedSlice:
+      {
+        OrthancPlugins::FullOrthancDataset dataset(answer, answerSize);
+
+        Orthanc::DicomMap slice;
+        MessagingToolbox::ConvertDataset(slice, dataset);
+        structureSet_->AddReferencedSlice(slice);
+
+        VolumeLoaderBase::NotifyContentChange();
+
+        break;
+      }
+      
+      default:
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+    }
+  } 
+
+  
+  StructureSetLoader::StructureSetLoader(IWebService& orthanc) :
+    orthanc_(orthanc)
+  {
+  }
+  
+
+  void StructureSetLoader::ScheduleLoadInstance(const std::string& instance)
+  {
+    if (structureSet_.get() != NULL)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      const std::string uri = "/instances/" + instance + "/tags?ignore-length=3006-0050";
+      orthanc_.ScheduleGetRequest(*this, uri, new Operation(Operation::Type_LoadStructureSet, instance));
+    }
+  }
+
+
+  DicomStructureSet& StructureSetLoader::GetStructureSet()
+  {
+    if (structureSet_.get() == NULL)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      return *structureSet_;
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Volumes/StructureSetLoader.h	Thu Nov 16 13:46:03 2017 +0100
@@ -0,0 +1,60 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017 Osimis, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../Toolbox/DicomStructureSet.h"
+#include "../Toolbox/IWebService.h"
+#include "VolumeLoaderBase.h"
+
+namespace OrthancStone
+{
+  class StructureSetLoader :
+    public VolumeLoaderBase,
+    private IWebService::ICallback
+  {
+  private:
+    class Operation;
+    
+    virtual void NotifyError(const std::string& uri,
+                             Orthanc::IDynamicObject* payload);
+
+    virtual void NotifySuccess(const std::string& uri,
+                               const void* answer,
+                               size_t answerSize,
+                               Orthanc::IDynamicObject* payload);
+
+    IWebService&                      orthanc_;
+    std::auto_ptr<DicomStructureSet>  structureSet_;
+
+  public:
+    StructureSetLoader(IWebService& orthanc);
+
+    void ScheduleLoadInstance(const std::string& instance);
+
+    bool HasStructureSet() const
+    {
+      return structureSet_.get() != NULL;
+    }
+
+    DicomStructureSet& GetStructureSet();
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Volumes/VolumeLoaderBase.cpp	Thu Nov 16 13:46:03 2017 +0100
@@ -0,0 +1,40 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017 Osimis, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "VolumeLoaderBase.h"
+
+namespace OrthancStone
+{
+  void VolumeLoaderBase::NotifyGeometryReady()
+  {
+    observers_.Apply(*this, &IObserver::NotifyGeometryReady);
+  }
+      
+  void VolumeLoaderBase::NotifyGeometryError()
+  {
+    observers_.Apply(*this, &IObserver::NotifyGeometryError);
+  }
+    
+  void VolumeLoaderBase::NotifyContentChange()
+  {
+    observers_.Apply(*this, &IObserver::NotifyContentChange);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Volumes/VolumeLoaderBase.h	Thu Nov 16 13:46:03 2017 +0100
@@ -0,0 +1,49 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017 Osimis, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "IVolumeLoader.h"
+#include "../Toolbox/ObserversRegistry.h"
+
+namespace OrthancStone
+{
+  class VolumeLoaderBase : public IVolumeLoader
+  {
+  private:
+    typedef ObserversRegistry<IVolumeLoader, IObserver>  Observers;
+
+    Observers  observers_;
+
+  protected:
+    virtual void NotifyGeometryReady();
+      
+    virtual void NotifyGeometryError();
+    
+    virtual void NotifyContentChange();
+
+  public:
+    virtual void Register(IObserver& observer)
+    {
+      observers_.Register(observer);
+    }
+  };
+}
--- a/Resources/CMake/OrthancStoneConfiguration.cmake	Thu Nov 16 12:50:22 2017 +0100
+++ b/Resources/CMake/OrthancStoneConfiguration.cmake	Thu Nov 16 13:46:03 2017 +0100
@@ -194,6 +194,8 @@
   ${ORTHANC_STONE_DIR}/Framework/Viewport/WidgetViewport.cpp
   ${ORTHANC_STONE_DIR}/Framework/Volumes/ImageBuffer3D.cpp
   ${ORTHANC_STONE_DIR}/Framework/Volumes/SlicedVolumeBase.cpp
+  ${ORTHANC_STONE_DIR}/Framework/Volumes/VolumeLoaderBase.cpp
+  ${ORTHANC_STONE_DIR}/Framework/Volumes/StructureSetLoader.cpp
   ${ORTHANC_STONE_DIR}/Framework/Widgets/CairoWidget.cpp
   ${ORTHANC_STONE_DIR}/Framework/Widgets/EmptyWidget.cpp
   ${ORTHANC_STONE_DIR}/Framework/Widgets/LayerWidget.cpp