changeset 1404:3e644f6fadd4

Three-viewport is now OK in SDL and Wasm
author Benjamin Golinvaux <bgo@osimis.io>
date Wed, 29 Apr 2020 22:06:24 +0200
parents 62dc0d737e7b
children e4fe346c021e
files Samples/Common/RtViewer.cpp Samples/Common/RtViewer.h Samples/Common/RtViewerApp.cpp Samples/Common/RtViewerApp.h Samples/Common/RtViewerView.cpp Samples/Common/RtViewerView.h Samples/Sdl/RtViewer/CMakeLists.txt Samples/Sdl/RtViewer/RtViewerSdl.cpp Samples/Sdl/SdlHelpers.h Samples/WebAssembly/RtViewer/CMakeLists.txt Samples/WebAssembly/RtViewer/RtViewerWasm.cpp Samples/WebAssembly/RtViewer/RtViewerWasmApp.js Samples/WebAssembly/RtViewer/index.html
diffstat 13 files changed, 1037 insertions(+), 1222 deletions(-) [+]
line wrap: on
line diff
--- a/Samples/Common/RtViewer.cpp	Wed Apr 29 18:11:49 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,549 +0,0 @@
-/**
- * Stone of Orthanc
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU Affero General Public License
- * as published by the Free Software Foundation, either version 3 of
- * the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-// Sample app
-#include "RtViewer.h"
-#include "SampleHelpers.h"
-
-// Stone of Orthanc
-#include <Framework/StoneInitialization.h>
-#include <Framework/Scene2D/CairoCompositor.h>
-#include <Framework/Scene2D/ColorTextureSceneLayer.h>
-#include <Framework/Scene2D/OpenGLCompositor.h>
-#include <Framework/Scene2D/PanSceneTracker.h>
-#include <Framework/Scene2D/ZoomSceneTracker.h>
-#include <Framework/Scene2D/RotateSceneTracker.h>
-
-#include <Framework/Scene2DViewport/UndoStack.h>
-#include <Framework/Scene2DViewport/CreateLineMeasureTracker.h>
-#include <Framework/Scene2DViewport/CreateAngleMeasureTracker.h>
-#include <Framework/Scene2DViewport/IFlexiblePointerTracker.h>
-#include <Framework/Scene2DViewport/MeasureTool.h>
-#include <Framework/Scene2DViewport/PredeclaredTypes.h>
-#include <Framework/Volumes/VolumeSceneLayerSource.h>
-
-#include <Framework/Oracle/GetOrthancWebViewerJpegCommand.h>
-#include <Framework/Scene2D/GrayscaleStyleConfigurator.h>
-#include <Framework/Scene2D/LookupTableStyleConfigurator.h>
-#include <Framework/Volumes/DicomVolumeImageMPRSlicer.h>
-#include <Framework/StoneException.h>
-
-// Orthanc
-#include <Core/Logging.h>
-#include <Core/OrthancException.h>
-
-// System 
-#include <boost/shared_ptr.hpp>
-#include <boost/weak_ptr.hpp>
-#include <boost/make_shared.hpp>
-
-#include <stdio.h>
-
-
-namespace OrthancStone
-{
-  const char* RtViewerGuiToolToString(size_t i)
-  {
-    static const char* descs[] = {
-      "RtViewerGuiTool_Rotate",
-      "RtViewerGuiTool_Pan",
-      "RtViewerGuiTool_Zoom",
-      "RtViewerGuiTool_LineMeasure",
-      "RtViewerGuiTool_CircleMeasure",
-      "RtViewerGuiTool_AngleMeasure",
-      "RtViewerGuiTool_EllipseMeasure",
-      "RtViewerGuiTool_LAST"
-    };
-    if (i >= RtViewerGuiTool_LAST)
-    {
-      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError, "Wrong tool index");
-    }
-    return descs[i];
-  }
-
-  void RtViewerApp::SelectNextTool()
-  {
-    currentTool_ = static_cast<RtViewerGuiTool>(currentTool_ + 1);
-    if (currentTool_ == RtViewerGuiTool_LAST)
-      currentTool_ = static_cast<RtViewerGuiTool>(0);;
-    printf("Current tool is now: %s\n", RtViewerGuiToolToString(currentTool_));
-  }
-
-  void RtViewerApp::DisplayInfoText()
-  {
-    std::unique_ptr<IViewport::ILock> lock(viewport_->Lock());
-    ViewportController& controller = lock->GetController();
-    Scene2D& scene = controller.GetScene();
-
-    // do not try to use stuff too early!
-    OrthancStone::ICompositor& compositor = lock->GetCompositor();
-
-    std::stringstream msg;
-
-    for (std::map<std::string, std::string>::const_iterator kv = infoTextMap_.begin();
-         kv != infoTextMap_.end(); ++kv)
-    {
-      msg << kv->first << " : " << kv->second << std::endl;
-    }
-    std::string msgS = msg.str();
-
-    TextSceneLayer* layerP = NULL;
-    if (scene.HasLayer(FIXED_INFOTEXT_LAYER_ZINDEX))
-    {
-      TextSceneLayer& layer = dynamic_cast<TextSceneLayer&>(
-        scene.GetLayer(FIXED_INFOTEXT_LAYER_ZINDEX));
-      layerP = &layer;
-    }
-    else
-    {
-      std::unique_ptr<TextSceneLayer> layer(new TextSceneLayer);
-      layerP = layer.get();
-      layer->SetColor(0, 255, 0);
-      layer->SetFontIndex(1);
-      layer->SetBorder(20);
-      layer->SetAnchor(BitmapAnchor_TopLeft);
-      //layer->SetPosition(0,0);
-      scene.SetLayer(FIXED_INFOTEXT_LAYER_ZINDEX, layer.release());
-    }
-    // position the fixed info text in the upper right corner
-    layerP->SetText(msgS.c_str());
-    double cX = compositor.GetCanvasWidth() * (-0.5);
-    double cY = compositor.GetCanvasHeight() * (-0.5);
-    scene.GetCanvasToSceneTransform().Apply(cX, cY);
-    layerP->SetPosition(cX, cY);
-    lock->Invalidate();
-  }
-
-  void RtViewerApp::DisplayFloatingCtrlInfoText(const PointerEvent& e)
-  {
-    std::unique_ptr<IViewport::ILock> lock(viewport_->Lock());
-    ViewportController& controller = lock->GetController();
-    Scene2D& scene = controller.GetScene();
-
-    ScenePoint2D p = e.GetMainPosition().Apply(scene.GetCanvasToSceneTransform());
-
-    char buf[128];
-    sprintf(buf, "S:(%0.02f,%0.02f) C:(%0.02f,%0.02f)",
-            p.GetX(), p.GetY(),
-            e.GetMainPosition().GetX(), e.GetMainPosition().GetY());
-
-    if (scene.HasLayer(FLOATING_INFOTEXT_LAYER_ZINDEX))
-    {
-      TextSceneLayer& layer =
-        dynamic_cast<TextSceneLayer&>(scene.GetLayer(FLOATING_INFOTEXT_LAYER_ZINDEX));
-      layer.SetText(buf);
-      layer.SetPosition(p.GetX(), p.GetY());
-    }
-    else
-    {
-      std::unique_ptr<TextSceneLayer> layer(new TextSceneLayer);
-      layer->SetColor(0, 255, 0);
-      layer->SetText(buf);
-      layer->SetBorder(20);
-      layer->SetAnchor(BitmapAnchor_BottomCenter);
-      layer->SetPosition(p.GetX(), p.GetY());
-      scene.SetLayer(FLOATING_INFOTEXT_LAYER_ZINDEX, layer.release());
-    }
-  }
-
-  void RtViewerApp::HideInfoText()
-  {
-    std::unique_ptr<IViewport::ILock> lock(viewport_->Lock());
-    ViewportController& controller = lock->GetController();
-    Scene2D& scene = controller.GetScene();
-
-    scene.DeleteLayer(FLOATING_INFOTEXT_LAYER_ZINDEX);
-  }
-
-
-
-  void RtViewerApp::OnSceneTransformChanged(
-    const ViewportController::SceneTransformChanged& message)
-  {
-    DisplayInfoText();
-  }
-
-  void RtViewerApp::RetrieveGeometry()
-  {
-    ORTHANC_ASSERT(geometryProvider_.get() != NULL);
-    ORTHANC_ASSERT(geometryProvider_->HasGeometry());
-    const VolumeImageGeometry& geometry = geometryProvider_->GetImageGeometry();
-
-    const unsigned int depth = geometry.GetProjectionDepth(projection_);
-    currentPlane_ = depth / 2;
-
-    planes_.resize(depth);
-
-    for (unsigned int z = 0; z < depth; z++)
-    {
-      planes_[z] = geometry.GetProjectionSlice(projection_, z);
-    }
-
-    UpdateLayers();
-
-    std::unique_ptr<OrthancStone::IViewport::ILock> lock(viewport_->Lock());
-    lock->GetCompositor().FitContent(lock->GetController().GetScene());
-    lock->Invalidate();
-  }
-
-  void RtViewerApp::FitContent()
-  {
-    std::unique_ptr<OrthancStone::IViewport::ILock> lock(viewport_->Lock());
-    lock->GetCompositor().FitContent(lock->GetController().GetScene());
-    lock->Invalidate();
-  }
-
-  void RtViewerApp::UpdateLayers()
-  {
-    std::unique_ptr<IViewport::ILock> lock(viewport_->Lock());
-    if ((planes_.size() == 0)
-        && (geometryProvider_.get() != NULL)
-        && (geometryProvider_->HasGeometry()))
-    {
-      RetrieveGeometry();
-    }
-
-    if (currentPlane_ < planes_.size())
-    {
-      if (ctVolumeLayerSource_.get() != NULL)
-      {
-        ctVolumeLayerSource_->Update(planes_[currentPlane_]);
-      }
-      if (doseVolumeLayerSource_.get() != NULL)
-      {
-        doseVolumeLayerSource_->Update(planes_[currentPlane_]);
-      }
-      if (structLayerSource_.get() != NULL)
-      {
-        structLayerSource_->Update(planes_[currentPlane_]);
-      }
-    }
-    lock->Invalidate();
-  }
-
-
-
-  RtViewerApp::RtViewerApp()
-    : currentTool_(RtViewerGuiTool_Rotate)
-    , undoStack_(new UndoStack)
-    , currentPlane_(0)
-    , projection_(VolumeProjection_Coronal)
-  {
-    // the viewport hosts the scene
-    CreateViewport();
-
-    std::unique_ptr<IViewport::ILock> lock(viewport_->Lock());
-    ViewportController& controller = lock->GetController();
-    Scene2D& scene = controller.GetScene();
-
-    // Create the volumes that will be filled later on
-    ctVolume_ = boost::make_shared<DicomVolumeImage>();
-    doseVolume_ = boost::make_shared<DicomVolumeImage>();
-
-    TEXTURE_2x2_1_ZINDEX = 1;
-    TEXTURE_1x1_ZINDEX = 2;
-    TEXTURE_2x2_2_ZINDEX = 3;
-    LINESET_1_ZINDEX = 4;
-    LINESET_2_ZINDEX = 5;
-    FLOATING_INFOTEXT_LAYER_ZINDEX = 6;
-    FIXED_INFOTEXT_LAYER_ZINDEX = 7;
-  }
-
-  void RtViewerApp::RegisterMessages()
-  {
-    std::unique_ptr<IViewport::ILock> lock(viewport_->Lock());
-    ViewportController& controller = lock->GetController();
-    Scene2D& scene = controller.GetScene();
-    Register<ViewportController::SceneTransformChanged>(controller, &RtViewerApp::OnSceneTransformChanged);
-  }
-
-  boost::shared_ptr<RtViewerApp> RtViewerApp::Create()
-  {
-    boost::shared_ptr<RtViewerApp> thisOne(new RtViewerApp());
-    thisOne->RegisterMessages();
-    return thisOne;
-  }
-
-#if 0
-  void RtViewerApp::PrepareScene()
-  {
-    std::unique_ptr<IViewport::ILock> lock(viewport_->Lock());
-    ViewportController& controller = lock->GetController();
-    Scene2D& scene = controller.GetScene();
-
-    // Texture of 2x2 size
-    {
-      Orthanc::Image i(Orthanc::PixelFormat_RGB24, 2, 2, false);
-
-      uint8_t* p = reinterpret_cast<uint8_t*>(i.GetRow(0));
-      p[0] = 255;
-      p[1] = 0;
-      p[2] = 0;
-
-      p[3] = 0;
-      p[4] = 255;
-      p[5] = 0;
-
-      p = reinterpret_cast<uint8_t*>(i.GetRow(1));
-      p[0] = 0;
-      p[1] = 0;
-      p[2] = 255;
-
-      p[3] = 255;
-      p[4] = 0;
-      p[5] = 0;
-
-      scene.SetLayer(TEXTURE_2x2_1_ZINDEX, new ColorTextureSceneLayer(i));
-    }
-  }
-#endif
-
-  void RtViewerApp::DisableTracker()
-  {
-    if (activeTracker_)
-    {
-      activeTracker_->Cancel();
-      activeTracker_.reset();
-    }
-  }
-  
-  void RtViewerApp::PrepareLoadersAndSlicers()
-  {
-
-    //{
-    //  Orthanc::WebServiceParameters p;
-    //  //p.SetUrl("http://localhost:8043/");
-    //  p.SetCredentials("orthanc", "orthanc");
-    //  oracle_.SetOrthancParameters(p);
-    //}
-
-    {
-      // "true" means use progressive quality (jpeg 50 --> jpeg 90 --> 16-bit raw)
-      // "false" means only using hi quality
-      // TODO: add flag for quality
-      ctLoader_ = OrthancSeriesVolumeProgressiveLoader::Create(*loadersContext_, ctVolume_, true);
-
-      // we need to store the CT loader to ask from geometry details later on when geometry is loaded
-      geometryProvider_ = ctLoader_;
-
-      doseLoader_ = OrthancMultiframeVolumeLoader::Create(*loadersContext_, doseVolume_);
-      rtstructLoader_ = DicomStructureSetLoader::Create(*loadersContext_);
-    }
-
-    /**
-    Register for notifications issued by the loaders
-    */
-
-    Register<DicomVolumeImage::GeometryReadyMessage>                              
-       (*ctLoader_, &RtViewerApp::HandleGeometryReady);
-    
-    Register<OrthancSeriesVolumeProgressiveLoader::VolumeImageReadyInHighQuality>
-      (*ctLoader_, &RtViewerApp::HandleCTLoaded);
-    
-    Register<DicomVolumeImage::ContentUpdatedMessage>                             
-      (*ctLoader_, &RtViewerApp::HandleCTContentUpdated);
-    
-    Register<DicomVolumeImage::ContentUpdatedMessage>                             
-      (*doseLoader_, &RtViewerApp::HandleDoseLoaded);
-    
-    Register<DicomStructureSetLoader::StructuresReady>                            
-      (*rtstructLoader_, &RtViewerApp::HandleStructuresReady);
-    
-    Register<DicomStructureSetLoader::StructuresUpdated>                          
-      (*rtstructLoader_, &RtViewerApp::HandleStructuresUpdated);
-
-    /**
-    Configure the CT
-    */
-
-
-    std::auto_ptr<GrayscaleStyleConfigurator> style(new GrayscaleStyleConfigurator);
-    style->SetLinearInterpolation(true);
-
-    this->SetCtVolumeSlicer(LAYER_POSITION + 0, ctLoader_, style.release());
-
-    {
-      std::unique_ptr<LookupTableStyleConfigurator> config(new LookupTableStyleConfigurator);
-      config->SetLookupTable(Orthanc::EmbeddedResources::COLORMAP_HOT);
-
-      boost::shared_ptr<DicomVolumeImageMPRSlicer> tmp(new DicomVolumeImageMPRSlicer(doseVolume_));
-      this->SetDoseVolumeSlicer(LAYER_POSITION + 1, tmp, config.release());
-    }
-
-    this->SetStructureSet(LAYER_POSITION + 2, rtstructLoader_);
-
-#if 1 
-    ORTHANC_ASSERT(HasArgument("ctseries") && HasArgument("rtdose") && HasArgument("rtstruct"));
-
-    LOG(INFO) << "About to load:";
-    LOG(INFO) << "  CT       : " << GetArgument("ctseries");
-    LOG(INFO) << "  RTDOSE   : " << GetArgument("rtdose");
-    LOG(INFO) << "  RTSTRUCT : " << GetArgument("rtstruct");
-    ctLoader_->LoadSeries(GetArgument("ctseries"));
-    doseLoader_->LoadInstance(GetArgument("rtdose"));
-    rtstructLoader_->LoadInstanceFullVisibility(GetArgument("rtstruct"));
-
-#elif 0
-    /*
-    BGO data
-    http://localhost:8042/twiga-orthanc-viewer-demo/twiga-orthanc-viewer-demo.html?ct-series=a04ecf01-79b2fc33-58239f7e-ad9db983-28e81afa
-    &
-    dose-instance=830a69ff-8e4b5ee3-b7f966c8-bccc20fb-d322dceb
-    &
-    struct-instance=54460695-ba3885ee-ddf61ac0-f028e31d-a6e474d9
-    */
-    ctLoader_->LoadSeries("a04ecf01-79b2fc33-58239f7e-ad9db983-28e81afa");  // CT
-    doseLoader_->LoadInstance("830a69ff-8e4b5ee3-b7f966c8-bccc20fb-d322dceb");  // RT-DOSE
-    rtstructLoader_->LoadInstanceFullVisibility("54460695-ba3885ee-ddf61ac0-f028e31d-a6e474d9");  // RT-STRUCT
-#else
-    //SJO data
-    //ctLoader->LoadSeries("cb3ea4d1-d08f3856-ad7b6314-74d88d77-60b05618");  // CT
-    //doseLoader->LoadInstance("41029085-71718346-811efac4-420e2c15-d39f99b6");  // RT-DOSE
-    //rtstructLoader->LoadInstanceFullVisibility("83d9c0c3-913a7fee-610097d7-cbf0522d-fd75bee6");  // RT-STRUCT
-
-    // 2017-05-16
-    ctLoader_->LoadSeries("a04ecf01-79b2fc33-58239f7e-ad9db983-28e81afa");  // CT
-    doseLoader_->LoadInstance("eac822ef-a395f94e-e8121fe0-8411fef8-1f7bffad");  // RT-DOSE
-    rtstructLoader_->LoadInstanceFullVisibility("54460695-ba3885ee-ddf61ac0-f028e31d-a6e474d9");  // RT-STRUCT
-#endif
-  }
-
-#if 0
-  void RtViewerApp::Handle(const OracleCommandExceptionMessage& message)
-  {
-    const OracleCommandBase& command = dynamic_cast<const OracleCommandBase&>(message.GetOrigin());
-
-    printf("EXCEPTION: [%s] on command type %d\n", message.GetException().What(), command.GetType());
-
-    switch (command.GetType())
-    {
-    case IOracleCommand::Type_GetOrthancWebViewerJpeg:
-      printf("URI: [%s]\n", dynamic_cast<const GetOrthancWebViewerJpegCommand&>(command).GetUri().c_str());
-      break;
-
-    default:
-      break;
-    }
-  }
-#endif
-
-
-  void RtViewerApp::HandleGeometryReady(const DicomVolumeImage::GeometryReadyMessage& message)
-  {
-    RetrieveGeometry();
-  }
-
-  void RtViewerApp::HandleCTLoaded(const OrthancSeriesVolumeProgressiveLoader::VolumeImageReadyInHighQuality& message)
-  {
-    UpdateLayers();
-  }
-
-  void RtViewerApp::HandleCTContentUpdated(const DicomVolumeImage::ContentUpdatedMessage& message)
-  {
-    UpdateLayers();
-  }
-
-  void RtViewerApp::HandleDoseLoaded(const DicomVolumeImage::ContentUpdatedMessage& message)
-  {
-    //TODO: compute dose extent, with outlier rejection
-    UpdateLayers();
-  }
-
-  void RtViewerApp::HandleStructuresReady(const DicomStructureSetLoader::StructuresReady& message)
-  {
-    UpdateLayers();
-  }
-
-  void RtViewerApp::HandleStructuresUpdated(const DicomStructureSetLoader::StructuresUpdated& message)
-  {
-    UpdateLayers();
-  }
-
-  void RtViewerApp::SetCtVolumeSlicer(int depth,
-                                            const boost::shared_ptr<OrthancStone::IVolumeSlicer>& volume,
-                                            OrthancStone::ILayerStyleConfigurator* style)
-  {
-    std::unique_ptr<IViewport::ILock> lock(viewport_->Lock());
-    ViewportController& controller = lock->GetController();
-    Scene2D& scene = controller.GetScene();
-
-    ctVolumeLayerSource_.reset(new OrthancStone::VolumeSceneLayerSource(scene, depth, volume));
-
-    if (style != NULL)
-    {
-      ctVolumeLayerSource_->SetConfigurator(style);
-    }
-  }
-
-  void RtViewerApp::SetDoseVolumeSlicer(int depth,
-                                            const boost::shared_ptr<OrthancStone::IVolumeSlicer>& volume,
-                                            OrthancStone::ILayerStyleConfigurator* style)
-  {
-    std::unique_ptr<IViewport::ILock> lock(viewport_->Lock());
-    ViewportController& controller = lock->GetController();
-    Scene2D& scene = controller.GetScene();
-
-    doseVolumeLayerSource_.reset(new OrthancStone::VolumeSceneLayerSource(scene, depth, volume));
-
-    if (style != NULL)
-    {
-      doseVolumeLayerSource_->SetConfigurator(style);
-    }
-  }
-
-  void RtViewerApp::SetStructureSet(int depth,
-                                                 const boost::shared_ptr<OrthancStone::DicomStructureSetLoader>& volume)
-  {
-    std::unique_ptr<IViewport::ILock> lock(viewport_->Lock());
-    ViewportController& controller = lock->GetController();
-    Scene2D& scene = controller.GetScene();
-
-    structLayerSource_.reset(new OrthancStone::VolumeSceneLayerSource(scene, depth, volume));
-  }
-
-  void RtViewerApp::SetArgument(const std::string& key, const std::string& value)
-  {
-    if (key == "loglevel")
-      OrthancStoneHelpers::SetLogLevel(value);
-    else
-      arguments_[key] = value;
-  }
-
-  const std::string& RtViewerApp::GetArgument(const std::string& key) const
-  {
-    ORTHANC_ASSERT(HasArgument(key));
-    return arguments_.at(key);
-  }
-  bool RtViewerApp::HasArgument(const std::string& key) const
-  {
-    return (arguments_.find(key) != arguments_.end());
-  }
-
-  void RtViewerApp::SetInfoDisplayMessage(
-    std::string key, std::string value)
-  {
-    if (value == "")
-      infoTextMap_.erase(key);
-    else
-      infoTextMap_[key] = value;
-    DisplayInfoText();
-  }
-}
-
--- a/Samples/Common/RtViewer.h	Wed Apr 29 18:11:49 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,248 +0,0 @@
-/**
- * Stone of Orthanc
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU Affero General Public License
- * as published by the Free Software Foundation, either version 3 of
- * the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-#include <Framework/Viewport/IViewport.h>
-
-#include <Framework/Loaders/DicomStructureSetLoader.h>
-#include <Framework/Loaders/OrthancMultiframeVolumeLoader.h>
-#include <Framework/Loaders/OrthancSeriesVolumeProgressiveLoader.h>
-#include <Framework/Loaders/ILoadersContext.h>
-#include <Framework/Messages/IMessageEmitter.h>
-#include <Framework/Messages/IObserver.h>
-#include <Framework/Messages/ObserverBase.h>
-#include <Framework/Oracle/OracleCommandExceptionMessage.h>
-#include <Framework/Scene2DViewport/ViewportController.h>
-#include <Framework/Volumes/DicomVolumeImage.h>
-
-#include <boost/enable_shared_from_this.hpp>
-#include <boost/thread.hpp>
-#include <boost/noncopyable.hpp>
-
-#if ORTHANC_ENABLE_SDL
-#include <SDL.h>
-#endif
-
-namespace OrthancStone
-{
-  class OpenGLCompositor;
-  class IVolumeSlicer;
-  class ILayerStyleConfigurator;
-  class DicomStructureSetLoader;
-  class IOracle;
-  class ThreadedOracle;
-  class VolumeSceneLayerSource;
-  class SdlOpenGLViewport;
-   
-  enum RtViewerGuiTool
-  {
-    RtViewerGuiTool_Rotate = 0,
-    RtViewerGuiTool_Pan,
-    RtViewerGuiTool_Zoom,
-    RtViewerGuiTool_LineMeasure,
-    RtViewerGuiTool_CircleMeasure,
-    RtViewerGuiTool_AngleMeasure,
-    RtViewerGuiTool_EllipseMeasure,
-    RtViewerGuiTool_LAST
-  };
-
-  const char* MeasureToolToString(size_t i);
-
-  static const unsigned int FONT_SIZE_0 = 32;
-  static const unsigned int FONT_SIZE_1 = 24;
-
-  class Scene2D;
-  class UndoStack;
-
-  /**
-  This application subclasses IMessageEmitter to use a mutex before forwarding Oracle messages (that
-  can be sent from multiple threads)
-  */
-  class RtViewerApp : public ObserverBase<RtViewerApp>
-  {
-  public:
-
-    void PrepareScene();
-
-#if ORTHANC_ENABLE_SDL
-  public:
-    void RunSdl(int argc, char* argv[]);
-  private:
-    void ProcessOptions(int argc, char* argv[]);
-    void HandleApplicationEvent(const SDL_Event& event);
-#elif ORTHANC_ENABLE_WASM
-  public:
-    void RunWasm();
-#else
-#  error Either ORTHANC_ENABLE_SDL or ORTHANC_ENABLE_WASM must be enabled
-#endif
-
-  public:
-    void SetInfoDisplayMessage(std::string key, std::string value);
-    void DisableTracker();
-
-    /**
-    Called by command-line option processing or when parsing the URL 
-    parameters.
-    */
-    void SetArgument(const std::string& key, const std::string& value);
-
-    /**
-    This method is called when the scene transform changes. It allows to
-    recompute the visual elements whose content depend upon the scene transform
-    */
-    void OnSceneTransformChanged(
-      const ViewportController::SceneTransformChanged& message);
-
-    /**
-    This method will ask the VolumeSceneLayerSource, that are responsible to 
-    generated 2D content based on a volume and a cutting plane, to regenerate
-    it. This is required if the volume itself changes (during loading) or if 
-    the cutting plane is changed
-    */
-    void UpdateLayers();
-
-    void Refresh();
-
-#if 0
-    virtual void EmitMessage(boost::weak_ptr<IObserver> observer,
-      const IMessage& message) ORTHANC_OVERRIDE
-    {
-      try
-      {
-        boost::unique_lock<boost::shared_mutex>  lock(mutex_);
-        oracleObservable_.EmitMessage(observer, message);
-      }
-      catch (Orthanc::OrthancException& e)
-      {
-        LOG(ERROR) << "Exception while emitting a message: " << e.What();
-        throw;
-      }
-    }
-#endif
-
-    static boost::shared_ptr<RtViewerApp> Create();
-    void RegisterMessages();
-
-  protected:
-    RtViewerApp();
-
-  private:
-    void PrepareLoadersAndSlicers();
-    void SelectNextTool();
-
-    // argument handling
-    // SetArgument is above (public section)
-    std::map<std::string, std::string> arguments_;
-
-    const std::string& GetArgument(const std::string& key) const;
-    bool HasArgument(const std::string& key) const;
-
-    void TakeScreenshot(
-      const std::string& target,
-      unsigned int canvasWidth,
-      unsigned int canvasHeight);
-
-    /**
-      This adds the command at the top of the undo stack
-    */
-    //void Commit(boost::shared_ptr<TrackerCommand> cmd);
-    void Undo();
-    void Redo();
-
-
-    void HandleGeometryReady(const DicomVolumeImage::GeometryReadyMessage& message);
-    
-    // TODO: wire this
-    void HandleCTLoaded(const OrthancSeriesVolumeProgressiveLoader::VolumeImageReadyInHighQuality& message);
-    void HandleCTContentUpdated(const OrthancStone::DicomVolumeImage::ContentUpdatedMessage& message);
-    void HandleDoseLoaded(const OrthancStone::DicomVolumeImage::ContentUpdatedMessage& message);
-    void HandleStructuresReady(const OrthancStone::DicomStructureSetLoader::StructuresReady& message);
-    void HandleStructuresUpdated(const OrthancStone::DicomStructureSetLoader::StructuresUpdated& message);
-
-    void SetCtVolumeSlicer(
-      int depth,
-      const boost::shared_ptr<IVolumeSlicer>& volume,
-      ILayerStyleConfigurator* style);
-    
-    void SetDoseVolumeSlicer(
-      int depth,
-      const boost::shared_ptr<IVolumeSlicer>& volume,
-      ILayerStyleConfigurator* style);
-
-    void SetStructureSet(
-      int depth, 
-      const boost::shared_ptr<DicomStructureSetLoader>& volume);
-
-  private:
-    void CreateViewport();
-    void DisplayFloatingCtrlInfoText(const PointerEvent& e);
-    void DisplayInfoText();
-    void HideInfoText();
-    void RetrieveGeometry();
-    void FitContent();
-
-  private:
-    boost::shared_ptr<DicomVolumeImage>  ctVolume_;
-    boost::shared_ptr<DicomVolumeImage>  doseVolume_;
-
-    boost::shared_ptr<OrthancSeriesVolumeProgressiveLoader> ctLoader_;
-    boost::shared_ptr<OrthancMultiframeVolumeLoader> doseLoader_;
-    boost::shared_ptr<DicomStructureSetLoader>  rtstructLoader_;
-
-    /** encapsulates resources shared by loaders */
-    boost::shared_ptr<ILoadersContext>                  loadersContext_;
-
-    boost::shared_ptr<VolumeSceneLayerSource>           ctVolumeLayerSource_, doseVolumeLayerSource_, structLayerSource_;
-    
-    /**
-    another interface to the ctLoader object (that also implements the IVolumeSlicer interface), that serves as the 
-    reference for the geometry (position and dimensions of the volume + size of each voxel). It could be changed to be 
-    the dose instead, but the CT is chosen because it usually has a better spatial resolution.
-    */
-    boost::shared_ptr<OrthancStone::IGeometryProvider>  geometryProvider_;
-
-    // collection of cutting planes for this particular view
-    std::vector<OrthancStone::CoordinateSystem3D>       planes_;
-    size_t                                              currentPlane_;
-
-    VolumeProjection                      projection_;
-
-    std::map<std::string, std::string> infoTextMap_;
-    boost::shared_ptr<IFlexiblePointerTracker> activeTracker_;
-
-    static const int LAYER_POSITION = 150;
-
-    int TEXTURE_2x2_1_ZINDEX;
-    int TEXTURE_1x1_ZINDEX;
-    int TEXTURE_2x2_2_ZINDEX;
-    int LINESET_1_ZINDEX;
-    int LINESET_2_ZINDEX;
-    int FLOATING_INFOTEXT_LAYER_ZINDEX;
-    int FIXED_INFOTEXT_LAYER_ZINDEX;
-
-    RtViewerGuiTool currentTool_;
-    boost::shared_ptr<UndoStack> undoStack_;
-    boost::shared_ptr<IViewport> viewport_;
-  };
-
-}
-
-
- 
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Common/RtViewerApp.cpp	Wed Apr 29 22:06:24 2020 +0200
@@ -0,0 +1,269 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+// Sample app
+#include "RtViewerApp.h"
+#include "RtViewerView.h"
+#include "SampleHelpers.h"
+
+// Stone of Orthanc
+#include <Framework/StoneInitialization.h>
+#include <Framework/Scene2D/CairoCompositor.h>
+#include <Framework/Scene2D/ColorTextureSceneLayer.h>
+#include <Framework/Scene2D/OpenGLCompositor.h>
+#include <Framework/Scene2D/PanSceneTracker.h>
+#include <Framework/Scene2D/ZoomSceneTracker.h>
+#include <Framework/Scene2D/RotateSceneTracker.h>
+
+#include <Framework/Scene2DViewport/UndoStack.h>
+#include <Framework/Scene2DViewport/CreateLineMeasureTracker.h>
+#include <Framework/Scene2DViewport/CreateAngleMeasureTracker.h>
+#include <Framework/Scene2DViewport/IFlexiblePointerTracker.h>
+#include <Framework/Scene2DViewport/MeasureTool.h>
+#include <Framework/Scene2DViewport/PredeclaredTypes.h>
+#include <Framework/Volumes/VolumeSceneLayerSource.h>
+
+#include <Framework/Oracle/GetOrthancWebViewerJpegCommand.h>
+#include <Framework/Scene2D/GrayscaleStyleConfigurator.h>
+#include <Framework/Scene2D/LookupTableStyleConfigurator.h>
+#include <Framework/Volumes/DicomVolumeImageMPRSlicer.h>
+#include <Framework/StoneException.h>
+
+// Orthanc
+#include <Core/Logging.h>
+#include <Core/OrthancException.h>
+
+// System 
+#include <boost/shared_ptr.hpp>
+#include <boost/weak_ptr.hpp>
+#include <boost/make_shared.hpp>
+
+#include <stdio.h>
+
+
+namespace OrthancStone
+{
+  const char* RtViewerGuiToolToString(size_t i)
+  {
+    static const char* descs[] = {
+      "RtViewerGuiTool_Rotate",
+      "RtViewerGuiTool_Pan",
+      "RtViewerGuiTool_Zoom",
+      "RtViewerGuiTool_LineMeasure",
+      "RtViewerGuiTool_CircleMeasure",
+      "RtViewerGuiTool_AngleMeasure",
+      "RtViewerGuiTool_EllipseMeasure",
+      "RtViewerGuiTool_LAST"
+    };
+    if (i >= RtViewerGuiTool_LAST)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError, "Wrong tool index");
+    }
+    return descs[i];
+  }
+
+  void RtViewerApp::SelectNextTool()
+  {
+    currentTool_ = static_cast<RtViewerGuiTool>(currentTool_ + 1);
+    if (currentTool_ == RtViewerGuiTool_LAST)
+      currentTool_ = static_cast<RtViewerGuiTool>(0);;
+    printf("Current tool is now: %s\n", RtViewerGuiToolToString(currentTool_));
+  }
+
+  void RtViewerApp::InvalidateAllViewports()
+  {
+    for (size_t i = 0; i < views_.size(); ++i)
+    {
+      views_[i]->Invalidate();
+    }
+  }
+
+  const VolumeImageGeometry& RtViewerApp::GetMainGeometry()
+  {
+    ORTHANC_ASSERT(geometryProvider_.get() != NULL);
+    ORTHANC_ASSERT(geometryProvider_->HasGeometry());
+    const VolumeImageGeometry& geometry = geometryProvider_->GetImageGeometry();
+    return geometry;
+  }
+
+  RtViewerApp::RtViewerApp()
+    : currentTool_(RtViewerGuiTool_Rotate)
+    , undoStack_(new UndoStack)
+  {
+    // Create the volumes that will be filled later on
+    ctVolume_ = boost::make_shared<DicomVolumeImage>();
+    doseVolume_ = boost::make_shared<DicomVolumeImage>();
+  }
+
+  boost::shared_ptr<RtViewerApp> RtViewerApp::Create()
+  {
+    boost::shared_ptr<RtViewerApp> thisOne(new RtViewerApp());
+    return thisOne;
+  }
+
+  void RtViewerApp::DisableTracker()
+  {
+    if (activeTracker_)
+    {
+      activeTracker_->Cancel();
+      activeTracker_.reset();
+    }
+  }
+
+  void RtViewerApp::CreateView(const std::string& canvasId, VolumeProjection projection)
+  {
+    boost::shared_ptr<RtViewerView> 
+      view(new RtViewerView(shared_from_this(), canvasId, projection));
+
+    view->RegisterMessages();
+
+    view->CreateLayers(ctLoader_, doseLoader_, doseVolume_, rtstructLoader_);
+
+    views_.push_back(view);
+  }
+
+  void RtViewerApp::CreateLoaders()
+  {
+    // the viewport hosts the scene
+    {
+      // "true" means use progressive quality (jpeg 50 --> jpeg 90 --> 16-bit raw)
+      // "false" means only using hi quality
+      // TODO: add flag for quality
+      ctLoader_ = OrthancSeriesVolumeProgressiveLoader::Create(*loadersContext_, ctVolume_, true);
+
+      // we need to store the CT loader to ask from geometry details later on when geometry is loaded
+      geometryProvider_ = ctLoader_;
+
+      doseLoader_ = OrthancMultiframeVolumeLoader::Create(*loadersContext_, doseVolume_);
+      rtstructLoader_ = DicomStructureSetLoader::Create(*loadersContext_);
+    }
+
+    /**
+    Register for notifications issued by the loaders
+    */
+
+    Register<DicomVolumeImage::GeometryReadyMessage>
+      (*ctLoader_, &RtViewerApp::HandleGeometryReady);
+
+    Register<OrthancSeriesVolumeProgressiveLoader::VolumeImageReadyInHighQuality>
+      (*ctLoader_, &RtViewerApp::HandleCTLoaded);
+
+    Register<DicomVolumeImage::ContentUpdatedMessage>
+      (*ctLoader_, &RtViewerApp::HandleCTContentUpdated);
+
+    Register<DicomVolumeImage::ContentUpdatedMessage>
+      (*doseLoader_, &RtViewerApp::HandleDoseLoaded);
+
+    Register<DicomStructureSetLoader::StructuresReady>
+      (*rtstructLoader_, &RtViewerApp::HandleStructuresReady);
+
+    Register<DicomStructureSetLoader::StructuresUpdated>
+      (*rtstructLoader_, &RtViewerApp::HandleStructuresUpdated);
+  }
+
+  void RtViewerApp::StartLoaders()
+  {
+    ORTHANC_ASSERT(HasArgument("ctseries") && HasArgument("rtdose") && HasArgument("rtstruct"));
+
+    LOG(INFO) << "About to load:";
+    LOG(INFO) << "  CT       : " << GetArgument("ctseries");
+    LOG(INFO) << "  RTDOSE   : " << GetArgument("rtdose");
+    LOG(INFO) << "  RTSTRUCT : " << GetArgument("rtstruct");
+    ctLoader_->LoadSeries(GetArgument("ctseries"));
+    doseLoader_->LoadInstance(GetArgument("rtdose"));
+    rtstructLoader_->LoadInstanceFullVisibility(GetArgument("rtstruct"));
+  }
+
+  void RtViewerApp::HandleGeometryReady(const DicomVolumeImage::GeometryReadyMessage& message)
+  {
+    for (size_t i = 0; i < views_.size(); ++i)
+    {
+      views_[i]->RetrieveGeometry();
+    }
+    FitContent();
+    UpdateLayersInAllViews();
+  }
+
+  void RtViewerApp::FitContent()
+  {
+    for (size_t i = 0; i < views_.size(); ++i)
+    {
+      views_[i]->FitContent();
+    }
+  }
+
+  void RtViewerApp::UpdateLayersInAllViews()
+  {
+    for (size_t i = 0; i < views_.size(); ++i)
+    {
+      views_[i]->UpdateLayers();
+    }
+  }
+
+  void RtViewerApp::HandleCTLoaded(const OrthancSeriesVolumeProgressiveLoader::VolumeImageReadyInHighQuality& message)
+  {
+    for (size_t i = 0; i < views_.size(); ++i)
+    {
+      views_[i]->RetrieveGeometry();
+    }
+    UpdateLayersInAllViews();
+  }
+
+  void RtViewerApp::HandleCTContentUpdated(const DicomVolumeImage::ContentUpdatedMessage& message)
+  {
+    UpdateLayersInAllViews();
+  }
+
+  void RtViewerApp::HandleDoseLoaded(const DicomVolumeImage::ContentUpdatedMessage& message)
+  {
+    //TODO: compute dose extent, with outlier rejection
+    UpdateLayersInAllViews();
+  }
+
+  void RtViewerApp::HandleStructuresReady(const DicomStructureSetLoader::StructuresReady& message)
+  {
+    UpdateLayersInAllViews();
+  }
+
+  void RtViewerApp::HandleStructuresUpdated(const DicomStructureSetLoader::StructuresUpdated& message)
+  {
+    UpdateLayersInAllViews();
+  }
+
+  void RtViewerApp::SetArgument(const std::string& key, const std::string& value)
+  {
+    if (key == "loglevel")
+      OrthancStoneHelpers::SetLogLevel(value);
+    else
+      arguments_[key] = value;
+  }
+
+  const std::string& RtViewerApp::GetArgument(const std::string& key) const
+  {
+    ORTHANC_ASSERT(HasArgument(key));
+    return arguments_.at(key);
+  }
+
+  bool RtViewerApp::HasArgument(const std::string& key) const
+  {
+    return (arguments_.find(key) != arguments_.end());
+  }
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Common/RtViewerApp.h	Wed Apr 29 22:06:24 2020 +0200
@@ -0,0 +1,180 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+#include <Framework/Viewport/IViewport.h>
+
+#include <Framework/Loaders/DicomStructureSetLoader.h>
+#include <Framework/Loaders/OrthancMultiframeVolumeLoader.h>
+#include <Framework/Loaders/OrthancSeriesVolumeProgressiveLoader.h>
+#include <Framework/Loaders/ILoadersContext.h>
+#include <Framework/Messages/IMessageEmitter.h>
+#include <Framework/Messages/IObserver.h>
+#include <Framework/Messages/ObserverBase.h>
+#include <Framework/Oracle/OracleCommandExceptionMessage.h>
+#include <Framework/Scene2DViewport/ViewportController.h>
+#include <Framework/Volumes/DicomVolumeImage.h>
+
+#include <boost/enable_shared_from_this.hpp>
+#include <boost/thread.hpp>
+#include <boost/noncopyable.hpp>
+
+#if ORTHANC_ENABLE_SDL
+#include <SDL.h>
+#endif
+
+namespace OrthancStone
+{
+  class OpenGLCompositor;
+  class IVolumeSlicer;
+  class ILayerStyleConfigurator;
+  class DicomStructureSetLoader;
+  class IOracle;
+  class ThreadedOracle;
+  class VolumeSceneLayerSource;
+  class SdlOpenGLViewport;
+  class RtViewerView;
+   
+  enum RtViewerGuiTool
+  {
+    RtViewerGuiTool_Rotate = 0,
+    RtViewerGuiTool_Pan,
+    RtViewerGuiTool_Zoom,
+    RtViewerGuiTool_LineMeasure,
+    RtViewerGuiTool_CircleMeasure,
+    RtViewerGuiTool_AngleMeasure,
+    RtViewerGuiTool_EllipseMeasure,
+    RtViewerGuiTool_LAST
+  };
+
+  const char* MeasureToolToString(size_t i);
+
+  static const unsigned int FONT_SIZE_0 = 32;
+  static const unsigned int FONT_SIZE_1 = 24;
+
+  class Scene2D;
+  class UndoStack;
+
+  /**
+  This application subclasses IMessageEmitter to use a mutex before forwarding Oracle messages (that
+  can be sent from multiple threads)
+  */
+  class RtViewerApp : public ObserverBase<RtViewerApp>
+  {
+  public:
+
+    void PrepareScene();
+
+#if ORTHANC_ENABLE_SDL
+  public:
+    void RunSdl(int argc, char* argv[]);
+  private:
+    void ProcessOptions(int argc, char* argv[]);
+    void HandleApplicationEvent(const SDL_Event& event);
+#elif ORTHANC_ENABLE_WASM
+  public:
+    void RunWasm();
+#else
+#  error Either ORTHANC_ENABLE_SDL or ORTHANC_ENABLE_WASM must be enabled
+#endif
+
+  public:
+    void DisableTracker();
+
+    /**
+    Called by command-line option processing or when parsing the URL 
+    parameters.
+    */
+    void SetArgument(const std::string& key, const std::string& value);
+
+    const VolumeImageGeometry& GetMainGeometry();
+
+    static boost::shared_ptr<RtViewerApp> Create();
+
+    void CreateView(const std::string& canvasId, VolumeProjection projection);
+
+  protected:
+    RtViewerApp();
+
+  private:
+    void CreateLoaders();
+    void StartLoaders();
+    void SelectNextTool();
+
+    // argument handling
+    // SetArgument is above (public section)
+    std::map<std::string, std::string> arguments_;
+
+    const std::string& GetArgument(const std::string& key) const;
+    bool HasArgument(const std::string& key) const;
+
+    /**
+      This adds the command at the top of the undo stack
+    */
+    //void Commit(boost::shared_ptr<TrackerCommand> cmd);
+    void Undo();
+    void Redo();
+
+    void HandleGeometryReady(const DicomVolumeImage::GeometryReadyMessage& message);
+    
+    // TODO: wire this
+    void HandleCTLoaded(const OrthancSeriesVolumeProgressiveLoader::VolumeImageReadyInHighQuality& message);
+    void HandleCTContentUpdated(const OrthancStone::DicomVolumeImage::ContentUpdatedMessage& message);
+    void HandleDoseLoaded(const OrthancStone::DicomVolumeImage::ContentUpdatedMessage& message);
+    void HandleStructuresReady(const OrthancStone::DicomStructureSetLoader::StructuresReady& message);
+    void HandleStructuresUpdated(const OrthancStone::DicomStructureSetLoader::StructuresUpdated& message);
+
+
+  private:
+    void RetrieveGeometry();
+    void FitContent();
+    void InvalidateAllViewports();
+    void UpdateLayersInAllViews();
+
+  private:
+    boost::shared_ptr<DicomVolumeImage>  ctVolume_;
+    boost::shared_ptr<DicomVolumeImage>  doseVolume_;
+
+    std::vector<boost::shared_ptr<RtViewerView> >  views_;
+
+    boost::shared_ptr<OrthancSeriesVolumeProgressiveLoader> ctLoader_;
+    boost::shared_ptr<OrthancMultiframeVolumeLoader> doseLoader_;
+    boost::shared_ptr<DicomStructureSetLoader>  rtstructLoader_;
+
+    /** encapsulates resources shared by loaders */
+    boost::shared_ptr<ILoadersContext>                  loadersContext_;
+
+    /**
+    another interface to the ctLoader object (that also implements the IVolumeSlicer interface), that serves as the 
+    reference for the geometry (position and dimensions of the volume + size of each voxel). It could be changed to be 
+    the dose instead, but the CT is chosen because it usually has a better spatial resolution.
+    */
+    boost::shared_ptr<OrthancStone::IGeometryProvider>  geometryProvider_;
+
+
+    boost::shared_ptr<IFlexiblePointerTracker> activeTracker_;
+
+    RtViewerGuiTool currentTool_;
+    boost::shared_ptr<UndoStack> undoStack_;
+  };
+
+}
+
+
+ 
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Common/RtViewerView.cpp	Wed Apr 29 22:06:24 2020 +0200
@@ -0,0 +1,319 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+// Sample app
+#include "RtViewerView.h"
+#include "RtViewerApp.h"
+#include "SampleHelpers.h"
+
+// Stone of Orthanc
+#include <Framework/StoneInitialization.h>
+#include <Framework/Scene2D/CairoCompositor.h>
+#include <Framework/Scene2D/ColorTextureSceneLayer.h>
+#include <Framework/Scene2D/OpenGLCompositor.h>
+#include <Framework/Scene2D/PanSceneTracker.h>
+#include <Framework/Scene2D/ZoomSceneTracker.h>
+#include <Framework/Scene2D/RotateSceneTracker.h>
+
+#include <Framework/Scene2DViewport/UndoStack.h>
+#include <Framework/Scene2DViewport/CreateLineMeasureTracker.h>
+#include <Framework/Scene2DViewport/CreateAngleMeasureTracker.h>
+#include <Framework/Scene2DViewport/IFlexiblePointerTracker.h>
+#include <Framework/Scene2DViewport/MeasureTool.h>
+#include <Framework/Scene2DViewport/PredeclaredTypes.h>
+#include <Framework/Volumes/VolumeSceneLayerSource.h>
+
+#include <Framework/Oracle/GetOrthancWebViewerJpegCommand.h>
+#include <Framework/Scene2D/GrayscaleStyleConfigurator.h>
+#include <Framework/Scene2D/LookupTableStyleConfigurator.h>
+#include <Framework/Volumes/DicomVolumeImageMPRSlicer.h>
+#include <Framework/StoneException.h>
+
+// Orthanc
+#include <Core/Logging.h>
+#include <Core/OrthancException.h>
+
+// System 
+#include <boost/shared_ptr.hpp>
+#include <boost/weak_ptr.hpp>
+#include <boost/make_shared.hpp>
+
+#include <stdio.h>
+
+
+namespace OrthancStone
+{
+  boost::shared_ptr<RtViewerApp> RtViewerView::GetApp()
+  {
+    return app_.lock();
+  }
+
+  void RtViewerView::DisplayInfoText()
+  {
+    std::unique_ptr<IViewport::ILock> lock(viewport_->Lock());
+    ViewportController& controller = lock->GetController();
+    Scene2D& scene = controller.GetScene();
+
+    // do not try to use stuff too early!
+    OrthancStone::ICompositor& compositor = lock->GetCompositor();
+
+    std::stringstream msg;
+
+    for (std::map<std::string, std::string>::const_iterator kv = infoTextMap_.begin();
+         kv != infoTextMap_.end(); ++kv)
+    {
+      msg << kv->first << " : " << kv->second << std::endl;
+    }
+    std::string msgS = msg.str();
+
+    TextSceneLayer* layerP = NULL;
+    if (scene.HasLayer(FIXED_INFOTEXT_LAYER_ZINDEX))
+    {
+      TextSceneLayer& layer = dynamic_cast<TextSceneLayer&>(
+        scene.GetLayer(FIXED_INFOTEXT_LAYER_ZINDEX));
+      layerP = &layer;
+    }
+    else
+    {
+      std::unique_ptr<TextSceneLayer> layer(new TextSceneLayer);
+      layerP = layer.get();
+      layer->SetColor(0, 255, 0);
+      layer->SetFontIndex(1);
+      layer->SetBorder(20);
+      layer->SetAnchor(BitmapAnchor_TopLeft);
+      //layer->SetPosition(0,0);
+      scene.SetLayer(FIXED_INFOTEXT_LAYER_ZINDEX, layer.release());
+    }
+    // position the fixed info text in the upper right corner
+    layerP->SetText(msgS.c_str());
+    double cX = compositor.GetCanvasWidth() * (-0.5);
+    double cY = compositor.GetCanvasHeight() * (-0.5);
+    scene.GetCanvasToSceneTransform().Apply(cX, cY);
+    layerP->SetPosition(cX, cY);
+    lock->Invalidate();
+  }
+
+  void RtViewerView::DisplayFloatingCtrlInfoText(const PointerEvent& e)
+  {
+    std::unique_ptr<IViewport::ILock> lock(viewport_->Lock());
+    ViewportController& controller = lock->GetController();
+    Scene2D& scene = controller.GetScene();
+
+    ScenePoint2D p = e.GetMainPosition().Apply(scene.GetCanvasToSceneTransform());
+
+    char buf[128];
+    sprintf(buf, "S:(%0.02f,%0.02f) C:(%0.02f,%0.02f)",
+            p.GetX(), p.GetY(),
+            e.GetMainPosition().GetX(), e.GetMainPosition().GetY());
+
+    if (scene.HasLayer(FLOATING_INFOTEXT_LAYER_ZINDEX))
+    {
+      TextSceneLayer& layer =
+        dynamic_cast<TextSceneLayer&>(scene.GetLayer(FLOATING_INFOTEXT_LAYER_ZINDEX));
+      layer.SetText(buf);
+      layer.SetPosition(p.GetX(), p.GetY());
+    }
+    else
+    {
+      std::unique_ptr<TextSceneLayer> layer(new TextSceneLayer);
+      layer->SetColor(0, 255, 0);
+      layer->SetText(buf);
+      layer->SetBorder(20);
+      layer->SetAnchor(BitmapAnchor_BottomCenter);
+      layer->SetPosition(p.GetX(), p.GetY());
+      scene.SetLayer(FLOATING_INFOTEXT_LAYER_ZINDEX, layer.release());
+    }
+  }
+
+  void RtViewerView::HideInfoText()
+  {
+    std::unique_ptr<IViewport::ILock> lock(viewport_->Lock());
+    ViewportController& controller = lock->GetController();
+    Scene2D& scene = controller.GetScene();
+
+    scene.DeleteLayer(FLOATING_INFOTEXT_LAYER_ZINDEX);
+  }
+
+  void RtViewerView::OnSceneTransformChanged(
+    const ViewportController::SceneTransformChanged& message)
+  {
+    DisplayInfoText();
+  }
+
+  void RtViewerView::Invalidate()
+  {
+    std::unique_ptr<OrthancStone::IViewport::ILock> lock(viewport_->Lock());
+    lock->GetCompositor().FitContent(lock->GetController().GetScene());
+    lock->Invalidate();
+  }
+
+  void RtViewerView::FitContent()
+  {
+    std::unique_ptr<OrthancStone::IViewport::ILock> lock(viewport_->Lock());
+    lock->GetCompositor().FitContent(lock->GetController().GetScene());
+    lock->Invalidate();
+  }
+
+  void RtViewerView::RetrieveGeometry()
+  {
+    const VolumeImageGeometry& geometry = GetApp()->GetMainGeometry();
+
+    const unsigned int depth = geometry.GetProjectionDepth(projection_);
+    currentPlane_ = depth / 2;
+
+    planes_.resize(depth);
+
+    for (unsigned int z = 0; z < depth; z++)
+    {
+      planes_[z] = geometry.GetProjectionSlice(projection_, z);
+    }
+
+    UpdateLayers();
+  }
+
+  void RtViewerView::UpdateLayers()
+  {
+    std::unique_ptr<IViewport::ILock> lock(viewport_->Lock());
+
+    if (planes_.size() == 0)
+    {
+      RetrieveGeometry();
+    }
+
+    if (currentPlane_ < planes_.size())
+    {
+      if (ctVolumeLayerSource_.get() != NULL)
+      {
+        ctVolumeLayerSource_->Update(planes_[currentPlane_]);
+      }
+      if (doseVolumeLayerSource_.get() != NULL)
+      {
+        doseVolumeLayerSource_->Update(planes_[currentPlane_]);
+      }
+      if (structLayerSource_.get() != NULL)
+      {
+        structLayerSource_->Update(planes_[currentPlane_]);
+      }
+    }
+    lock->Invalidate();
+  }
+
+  void RtViewerView::PrepareViewport()
+  {
+    std::unique_ptr<IViewport::ILock> lock(viewport_->Lock());
+    ViewportController& controller = lock->GetController();
+    Scene2D& scene = controller.GetScene();
+    ICompositor& compositor = lock->GetCompositor();
+
+    // False means we do NOT let a hi-DPI aware desktop managedr treat this as a legacy application that requires
+    // scaling.
+    controller.FitContent(compositor.GetCanvasWidth(), compositor.GetCanvasHeight());
+
+
+    compositor.SetFont(0, Orthanc::EmbeddedResources::UBUNTU_FONT,
+                       FONT_SIZE_0, Orthanc::Encoding_Latin1);
+    compositor.SetFont(1, Orthanc::EmbeddedResources::UBUNTU_FONT,
+                       FONT_SIZE_1, Orthanc::Encoding_Latin1);
+  }
+
+  void RtViewerView::SetInfoDisplayMessage(
+    std::string key, std::string value)
+  {
+    if (value == "")
+      infoTextMap_.erase(key);
+    else
+      infoTextMap_[key] = value;
+    DisplayInfoText();
+  }
+
+  void RtViewerView::RegisterMessages()
+  {
+    std::unique_ptr<IViewport::ILock> lock(viewport_->Lock());
+    ViewportController& controller = lock->GetController();
+    Scene2D& scene = controller.GetScene();
+    Register<ViewportController::SceneTransformChanged>(controller, &RtViewerView::OnSceneTransformChanged);
+  }
+
+  void RtViewerView::CreateLayers(boost::shared_ptr<OrthancSeriesVolumeProgressiveLoader> ctLoader,
+                                  boost::shared_ptr<OrthancMultiframeVolumeLoader>        doseLoader,
+                                  boost::shared_ptr<DicomVolumeImage>                     doseVolume,
+                                  boost::shared_ptr<DicomStructureSetLoader>              rtstructLoader)
+  {
+    /**
+    Configure the CT
+    */
+    std::auto_ptr<GrayscaleStyleConfigurator> style(new GrayscaleStyleConfigurator);
+    style->SetLinearInterpolation(true);
+
+    this->SetCtVolumeSlicer(ctLoader, style.release());
+
+    {
+      std::unique_ptr<LookupTableStyleConfigurator> config(new LookupTableStyleConfigurator);
+      config->SetLookupTable(Orthanc::EmbeddedResources::COLORMAP_HOT);
+
+      boost::shared_ptr<DicomVolumeImageMPRSlicer> tmp(new DicomVolumeImageMPRSlicer(doseVolume));
+      this->SetDoseVolumeSlicer(tmp, config.release());
+    }
+
+    this->SetStructureSet(rtstructLoader);
+  }
+
+  void RtViewerView::SetCtVolumeSlicer(const boost::shared_ptr<OrthancStone::IVolumeSlicer>& volume,
+                                       OrthancStone::ILayerStyleConfigurator* style)
+  {
+    std::unique_ptr<IViewport::ILock> lock(viewport_->Lock());
+    ViewportController& controller = lock->GetController();
+    Scene2D& scene = controller.GetScene();
+    int depth = scene.GetMaxDepth() + 1;
+
+    ctVolumeLayerSource_.reset(new OrthancStone::VolumeSceneLayerSource(scene, depth, volume));
+
+    if (style != NULL)
+    {
+      ctVolumeLayerSource_->SetConfigurator(style);
+    }
+  }
+
+  void RtViewerView::SetDoseVolumeSlicer(const boost::shared_ptr<OrthancStone::IVolumeSlicer>& volume,
+                                         OrthancStone::ILayerStyleConfigurator* style)
+  {
+    std::unique_ptr<IViewport::ILock> lock(viewport_->Lock());
+    ViewportController& controller = lock->GetController();
+    Scene2D& scene = controller.GetScene();
+    int depth = scene.GetMaxDepth() + 1;
+
+    doseVolumeLayerSource_.reset(new OrthancStone::VolumeSceneLayerSource(scene, depth, volume));
+
+    if (style != NULL)
+    {
+      doseVolumeLayerSource_->SetConfigurator(style);
+    }
+  }
+
+  void RtViewerView::SetStructureSet(const boost::shared_ptr<OrthancStone::DicomStructureSetLoader>& volume)
+  {
+    std::unique_ptr<IViewport::ILock> lock(viewport_->Lock());
+    ViewportController& controller = lock->GetController();
+    Scene2D& scene = controller.GetScene();
+    int depth = scene.GetMaxDepth() + 1;
+
+    structLayerSource_.reset(new OrthancStone::VolumeSceneLayerSource(scene, depth, volume));
+  }
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Common/RtViewerView.h	Wed Apr 29 22:06:24 2020 +0200
@@ -0,0 +1,133 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+#include <Framework/Viewport/IViewport.h>
+
+#include <Framework/Loaders/DicomStructureSetLoader.h>
+#include <Framework/Loaders/OrthancMultiframeVolumeLoader.h>
+#include <Framework/Loaders/OrthancSeriesVolumeProgressiveLoader.h>
+#include <Framework/Loaders/ILoadersContext.h>
+#include <Framework/Messages/IMessageEmitter.h>
+#include <Framework/Messages/IObserver.h>
+#include <Framework/Messages/ObserverBase.h>
+#include <Framework/Oracle/OracleCommandExceptionMessage.h>
+#include <Framework/Scene2DViewport/ViewportController.h>
+
+#include <Framework/Volumes/DicomVolumeImage.h>
+#include <Framework/Volumes/VolumeSceneLayerSource.h>
+
+#include <boost/enable_shared_from_this.hpp>
+#include <boost/thread.hpp>
+#include <boost/noncopyable.hpp>
+
+namespace OrthancStone
+{
+  class RtViewerApp;
+
+  class RtViewerView : public ObserverBase<RtViewerView>
+  {
+  public:
+    RtViewerView(boost::weak_ptr<RtViewerApp> app, 
+                 const std::string& canvasId, 
+                 VolumeProjection projection) 
+      : app_(app)
+      , currentPlane_(0)
+      , projection_(projection)
+    {
+      viewport_ = CreateViewport(canvasId);
+      FLOATING_INFOTEXT_LAYER_ZINDEX = 6;
+      FIXED_INFOTEXT_LAYER_ZINDEX = 7;
+    }
+
+    /**
+    This method is called when the scene transform changes. It allows to
+    recompute the visual elements whose content depend upon the scene transform
+    */
+    void OnSceneTransformChanged(
+      const ViewportController::SceneTransformChanged& message);
+
+    /**
+    This method will ask the VolumeSceneLayerSource, that are responsible to
+    generated 2D content based on a volume and a cutting plane, to regenerate
+    it. This is required if the volume itself changes (during loading) or if
+    the cutting plane is changed
+    */
+    void UpdateLayers();
+
+    void Refresh();
+
+    void TakeScreenshot(
+      const std::string& target,
+      unsigned int canvasWidth,
+      unsigned int canvasHeight);
+
+    void Invalidate();
+    void FitContent();
+    void RetrieveGeometry();
+    void PrepareViewport();
+    void RegisterMessages();
+
+#if ORTHANC_ENABLE_SDL == 1
+    void EnableGLDebugOutput();
+#endif
+
+    void CreateLayers(boost::shared_ptr<OrthancSeriesVolumeProgressiveLoader> ctLoader,
+                      boost::shared_ptr<OrthancMultiframeVolumeLoader>        doseLoader,
+                      boost::shared_ptr<DicomVolumeImage>                     doseVolume,
+                      boost::shared_ptr<DicomStructureSetLoader>              rtstructLoader);
+
+    boost::shared_ptr<IViewport> GetViewport()
+    {
+      return viewport_;
+    }
+
+  private:
+    void SetInfoDisplayMessage(std::string key, std::string value);
+    boost::shared_ptr<RtViewerApp> GetApp();
+    static boost::shared_ptr<IViewport> CreateViewport(const std::string& canvasId);
+    void DisplayInfoText();
+    void HideInfoText();
+    void DisplayFloatingCtrlInfoText(const PointerEvent& e);
+
+    void SetCtVolumeSlicer(const boost::shared_ptr<IVolumeSlicer>& volume,
+                           ILayerStyleConfigurator* style);
+
+    void SetDoseVolumeSlicer(const boost::shared_ptr<IVolumeSlicer>& volume,
+                             ILayerStyleConfigurator* style);
+
+    void SetStructureSet(const boost::shared_ptr<DicomStructureSetLoader>& volume);
+
+  private:
+    boost::weak_ptr<RtViewerApp> app_;
+    boost::shared_ptr<VolumeSceneLayerSource>  ctVolumeLayerSource_, doseVolumeLayerSource_, structLayerSource_;
+
+  // collection of cutting planes for this particular view
+    std::vector<OrthancStone::CoordinateSystem3D>       planes_;
+    size_t                                              currentPlane_;
+
+    VolumeProjection                                    projection_;
+
+    std::map<std::string, std::string> infoTextMap_;
+
+    int FLOATING_INFOTEXT_LAYER_ZINDEX;
+    int FIXED_INFOTEXT_LAYER_ZINDEX;
+    boost::shared_ptr<IViewport> viewport_;
+  };
+}
\ No newline at end of file
--- a/Samples/Sdl/RtViewer/CMakeLists.txt	Wed Apr 29 18:11:49 2020 +0200
+++ b/Samples/Sdl/RtViewer/CMakeLists.txt	Wed Apr 29 22:06:24 2020 +0200
@@ -45,8 +45,10 @@
 add_executable(RtViewerSdl
   RtViewerSdl.cpp
   ../SdlHelpers.h
-  ../../Common/RtViewer.cpp
-  ../../Common/RtViewer.h
+  ../../Common/RtViewerApp.cpp
+  ../../Common/RtViewerApp.h
+  ../../Common/RtViewerView.cpp
+  ../../Common/RtViewerView.h
   ../../Common/SampleHelpers.h
   ${ORTHANC_STONE_SOURCES}
   )
--- a/Samples/Sdl/RtViewer/RtViewerSdl.cpp	Wed Apr 29 18:11:49 2020 +0200
+++ b/Samples/Sdl/RtViewer/RtViewerSdl.cpp	Wed Apr 29 22:06:24 2020 +0200
@@ -18,7 +18,8 @@
  * along with this program. If not, see <http://www.gnu.org/licenses/>.
  **/
 
-#include "RtViewer.h"
+#include "RtViewerApp.h"
+#include "RtViewerView.h"
 #include "../SdlHelpers.h"
 
 // Stone of Orthanc includes
@@ -61,10 +62,16 @@
 
 namespace OrthancStone
 {
-  void RtViewerApp::CreateViewport()
+  void RtViewerView::EnableGLDebugOutput()
+  {
+    glEnable(GL_DEBUG_OUTPUT);
+    glDebugMessageCallback(OpenGLMessageCallback, 0);
+  }
+
+  boost::shared_ptr<IViewport> RtViewerView::CreateViewport(const std::string& canvasId)
   {
     // False means we do NOT let Windows treat this as a legacy application that needs to be scaled
-    viewport_ = SdlOpenGLViewport::Create("CT RTDOSE RTSTRUCT viewer", 1024, 1024, false);
+    return SdlOpenGLViewport::Create(canvasId, 1024, 1024, false);
   }
 
   void RtViewerApp::ProcessOptions(int argc, char* argv[])
@@ -113,25 +120,6 @@
   {
     ProcessOptions(argc, argv);
 
-    {
-      std::unique_ptr<IViewport::ILock> lock(viewport_->Lock());
-      ViewportController& controller = lock->GetController();
-      Scene2D& scene = controller.GetScene();
-      ICompositor& compositor = lock->GetCompositor();
-
-      // False means we do NOT let a hi-DPI aware desktop managedr treat this as a legacy application that requires
-      // scaling.
-      controller.FitContent(compositor.GetCanvasWidth(), compositor.GetCanvasHeight());
-
-      glEnable(GL_DEBUG_OUTPUT);
-      glDebugMessageCallback(OpenGLMessageCallback, 0);
-
-      compositor.SetFont(0, Orthanc::EmbeddedResources::UBUNTU_FONT,
-                         FONT_SIZE_0, Orthanc::Encoding_Latin1);
-      compositor.SetFont(1, Orthanc::EmbeddedResources::UBUNTU_FONT,
-                         FONT_SIZE_1, Orthanc::Encoding_Latin1);
-    }
-
     /**
     Create the shared loaders context
     */
@@ -170,22 +158,44 @@
 
     loadersContext->StartOracle();
 
+    CreateLoaders();
+
+    /**
+    Create viewports
+    */
+    CreateView("RtViewer Axial", VolumeProjection_Axial);
+    CreateView("RtViewer Coronal", VolumeProjection_Coronal);
+    CreateView("RtViewer Sagittal", VolumeProjection_Sagittal);
+
+    for (size_t i = 0; i < views_.size(); ++i)
+    {
+      views_[i]->PrepareViewport();
+      views_[i]->EnableGLDebugOutput();
+    }
+
+    DefaultViewportInteractor interactor;
+
     /**
     It is very important that the Oracle (responsible for network I/O) be started before creating and firing the 
     loaders, for any command scheduled by the loader before the oracle is started will be lost.
     */
-    PrepareLoadersAndSlicers();
+    StartLoaders();
 
-    DefaultViewportInteractor interactor;
 
-    boost::shared_ptr<SdlViewport> viewport = boost::dynamic_pointer_cast<SdlViewport>(viewport_);
-
-    OrthancStoneHelpers::SdlRunLoop(viewport, interactor);
-
+    std::vector<boost::shared_ptr<SdlViewport>> sdlViewports;
+    for(size_t i = 0; i < views_.size(); ++i)
+    {
+      boost::shared_ptr<RtViewerView> view = views_[i];
+      boost::shared_ptr<IViewport> viewport = view->GetViewport();
+      boost::shared_ptr<SdlViewport> sdlViewport = 
+        boost::dynamic_pointer_cast<SdlViewport>(viewport);
+      sdlViewports.push_back(sdlViewport);
+    }
+    OrthancStoneHelpers::SdlRunLoop(sdlViewports, interactor);
     loadersContext->StopOracle();
   }
 
-  void RtViewerApp::TakeScreenshot(const std::string& target,
+  void RtViewerView::TakeScreenshot(const std::string& target,
                                    unsigned int canvasWidth,
                                    unsigned int canvasHeight)
   {
@@ -206,328 +216,6 @@
     Orthanc::PngWriter writer;
     writer.WriteToFile(target, png);
   }
-
-
-#if 0
-  void RtViewerApp::HandleApplicationEvent(
-    const SDL_Event& event)
-  {
-    //DisplayInfoText();
-
-    std::unique_ptr<IViewport::ILock> lock(viewport_->Lock());
-    ViewportController& controller = lock->GetController();
-    Scene2D& scene = controller.GetScene();
-    ICompositor& compositor = lock->GetCompositor();
-
-    if (event.type == SDL_MOUSEMOTION)
-    {
-      int scancodeCount = 0;
-      const uint8_t* keyboardState = SDL_GetKeyboardState(&scancodeCount);
-
-      if (activeTracker_.get() == NULL &&
-          SDL_SCANCODE_LALT < scancodeCount &&
-          keyboardState[SDL_SCANCODE_LALT])
-      {
-        // The "left-ctrl" key is down, while no tracker is present
-        // Let's display the info text
-        PointerEvent e;
-        e.AddPosition(compositor.GetPixelCenterCoordinates(
-          event.button.x, event.button.y));
-
-        DisplayFloatingCtrlInfoText(e);
-      }
-      else
-      {
-        HideInfoText();
-        //LOG(TRACE) << "(event.type == SDL_MOUSEMOTION)";
-        if (activeTracker_.get() != NULL)
-        {
-          //LOG(TRACE) << "(activeTracker_.get() != NULL)";
-          PointerEvent e;
-          e.AddPosition(compositor.GetPixelCenterCoordinates(
-            event.button.x, event.button.y));
-
-          //LOG(TRACE) << "event.button.x = " << event.button.x << "     " <<
-          //  "event.button.y = " << event.button.y;
-          LOG(TRACE) << "activeTracker_->PointerMove(e); " <<
-            e.GetMainPosition().GetX() << " " << e.GetMainPosition().GetY();
-
-          activeTracker_->PointerMove(e);
-          if (!activeTracker_->IsAlive())
-            activeTracker_.reset();
-        }
-      }
-    }
-    else if (event.type == SDL_MOUSEBUTTONUP)
-    {
-      if (activeTracker_)
-      {
-        PointerEvent e;
-        e.AddPosition(compositor.GetPixelCenterCoordinates(event.button.x, event.button.y));
-        activeTracker_->PointerUp(e);
-        if (!activeTracker_->IsAlive())
-          activeTracker_.reset();
-      }
-    }
-    else if (event.type == SDL_MOUSEBUTTONDOWN)
-    {
-      PointerEvent e;
-      e.AddPosition(compositor.GetPixelCenterCoordinates(
-        event.button.x, event.button.y));
-      if (activeTracker_)
-      {
-        activeTracker_->PointerDown(e);
-        if (!activeTracker_->IsAlive())
-          activeTracker_.reset();
-      }
-      else
-      {
-        // we ATTEMPT to create a tracker if need be
-        activeTracker_ = CreateSuitableTracker(event, e);
-      }
-    }
-    else if (event.type == SDL_KEYDOWN &&
-             event.key.repeat == 0 /* Ignore key bounce */)
-    {
-      switch (event.key.keysym.sym)
-      {
-      case SDLK_ESCAPE:
-        if (activeTracker_)
-        {
-          activeTracker_->Cancel();
-          if (!activeTracker_->IsAlive())
-            activeTracker_.reset();
-        }
-        break;
-
-      case SDLK_r:
-        UpdateLayers();
-        {
-          std::unique_ptr<IViewport::ILock> lock(viewport_->Lock());
-          lock->Invalidate();
-        }
-        break;
-
-      case SDLK_s:
-        compositor.FitContent(scene);
-        break;
-
-      case SDLK_t:
-        if (!activeTracker_)
-          SelectNextTool();
-        else
-        {
-          LOG(WARNING) << "You cannot change the active tool when an interaction"
-            " is taking place";
-        }
-        break;
-
-      case SDLK_z:
-        LOG(TRACE) << "SDLK_z has been pressed. event.key.keysym.mod == " << event.key.keysym.mod;
-        if (event.key.keysym.mod & KMOD_CTRL)
-        {
-          if (controller.CanUndo())
-          {
-            LOG(TRACE) << "Undoing...";
-            controller.Undo();
-          }
-          else
-          {
-            LOG(WARNING) << "Nothing to undo!!!";
-          }
-        }
-        break;
-
-      case SDLK_y:
-        LOG(TRACE) << "SDLK_y has been pressed. event.key.keysym.mod == " << event.key.keysym.mod;
-        if (event.key.keysym.mod & KMOD_CTRL)
-        {
-          if (controller.CanRedo())
-          {
-            LOG(TRACE) << "Redoing...";
-            controller.Redo();
-          }
-          else
-          {
-            LOG(WARNING) << "Nothing to redo!!!";
-          }
-        }
-        break;
-
-      case SDLK_c:
-        TakeScreenshot(
-          "screenshot.png",
-          compositor.GetCanvasWidth(),
-          compositor.GetCanvasHeight());
-        break;
-
-      default:
-        break;
-      }
-    }
-    else if (viewport_->IsRefreshEvent(event))
-    {
-      // the viewport has been invalidated and requires repaint
-      viewport_->Paint();
-    }
-  }
-#endif 
-
-#if 0
-  void RtViewerApp::RunSdl(int argc, char* argv[])
-  {
-    ProcessOptions(argc, argv);
-
-    {
-      std::unique_ptr<IViewport::ILock> lock(viewport_->Lock());
-      ViewportController& controller = lock->GetController();
-      Scene2D& scene = controller.GetScene();
-      ICompositor& compositor = lock->GetCompositor();
-
-      // False means we do NOT let a hi-DPI aware desktop managedr treat this as a legacy application that requires
-      // scaling.
-      controller.FitContent(compositor.GetCanvasWidth(), compositor.GetCanvasHeight());
-
-      glEnable(GL_DEBUG_OUTPUT);
-      glDebugMessageCallback(OpenGLMessageCallback, 0);
-
-      compositor.SetFont(0, Orthanc::EmbeddedResources::UBUNTU_FONT,
-                         FONT_SIZE_0, Orthanc::Encoding_Latin1);
-      compositor.SetFont(1, Orthanc::EmbeddedResources::UBUNTU_FONT,
-                         FONT_SIZE_1, Orthanc::Encoding_Latin1);
-    }
-                     //////// from loader
-    
-    loadersContext_.reset(new GenericLoadersContext(1, 4, 1));
-    loadersContext_->StartOracle();
-
-    /**
-    It is very important that the Oracle (responsible for network I/O) be started before creating and firing the 
-    loaders, for any command scheduled by the loader before the oracle is started will be lost.
-    */
-    PrepareLoadersAndSlicers();
-
-    bool stopApplication = false;
-
-    while (!stopApplication)
-    {
-      SDL_Event event;
-      while (!stopApplication && SDL_PollEvent(&event))
-      {
-        if (event.type == SDL_QUIT)
-        {
-          stopApplication = true;
-          break;
-        }
-        else if (event.type == SDL_WINDOWEVENT &&
-                 event.window.event == SDL_WINDOWEVENT_SIZE_CHANGED)
-        {
-          DisableTracker();
-        }
-        else if (event.type == SDL_KEYDOWN &&
-                 event.key.repeat == 0 /* Ignore key bounce */)
-        {
-          switch (event.key.keysym.sym)
-          {
-          case SDLK_f:
-            // TODO: implement GetWindow to be able to do:
-            // viewport_->GetWindow().ToggleMaximize();
-            ORTHANC_ASSERT(false, "Please implement GetWindow()");
-            break;
-          case SDLK_q:
-            stopApplication = true;
-            break;
-          default:
-            break;
-          }
-        }
-        // the code above is rather application-neutral.
-        // the following call handles events specific to the application
-        HandleApplicationEvent(event);
-      }
-      SDL_Delay(1);
-    }
-    loadersContext_->StopOracle();
-  }
-#endif
-
-#if 0
-  boost::shared_ptr<IFlexiblePointerTracker> RtViewerApp::CreateSuitableTracker(
-    const SDL_Event& event,
-    const PointerEvent& e)
-  {
-    std::unique_ptr<IViewport::ILock> lock(viewport_->Lock());
-    ViewportController& controller = lock->GetController();
-    Scene2D& scene = controller.GetScene();
-    ICompositor& compositor = lock->GetCompositor();
-
-    using namespace Orthanc;
-
-    switch (event.button.button)
-    {
-    case SDL_BUTTON_MIDDLE:
-      return boost::shared_ptr<IFlexiblePointerTracker>(new PanSceneTracker
-      (viewport_, e));
-
-    case SDL_BUTTON_RIGHT:
-      return boost::shared_ptr<IFlexiblePointerTracker>(new ZoomSceneTracker
-      (viewport_, e, compositor.GetCanvasHeight()));
-
-    case SDL_BUTTON_LEFT:
-    {
-      //LOG(TRACE) << "CreateSuitableTracker: case SDL_BUTTON_LEFT:";
-      // TODO: we need to iterate on the set of measuring tool and perform
-      // a hit test to check if a tracker needs to be created for edition.
-      // Otherwise, depending upon the active tool, we might want to create
-      // a "measuring tool creation" tracker
-
-      // TODO: if there are conflicts, we should prefer a tracker that 
-      // pertains to the type of measuring tool currently selected (TBD?)
-      boost::shared_ptr<IFlexiblePointerTracker> hitTestTracker = TrackerHitTest(e);
-
-      if (hitTestTracker != NULL)
-      {
-        //LOG(TRACE) << "hitTestTracker != NULL";
-        return hitTestTracker;
-      }
-      else
-      {
-        switch (currentTool_)
-        {
-        case RtViewerGuiTool_Rotate:
-          //LOG(TRACE) << "Creating RotateSceneTracker";
-          return boost::shared_ptr<IFlexiblePointerTracker>(new RotateSceneTracker(viewport_, e));
-        case RtViewerGuiTool_Pan:
-          return boost::shared_ptr<IFlexiblePointerTracker>(new PanSceneTracker(viewport_, e));
-        case RtViewerGuiTool_Zoom:
-          return boost::shared_ptr<IFlexiblePointerTracker>(new ZoomSceneTracker(viewport_, e, compositor.GetCanvasHeight()));
-        //case GuiTool_AngleMeasure:
-        //  return new AngleMeasureTracker(GetScene(), e);
-        //case GuiTool_CircleMeasure:
-        //  return new CircleMeasureTracker(GetScene(), e);
-        //case GuiTool_EllipseMeasure:
-        //  return new EllipseMeasureTracker(GetScene(), e);
-        case RtViewerGuiTool_LineMeasure:
-          return boost::shared_ptr<IFlexiblePointerTracker>(new CreateLineMeasureTracker(viewport_, e));
-        case RtViewerGuiTool_AngleMeasure:
-          return boost::shared_ptr<IFlexiblePointerTracker>(new CreateAngleMeasureTracker(viewport_, e));
-        case RtViewerGuiTool_CircleMeasure:
-          LOG(ERROR) << "Not implemented yet!";
-          return boost::shared_ptr<IFlexiblePointerTracker>();
-        case RtViewerGuiTool_EllipseMeasure:
-          LOG(ERROR) << "Not implemented yet!";
-          return boost::shared_ptr<IFlexiblePointerTracker>();
-        default:
-          throw OrthancException(ErrorCode_InternalError, "Wrong tool!");
-        }
-      }
-    }
-    default:
-      return boost::shared_ptr<IFlexiblePointerTracker>();
-    }
-  }
-#endif
-
 }
 
 boost::weak_ptr<OrthancStone::RtViewerApp> g_app;
--- a/Samples/Sdl/SdlHelpers.h	Wed Apr 29 18:11:49 2020 +0200
+++ b/Samples/Sdl/SdlHelpers.h	Wed Apr 29 22:06:24 2020 +0200
@@ -99,8 +99,23 @@
     p.SetShiftModifier(modifiers & KeyboardModifiers_Shift);
   }
 
+  static boost::shared_ptr<OrthancStone::SdlViewport> GetSdlViewportFromWindowId(
+    const std::vector<boost::shared_ptr<OrthancStone::SdlViewport> >& viewports,
+    Uint32 windowID)
+  {
+    using namespace OrthancStone;
+    for (size_t i = 0; i < viewports.size(); ++i)
+    {
+      boost::shared_ptr<IViewport> viewport = viewports[i];
+      boost::shared_ptr<SdlViewport> sdlViewport = boost::dynamic_pointer_cast<SdlViewport>(viewport);
+      Uint32 curWindowID = sdlViewport->GetSdlWindowId();
+      if (windowID == curWindowID)
+        return sdlViewport;
+    }
+    return NULL;
+  }
 
-  inline void SdlRunLoop(boost::shared_ptr<OrthancStone::SdlViewport> viewport,
+  inline void SdlRunLoop(const std::vector<boost::shared_ptr<OrthancStone::SdlViewport> >& viewports,
                          OrthancStone::IViewportInteractor& interactor)
   {
     using namespace OrthancStone;
@@ -120,25 +135,28 @@
             stop = true;
             break;
           }
-          else if (viewport->IsRefreshEvent(event))
-          {
-            paint = true;
-          }
           else if (event.type == SDL_WINDOWEVENT &&
                    (event.window.event == SDL_WINDOWEVENT_RESIZED ||
                     event.window.event == SDL_WINDOWEVENT_SIZE_CHANGED))
           {
+            boost::shared_ptr<SdlViewport> viewport = GetSdlViewportFromWindowId(
+              viewports, event.window.windowID);
             viewport->UpdateSize(event.window.data1, event.window.data2);
           }
           else if (event.type == SDL_WINDOWEVENT &&
                    (event.window.event == SDL_WINDOWEVENT_SHOWN ||
                     event.window.event == SDL_WINDOWEVENT_EXPOSED))
           {
-            paint = true;
+            boost::shared_ptr<SdlViewport> viewport = GetSdlViewportFromWindowId(
+              viewports, event.window.windowID);
+            viewport->Paint();
           }
           else if (event.type == SDL_KEYDOWN &&
                    event.key.repeat == 0 /* Ignore key bounce */)
           {
+            boost::shared_ptr<SdlViewport> viewport = GetSdlViewportFromWindowId(
+              viewports, event.window.windowID);
+
             switch (event.key.keysym.sym)
             {
             case SDLK_f:
@@ -146,12 +164,12 @@
               break;
 
             case SDLK_s:
-              {
-                std::unique_ptr<OrthancStone::IViewport::ILock> lock(viewport->Lock());
-                lock->GetCompositor().FitContent(lock->GetController().GetScene());
-                lock->Invalidate();
-              }
-              break;
+            {
+              std::unique_ptr<OrthancStone::IViewport::ILock> lock(viewport->Lock());
+              lock->GetCompositor().FitContent(lock->GetController().GetScene());
+              lock->Invalidate();
+            }
+            break;
 
             case SDLK_q:
               stop = true;
@@ -165,6 +183,9 @@
                    event.type == SDL_MOUSEMOTION ||
                    event.type == SDL_MOUSEBUTTONUP)
           {
+            boost::shared_ptr<SdlViewport> viewport = GetSdlViewportFromWindowId(
+              viewports, event.window.windowID);
+
             std::auto_ptr<OrthancStone::IViewport::ILock> lock(viewport->Lock());
             if (lock->HasCompositor())
             {
@@ -198,21 +219,21 @@
               }
             }
           }
+          else
+          {
+            for (size_t i = 0; i < viewports.size(); ++i)
+            {
+              boost::shared_ptr<SdlViewport> viewport = viewports[i];
+              if (viewport->IsRefreshEvent(event))
+                viewport->Paint();
+            }
+          }
         }
-
-        if (paint)
-        {
-          viewport->Paint();
-        }
-
-        // Small delay to avoid using 100% of CPU
-        SDL_Delay(1);
       }
+      // Small delay to avoid using 100% of CPU
+      SDL_Delay(1);
     }
   }
-
+}
 
 
-
-}
-
--- a/Samples/WebAssembly/RtViewer/CMakeLists.txt	Wed Apr 29 18:11:49 2020 +0200
+++ b/Samples/WebAssembly/RtViewer/CMakeLists.txt	Wed Apr 29 22:06:24 2020 +0200
@@ -65,9 +65,10 @@
 # ---------------------------------------------------------------
 add_executable(RtViewerWasm
   RtViewerWasm.cpp
-  ../../Common/RtViewer.cpp
-  ../../Common/RtViewer.h
-
+  ../../Common/RtViewerApp.cpp
+  ../../Common/RtViewerApp.h
+  ../../Common/RtViewerView.cpp
+  ../../Common/RtViewerView.h
   ${ORTHANC_STONE_SOURCES}
   )
 
--- a/Samples/WebAssembly/RtViewer/RtViewerWasm.cpp	Wed Apr 29 18:11:49 2020 +0200
+++ b/Samples/WebAssembly/RtViewer/RtViewerWasm.cpp	Wed Apr 29 22:06:24 2020 +0200
@@ -18,7 +18,8 @@
  * along with this program. If not, see <http://www.gnu.org/licenses/>.
  **/
 
-#include "RtViewer.h"
+#include "RtViewerApp.h"
+#include "RtViewerView.h"
 #include "SampleHelpers.h"
 
 // Stone of Orthanc includes
@@ -74,12 +75,13 @@
 
 namespace OrthancStone
 {
-  void RtViewerApp::CreateViewport()
+  boost::shared_ptr<IViewport> RtViewerView::CreateViewport(
+    const std::string& canvasId)
   {
-    viewport_ = WebGLViewport::Create("mycanvas1");
+    return WebGLViewport::Create(canvasId);
   }
 
-  void RtViewerApp::TakeScreenshot(const std::string& target,
+  void RtViewerView::TakeScreenshot(const std::string& target,
                                    unsigned int canvasWidth,
                                    unsigned int canvasHeight)
   {
@@ -89,20 +91,6 @@
 
   void RtViewerApp::RunWasm()
   {
-    {
-      std::unique_ptr<IViewport::ILock> lock(viewport_->Lock());
-      ViewportController& controller = lock->GetController();
-      Scene2D& scene = controller.GetScene();
-      ICompositor& compositor = lock->GetCompositor();
-
-      controller.FitContent(compositor.GetCanvasWidth(), compositor.GetCanvasHeight());
-
-      compositor.SetFont(0, Orthanc::EmbeddedResources::UBUNTU_FONT,
-                         FONT_SIZE_0, Orthanc::Encoding_Latin1);
-      compositor.SetFont(1, Orthanc::EmbeddedResources::UBUNTU_FONT,
-                         FONT_SIZE_1, Orthanc::Encoding_Latin1);
-    }
-
     loadersContext_.reset(new OrthancStone::WebAssemblyLoadersContext(1, 4, 1));
 
     // we are in WASM --> downcast to concrete type
@@ -116,7 +104,18 @@
 
     loadersContext->SetDicomCacheSize(128 * 1024 * 1024);  // 128MB
 
-    PrepareLoadersAndSlicers();
+    CreateLoaders();
+    
+    CreateView("RtViewer_Axial", VolumeProjection_Axial);
+    CreateView("RtViewer_Coronal", VolumeProjection_Coronal);
+    CreateView("RtViewer_Sagittal", VolumeProjection_Sagittal);
+
+    for (size_t i = 0; i < views_.size(); ++i)
+    {
+      views_[i]->PrepareViewport();
+    }
+
+    StartLoaders();
 
     DefaultViewportInteractor interactor;
   }
--- a/Samples/WebAssembly/RtViewer/RtViewerWasmApp.js	Wed Apr 29 18:11:49 2020 +0200
+++ b/Samples/WebAssembly/RtViewer/RtViewerWasmApp.js	Wed Apr 29 22:06:24 2020 +0200
@@ -81,5 +81,5 @@
     });
 });
 
-// http://localhost:9979/rtviewer/index.html?loglevel=trace&ctseries=CTSERIES&rtdose=RTDOSE&rtstruct=RTSTRUCT
+// http://localhost:9979/rtviewer/index.html?loglevel=trace&ctseries=a04ecf01-79b2fc33-58239f7e-ad9db983-28e81afa&rtdose=830a69ff-8e4b5ee3-b7f966c8-bccc20fb-d322dceb&rtstruct=54460695-ba3885ee-ddf61ac0-f028e31d-a6e474d9
 
--- a/Samples/WebAssembly/RtViewer/index.html	Wed Apr 29 18:11:49 2020 +0200
+++ b/Samples/WebAssembly/RtViewer/index.html	Wed Apr 29 22:06:24 2020 +0200
@@ -1,4 +1,4 @@
-<!doctype html>
+<!doctype html>
 <html lang="en-us">
   <head>
     <title>Stone of Orthanc Single Frame Viewer </title>
@@ -23,38 +23,38 @@
       display: block;  /* No floating content on sides */
       }
 
-      #mycanvas1 {
-      position:absolute;
-      left:0%;
-      top:0%;
-      background-color: red;
-      width: 50%;
-      height: 100%;
+      #RtViewer_Axial {
+        position: absolute;
+        left: 0%;
+        top: 0%;
+        background-color: red;
+        width: 50%;
+        height: 100%;
       }
 
-      #mycanvas2 {
-      position:absolute;
-      left:50%;
-      top:0%;
-      background-color: green;
-      width: 50%;
-      height: 50%;
+      #RtViewer_Coronal {
+        position: absolute;
+        left: 50%;
+        top: 0%;
+        background-color: green;
+        width: 50%;
+        height: 50%;
       }
 
-      #mycanvas3 {
-      position:absolute;
-      left:50%;
-      top:50%;
-      background-color: blue;
-      width: 50%;
-      height: 50%;
+      #RtViewer_Sagittal {
+        position: absolute;
+        left: 50%;
+        top: 50%;
+        background-color: blue;
+        width: 50%;
+        height: 50%;
       }
     </style>
   </head>
   <body>
-    <canvas id="mycanvas1" oncontextmenu="return false;"></canvas>
-    <canvas id="mycanvas2" oncontextmenu="return false;"></canvas>
-    <canvas id="mycanvas3" oncontextmenu="return false;"></canvas>
+    <canvas id="RtViewer_Axial" oncontextmenu="return false;"></canvas>
+    <canvas id="RtViewer_Coronal" oncontextmenu="return false;"></canvas>
+    <canvas id="RtViewer_Sagittal" oncontextmenu="return false;"></canvas>
 
     <script src="https://code.jquery.com/jquery-3.4.1.js"></script>
     <script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.19.0/axios.js"></script>