changeset 699:5c551f078c18 refactor-viewport-controller

Merge from default
author Benjamin Golinvaux <bgo@osimis.io>
date Fri, 17 May 2019 09:20:46 +0200
parents 8b6adfb62a2f (current diff) 9a474e90e832 (diff)
children 059e1fd05fd6
files Framework/Scene2D/Internals/OpenGLLinesProgram.cpp Framework/Scene2D/Internals/OpenGLTextProgram.cpp Framework/Scene2D/Scene2D.h Framework/Scene2D/ScenePoint2D.h Resources/CMake/OrthancStoneConfiguration.cmake Samples/Sdl/TrackerSampleApp.cpp
diffstat 35 files changed, 1839 insertions(+), 759 deletions(-) [+]
line wrap: on
line diff
--- a/Applications/Sdl/SdlWindow.cpp	Wed May 15 16:56:17 2019 +0200
+++ b/Applications/Sdl/SdlWindow.cpp	Fri May 17 09:20:46 2019 +0200
@@ -27,7 +27,7 @@
 #include <Core/OrthancException.h>
 
 #ifdef WIN32 
-#include <windows.h> // for SetProcessDpiAware
+#include <windows.h> // for SetProcessDpiAware
 #endif 
 // WIN32
 
@@ -59,7 +59,7 @@
     }
 
 // TODO: probably required on MacOS X, too
-#ifdef WIN32
+#if defined(WIN32) && (_WIN32_WINNT >= 0x0600)
     if (!allowDpiScaling)
     {
       // if we do NOT allow DPI scaling, it means an SDL pixel will be a real
@@ -70,10 +70,10 @@
       // THE FOLLOWING HAS BEEN COMMENTED OUT BECAUSE IT WILL CRASH UNDER 
       // OLD WINDOWS VERSIONS
       // ADD THIS AT THE TOP TO ENABLE IT:
-      // 
-      //#pragma comment(lib, "Shcore.lib") THIS IS ONLY REQUIRED FOR SetProcessDpiAwareness
-      //#include <windows.h>
-      //#include <ShellScalingAPI.h> THIS IS ONLY REQUIRED FOR SetProcessDpiAwareness
+      // 
+      //#pragma comment(lib, "Shcore.lib") THIS IS ONLY REQUIRED FOR SetProcessDpiAwareness
+      //#include <windows.h>
+      //#include <ShellScalingAPI.h> THIS IS ONLY REQUIRED FOR SetProcessDpiAwareness
       //#include <comdef.h> THIS IS ONLY REQUIRED FOR SetProcessDpiAwareness
       // SetProcessDpiAwareness(PROCESS_PER_MONITOR_DPI_AWARE);
       
--- a/Applications/Wasm/StartupParametersBuilder.cpp	Wed May 15 16:56:17 2019 +0200
+++ b/Applications/Wasm/StartupParametersBuilder.cpp	Fri May 17 09:20:46 2019 +0200
@@ -1,9 +1,11 @@
 #include "StartupParametersBuilder.h"
 #include <iostream>
+#include <cstdio>
+#include "emscripten/html5.h"
 
 namespace OrthancStone
 {
-      void StartupParametersBuilder::Clear()
+  void StartupParametersBuilder::Clear()
   {
     startupParameters_.clear();
   }
@@ -27,21 +29,28 @@
     std::vector<const char*> argv(startupParameters_.size() + 1);
     
     int argCounter = 0;
-    argvStrings[argCounter] = "Toto.exe";
+    argvStrings[argCounter] = "dummy.exe";
     argv[argCounter] = argvStrings[argCounter].c_str();
     
     argCounter++;
-
+    
     std::string cmdLine = "";
     for ( StartupParameters::const_iterator it = startupParameters_.begin(); 
           it != startupParameters_.end(); 
           it++)
     {
-        std::stringstream argSs;
+      std::stringstream argSs;
 
-        argSs << "--" << std::get<0>(*it);
-        if(std::get<1>(*it).length() > 0)
-          argSs << "=" << std::get<1>(*it);
+      argSs << "--" << std::get<0>(*it);
+      if(std::get<1>(*it).length() > 0)
+        argSs << "=" << std::get<1>(*it);
+      
+      argvStrings[argCounter] = argSs.str();
+      cmdLine = cmdLine + " " + argvStrings[argCounter];
+      std::cout << cmdLine << std::endl;
+      argv[argCounter] = argvStrings[argCounter].c_str();
+      argCounter++;
+    }
 
 
     std::cout << "simulated cmdLine = \"" << cmdLine.c_str() << "\"\n";
@@ -56,6 +65,7 @@
     catch (boost::program_options::error& e)
     {
       std::cerr << "Error while parsing the command-line arguments: " <<
-        e.what() << std::endl;    }
+        e.what() << std::endl;    
+    }
   }
 }
--- a/Framework/Fonts/FontRenderer.cpp	Wed May 15 16:56:17 2019 +0200
+++ b/Framework/Fonts/FontRenderer.cpp	Fri May 17 09:20:46 2019 +0200
@@ -101,7 +101,9 @@
       
       const FT_Byte* data = reinterpret_cast<const FT_Byte*>(fontContent_.c_str());
 
-      CheckError(FT_New_Memory_Face(library_, data, fontContent_.size(), 0, &face_));
+      CheckError(FT_New_Memory_Face(
+        library_, data, static_cast<FT_Long>(fontContent_.size()), 0, &face_));
+
       CheckError(FT_Set_Char_Size(face_,         // handle to face object  
                                   0,             // char_width in 1/64th of points  
                                   fontSize * 64, // char_height in 1/64th of points 
--- a/Framework/Fonts/GlyphTextureAlphabet.cpp	Wed May 15 16:56:17 2019 +0200
+++ b/Framework/Fonts/GlyphTextureAlphabet.cpp	Fri May 17 09:20:46 2019 +0200
@@ -89,7 +89,7 @@
       column_(0),
       row_(0)
     {
-      int c = boost::math::iround<int>(sqrt(static_cast<float>(countGlyphs)));
+      int c = boost::math::iround<float>(sqrt(static_cast<float>(countGlyphs)));
 
       if (c <= 0)
       {
@@ -239,9 +239,9 @@
     sourceAlphabet.GetAlphabet().Apply(size);
 
     TextureGenerator generator(alphabet_,
-                               sourceAlphabet.GetAlphabet().GetSize(),
-                               size.GetMaxWidth(),
-                               size.GetMaxHeight());
+      static_cast<unsigned int>(sourceAlphabet.GetAlphabet().GetSize()),
+      size.GetMaxWidth(),
+      size.GetMaxHeight());
     sourceAlphabet.GetAlphabet().Apply(generator);
 
     texture_.reset(generator.ReleaseTexture());
--- a/Framework/Fonts/OpenGLTextCoordinates.cpp	Wed May 15 16:56:17 2019 +0200
+++ b/Framework/Fonts/OpenGLTextCoordinates.cpp	Fri May 17 09:20:46 2019 +0200
@@ -35,8 +35,8 @@
                                       const Orthanc::IDynamicObject* payload)
     {
       // Rendering coordinates
-      float rx1 = x - box_.GetLeft();
-      float ry1 = y - box_.GetTop();
+      float rx1 = static_cast<float>(x - box_.GetLeft());
+      float ry1 = static_cast<float>(y - box_.GetTop());
       float rx2 = rx1 + static_cast<float>(width);
       float ry2 = ry1 + static_cast<float>(height);
 
@@ -81,8 +81,8 @@
     OpenGLTextCoordinates::OpenGLTextCoordinates(const GlyphTextureAlphabet& alphabet,
                                                  const std::string& utf8) :
       box_(alphabet.GetAlphabet(), utf8),
-      textureWidth_(alphabet.GetTextureWidth()),
-      textureHeight_(alphabet.GetTextureHeight())
+      textureWidth_(static_cast<float>(alphabet.GetTextureWidth())),
+      textureHeight_(static_cast<float>(alphabet.GetTextureHeight()))
     {
       if (textureWidth_ <= 0 ||
           textureHeight_ <= 0)
--- a/Framework/Scene2D/Internals/OpenGLTextProgram.cpp	Wed May 15 16:56:17 2019 +0200
+++ b/Framework/Scene2D/Internals/OpenGLTextProgram.cpp	Fri May 17 09:20:46 2019 +0200
@@ -155,8 +155,9 @@
         program_->Use();
 
         double dx, dy;  // In pixels
-        ComputeAnchorTranslation(dx, dy, data.GetAnchor(), data.GetTextWidth(), 
-          data.GetTextHeight(), static_cast<unsigned int>(data.GetBorder()));
+        ComputeAnchorTranslation(dx, dy, data.GetAnchor(), 
+                                 data.GetTextWidth(), data.GetTextHeight(),
+                                 static_cast<unsigned int>(data.GetBorder()));
       
         double x = data.GetX();
         double y = data.GetY();
--- a/Framework/Scene2D/Scene2D.h	Wed May 15 16:56:17 2019 +0200
+++ b/Framework/Scene2D/Scene2D.h	Fri May 17 09:20:46 2019 +0200
@@ -119,4 +119,3 @@
                     unsigned int canvasHeight);
   };
 }
-
--- a/Framework/Scene2D/ScenePoint2D.h	Wed May 15 16:56:17 2019 +0200
+++ b/Framework/Scene2D/ScenePoint2D.h	Fri May 17 09:20:46 2019 +0200
@@ -23,6 +23,7 @@
 
 #include "../Toolbox/AffineTransform2D.h"
 
+
 namespace OrthancStone
 {
   class ScenePoint2D
--- a/Framework/StoneInitialization.cpp	Wed May 15 16:56:17 2019 +0200
+++ b/Framework/StoneInitialization.cpp	Fri May 17 09:20:46 2019 +0200
@@ -22,7 +22,6 @@
 #include "StoneInitialization.h"
 
 #include <Core/OrthancException.h>
-#include <Core/Logging.h>
 
 #if !defined(ORTHANC_ENABLE_SDL)
 #  error Macro ORTHANC_ENABLE_SDL must be defined
@@ -34,9 +33,17 @@
 
 namespace OrthancStone
 {
+#if ORTHANC_ENABLE_LOGGING_PLUGIN == 1
+  void StoneInitialize(OrthancPluginContext* context)
+#else
   void StoneInitialize()
+#endif
   {
+#if ORTHANC_ENABLE_LOGGING_PLUGIN == 1
+    Orthanc::Logging::Initialize(context);
+#else
     Orthanc::Logging::Initialize();
+#endif
 
 #if ORTHANC_ENABLE_SDL == 1
     OrthancStone::SdlWindow::GlobalInitialize();
--- a/Framework/StoneInitialization.h	Wed May 15 16:56:17 2019 +0200
+++ b/Framework/StoneInitialization.h	Fri May 17 09:20:46 2019 +0200
@@ -21,9 +21,15 @@
 
 #pragma once
 
+#include <Core/Logging.h>
+
 namespace OrthancStone
 {
+#if ORTHANC_ENABLE_LOGGING_PLUGIN == 1
+  void StoneInitialize(OrthancPluginContext* context);
+#else
   void StoneInitialize();
+#endif
 
   void StoneFinalize();
 }
--- a/Framework/Toolbox/DownloadStack.cpp	Wed May 15 16:56:17 2019 +0200
+++ b/Framework/Toolbox/DownloadStack.cpp	Fri May 17 09:20:46 2019 +0200
@@ -77,8 +77,8 @@
     {
       for (size_t i = 0; i < size; i++)
       {
-        nodes_[i].prev_ = i - 1;
-        nodes_[i].next_ = i + 1;
+        nodes_[i].prev_ = static_cast<int>(i - 1);
+        nodes_[i].next_ = static_cast<int>(i + 1);
         nodes_[i].dequeued_ = false;
       }
 
--- a/Framework/Toolbox/FiniteProjectiveCamera.cpp	Wed May 15 16:56:17 2019 +0200
+++ b/Framework/Toolbox/FiniteProjectiveCamera.cpp	Fri May 17 09:20:46 2019 +0200
@@ -316,7 +316,7 @@
     LOG(WARNING) << "Output pixel format: " << Orthanc::EnumerationToString(target.GetFormat());
 
     std::auto_ptr<OrthancStone::ParallelSlices> slices(source.GetGeometry(projection));
-    const OrthancStone::Vector pixelSpacing = source.GetVoxelDimensions(projection);
+    const OrthancStone::Vector pixelSpacing = source.GetGeometry().GetVoxelDimensions(projection);
     const unsigned int targetWidth = target.GetWidth();
     const unsigned int targetHeight = target.GetHeight();
 
@@ -360,7 +360,8 @@
 
             // Read and accumulate the value of the pixel
             float pixel;
-            if (pixelReader.GetFloatValue(pixel, ix, iy))
+            if (pixelReader.GetFloatValue(
+              pixel, static_cast<float>(ix), static_cast<float>(iy)))
             {
               if (MIP)
               {
--- a/Framework/Toolbox/OrientedBoundingBox.cpp	Wed May 15 16:56:17 2019 +0200
+++ b/Framework/Toolbox/OrientedBoundingBox.cpp	Fri May 17 09:20:46 2019 +0200
@@ -37,8 +37,8 @@
       throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageSize);      
     }
 
-    const CoordinateSystem3D& geometry = image.GetAxialGeometry();
-    Vector dim = image.GetVoxelDimensions(VolumeProjection_Axial);
+    const CoordinateSystem3D& geometry = image.GetGeometry().GetAxialGeometry();
+    Vector dim = image.GetGeometry().GetVoxelDimensions(VolumeProjection_Axial);
 
     u_ = geometry.GetAxisX();
     v_ = geometry.GetAxisY();
--- a/Framework/Toolbox/OrthancSlicesLoader.cpp	Wed May 15 16:56:17 2019 +0200
+++ b/Framework/Toolbox/OrthancSlicesLoader.cpp	Fri May 17 09:20:46 2019 +0200
@@ -771,16 +771,22 @@
     }
     
     orthanc_.GetBinaryAsync(uri, "image/png",
-                            new Callable<OrthancSlicesLoader, OrthancApiClient::BinaryResponseReadyMessage>(*this, &OrthancSlicesLoader::ParseSliceImagePng),
-                            new Callable<OrthancSlicesLoader, IWebService::HttpRequestErrorMessage>(*this, &OrthancSlicesLoader::OnSliceImageError),
-                            Operation::DownloadSliceImage(index, slice, SliceImageQuality_FullPng));
-  }
+      new Callable<OrthancSlicesLoader, 
+        OrthancApiClient::BinaryResponseReadyMessage>
+          (*this, &OrthancSlicesLoader::ParseSliceImagePng),
+      new Callable<OrthancSlicesLoader, 
+        IWebService::HttpRequestErrorMessage>
+          (*this, &OrthancSlicesLoader::OnSliceImageError),
+      Operation::DownloadSliceImage(
+        static_cast<unsigned int>(index), slice, SliceImageQuality_FullPng));
+}
   
   void OrthancSlicesLoader::ScheduleSliceImagePam(const Slice& slice,
                                                   size_t index)
   {
-    std::string uri = ("/instances/" + slice.GetOrthancInstanceId() + "/frames/" +
-                       boost::lexical_cast<std::string>(slice.GetFrame()));
+    std::string uri = 
+      ("/instances/" + slice.GetOrthancInstanceId() + "/frames/" +
+      boost::lexical_cast<std::string>(slice.GetFrame()));
 
     switch (slice.GetConverter().GetExpectedPixelFormat())
     {
@@ -801,9 +807,14 @@
     }
 
     orthanc_.GetBinaryAsync(uri, "image/x-portable-arbitrarymap",
-                            new Callable<OrthancSlicesLoader, OrthancApiClient::BinaryResponseReadyMessage>(*this, &OrthancSlicesLoader::ParseSliceImagePam),
-                            new Callable<OrthancSlicesLoader, IWebService::HttpRequestErrorMessage>(*this, &OrthancSlicesLoader::OnSliceImageError),
-                            Operation::DownloadSliceImage(index, slice, SliceImageQuality_FullPam));
+      new Callable<OrthancSlicesLoader, 
+        OrthancApiClient::BinaryResponseReadyMessage>
+          (*this, &OrthancSlicesLoader::ParseSliceImagePam),
+      new Callable<OrthancSlicesLoader, 
+        IWebService::HttpRequestErrorMessage>
+          (*this, &OrthancSlicesLoader::OnSliceImageError),
+      Operation::DownloadSliceImage(static_cast<unsigned int>(index), 
+                                    slice, SliceImageQuality_FullPam));
   }
 
 
@@ -839,9 +850,14 @@
                        boost::lexical_cast<std::string>(slice.GetFrame()));
 
     orthanc_.GetJsonAsync(uri,
-                          new Callable<OrthancSlicesLoader, OrthancApiClient::JsonResponseReadyMessage>(*this, &OrthancSlicesLoader::ParseSliceImageJpeg),
-                          new Callable<OrthancSlicesLoader, IWebService::HttpRequestErrorMessage>(*this, &OrthancSlicesLoader::OnSliceImageError),
-                          Operation::DownloadSliceImage(index, slice, quality));
+      new Callable<OrthancSlicesLoader, 
+        OrthancApiClient::JsonResponseReadyMessage>
+          (*this, &OrthancSlicesLoader::ParseSliceImageJpeg),
+      new Callable<OrthancSlicesLoader, 
+        IWebService::HttpRequestErrorMessage>
+          (*this, &OrthancSlicesLoader::OnSliceImageError),
+        Operation::DownloadSliceImage(
+          static_cast<unsigned int>(index), slice, quality));
   }
   
   
@@ -875,9 +891,14 @@
       std::string uri = ("/instances/" + slice.GetOrthancInstanceId() + "/frames/" +
                          boost::lexical_cast<std::string>(slice.GetFrame()) + "/raw.gz");
       orthanc_.GetBinaryAsync(uri, IWebService::HttpHeaders(),
-                              new Callable<OrthancSlicesLoader, OrthancApiClient::BinaryResponseReadyMessage>(*this, &OrthancSlicesLoader::ParseSliceRawImage),
-                              new Callable<OrthancSlicesLoader, IWebService::HttpRequestErrorMessage>(*this, &OrthancSlicesLoader::OnSliceImageError),
-                              Operation::DownloadSliceRawImage(index, slice));
+        new Callable<OrthancSlicesLoader, 
+          OrthancApiClient::BinaryResponseReadyMessage>
+            (*this, &OrthancSlicesLoader::ParseSliceRawImage),
+        new Callable<OrthancSlicesLoader,
+          IWebService::HttpRequestErrorMessage>
+            (*this, &OrthancSlicesLoader::OnSliceImageError),
+        Operation::DownloadSliceRawImage(
+          static_cast<unsigned int>(index), slice));
     }
   }
 }
--- a/Framework/Toolbox/ParallelSlicesCursor.cpp	Wed May 15 16:56:17 2019 +0200
+++ b/Framework/Toolbox/ParallelSlicesCursor.cpp	Fri May 17 09:20:46 2019 +0200
@@ -110,7 +110,7 @@
       return false;
     }
 
-    int count = slices_->GetSliceCount();
+    int count = static_cast<int>(slices_->GetSliceCount());
     if (count == 0)
     {
       return false;
@@ -123,7 +123,7 @@
     }
     else
     {
-      slice = currentSlice_;
+      slice = static_cast<int>(currentSlice_);
     }
 
     switch (mode)
--- a/Framework/Toolbox/ShearWarpProjectiveTransform.cpp	Wed May 15 16:56:17 2019 +0200
+++ b/Framework/Toolbox/ShearWarpProjectiveTransform.cpp	Fri May 17 09:20:46 2019 +0200
@@ -195,15 +195,20 @@
       throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
     }
 
-    intermediateWidth_ = std::ceil(extent.GetWidth() / maxScaling);
-    intermediateHeight_ = std::ceil(extent.GetHeight() / maxScaling);
+    intermediateWidth_ = 
+      static_cast<unsigned int>(std::ceil(extent.GetWidth() / maxScaling));
+    intermediateHeight_ = 
+      static_cast<unsigned int>(std::ceil(extent.GetHeight() / maxScaling));
 
     // This is the product "T * S" in Equation (A.16) on page 209
     Matrix TS = LinearAlgebra::Product(
-      GeometryToolbox::CreateTranslationMatrix(static_cast<double>(intermediateWidth_) / 2.0,
-                                               static_cast<double>(intermediateHeight_) / 2.0, 0),
-      GeometryToolbox::CreateScalingMatrix(1.0 / maxScaling, 1.0 / maxScaling, 1),
-      GeometryToolbox::CreateTranslationMatrix(-extent.GetCenterX(), -extent.GetCenterY(), 0));
+      GeometryToolbox::CreateTranslationMatrix(
+        static_cast<double>(intermediateWidth_) / 2.0,
+        static_cast<double>(intermediateHeight_) / 2.0, 0),
+      GeometryToolbox::CreateScalingMatrix(
+        1.0 / maxScaling, 1.0 / maxScaling, 1),
+      GeometryToolbox::CreateTranslationMatrix(
+        -extent.GetCenterX(), -extent.GetCenterY(), 0));
     
     // This is Equation (A.16) on page 209. WARNING: There is an
     // error in Lacroute's thesis: "inv(MM_shear)" is used instead
@@ -380,8 +385,8 @@
     
     // Compute the "world" matrix that maps the source volume to the
     // (0,0,0)->(1,1,1) unit cube
-    Vector origin = source.GetCoordinates(0, 0, 0);
-    Vector ps = source.GetVoxelDimensions(VolumeProjection_Axial);
+    Vector origin = source.GetGeometry().GetCoordinates(0, 0, 0);
+    Vector ps = source.GetGeometry().GetVoxelDimensions(VolumeProjection_Axial);
     Matrix world = LinearAlgebra::Product(
       GeometryToolbox::CreateScalingMatrix(1.0 / ps[0], 1.0 / ps[1], 1.0 / ps[2]),
       GeometryToolbox::CreateTranslationMatrix(-origin[0], -origin[1], -origin[2]));
--- a/Framework/Toolbox/SlicesSorter.cpp	Wed May 15 16:56:17 2019 +0200
+++ b/Framework/Toolbox/SlicesSorter.cpp	Fri May 17 09:20:46 2019 +0200
@@ -285,4 +285,42 @@
 
     return found;
   }
+
+
+  double SlicesSorter::ComputeSpacingBetweenSlices() const
+  {
+    if (GetSlicesCount() <= 1)
+    {
+      // This is a volume that is empty or that contains one single
+      // slice: Choose a dummy z-dimension for voxels
+      return 1.0;
+    }
+    
+    const OrthancStone::CoordinateSystem3D& reference = GetSliceGeometry(0);
+
+    double referencePosition = reference.ProjectAlongNormal(reference.GetOrigin());
+        
+    double p = reference.ProjectAlongNormal(GetSliceGeometry(1).GetOrigin());
+    double spacingZ = p - referencePosition;
+
+    if (spacingZ <= 0)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls,
+                                      "Please call the Sort() method before");
+    }
+
+    for (size_t i = 1; i < GetSlicesCount(); i++)
+    {
+      OrthancStone::Vector p = reference.GetOrigin() + spacingZ * static_cast<double>(i) * reference.GetNormal();        
+      double d = boost::numeric::ublas::norm_2(p - GetSliceGeometry(i).GetOrigin());
+
+      if (!OrthancStone::LinearAlgebra::IsNear(d, 0, 0.001 /* tolerance expressed in mm */))
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadGeometry,
+                                        "The origins of the slices of a volume image are not regularly spaced");
+      }
+    }
+
+    return spacingZ;
+  }
 }
--- a/Framework/Toolbox/SlicesSorter.h	Wed May 15 16:56:17 2019 +0200
+++ b/Framework/Toolbox/SlicesSorter.h	Fri May 17 09:20:46 2019 +0200
@@ -80,10 +80,17 @@
     
     const Orthanc::IDynamicObject& GetSlicePayload(size_t i) const;
 
+    // WARNING - Apply the sorting algorithm can reduce the number of
+    // slices. This is notably the case if all the slices are not
+    // parallel to the reference normal that will be selected.
     bool Sort();
-    
+
+    // TODO - Remove this
     bool LookupClosestSlice(size_t& index,
                             double& distance,
                             const CoordinateSystem3D& slice) const;
+
+    // WARNING - The slices must have been sorted before calling this method
+    double ComputeSpacingBetweenSlices() const;
   };
 }
--- a/Framework/Toolbox/ViewportGeometry.cpp	Wed May 15 16:56:17 2019 +0200
+++ b/Framework/Toolbox/ViewportGeometry.cpp	Fri May 17 09:20:46 2019 +0200
@@ -128,7 +128,12 @@
     sceneTouches.clear();
     for (size_t t = 0; t < displayTouches.size(); t++)
     {
-      MapPixelCenterToScene(sceneX, sceneY, displayTouches[t].x, displayTouches[t].y);
+      MapPixelCenterToScene(
+        sceneX,
+        sceneY, 
+        static_cast<int>(displayTouches[t].x), 
+        static_cast<int>(displayTouches[t].y));
+      
       sceneTouches.push_back(Touch((float)sceneX, (float)sceneY));
     }
   }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Toolbox/VolumeImageGeometry.cpp	Fri May 17 09:20:46 2019 +0200
@@ -0,0 +1,309 @@
+/**
+ * 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 "VolumeImageGeometry.h"
+
+#include "../Toolbox/GeometryToolbox.h"
+
+#include <Core/OrthancException.h>
+
+
+namespace OrthancStone
+{
+  void VolumeImageGeometry::Invalidate()
+  {
+    Vector p = (axialGeometry_.GetOrigin() +
+                static_cast<double>(depth_ - 1) * voxelDimensions_[2] * axialGeometry_.GetNormal());
+        
+    coronalGeometry_ = CoordinateSystem3D(p,
+                                          axialGeometry_.GetAxisX(),
+                                          -axialGeometry_.GetNormal());
+    
+    sagittalGeometry_ = CoordinateSystem3D(p,
+                                           axialGeometry_.GetAxisY(),
+                                           axialGeometry_.GetNormal());
+
+    Vector origin = (
+      axialGeometry_.MapSliceToWorldCoordinates(-0.5 * voxelDimensions_[0],
+                                                -0.5 * voxelDimensions_[1]) -
+      0.5 * voxelDimensions_[2] * axialGeometry_.GetNormal());
+
+    Vector scaling;
+    
+    if (width_ == 0 ||
+        height_ == 0 ||
+        depth_ == 0)
+    {
+      LinearAlgebra::AssignVector(scaling, 1, 1, 1);
+    }
+    else
+    {
+      scaling = (
+        axialGeometry_.GetAxisX() * voxelDimensions_[0] * static_cast<double>(width_) +
+        axialGeometry_.GetAxisY() * voxelDimensions_[1] * static_cast<double>(height_) +
+        axialGeometry_.GetNormal() * voxelDimensions_[2] * static_cast<double>(depth_));
+    }
+
+    transform_ = LinearAlgebra::Product(
+      GeometryToolbox::CreateTranslationMatrix(origin[0], origin[1], origin[2]),
+      GeometryToolbox::CreateScalingMatrix(scaling[0], scaling[1], scaling[2]));
+
+    LinearAlgebra::InvertMatrix(transformInverse_, transform_);
+  }
+
+  
+  VolumeImageGeometry::VolumeImageGeometry() :
+    width_(0),
+    height_(0),
+    depth_(0)
+  {
+    LinearAlgebra::AssignVector(voxelDimensions_, 1, 1, 1);
+    Invalidate();
+  }
+
+
+  void VolumeImageGeometry::SetSize(unsigned int width,
+                                    unsigned int height,
+                                    unsigned int depth)
+  {
+    width_ = width;
+    height_ = height;
+    depth_ = depth;
+    Invalidate();
+  }
+
+  
+  void VolumeImageGeometry::SetAxialGeometry(const CoordinateSystem3D& geometry)
+  {
+    axialGeometry_ = geometry;
+    Invalidate();
+  }
+
+
+  void VolumeImageGeometry::SetVoxelDimensions(double x,
+                                               double y,
+                                               double z)
+  {
+    if (x <= 0 ||
+        y <= 0 ||
+        z <= 0)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+    else
+    {
+      LinearAlgebra::AssignVector(voxelDimensions_, x, y, z);
+      Invalidate();
+    }
+  }
+
+
+  const CoordinateSystem3D& VolumeImageGeometry::GetProjectionGeometry(VolumeProjection projection) const
+  {
+    switch (projection)
+    {
+      case VolumeProjection_Axial:
+        return axialGeometry_;
+
+      case VolumeProjection_Coronal:
+        return coronalGeometry_;
+
+      case VolumeProjection_Sagittal:
+        return sagittalGeometry_;
+
+      default:
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+  
+  Vector VolumeImageGeometry::GetVoxelDimensions(VolumeProjection projection) const
+  {
+    switch (projection)
+    {
+      case VolumeProjection_Axial:
+        return voxelDimensions_;
+
+      case VolumeProjection_Coronal:
+        return LinearAlgebra::CreateVector(voxelDimensions_[0], voxelDimensions_[2], voxelDimensions_[1]);
+
+      case VolumeProjection_Sagittal:
+        return LinearAlgebra::CreateVector(voxelDimensions_[1], voxelDimensions_[2], voxelDimensions_[0]);
+
+      default:
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  unsigned int VolumeImageGeometry::GetProjectionWidth(VolumeProjection projection) const
+  {
+    switch (projection)
+    {
+      case VolumeProjection_Axial:
+        return width_;
+
+      case VolumeProjection_Coronal:
+        return width_;
+
+      case VolumeProjection_Sagittal:
+        return height_;
+
+      default:
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  unsigned int VolumeImageGeometry::GetProjectionHeight(VolumeProjection projection) const
+  {
+    switch (projection)
+    {
+      case VolumeProjection_Axial:
+        return height_;
+
+      case VolumeProjection_Coronal:
+        return depth_;
+
+      case VolumeProjection_Sagittal:
+        return depth_;
+
+      default:
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  unsigned int VolumeImageGeometry::GetProjectionDepth(VolumeProjection projection) const
+  {
+    switch (projection)
+    {
+      case VolumeProjection_Axial:
+        return depth_;
+
+      case VolumeProjection_Coronal:
+        return height_;
+
+      case VolumeProjection_Sagittal:
+        return width_;
+
+      default:
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }    
+  }
+
+
+  Vector VolumeImageGeometry::GetCoordinates(float x,
+                                             float y,
+                                             float z) const
+  {
+    Vector p = LinearAlgebra::Product(transform_, LinearAlgebra::CreateVector(x, y, z, 1));
+
+    assert(LinearAlgebra::IsNear(p[3], 1));  // Affine transform, no perspective effect
+
+    // Back to non-homogeneous coordinates
+    return LinearAlgebra::CreateVector(p[0], p[1], p[2]);
+  }
+
+
+  bool VolumeImageGeometry::DetectProjection(VolumeProjection& projection,
+                                             const Vector& planeNormal) const
+  {
+    if (GeometryToolbox::IsParallel(planeNormal, axialGeometry_.GetNormal()))
+    {
+      projection = VolumeProjection_Axial;
+      return true;
+    }
+    else if (GeometryToolbox::IsParallel(planeNormal, coronalGeometry_.GetNormal()))
+    {
+      projection = VolumeProjection_Coronal;
+      return true;
+    }
+    else if (GeometryToolbox::IsParallel(planeNormal, sagittalGeometry_.GetNormal()))
+    {
+      projection = VolumeProjection_Sagittal;
+      return true;
+    }
+    else
+    {
+      return false;
+    }
+  }
+
+  
+  bool VolumeImageGeometry::DetectSlice(VolumeProjection& projection,
+                                        unsigned int& slice,
+                                        const CoordinateSystem3D& plane) const
+  {
+    if (!DetectProjection(projection, plane.GetNormal()))
+    {
+      return false;
+    }
+
+    // Transforms the coordinates of the origin of the plane, into the
+    // coordinates of the axial geometry
+    const Vector& origin = plane.GetOrigin();
+    Vector p = LinearAlgebra::Product(
+      transformInverse_,
+      LinearAlgebra::CreateVector(origin[0], origin[1], origin[2], 1));
+
+    assert(LinearAlgebra::IsNear(p[3], 1));
+
+    double z;
+
+    switch (projection)
+    {
+      case VolumeProjection_Axial:
+        z = p[2];
+        break;
+
+      case VolumeProjection_Coronal:
+        z = p[1];
+        break;
+
+      case VolumeProjection_Sagittal:
+        z = p[0];
+        break;
+
+      default:
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+
+    const unsigned int projectionDepth = GetProjectionDepth(projection);
+    
+    z *= static_cast<double>(projectionDepth);
+    if (z < 0)
+    {
+      return false;
+    }
+        
+    unsigned int d = static_cast<unsigned int>(std::floor(z));
+    if (d >= projectionDepth)
+    {
+      return false;
+    }
+    else
+    {
+      slice = d;
+      return true;
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Toolbox/VolumeImageGeometry.h	Fri May 17 09:20:46 2019 +0200
@@ -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 "../StoneEnumerations.h"
+#include "CoordinateSystem3D.h"
+
+namespace OrthancStone
+{
+  class VolumeImageGeometry
+  {
+  private:
+    unsigned int           width_;
+    unsigned int           height_;
+    unsigned int           depth_;
+    CoordinateSystem3D     axialGeometry_;
+    CoordinateSystem3D     coronalGeometry_;
+    CoordinateSystem3D     sagittalGeometry_;
+    Vector                 voxelDimensions_;
+    Matrix                 transform_;
+    Matrix                 transformInverse_;
+
+    void Invalidate();
+
+  public:
+    VolumeImageGeometry();
+
+    unsigned int GetWidth() const
+    {
+      return width_;
+    }
+
+    unsigned int GetHeight() const
+    {
+      return height_;
+    }
+
+    unsigned int GetDepth() const
+    {
+      return depth_;
+    }
+
+    const CoordinateSystem3D& GetAxialGeometry() const
+    {
+      return axialGeometry_;
+    }
+
+    const CoordinateSystem3D& GetCoronalGeometry() const
+    {
+      return coronalGeometry_;
+    }
+
+    const CoordinateSystem3D& GetSagittalGeometry() const
+    {
+      return sagittalGeometry_;
+    }
+
+    const CoordinateSystem3D& GetProjectionGeometry(VolumeProjection projection) const;
+    
+    const Matrix& GetTransform() const
+    {
+      return transform_;
+    }
+
+    const Matrix& GetTransformInverse() const
+    {
+      return transformInverse_;
+    }
+
+    void SetSize(unsigned int width,
+                 unsigned int height,
+                 unsigned int depth);
+
+    // Set the geometry of the first axial slice (i.e. the one whose
+    // depth == 0)
+    void SetAxialGeometry(const CoordinateSystem3D& geometry);
+
+    void SetVoxelDimensions(double x,
+                            double y,
+                            double z);
+
+    Vector GetVoxelDimensions(VolumeProjection projection) const;
+
+    unsigned int GetProjectionWidth(VolumeProjection projection) const;
+
+    unsigned int GetProjectionHeight(VolumeProjection projection) const;
+
+    unsigned int GetProjectionDepth(VolumeProjection projection) const;
+
+    // Get the 3D position of a point in the volume, where x, y and z
+    // lie in the [0;1] range
+    Vector GetCoordinates(float x,
+                          float y,
+                          float z) const;
+
+    bool DetectProjection(VolumeProjection& projection,
+                          const Vector& planeNormal) const;
+
+    bool DetectSlice(VolumeProjection& projection,
+                     unsigned int& slice,
+                     const CoordinateSystem3D& plane) const;
+  };
+}
--- a/Framework/Volumes/ImageBuffer3D.cpp	Wed May 15 16:56:17 2019 +0200
+++ b/Framework/Volumes/ImageBuffer3D.cpp	Fri May 17 09:20:46 2019 +0200
@@ -21,6 +21,8 @@
 
 #include "ImageBuffer3D.h"
 
+#include "../Toolbox/GeometryToolbox.h"
+
 #include <Core/Images/ImageProcessing.h>
 #include <Core/Logging.h>
 #include <Core/OrthancException.h>
@@ -116,10 +118,11 @@
     computeRange_(computeRange),
     hasRange_(false)
   {
-    LinearAlgebra::AssignVector(voxelDimensions_, 1, 1, 1);
+    geometry_.SetSize(width, height, depth);
 
-    LOG(INFO) << "Created an image of "
-              << (GetEstimatedMemorySize() / (1024ll * 1024ll)) << "MB";
+    LOG(INFO) << "Created a 3D image of size " << width << "x" << height
+              << "x" << depth << " in " << Orthanc::EnumerationToString(format)
+              << " (" << (GetEstimatedMemorySize() / (1024ll * 1024ll)) << "MB)";
   }
 
 
@@ -129,127 +132,57 @@
   }
 
 
-  void ImageBuffer3D::SetAxialGeometry(const CoordinateSystem3D& geometry)
-  {
-    axialGeometry_ = geometry;
-  }
-
-
-  void ImageBuffer3D::SetVoxelDimensions(double x,
-                                         double y,
-                                         double z)
-  {
-    if (x <= 0 ||
-        y <= 0 ||
-        z <= 0)
-    {
-      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
-    }
-
-    {
-      LinearAlgebra::AssignVector(voxelDimensions_, x, y, z);
-    }
-  }
-
-
-  Vector ImageBuffer3D::GetVoxelDimensions(VolumeProjection projection) const
-  {
-    Vector result;
-    switch (projection)
-    {
-    case VolumeProjection_Axial:
-      result = voxelDimensions_;
-      break;
-
-    case VolumeProjection_Coronal:
-      LinearAlgebra::AssignVector(result, voxelDimensions_[0], voxelDimensions_[2], voxelDimensions_[1]);
-      break;
-
-    case VolumeProjection_Sagittal:
-      LinearAlgebra::AssignVector(result, voxelDimensions_[1], voxelDimensions_[2], voxelDimensions_[0]);
-      break;
-
-    default:
-      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
-    }
-
-    return result;
-  }
-
-
-  void ImageBuffer3D::GetSliceSize(unsigned int& width,
-                                   unsigned int& height,
-                                   VolumeProjection projection)
-  {
-    switch (projection)
-    {
-    case VolumeProjection_Axial:
-      width = width_;
-      height = height_;
-      break;
-
-    case VolumeProjection_Coronal:
-      width = width_;
-      height = depth_;
-      break;
-
-    case VolumeProjection_Sagittal:
-      width = height_;
-      height = depth_;
-      break;
-
-    default:
-      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
-    }
-  }
 
 
   ParallelSlices* ImageBuffer3D::GetGeometry(VolumeProjection projection) const
   {
+    const Vector dimensions = geometry_.GetVoxelDimensions(VolumeProjection_Axial);
+    const CoordinateSystem3D& axial = geometry_.GetAxialGeometry();
+    
     std::auto_ptr<ParallelSlices> result(new ParallelSlices);
 
     switch (projection)
     {
-    case VolumeProjection_Axial:
-      for (unsigned int z = 0; z < depth_; z++)
-      {
-        Vector origin = axialGeometry_.GetOrigin();
-        origin += static_cast<double>(z) * voxelDimensions_[2] * axialGeometry_.GetNormal();
+      case VolumeProjection_Axial:
+        for (unsigned int z = 0; z < depth_; z++)
+        {
+          Vector origin = axial.GetOrigin();
+          origin += static_cast<double>(z) * dimensions[2] * axial.GetNormal();
 
-        result->AddSlice(origin,
-                         axialGeometry_.GetAxisX(),
-                         axialGeometry_.GetAxisY());
-      }
-      break;
+          result->AddSlice(origin,
+                           axial.GetAxisX(),
+                           axial.GetAxisY());
+        }
+        break;
 
-    case VolumeProjection_Coronal:
-      for (unsigned int y = 0; y < height_; y++)
-      {
-        Vector origin = axialGeometry_.GetOrigin();
-        origin += static_cast<double>(y) * voxelDimensions_[1] * axialGeometry_.GetAxisY();
-        origin += static_cast<double>(depth_ - 1) * voxelDimensions_[2] * axialGeometry_.GetNormal();
+      case VolumeProjection_Coronal:
+        for (unsigned int y = 0; y < height_; y++)
+        {
+          Vector origin = axial.GetOrigin();
+          origin += static_cast<double>(y) * dimensions[1] * axial.GetAxisY();
+          origin += static_cast<double>(depth_ - 1) * dimensions[2] * axial.GetNormal();
 
-        result->AddSlice(origin,
-                         axialGeometry_.GetAxisX(),
-                         -axialGeometry_.GetNormal());
-      }
-      break;
+          result->AddSlice(origin,
+                           axial.GetAxisX(),
+                           -axial.GetNormal());
+        }
+        break;
 
-    case VolumeProjection_Sagittal:
-      for (unsigned int x = 0; x < width_; x++)
-      {
-        Vector origin = axialGeometry_.GetOrigin();
-        origin += static_cast<double>(x) * voxelDimensions_[0] * axialGeometry_.GetAxisX();
-        origin += static_cast<double>(depth_ - 1) * voxelDimensions_[2] * axialGeometry_.GetNormal();
+      case VolumeProjection_Sagittal:
+        for (unsigned int x = 0; x < width_; x++)
+        {
+          Vector origin = axial.GetOrigin();
+          origin += static_cast<double>(x) * dimensions[0] * axial.GetAxisX();
+          origin += static_cast<double>(depth_ - 1) * dimensions[2] * axial.GetNormal();
 
-        result->AddSlice(origin,
-                         axialGeometry_.GetAxisY(),
-                         -axialGeometry_.GetNormal());
-      }
-      break;
+          result->AddSlice(origin,
+                           axial.GetAxisY(),
+                           -axial.GetNormal());
+        }
+        break;
 
-    default:
-      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+      default:
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
     }
 
     return result.release();
@@ -275,24 +208,24 @@
 
     switch (slice.GetFormat())
     {
-    case Orthanc::PixelFormat_Grayscale8:
-    case Orthanc::PixelFormat_Grayscale16:
-    case Orthanc::PixelFormat_Grayscale32:
-    case Orthanc::PixelFormat_SignedGrayscale16:
-    {
-      int64_t a, b;
-      Orthanc::ImageProcessing::GetMinMaxIntegerValue(a, b, slice);
-      sliceMin = static_cast<float>(a);
-      sliceMax = static_cast<float>(b);
-      break;
-    }
+      case Orthanc::PixelFormat_Grayscale8:
+      case Orthanc::PixelFormat_Grayscale16:
+      case Orthanc::PixelFormat_Grayscale32:
+      case Orthanc::PixelFormat_SignedGrayscale16:
+      {
+        int64_t a, b;
+        Orthanc::ImageProcessing::GetMinMaxIntegerValue(a, b, slice);
+        sliceMin = static_cast<float>(a);
+        sliceMax = static_cast<float>(b);
+        break;
+      }
 
-    case Orthanc::PixelFormat_Float32:
-      Orthanc::ImageProcessing::GetMinMaxFloatValue(sliceMin, sliceMax, slice);
-      break;
+      case Orthanc::PixelFormat_Float32:
+        Orthanc::ImageProcessing::GetMinMaxFloatValue(sliceMin, sliceMax, slice);
+        break;
 
-    default:
-      return;
+      default:
+        return;
     }
 
     if (hasRange_)
@@ -331,8 +264,15 @@
     if (hasRange_)
     {
       style.windowing_ = ImageWindowing_Custom;
-      style.customWindowCenter_ = converter.Apply((minValue_ + maxValue_) / 2.0);
-      style.customWindowWidth_ = converter.Apply(maxValue_ - minValue_);
+      
+      // casting the narrower type to wider before calling the + operator
+      // will prevent overflowing (this is why the cast to double is only 
+      // done on the first operand)
+      style.customWindowCenter_ = static_cast<float>(
+        converter.Apply((static_cast<double>(minValue_) + maxValue_) / 2.0));
+      
+      style.customWindowWidth_ = static_cast<float>(
+        converter.Apply(static_cast<double>(maxValue_) - minValue_));
       
       if (style.customWindowWidth_ > 1)
       {
@@ -366,8 +306,8 @@
         sagittal_->GetReadOnlyAccessor(accessor_);
         break;
 
-    default:
-      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+      default:
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
     }
   }
 
@@ -410,8 +350,8 @@
         sagittal_->GetWriteableAccessor(accessor_);
         break;
 
-    default:
-      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+      default:
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
     }
   }
 
@@ -456,22 +396,4 @@
     const void* p = image_.GetConstRow(y + height_ * (depth_ - 1 - z));
     return reinterpret_cast<const uint16_t*>(p) [x];
   }
-
-
-  Vector ImageBuffer3D::GetCoordinates(float x,
-                                       float y,
-                                       float z) const
-  {
-    Vector ps = GetVoxelDimensions(OrthancStone::VolumeProjection_Axial);
-
-    const CoordinateSystem3D& axial = GetAxialGeometry();
-    
-    Vector origin = (axial.MapSliceToWorldCoordinates(-0.5 * ps[0], -0.5 * ps[1]) -
-        0.5 * ps[2] * axial.GetNormal());
-
-    return (origin +
-            axial.GetAxisX() * ps[0] * x * static_cast<double>(GetWidth()) +
-        axial.GetAxisY() * ps[1] * y * static_cast<double>(GetHeight()) +
-        axial.GetNormal() * ps[2] * z * static_cast<double>(GetDepth()));
-  }
 }
--- a/Framework/Volumes/ImageBuffer3D.h	Wed May 15 16:56:17 2019 +0200
+++ b/Framework/Volumes/ImageBuffer3D.h	Fri May 17 09:20:46 2019 +0200
@@ -23,7 +23,7 @@
 
 #include "../StoneEnumerations.h"
 #include "../Layers/RenderStyle.h"
-#include "../Toolbox/CoordinateSystem3D.h"
+#include "../Toolbox/VolumeImageGeometry.h"
 #include "../Toolbox/DicomFrameConverter.h"
 #include "../Toolbox/ParallelSlices.h"
 
@@ -34,8 +34,7 @@
   class ImageBuffer3D : public boost::noncopyable
   {
   private:
-    CoordinateSystem3D     axialGeometry_;
-    Vector                 voxelDimensions_;
+    VolumeImageGeometry    geometry_;  // TODO => Move this out of this class
     Orthanc::Image         image_;
     Orthanc::PixelFormat   format_;
     unsigned int           width_;
@@ -45,6 +44,8 @@
     bool                   hasRange_;
     float                  minValue_;
     float                  maxValue_;
+    Matrix                 transform_;
+    Matrix                 transformInverse_;
 
     void ExtendImageRange(const Orthanc::ImageAccessor& slice);
 
@@ -77,24 +78,15 @@
 
     void Clear();
 
-    // Set the geometry of the first axial slice (i.e. the one whose
-    // depth == 0)
-    void SetAxialGeometry(const CoordinateSystem3D& geometry);
-
-    const CoordinateSystem3D& GetAxialGeometry() const
+    VolumeImageGeometry& GetGeometry()
     {
-      return axialGeometry_;
+      return geometry_;
     }
 
-    void SetVoxelDimensions(double x,
-                            double y,
-                            double z);
-
-    Vector GetVoxelDimensions(VolumeProjection projection) const;
-
-    void GetSliceSize(unsigned int& width,
-                      unsigned int& height,
-                      VolumeProjection projection);
+    const VolumeImageGeometry& GetGeometry() const
+    {
+      return geometry_;
+    }
 
     const Orthanc::ImageAccessor& GetInternalImage() const
     {
@@ -121,6 +113,7 @@
       return format_;
     }
 
+    // TODO - Remove
     ParallelSlices* GetGeometry(VolumeProjection projection) const;
     
     uint64_t GetEstimatedMemorySize() const;
@@ -160,13 +153,7 @@
                                  unsigned int y,
                                  unsigned int z) const;
 
-    // Get the 3D position of a point in the volume, where x, y and z
-    // lie in the [0;1] range
-    Vector GetCoordinates(float x,
-                          float y,
-                          float z) const;
-
-
+    
     class SliceReader : public boost::noncopyable
     {
     private:
--- a/Framework/Volumes/VolumeReslicer.cpp	Wed May 15 16:56:17 2019 +0200
+++ b/Framework/Volumes/VolumeReslicer.cpp	Fri May 17 09:20:46 2019 +0200
@@ -749,7 +749,8 @@
   {
     // Choose the default voxel size as the finest voxel dimension
     // of the source volumetric image
-    const OrthancStone::Vector dim = source.GetVoxelDimensions(OrthancStone::VolumeProjection_Axial);
+    const OrthancStone::Vector dim =
+      source.GetGeometry().GetVoxelDimensions(OrthancStone::VolumeProjection_Axial);
     double voxelSize = dim[0];
     
     if (dim[1] < voxelSize)
--- a/Framework/Widgets/SliceViewerWidget.cpp	Wed May 15 16:56:17 2019 +0200
+++ b/Framework/Widgets/SliceViewerWidget.cpp	Fri May 17 09:20:46 2019 +0200
@@ -115,7 +115,7 @@
 
     unsigned int GetCountMissing() const
     {
-      return countMissing_;
+      return static_cast<unsigned int>(countMissing_);
     }
 
     bool RenderScene(CairoContext& context,
--- a/Framework/Widgets/WorldSceneWidget.cpp	Wed May 15 16:56:17 2019 +0200
+++ b/Framework/Widgets/WorldSceneWidget.cpp	Fri May 17 09:20:46 2019 +0200
@@ -83,8 +83,12 @@
       for (size_t t = 0; t < displayTouches.size(); t++)
       {
         double sx, sy;
-        view_.MapPixelCenterToScene(sx, sy, (int)displayTouches[t].x, (int)displayTouches[t].y);
-        sceneTouches.push_back(Touch(sx, sy));
+        
+        view_.MapPixelCenterToScene(
+          sx, sy, (int)displayTouches[t].x, (int)displayTouches[t].y);
+        
+        sceneTouches.push_back(
+          Touch(static_cast<float>(sx), static_cast<float>(sy)));
       }
       tracker_->MouseMove(x, y, sceneX, sceneY, displayTouches, sceneTouches);
     }
--- a/Framework/dev.h	Wed May 15 16:56:17 2019 +0200
+++ b/Framework/dev.h	Fri May 17 09:20:46 2019 +0200
@@ -157,9 +157,9 @@
                 << "x" << loader_.GetSlicesCount() << " in " << Orthanc::EnumerationToString(format);
 
       image_.reset(new ImageBuffer3D(format, width, height, static_cast<unsigned int>(loader_.GetSlicesCount()), computeRange_));
-      image_->SetAxialGeometry(loader_.GetSlice(0).GetGeometry());
-      image_->SetVoxelDimensions(loader_.GetSlice(0).GetPixelSpacingX(),
-                                 loader_.GetSlice(0).GetPixelSpacingY(), spacingZ);
+      image_->GetGeometry().SetAxialGeometry(loader_.GetSlice(0).GetGeometry());
+      image_->GetGeometry().SetVoxelDimensions(loader_.GetSlice(0).GetPixelSpacingX(),
+                                               loader_.GetSlice(0).GetPixelSpacingY(), spacingZ);
       image_->Clear();
 
       downloadStack_.reset(new DownloadStack(static_cast<unsigned int>(loader_.GetSlicesCount())));
@@ -301,7 +301,7 @@
   };
 
 
-  class VolumeImageGeometry
+  class OLD_VolumeImageGeometry
   {
   private:
     unsigned int         width_;
@@ -403,8 +403,8 @@
     }
 
   public:
-    VolumeImageGeometry(const OrthancVolumeImage& volume,
-                        VolumeProjection projection)
+    OLD_VolumeImageGeometry(const OrthancVolumeImage& volume,
+                            VolumeProjection projection)
     {
       if (volume.GetSlicesCount() == 0)
       {
@@ -521,9 +521,9 @@
 
 
     OrthancVolumeImage&                 volume_;
-    std::auto_ptr<VolumeImageGeometry>  axialGeometry_;
-    std::auto_ptr<VolumeImageGeometry>  coronalGeometry_;
-    std::auto_ptr<VolumeImageGeometry>  sagittalGeometry_;
+    std::auto_ptr<OLD_VolumeImageGeometry>  axialGeometry_;
+    std::auto_ptr<OLD_VolumeImageGeometry>  coronalGeometry_;
+    std::auto_ptr<OLD_VolumeImageGeometry>  sagittalGeometry_;
 
 
     bool IsGeometryReady() const
@@ -536,9 +536,9 @@
       assert(&message.GetOrigin() == &volume_);
 
       // These 3 values are only used to speed up the IVolumeSlicer
-      axialGeometry_.reset(new VolumeImageGeometry(volume_, VolumeProjection_Axial));
-      coronalGeometry_.reset(new VolumeImageGeometry(volume_, VolumeProjection_Coronal));
-      sagittalGeometry_.reset(new VolumeImageGeometry(volume_, VolumeProjection_Sagittal));
+      axialGeometry_.reset(new OLD_VolumeImageGeometry(volume_, VolumeProjection_Axial));
+      coronalGeometry_.reset(new OLD_VolumeImageGeometry(volume_, VolumeProjection_Coronal));
+      sagittalGeometry_.reset(new OLD_VolumeImageGeometry(volume_, VolumeProjection_Sagittal));
 
       BroadcastMessage(IVolumeSlicer::GeometryReadyMessage(*this));
     }
@@ -567,7 +567,7 @@
       BroadcastMessage(IVolumeSlicer::ContentChangedMessage(*this));
     }
 
-    const VolumeImageGeometry& GetProjectionGeometry(VolumeProjection projection)
+    const OLD_VolumeImageGeometry& GetProjectionGeometry(VolumeProjection projection)
     {
       if (!IsGeometryReady())
       {
@@ -676,7 +676,7 @@
       if (IsGeometryReady() &&
           DetectProjection(projection, viewportSlice))
       {
-        const VolumeImageGeometry& geometry = GetProjectionGeometry(projection);
+        const OLD_VolumeImageGeometry& geometry = GetProjectionGeometry(projection);
 
         size_t closest;
 
@@ -716,7 +716,7 @@
   private:
     SliceViewerWidget&                  widget_;
     VolumeProjection                    projection_;
-    std::auto_ptr<VolumeImageGeometry>  slices_;
+    std::auto_ptr<OLD_VolumeImageGeometry>  slices_;
     size_t                              slice_;
 
   protected:
@@ -727,7 +727,7 @@
         const OrthancVolumeImage& image =
           dynamic_cast<const OrthancVolumeImage&>(message.GetOrigin());
 
-        slices_.reset(new VolumeImageGeometry(image, projection_));
+        slices_.reset(new OLD_VolumeImageGeometry(image, projection_));
         SetSlice(slices_->GetSlicesCount() / 2);
 
         widget_.FitContent();
--- a/Platforms/Wasm/stone-framework-loader.ts	Wed May 15 16:56:17 2019 +0200
+++ b/Platforms/Wasm/stone-framework-loader.ts	Fri May 17 09:20:46 2019 +0200
@@ -81,12 +81,6 @@
           callback();
         }
       ],
-      print: function(text : string) {
-        Logger.defaultLogger.infoFromCpp(text);
-      },
-      printErr: function(text : string) {
-        Logger.defaultLogger.errorFromCpp(text);
-      },
       totalDependencies: 0
     };
 
--- a/Resources/CMake/OrthancStoneConfiguration.cmake	Wed May 15 16:56:17 2019 +0200
+++ b/Resources/CMake/OrthancStoneConfiguration.cmake	Fri May 17 09:20:46 2019 +0200
@@ -146,13 +146,19 @@
 
 
 if (ENABLE_OPENGL)
-  include(FindOpenGL)
-  if (NOT OPENGL_FOUND)
-    message(FATAL_ERROR "Cannot find OpenGL on your system")
+  if (NOT CMAKE_SYSTEM_NAME STREQUAL "Emscripten")
+    # If including "FindOpenGL.cmake" using Emscripten (targeting
+    # WebAssembly), the "OPENGL_LIBRARIES" value incorrectly includes
+    # the "nul" library, which leads to warning message in Emscripten:
+    # 'shared:WARNING: emcc: cannot find library "nul"'.
+    include(FindOpenGL)
+    if (NOT OPENGL_FOUND)
+      message(FATAL_ERROR "Cannot find OpenGL on your system")
+    endif()
+
+    link_libraries(${OPENGL_LIBRARIES})
   endif()
 
-  link_libraries(${OPENGL_LIBRARIES})
-
   add_definitions(
     -DGL_GLEXT_PROTOTYPES=1
     -DORTHANC_ENABLE_OPENGL=1
@@ -417,6 +423,7 @@
   ${ORTHANC_STONE_ROOT}/Framework/Toolbox/SlicesSorter.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Toolbox/UndoRedoStack.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Toolbox/ViewportGeometry.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Toolbox/VolumeImageGeometry.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Viewport/CairoContext.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Viewport/CairoSurface.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Viewport/IMouseTracker.h
--- a/Resources/CodeGeneration/stonegentool.py	Wed May 15 16:56:17 2019 +0200
+++ b/Resources/CodeGeneration/stonegentool.py	Fri May 17 09:20:46 2019 +0200
@@ -186,6 +186,7 @@
   RegisterTemplateFunction(template,NeedsCppConstruction)
   RegisterTemplateFunction(template, DefaultValueToTs)
   RegisterTemplateFunction(template, DefaultValueToCpp)
+  RegisterTemplateFunction(template, sorted)
   return template
 
 def MakeTemplateFromFile(templateFileName):
@@ -532,7 +533,7 @@
         if not (nextCh == ' ' or nextCh == '\n'):
           lineNumber = schemaText.count("\n",0,i) + 1
           raise RuntimeError("Error at line " + str(lineNumber) + " in the schema: colons must be followed by a space or a newline!")
-    schema = yaml.load(schemaText)
+    schema = yaml.load(schemaText, Loader = yaml.SafeLoader)
     return schema
 
 def GetTemplatingDictFromSchemaFilename(fn):
--- a/Resources/CodeGeneration/template.in.h.j2	Wed May 15 16:56:17 2019 +0200
+++ b/Resources/CodeGeneration/template.in.h.j2	Fri May 17 09:20:46 2019 +0200
@@ -394,9 +394,9 @@
 
   struct {{struct['name']}}
   {
-{% if struct %}{% if struct['fields'] %}{% for key in struct['fields']%}    {{CanonToCpp(struct['fields'][key]['type'])}} {{key}};
+{% if struct %}{% if struct['fields'] %}{% for key in sorted(struct['fields']) %}    {{CanonToCpp(struct['fields'][key]['type'])}} {{key}};
 {% endfor %}{% endif %}{% endif %}
-    {{struct['name']}}({% if struct %}{% if struct['fields'] %}{% for key in struct['fields']%}{{CanonToCpp(struct['fields'][key]['type'])}} {{key}} = {% if struct['fields'][key]['defaultValue'] %}{{DefaultValueToCpp(rootName,enums,struct['fields'][key])}} {%else%} {{CanonToCpp(struct['fields'][key]['type'])}}() {%endif%} {{ ", " if not loop.last }}{% endfor %}{% endif %}{% endif %})
+    {{struct['name']}}({% if struct %}{% if struct['fields'] %}{% for key in sorted(struct['fields']) %}{{CanonToCpp(struct['fields'][key]['type'])}} {{key}} = {% if struct['fields'][key]['defaultValue'] %}{{DefaultValueToCpp(rootName,enums,struct['fields'][key])}} {%else%} {{CanonToCpp(struct['fields'][key]['type'])}}() {%endif%} {{ ", " if not loop.last }}{% endfor %}{% endif %}{% endif %})
     {
 {% if struct %}{% if struct['fields'] %}{% for key in struct['fields']%}      this->{{key}} = {{key}};
 {% endfor %}{% endif %}{% endif %}    }
--- a/Samples/Sdl/Loader.cpp	Wed May 15 16:56:17 2019 +0200
+++ b/Samples/Sdl/Loader.cpp	Fri May 17 09:20:46 2019 +0200
@@ -25,8 +25,9 @@
 #include "../../Framework/Messages/MessageBroker.h"
 #include "../../Framework/StoneInitialization.h"
 #include "../../Framework/Toolbox/GeometryToolbox.h"
+#include "../../Framework/Toolbox/SlicesSorter.h"
 #include "../../Framework/Volumes/ImageBuffer3D.h"
-#include "../../Framework/Toolbox/SlicesSorter.h"
+#include "../../Framework/Scene2D/Scene2D.h"
 
 // From Orthanc framework
 #include <Core/Compression/GzipCompressor.h>
@@ -44,8 +45,8 @@
 #include <Core/Logging.h>
 #include <Core/MultiThreading/SharedMessageQueue.h>
 #include <Core/OrthancException.h>
+#include <Core/SystemToolbox.h>
 #include <Core/Toolbox.h>
-#include <Core/SystemToolbox.h>
 
 #include <json/reader.h>
 #include <json/value.h>
@@ -101,6 +102,17 @@
 
 
 
+  class IVolumeSlicer : public boost::noncopyable
+  {
+  public:
+    virtual ~IVolumeSlicer()
+    {
+    }
+
+    virtual void SetViewportPlane(const OrthancStone::CoordinateSystem3D& plane) = 0;
+  };
+
+
 
   class OracleCommandWithPayload : public IOracleCommand
   {
@@ -354,9 +366,11 @@
 
 
   private:
-    std::string    uri_;
-    HttpHeaders    headers_;
-    unsigned int   timeout_;
+    std::string           uri_;
+    HttpHeaders           headers_;
+    unsigned int          timeout_;
+    bool                  hasExpectedFormat_;
+    Orthanc::PixelFormat  expectedFormat_;
 
     std::auto_ptr< OrthancStone::MessageHandler<SuccessMessage> >  successCallback_;
     std::auto_ptr< OrthancStone::MessageHandler<OracleCommandExceptionMessage> >  failureCallback_;
@@ -364,7 +378,8 @@
   public:
     GetOrthancImageCommand() :
       uri_("/"),
-      timeout_(10)
+      timeout_(10),
+      hasExpectedFormat_(false)
     {
     }
 
@@ -373,6 +388,12 @@
       return Type_GetOrthancImage;
     }
 
+    void SetExpectedPixelFormat(Orthanc::PixelFormat format)
+    {
+      hasExpectedFormat_ = true;
+      expectedFormat_ = format;
+    }
+
     void SetUri(const std::string& uri)
     {
       uri_ = uri;
@@ -455,6 +476,20 @@
                                           std::string(Orthanc::EnumerationToString(contentType)));
       }
 
+      if (hasExpectedFormat_)
+      {
+        if (expectedFormat_ == Orthanc::PixelFormat_SignedGrayscale16 &&
+            image->GetFormat() == Orthanc::PixelFormat_Grayscale16)
+        {
+          image->SetFormat(Orthanc::PixelFormat_SignedGrayscale16);
+        }
+
+        if (expectedFormat_ != image->GetFormat())
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageFormat);
+        }
+      }
+
       SuccessMessage message(*this, image.release(), contentType);
       emitter.EmitMessage(receiver, message);
     }
@@ -515,7 +550,7 @@
       return Type_GetOrthancWebViewerJpeg;
     }
 
-    void SetExpectedFormat(Orthanc::PixelFormat format)
+    void SetExpectedPixelFormat(Orthanc::PixelFormat format)
     {
       expectedFormat_ = format;
     }
@@ -549,7 +584,7 @@
       headers_[key] = value;
     }
 
-    Orthanc::PixelFormat GetExpectedFormat() const
+    Orthanc::PixelFormat GetExpectedPixelFormat() const
     {
       return expectedFormat_;
     }
@@ -734,6 +769,974 @@
 
 
 
+  class DicomInstanceParameters :
+    public Orthanc::IDynamicObject  /* to be used as a payload of SlicesSorter */
+  {
+  private:
+    struct Data   // Struct to ease the copy constructor
+    {
+      std::string                       orthancInstanceId_;
+      std::string                       studyInstanceUid_;
+      std::string                       seriesInstanceUid_;
+      std::string                       sopInstanceUid_;
+      Orthanc::DicomImageInformation    imageInformation_;
+      OrthancStone::SopClassUid         sopClassUid_;
+      double                            thickness_;
+      double                            pixelSpacingX_;
+      double                            pixelSpacingY_;
+      OrthancStone::CoordinateSystem3D  geometry_;
+      OrthancStone::Vector              frameOffsets_;
+      bool                              isColor_;
+      bool                              hasRescale_;
+      double                            rescaleOffset_;
+      double                            rescaleSlope_;
+      bool                              hasDefaultWindowing_;
+      float                             defaultWindowingCenter_;
+      float                             defaultWindowingWidth_;
+      Orthanc::PixelFormat              expectedPixelFormat_;
+
+      void ComputeDoseOffsets(const Orthanc::DicomMap& dicom)
+      {
+        // http://dicom.nema.org/medical/Dicom/2016a/output/chtml/part03/sect_C.8.8.3.2.html
+
+        {
+          std::string increment;
+
+          if (dicom.CopyToString(increment, Orthanc::DICOM_TAG_FRAME_INCREMENT_POINTER, false))
+          {
+            Orthanc::Toolbox::ToUpperCase(increment);
+            if (increment != "3004,000C")  // This is the "Grid Frame Offset Vector" tag
+            {
+              LOG(ERROR) << "RT-DOSE: Bad value for the \"FrameIncrementPointer\" tag";
+              return;
+            }
+          }
+        }
+
+        if (!OrthancStone::LinearAlgebra::ParseVector(frameOffsets_, dicom, Orthanc::DICOM_TAG_GRID_FRAME_OFFSET_VECTOR) ||
+            frameOffsets_.size() < imageInformation_.GetNumberOfFrames())
+        {
+          LOG(ERROR) << "RT-DOSE: No information about the 3D location of some slice(s)";
+          frameOffsets_.clear();
+        }
+        else
+        {
+          if (frameOffsets_.size() >= 2)
+          {
+            thickness_ = frameOffsets_[1] - frameOffsets_[0];
+
+            if (thickness_ < 0)
+            {
+              thickness_ = -thickness_;
+            }
+          }
+        }
+      }
+
+      Data(const Orthanc::DicomMap& dicom) :
+        imageInformation_(dicom)
+      {
+        if (imageInformation_.GetNumberOfFrames() <= 0)
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+        }
+
+        if (!dicom.CopyToString(studyInstanceUid_, Orthanc::DICOM_TAG_STUDY_INSTANCE_UID, false) ||
+            !dicom.CopyToString(seriesInstanceUid_, Orthanc::DICOM_TAG_SERIES_INSTANCE_UID, false) ||
+            !dicom.CopyToString(sopInstanceUid_, Orthanc::DICOM_TAG_SOP_INSTANCE_UID, false))
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+        }
+        
+        std::string s;
+        if (!dicom.CopyToString(s, Orthanc::DICOM_TAG_SOP_CLASS_UID, false))
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+        }
+        else
+        {
+          sopClassUid_ = OrthancStone::StringToSopClassUid(s);
+        }
+
+        if (!dicom.ParseDouble(thickness_, Orthanc::DICOM_TAG_SLICE_THICKNESS))
+        {
+          thickness_ = 100.0 * std::numeric_limits<double>::epsilon();
+        }
+
+        OrthancStone::GeometryToolbox::GetPixelSpacing(pixelSpacingX_, pixelSpacingY_, dicom);
+
+        std::string position, orientation;
+        if (dicom.CopyToString(position, Orthanc::DICOM_TAG_IMAGE_POSITION_PATIENT, false) &&
+            dicom.CopyToString(orientation, Orthanc::DICOM_TAG_IMAGE_ORIENTATION_PATIENT, false))
+        {
+          geometry_ = OrthancStone::CoordinateSystem3D(position, orientation);
+        }
+
+        if (sopClassUid_ == OrthancStone::SopClassUid_RTDose)
+        {
+          ComputeDoseOffsets(dicom);
+        }
+
+        isColor_ = (imageInformation_.GetPhotometricInterpretation() != Orthanc::PhotometricInterpretation_Monochrome1 &&
+                    imageInformation_.GetPhotometricInterpretation() != Orthanc::PhotometricInterpretation_Monochrome2);
+
+        double doseGridScaling;
+
+        if (dicom.ParseDouble(rescaleOffset_, Orthanc::DICOM_TAG_RESCALE_INTERCEPT) &&
+            dicom.ParseDouble(rescaleSlope_, Orthanc::DICOM_TAG_RESCALE_SLOPE))
+        {
+          hasRescale_ = true;
+        }
+        else if (dicom.ParseDouble(doseGridScaling, Orthanc::DICOM_TAG_DOSE_GRID_SCALING))
+        {
+          hasRescale_ = true;
+          rescaleOffset_ = 0;
+          rescaleSlope_ = doseGridScaling;
+        }
+        else
+        {
+          hasRescale_ = false;
+        }
+
+        OrthancStone::Vector c, w;
+        if (OrthancStone::LinearAlgebra::ParseVector(c, dicom, Orthanc::DICOM_TAG_WINDOW_CENTER) &&
+            OrthancStone::LinearAlgebra::ParseVector(w, dicom, Orthanc::DICOM_TAG_WINDOW_WIDTH) &&
+            c.size() > 0 && 
+            w.size() > 0)
+        {
+          hasDefaultWindowing_ = true;
+          defaultWindowingCenter_ = static_cast<float>(c[0]);
+          defaultWindowingWidth_ = static_cast<float>(w[0]);
+        }
+        else
+        {
+          hasDefaultWindowing_ = false;
+        }
+
+        if (sopClassUid_ == OrthancStone::SopClassUid_RTDose)
+        {
+          switch (imageInformation_.GetBitsStored())
+          {
+            case 16:
+              expectedPixelFormat_ = Orthanc::PixelFormat_Grayscale16;
+              break;
+
+            case 32:
+              expectedPixelFormat_ = Orthanc::PixelFormat_Grayscale32;
+              break;
+
+            default:
+              throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+          } 
+        }
+        else if (isColor_)
+        {
+          expectedPixelFormat_ = Orthanc::PixelFormat_RGB24;
+        }
+        else if (imageInformation_.IsSigned())
+        {
+          expectedPixelFormat_ = Orthanc::PixelFormat_SignedGrayscale16;
+        }
+        else
+        {
+          expectedPixelFormat_ = Orthanc::PixelFormat_Grayscale16;
+        }
+      }
+
+      OrthancStone::CoordinateSystem3D  GetFrameGeometry(unsigned int frame) const
+      {
+        if (frame == 0)
+        {
+          return geometry_;
+        }
+        else if (frame >= imageInformation_.GetNumberOfFrames())
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+        }
+        else if (sopClassUid_ == OrthancStone::SopClassUid_RTDose)
+        {
+          if (frame >= frameOffsets_.size())
+          {
+            throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+          }
+
+          return OrthancStone::CoordinateSystem3D(
+            geometry_.GetOrigin() + frameOffsets_[frame] * geometry_.GetNormal(),
+            geometry_.GetAxisX(),
+            geometry_.GetAxisY());
+        }
+        else
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+        }
+      }
+
+      // TODO - Is this necessary?
+      bool FrameContainsPlane(unsigned int frame,
+                              const OrthancStone::CoordinateSystem3D& plane) const
+      {
+        if (frame >= imageInformation_.GetNumberOfFrames())
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+        }
+
+        OrthancStone::CoordinateSystem3D tmp = geometry_;
+
+        if (frame != 0)
+        {
+          tmp = GetFrameGeometry(frame);
+        }
+
+        double distance;
+
+        return (OrthancStone::CoordinateSystem3D::GetDistance(distance, tmp, plane) &&
+                distance <= thickness_ / 2.0);
+      }
+    };
+    
+    Data  data_;
+
+
+  public:
+    DicomInstanceParameters(const DicomInstanceParameters& other) :
+      data_(other.data_)
+    {
+    }
+
+    DicomInstanceParameters(const Orthanc::DicomMap& dicom) :
+      data_(dicom)
+    {
+    }
+
+    void SetOrthancInstanceIdentifier(const std::string& id)
+    {
+      data_.orthancInstanceId_ = id;
+    }
+
+    const std::string& GetOrthancInstanceIdentifier() const
+    {
+      return data_.orthancInstanceId_;
+    }
+
+    const Orthanc::DicomImageInformation& GetImageInformation() const
+    {
+      return data_.imageInformation_;
+    }
+
+    const std::string& GetStudyInstanceUid() const
+    {
+      return data_.studyInstanceUid_;
+    }
+
+    const std::string& GetSeriesInstanceUid() const
+    {
+      return data_.seriesInstanceUid_;
+    }
+
+    const std::string& GetSopInstanceUid() const
+    {
+      return data_.sopInstanceUid_;
+    }
+
+    OrthancStone::SopClassUid GetSopClassUid() const
+    {
+      return data_.sopClassUid_;
+    }
+
+    double GetThickness() const
+    {
+      return data_.thickness_;
+    }
+
+    double GetPixelSpacingX() const
+    {
+      return data_.pixelSpacingX_;
+    }
+
+    double GetPixelSpacingY() const
+    {
+      return data_.pixelSpacingY_;
+    }
+
+    const OrthancStone::CoordinateSystem3D&  GetGeometry() const
+    {
+      return data_.geometry_;
+    }
+
+    OrthancStone::CoordinateSystem3D  GetFrameGeometry(unsigned int frame) const
+    {
+      return data_.GetFrameGeometry(frame);
+    }
+
+    // TODO - Is this necessary?
+    bool FrameContainsPlane(unsigned int frame,
+                            const OrthancStone::CoordinateSystem3D& plane) const
+    {
+      return data_.FrameContainsPlane(frame, plane);
+    }
+
+    bool IsColor() const
+    {
+      return data_.isColor_;
+    }
+
+    bool HasRescale() const
+    {
+      return data_.hasRescale_;
+    }
+
+    double GetRescaleOffset() const
+    {
+      if (data_.hasRescale_)
+      {
+        return data_.rescaleOffset_;
+      }
+      else
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+      }
+    }
+
+    double GetRescaleSlope() const
+    {
+      if (data_.hasRescale_)
+      {
+        return data_.rescaleSlope_;
+      }
+      else
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+      }
+    }
+
+    bool HasDefaultWindowing() const
+    {
+      return data_.hasDefaultWindowing_;
+    }
+
+    float GetDefaultWindowingCenter() const
+    {
+      if (data_.hasDefaultWindowing_)
+      {
+        return data_.defaultWindowingCenter_;
+      }
+      else
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+      }
+    }
+
+    float GetDefaultWindowingWidth() const
+    {
+      if (data_.hasDefaultWindowing_)
+      {
+        return data_.defaultWindowingWidth_;
+      }
+      else
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+      }
+    }
+
+    Orthanc::PixelFormat GetExpectedPixelFormat() const
+    {
+      return data_.expectedPixelFormat_;
+    }
+  };
+
+
+  class DicomVolumeImage : public boost::noncopyable
+  {
+  private:
+    std::auto_ptr<OrthancStone::ImageBuffer3D>  image_;
+    std::vector<DicomInstanceParameters*>       slices_;
+    uint64_t                                    revision_;
+    std::vector<uint64_t>                       slicesRevision_;
+
+    void CheckSlice(size_t index,
+                    const DicomInstanceParameters& reference) const
+    {
+      const DicomInstanceParameters& slice = *slices_[index];
+      
+      if (!OrthancStone::GeometryToolbox::IsParallel(
+            reference.GetGeometry().GetNormal(),
+            slice.GetGeometry().GetNormal()))
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadGeometry,
+                                        "A slice in the volume image is not parallel to the others");
+      }
+
+      if (reference.GetExpectedPixelFormat() != slice.GetExpectedPixelFormat())
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageFormat,
+                                        "The pixel format changes across the slices of the volume image");
+      }
+
+      if (reference.GetImageInformation().GetWidth() != slice.GetImageInformation().GetWidth() ||
+          reference.GetImageInformation().GetHeight() != slice.GetImageInformation().GetHeight())
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageSize,
+                                        "The width/height of slices are not constant in the volume image");
+      }
+
+      if (!OrthancStone::LinearAlgebra::IsNear(reference.GetPixelSpacingX(), slice.GetPixelSpacingX()) ||
+          !OrthancStone::LinearAlgebra::IsNear(reference.GetPixelSpacingY(), slice.GetPixelSpacingY()))
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadGeometry,
+                                        "The pixel spacing of the slices change across the volume image");
+      }
+    }
+
+    
+    void CheckVolume() const
+    {
+      for (size_t i = 0; i < slices_.size(); i++)
+      {
+        assert(slices_[i] != NULL);
+        if (slices_[i]->GetImageInformation().GetNumberOfFrames() != 1)
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_BadGeometry,
+                                          "This class does not support multi-frame images");
+        }
+      }
+
+      if (slices_.size() != 0)
+      {
+        const DicomInstanceParameters& reference = *slices_[0];
+
+        for (size_t i = 1; i < slices_.size(); i++)
+        {
+          CheckSlice(i, reference);
+        }
+      }
+    }
+
+
+    void Clear()
+    {
+      image_.reset();
+      
+      for (size_t i = 0; i < slices_.size(); i++)
+      {
+        assert(slices_[i] != NULL);
+        delete slices_[i];
+      }
+    }
+
+
+    void CheckSliceIndex(size_t index) const
+    {
+      assert(slices_.size() == image_->GetDepth() &&
+             slices_.size() == slicesRevision_.size());
+
+      if (!HasGeometry())
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+      }
+      else if (index >= slices_.size())
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+      }
+    }
+
+    
+  public:
+    DicomVolumeImage()
+    {
+    }
+
+    ~DicomVolumeImage()
+    {
+      Clear();
+    }
+
+    // WARNING: The payload of "slices" must be of class "DicomInstanceParameters"
+    void SetGeometry(OrthancStone::SlicesSorter& slices)
+    {
+      Clear();
+      
+      if (!slices.Sort())
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange,
+                                        "Cannot sort the 3D slices of a DICOM series");          
+      }
+
+      if (slices.GetSlicesCount() == 0)
+      {
+        // Empty volume
+        image_.reset(new OrthancStone::ImageBuffer3D(Orthanc::PixelFormat_Grayscale8, 0, 0, 0,
+                                                     false /* don't compute range */));
+      }
+      else
+      {
+        slices_.reserve(slices.GetSlicesCount());
+        slicesRevision_.resize(slices.GetSlicesCount());
+
+        for (size_t i = 0; i < slices.GetSlicesCount(); i++)
+        {
+          const DicomInstanceParameters& slice =
+            dynamic_cast<const DicomInstanceParameters&>(slices.GetSlicePayload(i));
+          slices_.push_back(new DicomInstanceParameters(slice));
+          slicesRevision_[i] = 0;
+        }
+
+        CheckVolume();
+
+        const double spacingZ = slices.ComputeSpacingBetweenSlices();
+        LOG(INFO) << "Computed spacing between slices: " << spacingZ << "mm";
+      
+        const DicomInstanceParameters& parameters = *slices_[0];
+
+        image_.reset(new OrthancStone::ImageBuffer3D(parameters.GetExpectedPixelFormat(),
+                                                     parameters.GetImageInformation().GetWidth(),
+                                                     parameters.GetImageInformation().GetHeight(),
+                                                     slices.GetSlicesCount(), false /* don't compute range */));      
+
+        image_->GetGeometry().SetAxialGeometry(slices.GetSliceGeometry(0));
+        image_->GetGeometry().SetVoxelDimensions(parameters.GetPixelSpacingX(),
+                                                 parameters.GetPixelSpacingY(), spacingZ);
+      }
+      
+      image_->Clear();
+
+      revision_++;
+    }
+
+    uint64_t GetRevision() const
+    {
+      return revision_;
+    }
+
+    bool HasGeometry() const
+    {
+      return (image_.get() != NULL);
+    }
+
+    const OrthancStone::ImageBuffer3D& GetImage() const
+    {
+      if (!HasGeometry())
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+      }
+      else
+      {
+        return *image_;
+      }
+    }
+
+    size_t GetSlicesCount() const
+    {
+      if (!HasGeometry())
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+      }
+      else
+      {
+        return slices_.size();
+      }
+    }
+
+    const DicomInstanceParameters& GetSliceParameters(size_t index) const
+    {
+      CheckSliceIndex(index);
+      return *slices_[index];
+    }
+
+    uint64_t GetSliceRevision(size_t index) const
+    {
+      CheckSliceIndex(index);
+      return slicesRevision_[index];
+    }
+
+    void SetSliceContent(size_t index,
+                         const Orthanc::ImageAccessor& image)
+    {
+      CheckSliceIndex(index);
+      
+      {
+        OrthancStone::ImageBuffer3D::SliceWriter writer
+          (*image_, OrthancStone::VolumeProjection_Axial, index);
+        Orthanc::ImageProcessing::Copy(writer.GetAccessor(), image);
+      }
+
+      revision_ ++;
+      slicesRevision_[index] += 1;
+    }
+  };
+  
+  
+
+  class VolumeSeriesOrthancLoader : public OrthancStone::IObserver
+  {
+  private:
+    class MessageHandler : public Orthanc::IDynamicObject
+    {
+    public:
+      virtual void Handle(const Json::Value& body) const
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+      }
+
+      virtual void Handle(const Orthanc::ImageAccessor& image) const
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+      }
+    };
+
+    void Handle(const OrthancRestApiCommand::SuccessMessage& message)
+    {
+      Json::Value body;
+      message.ParseJsonBody(body);
+
+      if (body.type() != Json::objectValue)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol);
+      }
+
+      dynamic_cast<const MessageHandler&>(message.GetOrigin().GetPayload()).Handle(body);
+    }
+
+    void Handle(const Refactoring::GetOrthancImageCommand::SuccessMessage& message)
+    {
+      dynamic_cast<const MessageHandler&>(message.GetOrigin().GetPayload()).Handle(message.GetImage());
+    }
+
+    void Handle(const Refactoring::GetOrthancWebViewerJpegCommand::SuccessMessage& message)
+    {
+      dynamic_cast<const MessageHandler&>(message.GetOrigin().GetPayload()).Handle(message.GetImage());
+    }
+
+
+    class LoadSliceImage : public MessageHandler
+    {
+    private:
+      DicomVolumeImage&   target_;
+      size_t              slice_;
+
+    public:
+      LoadSliceImage(DicomVolumeImage& target,
+                     size_t slice) :
+        target_(target),
+        slice_(slice)
+      {
+      }
+
+      virtual void Handle(const Orthanc::ImageAccessor& image) const
+      {
+        target_.SetSliceContent(slice_, image);
+      }
+    };
+
+
+    class LoadSeriesGeometryHandler : public MessageHandler
+    {
+    private:
+      VolumeSeriesOrthancLoader&  that_;
+
+    public:
+      LoadSeriesGeometryHandler(VolumeSeriesOrthancLoader& that) :
+      that_(that)
+      {
+      }
+
+      virtual void Handle(const Json::Value& body) const
+      {
+        Json::Value::Members instances = body.getMemberNames();
+
+        OrthancStone::SlicesSorter slices;
+        
+        for (size_t i = 0; i < instances.size(); i++)
+        {
+          Orthanc::DicomMap dicom;
+          dicom.FromDicomAsJson(body[instances[i]]);
+
+          std::auto_ptr<DicomInstanceParameters> instance(new DicomInstanceParameters(dicom));
+          instance->SetOrthancInstanceIdentifier(instances[i]);
+
+          OrthancStone::CoordinateSystem3D geometry = instance->GetGeometry();
+          slices.AddSlice(geometry, instance.release());
+        }
+
+        that_.volume_.SetGeometry(slices);
+
+        {
+          OrthancStone::LinearAlgebra::Print(that_.volume_.GetImage().GetGeometry().GetCoordinates(0, 0, 0));
+          OrthancStone::LinearAlgebra::Print(that_.volume_.GetImage().GetGeometry().GetCoordinates(1, 1, 1));
+          return;
+        }
+
+        for (size_t i = 0; i < that_.volume_.GetSlicesCount(); i++)
+        {
+          const DicomInstanceParameters& slice = that_.volume_.GetSliceParameters(i);
+          
+          const std::string& instance = slice.GetOrthancInstanceIdentifier();
+          if (instance.empty())
+          {
+            throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+          }
+
+#if 0
+          std::auto_ptr<Refactoring::GetOrthancWebViewerJpegCommand> command(
+            new Refactoring::GetOrthancWebViewerJpegCommand);
+          command->SetInstance(instance);
+          command->SetQuality(95);
+#else
+          std::string uri = "/instances/" + instance;
+          
+          switch (slice.GetExpectedPixelFormat())
+          {
+            case Orthanc::PixelFormat_RGB24:
+              uri += "/preview";
+              break;
+      
+            case Orthanc::PixelFormat_Grayscale16:
+              uri += "/image-uint16";
+              break;
+      
+            case Orthanc::PixelFormat_SignedGrayscale16:
+              uri += "/image-int16";
+              break;
+      
+            default:
+              throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+          }
+          
+          std::auto_ptr<Refactoring::GetOrthancImageCommand> command(
+            new Refactoring::GetOrthancImageCommand);
+          command->SetHttpHeader("Accept-Encoding", "gzip");
+          command->SetHttpHeader("Accept", std::string(Orthanc::EnumerationToString(Orthanc::MimeType_Pam)));
+          command->SetUri(uri);
+#endif
+
+          command->SetExpectedPixelFormat(slice.GetExpectedPixelFormat());
+          command->SetPayload(new LoadSliceImage(that_.volume_, i));
+
+          that_.oracle_.Schedule(that_, command.release());
+        }
+      }
+    };
+
+
+    class LoadInstanceGeometryHandler : public MessageHandler
+    {
+    private:
+      VolumeSeriesOrthancLoader&  that_;
+
+    public:
+      LoadInstanceGeometryHandler(VolumeSeriesOrthancLoader& that) :
+      that_(that)
+      {
+      }
+
+      virtual void Handle(const Json::Value& body) const
+      {
+        Orthanc::DicomMap dicom;
+        dicom.FromDicomAsJson(body);
+
+        DicomInstanceParameters instance(dicom);
+      }
+    };
+
+
+    IOracle&          oracle_;
+    bool              active_;
+    DicomVolumeImage  volume_;
+
+  public:
+    VolumeSeriesOrthancLoader(IOracle& oracle,
+                              OrthancStone::IObservable& oracleObservable) :
+      IObserver(oracleObservable.GetBroker()),
+      oracle_(oracle),
+      active_(false)
+    {
+      oracleObservable.RegisterObserverCallback(
+        new OrthancStone::Callable<VolumeSeriesOrthancLoader, OrthancRestApiCommand::SuccessMessage>
+        (*this, &VolumeSeriesOrthancLoader::Handle));
+
+      oracleObservable.RegisterObserverCallback(
+        new OrthancStone::Callable<VolumeSeriesOrthancLoader, GetOrthancImageCommand::SuccessMessage>
+        (*this, &VolumeSeriesOrthancLoader::Handle));
+
+      oracleObservable.RegisterObserverCallback(
+        new OrthancStone::Callable<VolumeSeriesOrthancLoader, GetOrthancWebViewerJpegCommand::SuccessMessage>
+        (*this, &VolumeSeriesOrthancLoader::Handle));
+    }
+
+    void LoadSeries(const std::string& seriesId)
+    {
+      if (active_)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+      }
+
+      active_ = true;
+
+      std::auto_ptr<Refactoring::OrthancRestApiCommand> command(new Refactoring::OrthancRestApiCommand);
+      command->SetUri("/series/" + seriesId + "/instances-tags");
+      command->SetPayload(new LoadSeriesGeometryHandler(*this));
+
+      oracle_.Schedule(*this, command.release());
+    }
+
+    void LoadInstance(const std::string& instanceId)
+    {
+      if (active_)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+      }
+
+      active_ = true;
+
+      // Tag "3004-000c" is "Grid Frame Offset Vector", which is
+      // mandatory to read RT DOSE, but is too long to be returned by default
+
+      // TODO => Should be part of a second call if needed
+
+      std::auto_ptr<Refactoring::OrthancRestApiCommand> command(new Refactoring::OrthancRestApiCommand);
+      command->SetUri("/instances/" + instanceId + "/tags?ignore-length=3004-000c");
+      command->SetPayload(new LoadInstanceGeometryHandler(*this));
+
+      oracle_.Schedule(*this, command.release());
+    }
+  };
+
+
+
+
+  /*  class VolumeSlicerBase : public IVolumeSlicer
+  {
+  private:
+    OrthancStone::Scene2D&            scene_;
+    int                               layerDepth_;
+    bool                              first_;
+    OrthancStone::CoordinateSystem3D  lastPlane_;
+
+  protected:
+    bool HasViewportPlaneChanged(const OrthancStone::CoordinateSystem3D& plane) const
+    {
+      if (first_ ||
+          !OrthancStone::LinearAlgebra::IsCloseToZero(
+            boost::numeric::ublas::norm_2(lastPlane_.GetNormal() - plane.GetNormal())))
+      {
+        // This is the first rendering, or the plane has not the same orientation
+        return false;
+      }
+      else
+      {
+        double offset1 = lastPlane_.ProjectAlongNormal(plane.GetOrigin());
+        double offset2 = lastPlane_.ProjectAlongNormal(lastPlane_.GetOrigin());
+        return OrthancStone::LinearAlgebra::IsCloseToZero(offset2 - offset1);
+      }
+    }
+
+    void SetLastViewportPlane(const OrthancStone::CoordinateSystem3D& plane)
+    {
+      first_ = false;
+      lastPlane_ = plane;
+    }
+
+    void SetLayer(OrthancStone::ISceneLayer* layer)
+    {
+      scene_.SetLayer(layerDepth_, layer);
+    }
+
+    void DeleteLayer()
+    {
+      scene_.DeleteLayer(layerDepth_);
+    }
+    
+  public:
+    VolumeSlicerBase(OrthancStone::Scene2D& scene,
+                     int layerDepth) :
+      scene_(scene),
+      layerDepth_(layerDepth),
+      first_(true)
+    {
+    }
+    };*/
+  
+
+
+  class DicomVolumeSlicer : public IVolumeSlicer
+  {
+  private:
+    OrthancStone::Scene2D&          scene_;
+    int                             layerDepth_;
+    const DicomVolumeImage&         volume_;
+    bool                            first_;
+    OrthancStone::VolumeProjection  lastProjection_;
+    unsigned int                    lastSliceIndex_;
+    uint64_t                        lastSliceRevision_;
+
+  public:
+    DicomVolumeSlicer(OrthancStone::Scene2D& scene,
+                      int layerDepth,
+                      const DicomVolumeImage& volume) :
+      scene_(scene),
+      layerDepth_(layerDepth),
+      volume_(volume),
+      first_(true)
+    {
+    }
+    
+    virtual void SetViewportPlane(const OrthancStone::CoordinateSystem3D& plane)
+    {
+      if (!volume_.HasGeometry())
+      {
+        scene_.DeleteLayer(layerDepth_);
+        return;
+      }
+
+      OrthancStone::VolumeProjection projection;
+      unsigned int sliceIndex;
+      if (!volume_.GetImage().GetGeometry().DetectSlice(projection, sliceIndex, plane))
+      {
+        // The cutting plane is neither axial, nor coronal, nor
+        // sagittal. Could use "VolumeReslicer" here.
+        scene_.DeleteLayer(layerDepth_);
+        return;
+      }
+
+      uint64_t sliceRevision;
+      if (projection == OrthancStone::VolumeProjection_Axial)
+      {
+        sliceRevision = volume_.GetSliceRevision(sliceIndex);
+      }
+      else
+      {
+        // For coronal and sagittal projections, we take the global
+        // revision of the volume
+        sliceRevision = volume_.GetRevision();
+      }
+
+      if (first_ ||
+          lastProjection_ == projection ||
+          lastSliceIndex_ == sliceIndex ||
+          lastSliceRevision_ == sliceRevision)
+      {
+        // Eiter the viewport plane, or the content of the slice have not
+        // changed since the last time the layer was set: Update is needed
+
+        first_ = false;
+        lastProjection_ = projection;
+        lastSliceIndex_ = sliceIndex;
+        lastSliceRevision_ = sliceRevision;
+        
+        {
+          OrthancStone::ImageBuffer3D::SliceReader reader(volume_.GetImage(), projection, sliceIndex);
+
+          // TODO: Convert the image to Float32 or RGB24
+          
+          // TODO: Set the layer
+        }
+      }
+    }
+  };
+  
+  
+
+
+
   class NativeOracle : public IOracle
   {
   private:
@@ -1067,7 +2070,6 @@
   };
 
 
-
   class NativeApplicationContext : public IMessageEmitter
   {
   private:
@@ -1136,463 +2138,6 @@
       }
     };
   };
-
-
-
-  class DicomInstanceParameters :
-    public Orthanc::IDynamicObject  /* to be used as a payload of SlicesSorter */
-  {
-  private:
-    Orthanc::DicomImageInformation    imageInformation_;
-    OrthancStone::SopClassUid         sopClassUid_;
-    double                            thickness_;
-    double                            pixelSpacingX_;
-    double                            pixelSpacingY_;
-    OrthancStone::CoordinateSystem3D  geometry_;
-    OrthancStone::Vector              frameOffsets_;
-    bool                              isColor_;
-    bool                              hasRescale_;
-    double                            rescaleOffset_;
-    double                            rescaleSlope_;
-    bool                              hasDefaultWindowing_;
-    float                             defaultWindowingCenter_;
-    float                             defaultWindowingWidth_;
-    Orthanc::PixelFormat              expectedPixelFormat_;
-
-    void ComputeDoseOffsets(const Orthanc::DicomMap& dicom)
-    {
-      // http://dicom.nema.org/medical/Dicom/2016a/output/chtml/part03/sect_C.8.8.3.2.html
-
-      {
-        std::string increment;
-
-        if (dicom.CopyToString(increment, Orthanc::DICOM_TAG_FRAME_INCREMENT_POINTER, false))
-        {
-          Orthanc::Toolbox::ToUpperCase(increment);
-          if (increment != "3004,000C")  // This is the "Grid Frame Offset Vector" tag
-          {
-            LOG(ERROR) << "RT-DOSE: Bad value for the \"FrameIncrementPointer\" tag";
-            return;
-          }
-        }
-      }
-
-      if (!OrthancStone::LinearAlgebra::ParseVector(frameOffsets_, dicom, Orthanc::DICOM_TAG_GRID_FRAME_OFFSET_VECTOR) ||
-          frameOffsets_.size() < imageInformation_.GetNumberOfFrames())
-      {
-        LOG(ERROR) << "RT-DOSE: No information about the 3D location of some slice(s)";
-        frameOffsets_.clear();
-      }
-      else
-      {
-        if (frameOffsets_.size() >= 2)
-        {
-          thickness_ = frameOffsets_[1] - frameOffsets_[0];
-
-          if (thickness_ < 0)
-          {
-            thickness_ = -thickness_;
-          }
-        }
-      }
-    }
-
-  public:
-    DicomInstanceParameters(const Orthanc::DicomMap& dicom) :
-      imageInformation_(dicom)
-    {
-      if (imageInformation_.GetNumberOfFrames() <= 0)
-      {
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
-      }
-            
-      std::string s;
-      if (!dicom.CopyToString(s, Orthanc::DICOM_TAG_SOP_CLASS_UID, false))
-      {
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
-      }
-      else
-      {
-        sopClassUid_ = OrthancStone::StringToSopClassUid(s);
-      }
-
-      if (!dicom.ParseDouble(thickness_, Orthanc::DICOM_TAG_SLICE_THICKNESS))
-      {
-        thickness_ = 100.0 * std::numeric_limits<double>::epsilon();
-      }
-
-      OrthancStone::GeometryToolbox::GetPixelSpacing(pixelSpacingX_, pixelSpacingY_, dicom);
-
-      std::string position, orientation;
-      if (dicom.CopyToString(position, Orthanc::DICOM_TAG_IMAGE_POSITION_PATIENT, false) &&
-          dicom.CopyToString(orientation, Orthanc::DICOM_TAG_IMAGE_ORIENTATION_PATIENT, false))
-      {
-        geometry_ = OrthancStone::CoordinateSystem3D(position, orientation);
-      }
-
-      if (sopClassUid_ == OrthancStone::SopClassUid_RTDose)
-      {
-        ComputeDoseOffsets(dicom);
-      }
-
-      isColor_ = (imageInformation_.GetPhotometricInterpretation() != Orthanc::PhotometricInterpretation_Monochrome1 &&
-                  imageInformation_.GetPhotometricInterpretation() != Orthanc::PhotometricInterpretation_Monochrome2);
-
-      double doseGridScaling;
-
-      if (dicom.ParseDouble(rescaleOffset_, Orthanc::DICOM_TAG_RESCALE_INTERCEPT) &&
-          dicom.ParseDouble(rescaleSlope_, Orthanc::DICOM_TAG_RESCALE_SLOPE))
-      {
-        hasRescale_ = true;
-      }
-      else if (dicom.ParseDouble(doseGridScaling, Orthanc::DICOM_TAG_DOSE_GRID_SCALING))
-      {
-        hasRescale_ = true;
-        rescaleOffset_ = 0;
-        rescaleSlope_ = doseGridScaling;
-      }
-      else
-      {
-        hasRescale_ = false;
-      }
-
-      OrthancStone::Vector c, w;
-      if (OrthancStone::LinearAlgebra::ParseVector(c, dicom, Orthanc::DICOM_TAG_WINDOW_CENTER) &&
-          OrthancStone::LinearAlgebra::ParseVector(w, dicom, Orthanc::DICOM_TAG_WINDOW_WIDTH) &&
-          c.size() > 0 && 
-          w.size() > 0)
-      {
-        hasDefaultWindowing_ = true;
-        defaultWindowingCenter_ = static_cast<float>(c[0]);
-        defaultWindowingWidth_ = static_cast<float>(w[0]);
-      }
-      else
-      {
-        hasDefaultWindowing_ = false;
-      }
-
-      if (sopClassUid_ == OrthancStone::SopClassUid_RTDose)
-      {
-        switch (imageInformation_.GetBitsStored())
-        {
-          case 16:
-            expectedPixelFormat_ = Orthanc::PixelFormat_Grayscale16;
-            break;
-
-          case 32:
-            expectedPixelFormat_ = Orthanc::PixelFormat_Grayscale32;
-            break;
-
-          default:
-            throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
-        } 
-      }
-      else if (isColor_)
-      {
-        expectedPixelFormat_ = Orthanc::PixelFormat_RGB24;
-      }
-      else if (imageInformation_.IsSigned())
-      {
-        expectedPixelFormat_ = Orthanc::PixelFormat_SignedGrayscale16;
-      }
-      else
-      {
-        expectedPixelFormat_ = Orthanc::PixelFormat_Grayscale16;
-      }
-    }
-
-    const Orthanc::DicomImageInformation& GetImageInformation() const
-    {
-      return imageInformation_;
-    }
-
-    OrthancStone::SopClassUid GetSopClassUid() const
-    {
-      return sopClassUid_;
-    }
-
-    double GetThickness() const
-    {
-      return thickness_;
-    }
-
-    double GetPixelSpacingX() const
-    {
-      return pixelSpacingX_;
-    }
-
-    double GetPixelSpacingY() const
-    {
-      return pixelSpacingY_;
-    }
-
-    const OrthancStone::CoordinateSystem3D&  GetGeometry() const
-    {
-      return geometry_;
-    }
-
-    OrthancStone::CoordinateSystem3D  GetFrameGeometry(unsigned int frame) const
-    {
-      if (frame == 0)
-      {
-        return geometry_;
-      }
-      else if (frame >= imageInformation_.GetNumberOfFrames())
-      {
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
-      }
-      else if (sopClassUid_ == OrthancStone::SopClassUid_RTDose)
-      {
-        if (frame >= frameOffsets_.size())
-        {
-          throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
-        }
-
-        return OrthancStone::CoordinateSystem3D(
-          geometry_.GetOrigin() + frameOffsets_[frame] * geometry_.GetNormal(),
-          geometry_.GetAxisX(),
-          geometry_.GetAxisY());
-      }
-      else
-      {
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
-      }
-    }
-
-    bool FrameContainsPlane(unsigned int frame,
-                            const OrthancStone::CoordinateSystem3D& plane) const
-    {
-      if (frame >= imageInformation_.GetNumberOfFrames())
-      {
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
-      }
-
-      OrthancStone::CoordinateSystem3D tmp = geometry_;
-
-      if (frame != 0)
-      {
-        tmp = GetFrameGeometry(frame);
-      }
-
-      double distance;
-
-      return (OrthancStone::CoordinateSystem3D::GetDistance(distance, tmp, plane) &&
-              distance <= thickness_ / 2.0);
-    }
-
-    bool IsColor() const
-    {
-      return isColor_;
-    }
-
-    bool HasRescale() const
-    {
-      return hasRescale_;
-    }
-
-    double GetRescaleOffset() const
-    {
-      if (hasRescale_)
-      {
-        return rescaleOffset_;
-      }
-      else
-      {
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
-      }
-    }
-
-    double GetRescaleSlope() const
-    {
-      if (hasRescale_)
-      {
-        return rescaleSlope_;
-      }
-      else
-      {
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
-      }
-    }
-
-    bool HasDefaultWindowing() const
-    {
-      return hasDefaultWindowing_;
-    }
-
-    float GetDefaultWindowingCenter() const
-    {
-      if (hasDefaultWindowing_)
-      {
-        return defaultWindowingCenter_;
-      }
-      else
-      {
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
-      }
-    }
-
-    float GetDefaultWindowingWidth() const
-    {
-      if (hasDefaultWindowing_)
-      {
-        return defaultWindowingWidth_;
-      }
-      else
-      {
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
-      }
-    }
-
-    Orthanc::PixelFormat GetExpectedPixelFormat() const
-    {
-      return expectedPixelFormat_;
-    }
-  };
-
-
-  class AxialVolumeOrthancLoader : public OrthancStone::IObserver
-  {
-  private:
-    class MessageHandler : public Orthanc::IDynamicObject
-    {
-    public:
-      virtual void Handle(const OrthancRestApiCommand::SuccessMessage& message) const = 0;
-    };
-
-    void Handle(const OrthancRestApiCommand::SuccessMessage& message)
-    {
-      dynamic_cast<const MessageHandler&>(message.GetOrigin().GetPayload()).Handle(message);
-    }
-
-
-    class LoadSeriesGeometryHandler : public MessageHandler
-    {
-    private:
-      AxialVolumeOrthancLoader&  that_;
-
-    public:
-      LoadSeriesGeometryHandler(AxialVolumeOrthancLoader& that) :
-      that_(that)
-      {
-      }
-
-      virtual void Handle(const OrthancRestApiCommand::SuccessMessage& message) const
-      {
-        Json::Value value;
-        message.ParseJsonBody(value);
-
-        if (value.type() != Json::objectValue)
-        {
-          throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol);
-        }
-
-        Json::Value::Members instances = value.getMemberNames();
-
-        for (size_t i = 0; i < instances.size(); i++)
-        {
-          Orthanc::DicomMap dicom;
-          dicom.FromDicomAsJson(value[instances[i]]);
-
-          std::auto_ptr<DicomInstanceParameters> instance(new DicomInstanceParameters(dicom));
-
-          OrthancStone::CoordinateSystem3D geometry = instance->GetGeometry();
-          that_.slices_.AddSlice(geometry, instance.release());
-        }
-
-        if (!that_.slices_.Sort())
-        {
-          throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange,
-                                          "Cannot sort the 3D slices of a DICOM series");          
-        }
-
-        printf("series sorted\n");
-      }
-    };
-
-
-    class LoadInstanceGeometryHandler : public MessageHandler
-    {
-    private:
-      AxialVolumeOrthancLoader&  that_;
-
-    public:
-      LoadInstanceGeometryHandler(AxialVolumeOrthancLoader& that) :
-      that_(that)
-      {
-      }
-
-      virtual void Handle(const OrthancRestApiCommand::SuccessMessage& message) const
-      {
-        Json::Value value;
-        message.ParseJsonBody(value);
-
-        if (value.type() != Json::objectValue)
-        {
-          throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol);
-        }
-
-        Orthanc::DicomMap dicom;
-        dicom.FromDicomAsJson(value);
-
-        DicomInstanceParameters instance(dicom);
-      }
-    };
-
-
-    bool                                        active_;
-    std::auto_ptr<OrthancStone::ImageBuffer3D>  image_;
-    OrthancStone::SlicesSorter                  slices_;
-
-  public:
-    AxialVolumeOrthancLoader(OrthancStone::IObservable& oracle) :
-      IObserver(oracle.GetBroker()),
-      active_(false)
-    {
-      oracle.RegisterObserverCallback(
-        new OrthancStone::Callable<AxialVolumeOrthancLoader, OrthancRestApiCommand::SuccessMessage>
-        (*this, &AxialVolumeOrthancLoader::Handle));
-    }
-
-    void LoadSeries(IOracle& oracle,
-                    const std::string& seriesId)
-    {
-      if (active_)
-      {
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
-      }
-
-      active_ = true;
-
-      std::auto_ptr<Refactoring::OrthancRestApiCommand> command(new Refactoring::OrthancRestApiCommand);
-      command->SetUri("/series/" + seriesId + "/instances-tags");
-      command->SetPayload(new LoadSeriesGeometryHandler(*this));
-
-      oracle.Schedule(*this, command.release());
-    }
-
-    void LoadInstance(IOracle& oracle,
-                      const std::string& instanceId)
-    {
-      if (active_)
-      {
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
-      }
-
-      active_ = true;
-
-      // Tag "3004-000c" is "Grid Frame Offset Vector", which is
-      // mandatory to read RT DOSE, but is too long to be returned by default
-
-      // TODO => Should be part of a second call if needed
-
-      std::auto_ptr<Refactoring::OrthancRestApiCommand> command(new Refactoring::OrthancRestApiCommand);
-      command->SetUri("/instances/" + instanceId + "/tags?ignore-length=3004-000c");
-      command->SetPayload(new LoadInstanceGeometryHandler(*this));
-
-      oracle.Schedule(*this, command.release());
-    }
-  };
-
 }
 
 
@@ -1657,29 +2202,19 @@
 };
 
 
-void Run(Refactoring::NativeApplicationContext& context)
+void Run(Refactoring::NativeApplicationContext& context,
+         Refactoring::IOracle& oracle)
 {
   std::auto_ptr<Toto> toto;
-  std::auto_ptr<Refactoring::AxialVolumeOrthancLoader> loader1, loader2;
+  std::auto_ptr<Refactoring::VolumeSeriesOrthancLoader> loader1, loader2;
 
   {
     Refactoring::NativeApplicationContext::WriterLock lock(context);
     toto.reset(new Toto(lock.GetOracleObservable()));
-    loader1.reset(new Refactoring::AxialVolumeOrthancLoader(lock.GetOracleObservable()));
-    loader2.reset(new Refactoring::AxialVolumeOrthancLoader(lock.GetOracleObservable()));
+    loader1.reset(new Refactoring::VolumeSeriesOrthancLoader(oracle, lock.GetOracleObservable()));
+    loader2.reset(new Refactoring::VolumeSeriesOrthancLoader(oracle, lock.GetOracleObservable()));
   }
 
-  Refactoring::NativeOracle oracle(context);
-
-  {
-    Orthanc::WebServiceParameters p;
-    //p.SetUrl("http://localhost:8043/");
-    p.SetCredentials("orthanc", "orthanc");
-    oracle.SetOrthancParameters(p);
-  }
-
-  oracle.Start();
-
   if (1)
   {
     Json::Value v = Json::objectValue;
@@ -1746,14 +2281,15 @@
 
 
   // 2017-11-17-Anonymized
-  loader1->LoadSeries(oracle, "cb3ea4d1-d08f3856-ad7b6314-74d88d77-60b05618");  // CT
-  loader2->LoadInstance(oracle, "41029085-71718346-811efac4-420e2c15-d39f99b6");  // RT-DOSE
+  //loader1->LoadSeries("cb3ea4d1-d08f3856-ad7b6314-74d88d77-60b05618");  // CT
+  loader2->LoadInstance("41029085-71718346-811efac4-420e2c15-d39f99b6");  // RT-DOSE
+
+  // Delphine
+  loader1->LoadSeries("5990e39c-51e5f201-fe87a54c-31a55943-e59ef80e");  // CT
 
   LOG(WARNING) << "...Waiting for Ctrl-C...";
   Orthanc::SystemToolbox::ServerBarrier();
   //boost::this_thread::sleep(boost::posix_time::seconds(1));
-
-  oracle.Stop();
 }
 
 
@@ -1771,7 +2307,21 @@
   try
   {
     Refactoring::NativeApplicationContext context;
-    Run(context);
+
+    Refactoring::NativeOracle oracle(context);
+
+    {
+      Orthanc::WebServiceParameters p;
+      //p.SetUrl("http://localhost:8043/");
+      p.SetCredentials("orthanc", "orthanc");
+      oracle.SetOrthancParameters(p);
+    }
+
+    oracle.Start();
+
+    Run(context, oracle);
+
+    oracle.Stop();
   }
   catch (Orthanc::OrthancException& e)
   {
--- a/Samples/Sdl/TrackerSampleApp.cpp	Wed May 15 16:56:17 2019 +0200
+++ b/Samples/Sdl/TrackerSampleApp.cpp	Fri May 17 09:20:46 2019 +0200
@@ -349,6 +349,7 @@
 
 
   TrackerSampleApp::TrackerSampleApp(MessageBroker& broker) : IObserver(broker)
+    , scene_(broker)
     , currentTool_(GuiTool_Rotate)
   {
     controller_ = ViewportControllerPtr(new ViewportController(broker));
--- a/Samples/WebAssembly/BasicScene.cpp	Wed May 15 16:56:17 2019 +0200
+++ b/Samples/WebAssembly/BasicScene.cpp	Fri May 17 09:20:46 2019 +0200
@@ -153,8 +153,10 @@
     void SetupEvents(const std::string& canvas);
 
   public:
-    WebAssemblyViewport(const std::string& canvas) :
+    WebAssemblyViewport(MessageBroker& broker,
+                        const std::string& canvas) :
       context_(canvas),
+      scene_(broker),
       compositor_(context_, scene_)
     {
       compositor_.SetFont(0, Orthanc::EmbeddedResources::UBUNTU_FONT, 
@@ -361,6 +363,7 @@
 std::auto_ptr<OrthancStone::WebAssemblyViewport>  viewport1_;
 std::auto_ptr<OrthancStone::WebAssemblyViewport>  viewport2_;
 std::auto_ptr<OrthancStone::WebAssemblyViewport>  viewport3_;
+OrthancStone::MessageBroker  broker_;
 
 
 EM_BOOL OnWindowResize(int eventType, const EmscriptenUiEvent *uiEvent, void *userData)
@@ -396,15 +399,15 @@
   EMSCRIPTEN_KEEPALIVE
   void Initialize()
   {
-    viewport1_.reset(new OrthancStone::WebAssemblyViewport("mycanvas1"));
+    viewport1_.reset(new OrthancStone::WebAssemblyViewport(broker_, "mycanvas1"));
     PrepareScene(viewport1_->GetScene());
     viewport1_->UpdateSize();
 
-    viewport2_.reset(new OrthancStone::WebAssemblyViewport("mycanvas2"));
+    viewport2_.reset(new OrthancStone::WebAssemblyViewport(broker_, "mycanvas2"));
     PrepareScene(viewport2_->GetScene());
     viewport2_->UpdateSize();
 
-    viewport3_.reset(new OrthancStone::WebAssemblyViewport("mycanvas3"));
+    viewport3_.reset(new OrthancStone::WebAssemblyViewport(broker_, "mycanvas3"));
     PrepareScene(viewport3_->GetScene());
     viewport3_->UpdateSize();
 
--- a/UnitTestsSources/UnitTestsMain.cpp	Wed May 15 16:56:17 2019 +0200
+++ b/UnitTestsSources/UnitTestsMain.cpp	Fri May 17 09:20:46 2019 +0200
@@ -730,6 +730,82 @@
   ASSERT_TRUE(OrthancStone::MessagingToolbox::ParseJson(response, source.c_str(), source.size()));
 }
 
+TEST(VolumeImageGeometry, Basic)
+{
+  OrthancStone::VolumeImageGeometry g;
+  g.SetSize(10, 20, 30);
+  g.SetVoxelDimensions(1, 2, 3);
+
+  OrthancStone::Vector p = g.GetCoordinates(0, 0, 0);
+  ASSERT_EQ(3u, p.size());
+  ASSERT_FLOAT_EQ(-1.0 / 2.0, p[0]);
+  ASSERT_FLOAT_EQ(-2.0 / 2.0, p[1]);
+  ASSERT_FLOAT_EQ(-3.0 / 2.0, p[2]);
+  
+  p = g.GetCoordinates(1, 1, 1);
+  ASSERT_FLOAT_EQ(-1.0 / 2.0 + 10.0 * 1.0, p[0]);
+  ASSERT_FLOAT_EQ(-2.0 / 2.0 + 20.0 * 2.0, p[1]);
+  ASSERT_FLOAT_EQ(-3.0 / 2.0 + 30.0 * 3.0, p[2]);
+
+  OrthancStone::VolumeProjection proj;
+  ASSERT_TRUE(g.DetectProjection(proj, g.GetAxialGeometry().GetNormal()));
+  ASSERT_EQ(OrthancStone::VolumeProjection_Axial, proj);
+  ASSERT_TRUE(g.DetectProjection(proj, g.GetCoronalGeometry().GetNormal()));
+  ASSERT_EQ(OrthancStone::VolumeProjection_Coronal, proj);
+  ASSERT_TRUE(g.DetectProjection(proj, g.GetSagittalGeometry().GetNormal()));
+  ASSERT_EQ(OrthancStone::VolumeProjection_Sagittal, proj);
+
+  ASSERT_EQ(10u, g.GetProjectionWidth(OrthancStone::VolumeProjection_Axial));
+  ASSERT_EQ(20u, g.GetProjectionHeight(OrthancStone::VolumeProjection_Axial));
+  ASSERT_EQ(30u, g.GetProjectionDepth(OrthancStone::VolumeProjection_Axial));
+  ASSERT_EQ(10u, g.GetProjectionWidth(OrthancStone::VolumeProjection_Coronal));
+  ASSERT_EQ(30u, g.GetProjectionHeight(OrthancStone::VolumeProjection_Coronal));
+  ASSERT_EQ(20u, g.GetProjectionDepth(OrthancStone::VolumeProjection_Coronal));
+  ASSERT_EQ(20u, g.GetProjectionWidth(OrthancStone::VolumeProjection_Sagittal));
+  ASSERT_EQ(30u, g.GetProjectionHeight(OrthancStone::VolumeProjection_Sagittal));
+  ASSERT_EQ(10u, g.GetProjectionDepth(OrthancStone::VolumeProjection_Sagittal));
+
+  p = g.GetVoxelDimensions(OrthancStone::VolumeProjection_Axial);
+  ASSERT_EQ(3u, p.size());
+  ASSERT_FLOAT_EQ(1, p[0]);
+  ASSERT_FLOAT_EQ(2, p[1]);
+  ASSERT_FLOAT_EQ(3, p[2]);
+  p = g.GetVoxelDimensions(OrthancStone::VolumeProjection_Coronal);
+  ASSERT_EQ(3u, p.size());
+  ASSERT_FLOAT_EQ(1, p[0]);
+  ASSERT_FLOAT_EQ(3, p[1]);
+  ASSERT_FLOAT_EQ(2, p[2]);
+  p = g.GetVoxelDimensions(OrthancStone::VolumeProjection_Sagittal);
+  ASSERT_EQ(3u, p.size());
+  ASSERT_FLOAT_EQ(2, p[0]);
+  ASSERT_FLOAT_EQ(3, p[1]);
+  ASSERT_FLOAT_EQ(1, p[2]);
+
+  ASSERT_EQ(0, (int) OrthancStone::VolumeProjection_Axial);
+  ASSERT_EQ(1, (int) OrthancStone::VolumeProjection_Coronal);
+  ASSERT_EQ(2, (int) OrthancStone::VolumeProjection_Sagittal);
+  
+  for (int p = 0; p < 3; p++)
+  {
+    OrthancStone::VolumeProjection projection = (OrthancStone::VolumeProjection) p;
+    const OrthancStone::CoordinateSystem3D& s = g.GetProjectionGeometry(projection);
+    
+    for (unsigned int i = 0; i < g.GetProjectionDepth(projection); i++)
+    {
+      OrthancStone::CoordinateSystem3D plane(
+        s.GetOrigin() + static_cast<double>(i) * s.GetNormal() * g.GetVoxelDimensions(projection)[2],
+        s.GetAxisX(),
+        s.GetAxisY());
+
+      unsigned int slice;
+      OrthancStone::VolumeProjection q;
+      ASSERT_TRUE(g.DetectSlice(q, slice, plane));
+      ASSERT_EQ(projection, q);
+      ASSERT_EQ(i, slice);
+    }
+  }
+}
+
 int main(int argc, char **argv)
 {
   Orthanc::Logging::Initialize();