changeset 1383:ab871499ed30

SingleFrameViewer: refactored file locations + names to share files for RtViewer
author Benjamin Golinvaux <bgo@osimis.io>
date Mon, 27 Apr 2020 10:01:03 +0200
parents 9d138883be66
children 24bcff8ea58f
files Samples/Common/RtViewer.cpp Samples/Common/RtViewer.h Samples/Common/SampleHelpers.h Samples/CommonHelpers.h Samples/Sdl/SimpleViewer/CMakeLists.txt Samples/Sdl/SimpleViewer/CMakeSettings.json Samples/Sdl/SimpleViewer/SdlSimpleViewer.cpp Samples/Sdl/SimpleViewer/SdlSimpleViewerApplication.h Samples/Sdl/SimpleViewer/SimpleViewer.cpp Samples/Sdl/SingleFrameViewer/CMakeLists.txt Samples/Sdl/SingleFrameViewer/CMakeSettings.json Samples/Sdl/SingleFrameViewer/SdlSimpleViewer.cpp Samples/Sdl/SingleFrameViewer/SdlSimpleViewerApplication.h Samples/Sdl/SingleFrameViewer/SimpleViewer.cpp Samples/WebAssembly/SingleFrameViewer/CMakeLists.txt
diffstat 15 files changed, 1960 insertions(+), 812 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Common/RtViewer.cpp	Mon Apr 27 10:01:03 2020 +0200
@@ -0,0 +1,923 @@
+/**
+ * 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 "RtViewer.h"
+
+#include <stdio.h>
+
+#include <boost/shared_ptr.hpp>
+#include <boost/weak_ptr.hpp>
+#include <boost/make_shared.hpp>
+
+#include <Core/Images/Image.h>
+#include <Core/Images/ImageProcessing.h>
+#include <Core/Images/PngWriter.h>
+#include <Core/Logging.h>
+#include <Core/OrthancException.h>
+
+#include <Framework/OpenGL/SdlOpenGLContext.h>
+#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/Oracle/ThreadedOracle.h>
+#include <Framework/Scene2D/GrayscaleStyleConfigurator.h>
+#include <Framework/Scene2D/LookupTableStyleConfigurator.h>
+#include <Framework/Volumes/DicomVolumeImageMPRSlicer.h>
+#include <Framework/StoneException.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::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_t:
+        if (!activeTracker_)
+          SelectNextTool();
+        else
+        {
+          LOG(WARNING) << "You cannot change the active tool when an interaction"
+            " is taking place";
+        }
+        break;
+      case SDLK_s:
+        compositor.FitContent(scene);
+        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))
+    {
+      viewport_->Paint();
+    }
+  }
+
+  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();
+  }
+
+  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>();
+    }
+  }
+
+
+  RtViewerApp::RtViewerApp()
+    : oracle_(*this)
+    , currentTool_(RtViewerGuiTool_Rotate)
+    , undoStack_(new UndoStack)
+    , currentPlane_(0)
+    , projection_(VolumeProjection_Coronal)
+  {
+    loadersContext_.reset(new GenericLoadersContext(1, 4, 1));
+    loadersContext_->StartOracle();
+
+    // 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);
+
+    std::unique_ptr<IViewport::ILock> lock(viewport_->Lock());
+    ViewportController& controller = lock->GetController();
+    Scene2D& scene = controller.GetScene();
+
+    //oracleObservable.RegisterObserverCallback
+    //(new Callable
+    //  <RtViewerApp, SleepOracleCommand::TimeoutMessage>(*this, &RtViewerApp::Handle));
+
+    //oracleObservable.RegisterObserverCallback
+    //(new Callable
+    //  <Toto, GetOrthancImageCommand::SuccessMessage>(*this, &RtViewerApp::Handle));
+
+    //oracleObservable.RegisterObserverCallback
+    //(new Callable
+    //  <RtViewerApp, GetOrthancWebViewerJpegCommand::SuccessMessage>(*this, &ToRtViewerAppto::Handle));
+
+
+    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<OracleCommandExceptionMessage>(oracleObservable_, &RtViewerApp::Handle);
+    Register<ViewportController::SceneTransformChanged>(controller, &RtViewerApp::OnSceneTransformChanged);
+  }
+
+  boost::shared_ptr<RtViewerApp> RtViewerApp::Create()
+  {
+    boost::shared_ptr<RtViewerApp> thisOne(new RtViewerApp());
+    thisOne->RegisterMessages();
+    return thisOne;
+  }
+
+  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));
+    }
+  }
+
+  void RtViewerApp::DisableTracker()
+  {
+    if (activeTracker_)
+    {
+      activeTracker_->Cancel();
+      activeTracker_.reset();
+    }
+  }
+
+  void RtViewerApp::TakeScreenshot(const std::string& target,
+                                                unsigned int canvasWidth,
+                                                unsigned int canvasHeight)
+  {
+    std::unique_ptr<IViewport::ILock> lock(viewport_->Lock());
+    ViewportController& controller = lock->GetController();
+    Scene2D& scene = controller.GetScene();
+
+    CairoCompositor compositor(canvasWidth, canvasHeight);
+    compositor.SetFont(0, Orthanc::EmbeddedResources::UBUNTU_FONT, FONT_SIZE_0, Orthanc::Encoding_Latin1);
+    compositor.Refresh(scene);
+
+    Orthanc::ImageAccessor canvas;
+    compositor.GetCanvas().GetReadOnlyAccessor(canvas);
+
+    Orthanc::Image png(Orthanc::PixelFormat_RGB24, canvas.GetWidth(), canvas.GetHeight(), false);
+    Orthanc::ImageProcessing::Convert(png, canvas);
+
+    Orthanc::PngWriter writer;
+    writer.WriteToFile(target, png);
+  }
+
+  boost::shared_ptr<IFlexiblePointerTracker> RtViewerApp::TrackerHitTest(const PointerEvent& e)
+  {
+    // std::vector<boost::shared_ptr<MeasureTool>> measureTools_;
+    return boost::shared_ptr<IFlexiblePointerTracker>();
+  }
+
+  static void GLAPIENTRY
+    OpenGLMessageCallback(GLenum source,
+                          GLenum type,
+                          GLuint id,
+                          GLenum severity,
+                          GLsizei length,
+                          const GLchar* message,
+                          const void* userParam)
+  {
+    if (severity != GL_DEBUG_SEVERITY_NOTIFICATION)
+    {
+      fprintf(stderr, "GL CALLBACK: %s type = 0x%x, severity = 0x%x, message = %s\n",
+              (type == GL_DEBUG_TYPE_ERROR ? "** GL ERROR **" : ""),
+              type, severity, message);
+    }
+  }
+
+  static bool g_stopApplication = false;
+
+
+  void RtViewerApp::Handle(const DicomVolumeImage::GeometryReadyMessage& message)
+  {
+    RetrieveGeometry();
+  }
+
+
+  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;
+    }
+  }
+
+
+  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::SetCtVolume(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::SetDoseVolume(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::Run()
+  {
+    {
+      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 Windows treat this as a legacy application
+      // that needs to be scaled
+      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
+    {
+      Orthanc::WebServiceParameters p;
+      //p.SetUrl("http://localhost:8043/");
+      p.SetCredentials("orthanc", "orthanc");
+      oracle_.SetOrthancParameters(p);
+    }
+
+    //////// from Run
+
+    boost::shared_ptr<DicomVolumeImage>  ctVolume(new DicomVolumeImage);
+    boost::shared_ptr<DicomVolumeImage>  doseVolume(new DicomVolumeImage);
+
+
+    boost::shared_ptr<OrthancSeriesVolumeProgressiveLoader> ctLoader;
+    boost::shared_ptr<OrthancMultiframeVolumeLoader> doseLoader;
+    boost::shared_ptr<DicomStructureSetLoader>  rtstructLoader;
+
+    {
+      // "true" means use progressive quality (jpeg 50 --> jpeg 90 --> 16-bit raw)
+      ctLoader = OrthancSeriesVolumeProgressiveLoader::Create(*loadersContext_, ctVolume, false);
+      
+      // 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<DicomVolumeImage::GeometryReadyMessage>(*ctLoader, &RtViewerApp::Handle);
+    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);
+
+    std::auto_ptr<GrayscaleStyleConfigurator> style(new GrayscaleStyleConfigurator);
+    style->SetLinearInterpolation(true);
+
+    this->SetCtVolume(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->SetDoseVolume(LAYER_POSITION + 1, tmp, config.release());
+    }
+
+    this->SetStructureSet(LAYER_POSITION + 2, rtstructLoader);
+
+#if 1
+    /*
+    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->LoadInstance("54460695-ba3885ee-ddf61ac0-f028e31d-a6e474d9");  // RT-STRUCT
+#else
+    //ctLoader->LoadSeries("cb3ea4d1-d08f3856-ad7b6314-74d88d77-60b05618");  // CT
+    //doseLoader->LoadInstance("41029085-71718346-811efac4-420e2c15-d39f99b6");  // RT-DOSE
+    //rtstructLoader->LoadInstance("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->LoadInstance("54460695-ba3885ee-ddf61ac0-f028e31d-a6e474d9");  // RT-STRUCT
+#endif
+
+    oracle_.Start();
+
+//// END from loader
+
+    while (!g_stopApplication)
+    {
+      //compositor.Refresh(scene);
+
+//////// from loader
+//// END from loader
+
+      SDL_Event event;
+      while (!g_stopApplication && SDL_PollEvent(&event))
+      {
+        if (event.type == SDL_QUIT)
+        {
+          g_stopApplication = true;
+          break;
+        }
+        else if (event.type == SDL_WINDOWEVENT &&
+                 event.window.event == SDL_WINDOWEVENT_SIZE_CHANGED)
+        {
+          DisableTracker(); // was: tracker.reset(NULL);
+        }
+        else if (event.type == SDL_KEYDOWN &&
+                 event.key.repeat == 0 /* Ignore key bounce */)
+        {
+          switch (event.key.keysym.sym)
+          {
+          case SDLK_f:
+            // TODO: implement GetWindow!!!
+            // viewport_->GetContext()->GetWindow().ToggleMaximize();
+            ORTHANC_ASSERT(false, "Please implement GetWindow()");
+            break;
+
+          case SDLK_r:
+            break;
+
+          case SDLK_s:
+            FitContent();
+            break;
+
+          case SDLK_q:
+            g_stopApplication = true;
+            break;
+          default:
+            break;
+          }
+        }
+        HandleApplicationEvent(event);
+      }
+      SDL_Delay(1);
+    }
+
+    //// from loader
+
+    //Orthanc::SystemToolbox::ServerBarrier();
+
+    /**
+     * WARNING => The oracle must be stopped BEFORE the objects using
+     * it are destroyed!!! This forces to wait for the completion of
+     * the running callback methods. Otherwise, the callbacks methods
+     * might still be running while their parent object is destroyed,
+     * resulting in crashes. This is very visible if adding a sleep(),
+     * as in (*).
+     **/
+
+    oracle_.Stop();
+    //// END from loader
+  }
+
+  void RtViewerApp::SetInfoDisplayMessage(
+    std::string key, std::string value)
+  {
+    if (value == "")
+      infoTextMap_.erase(key);
+    else
+      infoTextMap_[key] = value;
+    DisplayInfoText();
+  }
+
+}
+
+
+boost::weak_ptr<OrthancStone::RtViewerApp> g_app;
+
+void RtViewer_SetInfoDisplayMessage(std::string key, std::string value)
+{
+  boost::shared_ptr<OrthancStone::RtViewerApp> app = g_app.lock();
+  if (app)
+  {
+    app->SetInfoDisplayMessage(key, value);
+  }
+}
+
+/**
+ * IMPORTANT: The full arguments to "main()" are needed for SDL on
+ * Windows. Otherwise, one gets the linking error "undefined reference
+ * to `SDL_main'". https://wiki.libsdl.org/FAQWindows
+ **/
+int main(int argc, char* argv[])
+{
+  using namespace OrthancStone;
+
+  StoneInitialize();
+  Orthanc::Logging::EnableInfoLevel(true);
+  //Orthanc::Logging::EnableTraceLevel(true);
+
+  try
+  {
+    boost::shared_ptr<RtViewerApp> app = RtViewerApp::Create();
+    g_app = app;
+    //app->PrepareScene();
+    app->Run();
+  }
+  catch (Orthanc::OrthancException& e)
+  {
+    LOG(ERROR) << "EXCEPTION: " << e.What();
+  }
+
+  StoneFinalize();
+
+  return 0;
+}
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Common/RtViewer.h	Mon Apr 27 10:01:03 2020 +0200
@@ -0,0 +1,223 @@
+/**
+ * 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/SdlViewport.h>
+#include <Framework/Loaders/GenericLoadersContext.h>
+#include <Framework/Messages/IObserver.h>
+#include <Framework/Messages/IMessageEmitter.h>
+#include <Framework/Oracle/OracleCommandExceptionMessage.h>
+#include <Framework/Scene2DViewport/ViewportController.h>
+#include <Framework/Volumes/DicomVolumeImage.h>
+#include <Framework/Oracle/ThreadedOracle.h>
+#include <Framework/Loaders/OrthancSeriesVolumeProgressiveLoader.h>
+#include <Framework/Loaders/OrthancMultiframeVolumeLoader.h>
+#include <Framework/Loaders/DicomStructureSetLoader.h>
+
+#include <Framework/Messages/ObserverBase.h>
+
+#include <boost/enable_shared_from_this.hpp>
+#include <boost/thread.hpp>
+#include <boost/noncopyable.hpp>
+
+#include <SDL.h>
+
+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 IMessageEmitter
+  {
+  public:
+
+
+    void PrepareScene();
+    void Run();
+    void SetInfoDisplayMessage(std::string key, std::string value);
+    void DisableTracker();
+
+    void HandleApplicationEvent(const SDL_Event& event);
+
+    /**
+    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();
+
+    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;
+      }
+    }
+
+    static boost::shared_ptr<RtViewerApp> Create();
+    void RegisterMessages();
+
+  protected:
+    RtViewerApp();
+
+  private:
+#if 1
+    // if threaded (not wasm)
+    IObservable oracleObservable_;
+    ThreadedOracle oracle_;
+    boost::shared_mutex mutex_; // to serialize messages from the ThreadedOracle
+#endif
+
+    void SelectNextTool();
+
+    /**
+    This returns a random point in the canvas part of the scene, but in
+    scene coordinates
+    */
+    ScenePoint2D GetRandomPointInScene() const;
+
+    boost::shared_ptr<IFlexiblePointerTracker> TrackerHitTest(const PointerEvent& e);
+
+    boost::shared_ptr<IFlexiblePointerTracker> CreateSuitableTracker(
+      const SDL_Event& event,
+      const PointerEvent& e);
+
+    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 Handle(const DicomVolumeImage::GeometryReadyMessage& message);
+    void Handle(const OracleCommandExceptionMessage& 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 SetCtVolume(
+      int depth,
+      const boost::shared_ptr<IVolumeSlicer>& volume,
+      ILayerStyleConfigurator* style);
+    
+    void SetDoseVolume(
+      int depth,
+      const boost::shared_ptr<IVolumeSlicer>& volume,
+      ILayerStyleConfigurator* style);
+
+    void SetStructureSet(
+      int depth, 
+      const boost::shared_ptr<DicomStructureSetLoader>& volume);
+
+  private:
+    void DisplayFloatingCtrlInfoText(const PointerEvent& e);
+    void DisplayInfoText();
+    void HideInfoText();
+    void RetrieveGeometry();
+    void FitContent();
+
+  private:
+    boost::shared_ptr<GenericLoadersContext> loadersContext_;
+    boost::shared_ptr<VolumeSceneLayerSource>  ctVolumeLayerSource_, doseVolumeLayerSource_, structLayerSource_;
+    boost::shared_ptr<OrthancStone::IGeometryProvider> geometryProvider_;
+    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<SdlOpenGLViewport> viewport_;
+  };
+
+}
+
+
+ 
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Common/SampleHelpers.h	Mon Apr 27 10:01:03 2020 +0200
@@ -0,0 +1,35 @@
+#pragma once
+
+#include <string>
+#include <iostream>
+
+#include <Core/Logging.h>
+
+namespace OrthancStoneHelpers
+{
+  inline void SetLogLevel(std::string logLevel)
+  {
+    boost::to_lower(logLevel);
+    if (logLevel == "warning")
+    {
+      Orthanc::Logging::EnableInfoLevel(false);
+      Orthanc::Logging::EnableTraceLevel(false);
+    }
+    else if (logLevel == "info")
+    {
+      Orthanc::Logging::EnableInfoLevel(true);
+      Orthanc::Logging::EnableTraceLevel(false);
+    }
+    else if (logLevel == "trace")
+    {
+      Orthanc::Logging::EnableInfoLevel(true);
+      Orthanc::Logging::EnableTraceLevel(true);
+    }
+    else
+    {
+      std::cerr << "Unknown log level \"" << logLevel << "\". Will use TRACE as default!";
+      Orthanc::Logging::EnableInfoLevel(true);
+      Orthanc::Logging::EnableTraceLevel(true);
+    }
+  }
+}
\ No newline at end of file
--- a/Samples/CommonHelpers.h	Wed Apr 22 19:55:34 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,35 +0,0 @@
-#pragma once
-
-#include <string>
-#include <iostream>
-
-#include <Core/Logging.h>
-
-namespace OrthancStoneHelpers
-{
-  inline void SetLogLevel(std::string logLevel)
-  {
-    boost::to_lower(logLevel);
-    if (logLevel == "warning")
-    {
-      Orthanc::Logging::EnableInfoLevel(false);
-      Orthanc::Logging::EnableTraceLevel(false);
-    }
-    else if (logLevel == "info")
-    {
-      Orthanc::Logging::EnableInfoLevel(true);
-      Orthanc::Logging::EnableTraceLevel(false);
-    }
-    else if (logLevel == "trace")
-    {
-      Orthanc::Logging::EnableInfoLevel(true);
-      Orthanc::Logging::EnableTraceLevel(true);
-    }
-    else
-    {
-      std::cerr << "Unknown log level \"" << logLevel << "\". Will use TRACE as default!";
-      Orthanc::Logging::EnableInfoLevel(true);
-      Orthanc::Logging::EnableTraceLevel(true);
-    }
-  }
-}
\ No newline at end of file
--- a/Samples/Sdl/SimpleViewer/CMakeLists.txt	Wed Apr 22 19:55:34 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,44 +0,0 @@
-cmake_minimum_required(VERSION 2.8.10)
-
-project(SdlSimpleViewer)
-
-set(ORTHANC_FRAMEWORK_SOURCE "path")
-set(ORTHANC_FRAMEWORK_ROOT ${CMAKE_CURRENT_LIST_DIR}/../../../../orthanc)
-set(STONE_ROOT ${CMAKE_CURRENT_LIST_DIR}/../../../)
-
-include(${STONE_ROOT}/Resources/CMake/OrthancStoneParameters.cmake)
-
-SET(ENABLE_GOOGLE_TEST OFF)
-SET(ENABLE_LOCALE ON)  # Necessary for text rendering
-SET(ENABLE_QT OFF)
-SET(ENABLE_SDL ON)
-SET(ENABLE_DCMTK ON)  # <==
-SET(ENABLE_OPENGL ON)  #  <==
-SET(ENABLE_WEB_CLIENT ON)
-SET(ORTHANC_SANDBOXED OFF)
-
-include(${STONE_ROOT}/Resources/CMake/OrthancStoneConfiguration.cmake)
-include(${STONE_ROOT}/Resources/CMake/Utilities.cmake)
-
-include_directories(${STONE_ROOT})
-
-add_definitions(
-  -DORTHANC_ENABLE_LOGGING=1
-  -DORTHANC_ENABLE_LOGGING_PLUGIN=0
-  -DORTHANC_ENABLE_PUGIXML=0
-  -DORTHANC_DEFAULT_DICOM_ENCODING=Encoding_Latin1
-  )
-
-SortFilesInSourceGroups()
-
-add_executable(SdlSimpleViewer
-  ../SdlHelpers.h
-  ../../CommonHelpers.h
-  SdlSimpleViewerApplication.h
-  SdlSimpleViewer.cpp
-  ${ORTHANC_STONE_SOURCES}
-  )
-
-
-target_link_libraries(SdlSimpleViewer ${DCMTK_LIBRARIES})
-
--- a/Samples/Sdl/SimpleViewer/CMakeSettings.json	Wed Apr 22 19:55:34 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,37 +0,0 @@
-{
-  "configurations": [
-    {
-      "name": "x64-Debug",
-      "generator": "Ninja",
-      "configurationType": "Debug",
-      "inheritEnvironments": [ "msvc_x64_x64" ],
-      "buildRoot": "${projectDir}\\out\\build\\${name}",
-      "installRoot": "${projectDir}\\out\\install\\${name}",
-      "cmakeCommandArgs": "",
-      "buildCommandArgs": "-v",
-      "ctestCommandArgs": "",
-      "variables": [
-        {
-          "name": "MSVC_MULTIPLE_PROCESSES",
-          "value": "True",
-          "type": "BOOL"
-        },
-        {
-          "name": "ALLOW_DOWNLOADS",
-          "value": "True",
-          "type": "BOOL"
-        },
-        {
-          "name": "STATIC_BUILD",
-          "value": "True",
-          "type": "BOOL"
-        },
-        {
-          "name": "OPENSSL_NO_CAPIENG",
-          "value": "True",
-          "type": "BOOL"
-        },
-      ]
-    }
-  ]
-}
\ No newline at end of file
--- a/Samples/Sdl/SimpleViewer/SdlSimpleViewer.cpp	Wed Apr 22 19:55:34 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,258 +0,0 @@
-
-#include "SdlSimpleViewerApplication.h"
-
-#include <string>
-
-#include <boost/program_options.hpp>
-
-#include <SDL.h>
-
-#include <Core/OrthancException.h>
-
-#include <Framework/Loaders/GenericLoadersContext.h>
-#include <Framework/StoneException.h>
-#include <Framework/StoneEnumerations.h>
-#include <Framework/StoneInitialization.h>
-#include <Framework/Viewport/SdlViewport.h>
-
-#include "../SdlHelpers.h"
-#include "../../CommonHelpers.h"
-
-std::string orthancUrl;
-std::string instanceId;
-int frameIndex = 0;
-
-static void ProcessOptions(int argc, char* argv[])
-{
-  namespace po = boost::program_options;
-  po::options_description desc("Usage:");
-
-  desc.add_options()
-    ("log_level", po::value<std::string>()->default_value("WARNING"),
-     "You can choose WARNING, INFO or TRACE for the logging level: Errors and warnings will always be displayed. (default: WARNING)")
-
-    ("orthanc", po::value<std::string>()->default_value("http://localhost:8042"),
-     "Base URL of the Orthanc instance")
-
-    ("instance", po::value<std::string>()->default_value("285dece8-e1956b38-cdc7d084-6ce3371e-536a9ffc"),
-     "Orthanc ID of the instance to display")
-
-    ("frame_index", po::value<int>()->default_value(0),
-     "The zero-based index of the frame (for multi-frame instances)")
-    ;
-
-  po::variables_map vm;
-  try
-  {
-    po::store(po::parse_command_line(argc, argv, desc), vm);
-    po::notify(vm);
-  }
-  catch (std::exception& e)
-  {
-    std::cerr << "Please check your command line options! (\"" << e.what() << "\")" << std::endl;
-  }
-
-  if (vm.count("log_level") > 0)
-  {
-    std::string logLevel = vm["log_level"].as<std::string>();
-    OrthancStoneHelpers::SetLogLevel(logLevel);
-  }
-
-  if (vm.count("orthanc") > 0)
-  {
-    // maybe check URL validity here
-    orthancUrl = vm["orthanc"].as<std::string>();
-  }
-
-  if (vm.count("instance") > 0)
-  {
-    instanceId = vm["instance"].as<std::string>();
-  }
-
-  if (vm.count("frame_index") > 0)
-  {
-    frameIndex = vm["frame_index"].as<int>();
-  }
-
-}
-
-extern void f()
-{
-  std::cout << "f()" << std::endl;
-}
-
-/**
- * IMPORTANT: The full arguments to "main()" are needed for SDL on
- * Windows. Otherwise, one gets the linking error "undefined reference
- * to `SDL_main'". https://wiki.libsdl.org/FAQWindows
- **/
-int main(int argc, char* argv[])
-{
-  f();
-
-  try
-  {
-    OrthancStone::StoneInitialize();
-
-    ProcessOptions(argc, argv);
-
-    //Orthanc::Logging::EnableInfoLevel(true);
-    //Orthanc::Logging::EnableTraceLevel(true);
-
-    {
-
-#if 1
-      boost::shared_ptr<OrthancStone::SdlViewport> viewport =
-        OrthancStone::SdlOpenGLViewport::Create("Stone of Orthanc", 800, 600);
-#else
-      boost::shared_ptr<OrthancStone::SdlViewport> viewport =
-        OrthancStone::SdlCairoViewport::Create("Stone of Orthanc", 800, 600);
-#endif
-
-      OrthancStone::GenericLoadersContext context(1, 4, 1);
-
-      context.StartOracle();
-
-      {
-
-        boost::shared_ptr<SdlSimpleViewerApplication> application(
-          SdlSimpleViewerApplication::Create(context, viewport));
-
-        OrthancStone::DicomSource source;
-
-        application->LoadOrthancFrame(source, instanceId, frameIndex);
-
-        OrthancStone::DefaultViewportInteractor interactor;
-
-        {
-          int scancodeCount = 0;
-          const uint8_t* keyboardState = SDL_GetKeyboardState(&scancodeCount);
-
-          bool stop = false;
-          while (!stop)
-          {
-            bool paint = false;
-            SDL_Event event;
-            while (SDL_PollEvent(&event))
-            {
-              if (event.type == SDL_QUIT)
-              {
-                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))
-              {
-                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;
-              }
-              else if (event.type == SDL_KEYDOWN &&
-                       event.key.repeat == 0 /* Ignore key bounce */)
-              {
-                switch (event.key.keysym.sym)
-                {
-                case SDLK_f:
-                  viewport->ToggleMaximize();
-                  break;
-
-                case SDLK_s:
-                  application->FitContent();
-                  break;
-
-                case SDLK_q:
-                  stop = true;
-                  break;
-
-                default:
-                  break;
-                }
-              }
-              else if (event.type == SDL_MOUSEBUTTONDOWN ||
-                       event.type == SDL_MOUSEMOTION ||
-                       event.type == SDL_MOUSEBUTTONUP)
-              {
-                std::auto_ptr<OrthancStone::IViewport::ILock> lock(viewport->Lock());
-                if (lock->HasCompositor())
-                {
-                  OrthancStone::PointerEvent p;
-                  OrthancStoneHelpers::GetPointerEvent(p, lock->GetCompositor(),
-                                                       event, keyboardState, scancodeCount);
-
-                  switch (event.type)
-                  {
-                  case SDL_MOUSEBUTTONDOWN:
-                    lock->GetController().HandleMousePress(interactor, p,
-                                                           lock->GetCompositor().GetCanvasWidth(),
-                                                           lock->GetCompositor().GetCanvasHeight());
-                    lock->Invalidate();
-                    break;
-
-                  case SDL_MOUSEMOTION:
-                    if (lock->GetController().HandleMouseMove(p))
-                    {
-                      lock->Invalidate();
-                    }
-                    break;
-
-                  case SDL_MOUSEBUTTONUP:
-                    lock->GetController().HandleMouseRelease(p);
-                    lock->Invalidate();
-                    break;
-
-                  default:
-                    throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
-                  }
-                }
-              }
-            }
-
-            if (paint)
-            {
-              viewport->Paint();
-            }
-
-            // Small delay to avoid using 100% of CPU
-            SDL_Delay(1);
-          }
-        }
-
-        context.StopOracle();
-      }
-    }
-
-    OrthancStone::StoneFinalize();
-    return 0;
-  }
-  catch (Orthanc::OrthancException& e)
-  {
-    auto test = e.What();
-    fprintf(stdout, test);
-    LOG(ERROR) << "OrthancException: " << e.What();
-    return -1;
-  }
-  catch (OrthancStone::StoneException& e)
-  {
-    LOG(ERROR) << "StoneException: " << e.What();
-    return -1;
-  }
-  catch (std::runtime_error& e)
-  {
-    LOG(ERROR) << "Runtime error: " << e.what();
-    return -1;
-  }
-  catch (...)
-  {
-    LOG(ERROR) << "Native exception";
-    return -1;
-  }
-}
--- a/Samples/Sdl/SimpleViewer/SdlSimpleViewerApplication.h	Wed Apr 22 19:55:34 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,146 +0,0 @@
-#pragma once
-
-#include <Framework/Viewport/IViewport.h>
-#include <Framework/Loaders/DicomResourcesLoader.h>
-#include <Framework/Loaders/ILoadersContext.h>
-#include <Framework/Loaders/SeriesFramesLoader.h>
-#include <Framework/Loaders/SeriesThumbnailsLoader.h>
-
-#include <boost/make_shared.hpp>
-
-
-using OrthancStone::ILoadersContext;
-using OrthancStone::ObserverBase;
-using OrthancStone::IViewport;
-using OrthancStone::DicomResourcesLoader;
-using OrthancStone::SeriesFramesLoader;
-using OrthancStone::TextureBaseSceneLayer;
-using OrthancStone::DicomSource;
-using OrthancStone::SeriesThumbnailsLoader;
-using OrthancStone::LoadedDicomResources;
-using OrthancStone::SeriesThumbnailType;
-using OrthancStone::OracleScheduler;
-using OrthancStone::OrthancRestApiCommand;
-using OrthancStone::OracleScheduler;
-using OrthancStone::OracleScheduler;
-using OrthancStone::OracleScheduler;
-
-
-class SdlSimpleViewerApplication : public ObserverBase<SdlSimpleViewerApplication>
-{
-
-public:
-  static boost::shared_ptr<SdlSimpleViewerApplication> Create(ILoadersContext& context, boost::shared_ptr<IViewport> viewport)
-  {
-    boost::shared_ptr<SdlSimpleViewerApplication> application(new SdlSimpleViewerApplication(context, viewport));
-
-    {
-      std::auto_ptr<ILoadersContext::ILock> lock(context.Lock());
-      DicomResourcesLoader::Factory f;
-      application->dicomLoader_ = boost::dynamic_pointer_cast<DicomResourcesLoader>(f.Create(*lock));
-    }
-
-    application->Register<DicomResourcesLoader::SuccessMessage>(*application->dicomLoader_, &SdlSimpleViewerApplication::Handle);
-
-    return application;
-  }
-
-  void LoadOrthancFrame(const DicomSource& source, const std::string& instanceId, unsigned int frame)
-  {
-    std::auto_ptr<ILoadersContext::ILock> lock(context_.Lock());
-
-    dicomLoader_->ScheduleLoadOrthancResource(boost::make_shared<LoadedDicomResources>(Orthanc::DICOM_TAG_SOP_INSTANCE_UID),
-                                              0, source, Orthanc::ResourceType_Instance, instanceId,
-                                              new Orthanc::SingleValueObject<unsigned int>(frame));
-  }
-
-#if 0
-  void LoadDicomWebFrame(const DicomSource& source,
-                         const std::string& studyInstanceUid,
-                         const std::string& seriesInstanceUid,
-                         const std::string& sopInstanceUid,
-                         unsigned int frame)
-  {
-    std::auto_ptr<ILoadersContext::ILock> lock(context_.Lock());
-
-    // We first must load the "/metadata" to know the number of frames
-    dicomLoader_->ScheduleGetDicomWeb(
-      boost::make_shared<LoadedDicomResources>(Orthanc::DICOM_TAG_SOP_INSTANCE_UID), 0, source,
-      "/studies/" + studyInstanceUid + "/series/" + seriesInstanceUid + "/instances/" + sopInstanceUid + "/metadata",
-      new Orthanc::SingleValueObject<unsigned int>(frame));
-  }
-#endif 
-
-  void FitContent()
-  {
-    std::auto_ptr<IViewport::ILock> lock(viewport_->Lock());
-    lock->GetCompositor().FitContent(lock->GetController().GetScene());
-    lock->Invalidate();
-  }
-
-private:
-  ILoadersContext& context_;
-  boost::shared_ptr<IViewport>             viewport_;
-  boost::shared_ptr<DicomResourcesLoader>  dicomLoader_;
-  boost::shared_ptr<SeriesFramesLoader>    framesLoader_;
-
-  SdlSimpleViewerApplication(ILoadersContext& context,
-                             boost::shared_ptr<IViewport> viewport) :
-    context_(context),
-    viewport_(viewport)
-  {
-  }
-
-  void Handle(const SeriesFramesLoader::FrameLoadedMessage& message)
-  {
-    LOG(INFO) << "Frame decoded! "
-      << message.GetImage().GetWidth() << "x" << message.GetImage().GetHeight()
-      << " " << Orthanc::EnumerationToString(message.GetImage().GetFormat());
-
-    std::auto_ptr<TextureBaseSceneLayer> layer(
-      message.GetInstanceParameters().CreateTexture(message.GetImage()));
-    layer->SetLinearInterpolation(true);
-
-    {
-      std::auto_ptr<IViewport::ILock> lock(viewport_->Lock());
-      lock->GetController().GetScene().SetLayer(0, layer.release());
-      lock->GetCompositor().FitContent(lock->GetController().GetScene());
-      lock->Invalidate();
-    }
-  }
-
-  void Handle(const DicomResourcesLoader::SuccessMessage& message)
-  {
-    if (message.GetResources()->GetSize() != 1)
-    {
-      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
-    }
-
-    //message.GetResources()->GetResource(0).Print(stdout);
-
-    {
-      std::auto_ptr<ILoadersContext::ILock> lock(context_.Lock());
-      SeriesFramesLoader::Factory f(*message.GetResources());
-
-      framesLoader_ = boost::dynamic_pointer_cast<SeriesFramesLoader>(
-        f.Create(*lock));
-      
-      Register<SeriesFramesLoader::FrameLoadedMessage>(
-        *framesLoader_, &SdlSimpleViewerApplication::Handle);
-
-      assert(message.HasUserPayload());
-
-      const Orthanc::SingleValueObject<unsigned int>& payload =
-        dynamic_cast<const Orthanc::SingleValueObject<unsigned int>&>(
-          message.GetUserPayload());
-
-      LOG(INFO) << "Loading pixel data of frame: " << payload.GetValue();
-      framesLoader_->ScheduleLoadFrame(
-        0, message.GetDicomSource(), payload.GetValue(),
-        message.GetDicomSource().GetQualityCount() - 1 /* download best quality available */,
-        NULL);
-    }
-  }
-
-};
-
--- a/Samples/Sdl/SimpleViewer/SimpleViewer.cpp	Wed Apr 22 19:55:34 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,292 +0,0 @@
-
-#include "SdlSimpleViewerApplication.h"
-
-#include <Core/OrthancException.h>
-
-#include <Framework/Loaders/GenericLoadersContext.h>
-#include <Framework/StoneException.h>
-#include <Framework/StoneEnumerations.h>
-#include <Framework/StoneInitialization.h>
-#include <Framework/Viewport/SdlViewport.h>
-
-#include <SDL.h>
-
-namespace OrthancStone
-{
-  static KeyboardModifiers GetKeyboardModifiers(const uint8_t* keyboardState,
-                                                const int scancodeCount)
-  {
-    int result = KeyboardModifiers_None;
-
-    if (keyboardState != NULL)
-    {
-      if (SDL_SCANCODE_LSHIFT < scancodeCount &&
-          keyboardState[SDL_SCANCODE_LSHIFT])
-      {
-        result |= KeyboardModifiers_Shift;
-      }
-
-      if (SDL_SCANCODE_RSHIFT < scancodeCount &&
-          keyboardState[SDL_SCANCODE_RSHIFT])
-      {
-        result |= KeyboardModifiers_Shift;
-      }
-
-      if (SDL_SCANCODE_LCTRL < scancodeCount &&
-          keyboardState[SDL_SCANCODE_LCTRL])
-      {
-        result |= KeyboardModifiers_Control;
-      }
-
-      if (SDL_SCANCODE_RCTRL < scancodeCount &&
-          keyboardState[SDL_SCANCODE_RCTRL])
-      {
-        result |= KeyboardModifiers_Control;
-      }
-
-      if (SDL_SCANCODE_LALT < scancodeCount &&
-          keyboardState[SDL_SCANCODE_LALT])
-      {
-        result |= KeyboardModifiers_Alt;
-      }
-
-      if (SDL_SCANCODE_RALT < scancodeCount &&
-          keyboardState[SDL_SCANCODE_RALT])
-      {
-        result |= KeyboardModifiers_Alt;
-      }
-    }
-
-    return static_cast<KeyboardModifiers>(result);
-  }
-
-
-  static void GetPointerEvent(PointerEvent& p,
-                              const ICompositor& compositor,
-                              SDL_Event event,
-                              const uint8_t* keyboardState,
-                              const int scancodeCount)
-  {
-    KeyboardModifiers modifiers = GetKeyboardModifiers(keyboardState, scancodeCount);
-
-    switch (event.button.button)
-    {
-    case SDL_BUTTON_LEFT:
-      p.SetMouseButton(OrthancStone::MouseButton_Left);
-      break;
-
-    case SDL_BUTTON_RIGHT:
-      p.SetMouseButton(OrthancStone::MouseButton_Right);
-      break;
-
-    case SDL_BUTTON_MIDDLE:
-      p.SetMouseButton(OrthancStone::MouseButton_Middle);
-      break;
-
-    default:
-      p.SetMouseButton(OrthancStone::MouseButton_None);
-      break;
-    }
-
-    p.AddPosition(compositor.GetPixelCenterCoordinates(event.button.x, event.button.y));
-    p.SetAltModifier(modifiers & KeyboardModifiers_Alt);
-    p.SetControlModifier(modifiers & KeyboardModifiers_Control);
-    p.SetShiftModifier(modifiers & KeyboardModifiers_Shift);
-  }
-
-}
-
-/**
- * IMPORTANT: The full arguments to "main()" are needed for SDL on
- * Windows. Otherwise, one gets the linking error "undefined reference
- * to `SDL_main'". https://wiki.libsdl.org/FAQWindows
- **/
-int main(int argc, char* argv[])
-{
-  try
-  {
-    OrthancStone::StoneInitialize();
-    Orthanc::Logging::EnableInfoLevel(true);
-    //Orthanc::Logging::EnableTraceLevel(true);
-
-    {
-
-#if 1
-      boost::shared_ptr<OrthancStone::SdlViewport> viewport = 
-        OrthancStone::SdlOpenGLViewport::Create("Stone of Orthanc", 800, 600);
-#else
-      boost::shared_ptr<OrthancStone::SdlViewport> viewport =
-        OrthancStone::SdlCairoViewport::Create("Stone of Orthanc", 800, 600);
-#endif
-
-      OrthancStone::GenericLoadersContext context(1, 4, 1);
-      
-      context.StartOracle();
-
-      {
-
-        boost::shared_ptr<SdlSimpleViewerApplication> application(
-          SdlSimpleViewerApplication::Create(context, viewport));
-
-        OrthancStone::DicomSource source;
-
-        // Default and command-line parameters
-        const char* instanceId = "285dece8-e1956b38-cdc7d084-6ce3371e-536a9ffc";
-        unsigned int frameIndex = 0;
-
-        if (argc == 1)
-        {
-          LOG(ERROR) << "No instanceId supplied. The default of " << instanceId << " will be used. "
-            << "Please supply the Orthanc instance ID of the frame you wish to display then, optionally, "
-            << "the zero-based index of the frame (for multi-frame instances)";
-          // TODO: frame number as second argument...
-        }
-
-        if (argc >= 2)
-          instanceId = argv[1];
-
-        if (argc >= 3)
-          frameIndex = atoi(argv[1]);
-
-        if (argc > 3)
-        {
-          LOG(ERROR) << "Extra arguments ignored!";
-        }
-         
-
-        application->LoadOrthancFrame(source, instanceId, frameIndex);
-
-        OrthancStone::DefaultViewportInteractor interactor;
-
-        {
-          int scancodeCount = 0;
-          const uint8_t* keyboardState = SDL_GetKeyboardState(&scancodeCount);
-
-          bool stop = false;
-          while (!stop)
-          {
-            bool paint = false;
-            SDL_Event event;
-            while (SDL_PollEvent(&event))
-            {
-              if (event.type == SDL_QUIT)
-              {
-                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))
-              {
-                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;
-              }
-              else if (event.type == SDL_KEYDOWN &&
-                       event.key.repeat == 0 /* Ignore key bounce */)
-              {
-                switch (event.key.keysym.sym)
-                {
-                case SDLK_f:
-                  viewport->ToggleMaximize();
-                  break;
-
-                case SDLK_s:
-                  application->FitContent();
-                  break;
-
-                case SDLK_q:
-                  stop = true;
-                  break;
-
-                default:
-                  break;
-                }
-              }
-              else if (event.type == SDL_MOUSEBUTTONDOWN ||
-                       event.type == SDL_MOUSEMOTION ||
-                       event.type == SDL_MOUSEBUTTONUP)
-              {
-                std::auto_ptr<OrthancStone::IViewport::ILock> lock(viewport->Lock());
-                if (lock->HasCompositor())
-                {
-                  OrthancStone::PointerEvent p;
-                  OrthancStone::GetPointerEvent(p, lock->GetCompositor(),
-                                                event, keyboardState, scancodeCount);
-
-                  switch (event.type)
-                  {
-                  case SDL_MOUSEBUTTONDOWN:
-                    lock->GetController().HandleMousePress(interactor, p,
-                                                           lock->GetCompositor().GetCanvasWidth(),
-                                                           lock->GetCompositor().GetCanvasHeight());
-                    lock->Invalidate();
-                    break;
-
-                  case SDL_MOUSEMOTION:
-                    if (lock->GetController().HandleMouseMove(p))
-                    {
-                      lock->Invalidate();
-                    }
-                    break;
-
-                  case SDL_MOUSEBUTTONUP:
-                    lock->GetController().HandleMouseRelease(p);
-                    lock->Invalidate();
-                    break;
-
-                  default:
-                    throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
-                  }
-                }
-              }
-            }
-
-            if (paint)
-            {
-              viewport->Paint();
-            }
-
-            // Small delay to avoid using 100% of CPU
-            SDL_Delay(1);
-          }
-        }
-
-        context.StopOracle();
-      }
-    }
-
-    OrthancStone::StoneFinalize();
-    return 0;
-  }
-  catch (Orthanc::OrthancException & e)
-  {
-    auto test = e.What();
-    fprintf(stdout, test);
-    LOG(ERROR) << "OrthancException: " << e.What();
-    return -1;
-  }
-  catch (OrthancStone::StoneException & e)
-  {
-    LOG(ERROR) << "StoneException: " << e.What();
-    return -1;
-  }
-  catch (std::runtime_error & e)
-  {
-    LOG(ERROR) << "Runtime error: " << e.what();
-    return -1;
-  }
-  catch (...)
-  {
-    LOG(ERROR) << "Native exception";
-    return -1;
-  }
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Sdl/SingleFrameViewer/CMakeLists.txt	Mon Apr 27 10:01:03 2020 +0200
@@ -0,0 +1,44 @@
+cmake_minimum_required(VERSION 2.8.10)
+
+project(SdlSimpleViewer)
+
+set(ORTHANC_FRAMEWORK_SOURCE "path")
+set(ORTHANC_FRAMEWORK_ROOT ${CMAKE_CURRENT_LIST_DIR}/../../../../orthanc)
+set(STONE_ROOT ${CMAKE_CURRENT_LIST_DIR}/../../../)
+
+include(${STONE_ROOT}/Resources/CMake/OrthancStoneParameters.cmake)
+
+SET(ENABLE_GOOGLE_TEST OFF)
+SET(ENABLE_LOCALE ON)  # Necessary for text rendering
+SET(ENABLE_QT OFF)
+SET(ENABLE_SDL ON)
+SET(ENABLE_DCMTK ON)  # <==
+SET(ENABLE_OPENGL ON)  #  <==
+SET(ENABLE_WEB_CLIENT ON)
+SET(ORTHANC_SANDBOXED OFF)
+
+include(${STONE_ROOT}/Resources/CMake/OrthancStoneConfiguration.cmake)
+include(${STONE_ROOT}/Resources/CMake/Utilities.cmake)
+
+include_directories(${STONE_ROOT})
+
+add_definitions(
+  -DORTHANC_ENABLE_LOGGING=1
+  -DORTHANC_ENABLE_LOGGING_PLUGIN=0
+  -DORTHANC_ENABLE_PUGIXML=0
+  -DORTHANC_DEFAULT_DICOM_ENCODING=Encoding_Latin1
+  )
+
+SortFilesInSourceGroups()
+
+add_executable(SdlSimpleViewer
+  ../SdlHelpers.h
+  ../../Common/SampleHelpers.h
+  SdlSimpleViewerApplication.h
+  SdlSimpleViewer.cpp
+  ${ORTHANC_STONE_SOURCES}
+  )
+
+
+target_link_libraries(SdlSimpleViewer ${DCMTK_LIBRARIES})
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Sdl/SingleFrameViewer/CMakeSettings.json	Mon Apr 27 10:01:03 2020 +0200
@@ -0,0 +1,37 @@
+{
+  "configurations": [
+    {
+      "name": "x64-Debug",
+      "generator": "Ninja",
+      "configurationType": "Debug",
+      "inheritEnvironments": [ "msvc_x64_x64" ],
+      "buildRoot": "${projectDir}\\out\\build\\${name}",
+      "installRoot": "${projectDir}\\out\\install\\${name}",
+      "cmakeCommandArgs": "",
+      "buildCommandArgs": "-v",
+      "ctestCommandArgs": "",
+      "variables": [
+        {
+          "name": "MSVC_MULTIPLE_PROCESSES",
+          "value": "True",
+          "type": "BOOL"
+        },
+        {
+          "name": "ALLOW_DOWNLOADS",
+          "value": "True",
+          "type": "BOOL"
+        },
+        {
+          "name": "STATIC_BUILD",
+          "value": "True",
+          "type": "BOOL"
+        },
+        {
+          "name": "OPENSSL_NO_CAPIENG",
+          "value": "True",
+          "type": "BOOL"
+        },
+      ]
+    }
+  ]
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Sdl/SingleFrameViewer/SdlSimpleViewer.cpp	Mon Apr 27 10:01:03 2020 +0200
@@ -0,0 +1,258 @@
+
+#include "SdlSimpleViewerApplication.h"
+
+#include <string>
+
+#include <boost/program_options.hpp>
+
+#include <SDL.h>
+
+#include <Core/OrthancException.h>
+
+#include <Framework/Loaders/GenericLoadersContext.h>
+#include <Framework/StoneException.h>
+#include <Framework/StoneEnumerations.h>
+#include <Framework/StoneInitialization.h>
+#include <Framework/Viewport/SdlViewport.h>
+
+#include "../SdlHelpers.h"
+#include "../../Common/SampleHelpers.h"
+
+std::string orthancUrl;
+std::string instanceId;
+int frameIndex = 0;
+
+static void ProcessOptions(int argc, char* argv[])
+{
+  namespace po = boost::program_options;
+  po::options_description desc("Usage:");
+
+  desc.add_options()
+    ("log_level", po::value<std::string>()->default_value("WARNING"),
+     "You can choose WARNING, INFO or TRACE for the logging level: Errors and warnings will always be displayed. (default: WARNING)")
+
+    ("orthanc", po::value<std::string>()->default_value("http://localhost:8042"),
+     "Base URL of the Orthanc instance")
+
+    ("instance", po::value<std::string>()->default_value("285dece8-e1956b38-cdc7d084-6ce3371e-536a9ffc"),
+     "Orthanc ID of the instance to display")
+
+    ("frame_index", po::value<int>()->default_value(0),
+     "The zero-based index of the frame (for multi-frame instances)")
+    ;
+
+  po::variables_map vm;
+  try
+  {
+    po::store(po::parse_command_line(argc, argv, desc), vm);
+    po::notify(vm);
+  }
+  catch (std::exception& e)
+  {
+    std::cerr << "Please check your command line options! (\"" << e.what() << "\")" << std::endl;
+  }
+
+  if (vm.count("log_level") > 0)
+  {
+    std::string logLevel = vm["log_level"].as<std::string>();
+    OrthancStoneHelpers::SetLogLevel(logLevel);
+  }
+
+  if (vm.count("orthanc") > 0)
+  {
+    // maybe check URL validity here
+    orthancUrl = vm["orthanc"].as<std::string>();
+  }
+
+  if (vm.count("instance") > 0)
+  {
+    instanceId = vm["instance"].as<std::string>();
+  }
+
+  if (vm.count("frame_index") > 0)
+  {
+    frameIndex = vm["frame_index"].as<int>();
+  }
+
+}
+
+extern void f()
+{
+  std::cout << "f()" << std::endl;
+}
+
+/**
+ * IMPORTANT: The full arguments to "main()" are needed for SDL on
+ * Windows. Otherwise, one gets the linking error "undefined reference
+ * to `SDL_main'". https://wiki.libsdl.org/FAQWindows
+ **/
+int main(int argc, char* argv[])
+{
+  f();
+
+  try
+  {
+    OrthancStone::StoneInitialize();
+
+    ProcessOptions(argc, argv);
+
+    //Orthanc::Logging::EnableInfoLevel(true);
+    //Orthanc::Logging::EnableTraceLevel(true);
+
+    {
+
+#if 1
+      boost::shared_ptr<OrthancStone::SdlViewport> viewport =
+        OrthancStone::SdlOpenGLViewport::Create("Stone of Orthanc", 800, 600);
+#else
+      boost::shared_ptr<OrthancStone::SdlViewport> viewport =
+        OrthancStone::SdlCairoViewport::Create("Stone of Orthanc", 800, 600);
+#endif
+
+      OrthancStone::GenericLoadersContext context(1, 4, 1);
+
+      context.StartOracle();
+
+      {
+
+        boost::shared_ptr<SdlSimpleViewerApplication> application(
+          SdlSimpleViewerApplication::Create(context, viewport));
+
+        OrthancStone::DicomSource source;
+
+        application->LoadOrthancFrame(source, instanceId, frameIndex);
+
+        OrthancStone::DefaultViewportInteractor interactor;
+
+        {
+          int scancodeCount = 0;
+          const uint8_t* keyboardState = SDL_GetKeyboardState(&scancodeCount);
+
+          bool stop = false;
+          while (!stop)
+          {
+            bool paint = false;
+            SDL_Event event;
+            while (SDL_PollEvent(&event))
+            {
+              if (event.type == SDL_QUIT)
+              {
+                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))
+              {
+                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;
+              }
+              else if (event.type == SDL_KEYDOWN &&
+                       event.key.repeat == 0 /* Ignore key bounce */)
+              {
+                switch (event.key.keysym.sym)
+                {
+                case SDLK_f:
+                  viewport->ToggleMaximize();
+                  break;
+
+                case SDLK_s:
+                  application->FitContent();
+                  break;
+
+                case SDLK_q:
+                  stop = true;
+                  break;
+
+                default:
+                  break;
+                }
+              }
+              else if (event.type == SDL_MOUSEBUTTONDOWN ||
+                       event.type == SDL_MOUSEMOTION ||
+                       event.type == SDL_MOUSEBUTTONUP)
+              {
+                std::auto_ptr<OrthancStone::IViewport::ILock> lock(viewport->Lock());
+                if (lock->HasCompositor())
+                {
+                  OrthancStone::PointerEvent p;
+                  OrthancStoneHelpers::GetPointerEvent(p, lock->GetCompositor(),
+                                                       event, keyboardState, scancodeCount);
+
+                  switch (event.type)
+                  {
+                  case SDL_MOUSEBUTTONDOWN:
+                    lock->GetController().HandleMousePress(interactor, p,
+                                                           lock->GetCompositor().GetCanvasWidth(),
+                                                           lock->GetCompositor().GetCanvasHeight());
+                    lock->Invalidate();
+                    break;
+
+                  case SDL_MOUSEMOTION:
+                    if (lock->GetController().HandleMouseMove(p))
+                    {
+                      lock->Invalidate();
+                    }
+                    break;
+
+                  case SDL_MOUSEBUTTONUP:
+                    lock->GetController().HandleMouseRelease(p);
+                    lock->Invalidate();
+                    break;
+
+                  default:
+                    throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+                  }
+                }
+              }
+            }
+
+            if (paint)
+            {
+              viewport->Paint();
+            }
+
+            // Small delay to avoid using 100% of CPU
+            SDL_Delay(1);
+          }
+        }
+
+        context.StopOracle();
+      }
+    }
+
+    OrthancStone::StoneFinalize();
+    return 0;
+  }
+  catch (Orthanc::OrthancException& e)
+  {
+    auto test = e.What();
+    fprintf(stdout, test);
+    LOG(ERROR) << "OrthancException: " << e.What();
+    return -1;
+  }
+  catch (OrthancStone::StoneException& e)
+  {
+    LOG(ERROR) << "StoneException: " << e.What();
+    return -1;
+  }
+  catch (std::runtime_error& e)
+  {
+    LOG(ERROR) << "Runtime error: " << e.what();
+    return -1;
+  }
+  catch (...)
+  {
+    LOG(ERROR) << "Native exception";
+    return -1;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Sdl/SingleFrameViewer/SdlSimpleViewerApplication.h	Mon Apr 27 10:01:03 2020 +0200
@@ -0,0 +1,146 @@
+#pragma once
+
+#include <Framework/Viewport/IViewport.h>
+#include <Framework/Loaders/DicomResourcesLoader.h>
+#include <Framework/Loaders/ILoadersContext.h>
+#include <Framework/Loaders/SeriesFramesLoader.h>
+#include <Framework/Loaders/SeriesThumbnailsLoader.h>
+
+#include <boost/make_shared.hpp>
+
+
+using OrthancStone::ILoadersContext;
+using OrthancStone::ObserverBase;
+using OrthancStone::IViewport;
+using OrthancStone::DicomResourcesLoader;
+using OrthancStone::SeriesFramesLoader;
+using OrthancStone::TextureBaseSceneLayer;
+using OrthancStone::DicomSource;
+using OrthancStone::SeriesThumbnailsLoader;
+using OrthancStone::LoadedDicomResources;
+using OrthancStone::SeriesThumbnailType;
+using OrthancStone::OracleScheduler;
+using OrthancStone::OrthancRestApiCommand;
+using OrthancStone::OracleScheduler;
+using OrthancStone::OracleScheduler;
+using OrthancStone::OracleScheduler;
+
+
+class SdlSimpleViewerApplication : public ObserverBase<SdlSimpleViewerApplication>
+{
+
+public:
+  static boost::shared_ptr<SdlSimpleViewerApplication> Create(ILoadersContext& context, boost::shared_ptr<IViewport> viewport)
+  {
+    boost::shared_ptr<SdlSimpleViewerApplication> application(new SdlSimpleViewerApplication(context, viewport));
+
+    {
+      std::auto_ptr<ILoadersContext::ILock> lock(context.Lock());
+      DicomResourcesLoader::Factory f;
+      application->dicomLoader_ = boost::dynamic_pointer_cast<DicomResourcesLoader>(f.Create(*lock));
+    }
+
+    application->Register<DicomResourcesLoader::SuccessMessage>(*application->dicomLoader_, &SdlSimpleViewerApplication::Handle);
+
+    return application;
+  }
+
+  void LoadOrthancFrame(const DicomSource& source, const std::string& instanceId, unsigned int frame)
+  {
+    std::auto_ptr<ILoadersContext::ILock> lock(context_.Lock());
+
+    dicomLoader_->ScheduleLoadOrthancResource(boost::make_shared<LoadedDicomResources>(Orthanc::DICOM_TAG_SOP_INSTANCE_UID),
+                                              0, source, Orthanc::ResourceType_Instance, instanceId,
+                                              new Orthanc::SingleValueObject<unsigned int>(frame));
+  }
+
+#if 0
+  void LoadDicomWebFrame(const DicomSource& source,
+                         const std::string& studyInstanceUid,
+                         const std::string& seriesInstanceUid,
+                         const std::string& sopInstanceUid,
+                         unsigned int frame)
+  {
+    std::auto_ptr<ILoadersContext::ILock> lock(context_.Lock());
+
+    // We first must load the "/metadata" to know the number of frames
+    dicomLoader_->ScheduleGetDicomWeb(
+      boost::make_shared<LoadedDicomResources>(Orthanc::DICOM_TAG_SOP_INSTANCE_UID), 0, source,
+      "/studies/" + studyInstanceUid + "/series/" + seriesInstanceUid + "/instances/" + sopInstanceUid + "/metadata",
+      new Orthanc::SingleValueObject<unsigned int>(frame));
+  }
+#endif 
+
+  void FitContent()
+  {
+    std::auto_ptr<IViewport::ILock> lock(viewport_->Lock());
+    lock->GetCompositor().FitContent(lock->GetController().GetScene());
+    lock->Invalidate();
+  }
+
+private:
+  ILoadersContext& context_;
+  boost::shared_ptr<IViewport>             viewport_;
+  boost::shared_ptr<DicomResourcesLoader>  dicomLoader_;
+  boost::shared_ptr<SeriesFramesLoader>    framesLoader_;
+
+  SdlSimpleViewerApplication(ILoadersContext& context,
+                             boost::shared_ptr<IViewport> viewport) :
+    context_(context),
+    viewport_(viewport)
+  {
+  }
+
+  void Handle(const SeriesFramesLoader::FrameLoadedMessage& message)
+  {
+    LOG(INFO) << "Frame decoded! "
+      << message.GetImage().GetWidth() << "x" << message.GetImage().GetHeight()
+      << " " << Orthanc::EnumerationToString(message.GetImage().GetFormat());
+
+    std::auto_ptr<TextureBaseSceneLayer> layer(
+      message.GetInstanceParameters().CreateTexture(message.GetImage()));
+    layer->SetLinearInterpolation(true);
+
+    {
+      std::auto_ptr<IViewport::ILock> lock(viewport_->Lock());
+      lock->GetController().GetScene().SetLayer(0, layer.release());
+      lock->GetCompositor().FitContent(lock->GetController().GetScene());
+      lock->Invalidate();
+    }
+  }
+
+  void Handle(const DicomResourcesLoader::SuccessMessage& message)
+  {
+    if (message.GetResources()->GetSize() != 1)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+    }
+
+    //message.GetResources()->GetResource(0).Print(stdout);
+
+    {
+      std::auto_ptr<ILoadersContext::ILock> lock(context_.Lock());
+      SeriesFramesLoader::Factory f(*message.GetResources());
+
+      framesLoader_ = boost::dynamic_pointer_cast<SeriesFramesLoader>(
+        f.Create(*lock));
+      
+      Register<SeriesFramesLoader::FrameLoadedMessage>(
+        *framesLoader_, &SdlSimpleViewerApplication::Handle);
+
+      assert(message.HasUserPayload());
+
+      const Orthanc::SingleValueObject<unsigned int>& payload =
+        dynamic_cast<const Orthanc::SingleValueObject<unsigned int>&>(
+          message.GetUserPayload());
+
+      LOG(INFO) << "Loading pixel data of frame: " << payload.GetValue();
+      framesLoader_->ScheduleLoadFrame(
+        0, message.GetDicomSource(), payload.GetValue(),
+        message.GetDicomSource().GetQualityCount() - 1 /* download best quality available */,
+        NULL);
+    }
+  }
+
+};
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Sdl/SingleFrameViewer/SimpleViewer.cpp	Mon Apr 27 10:01:03 2020 +0200
@@ -0,0 +1,292 @@
+
+#include "SdlSimpleViewerApplication.h"
+
+#include <Core/OrthancException.h>
+
+#include <Framework/Loaders/GenericLoadersContext.h>
+#include <Framework/StoneException.h>
+#include <Framework/StoneEnumerations.h>
+#include <Framework/StoneInitialization.h>
+#include <Framework/Viewport/SdlViewport.h>
+
+#include <SDL.h>
+
+namespace OrthancStone
+{
+  static KeyboardModifiers GetKeyboardModifiers(const uint8_t* keyboardState,
+                                                const int scancodeCount)
+  {
+    int result = KeyboardModifiers_None;
+
+    if (keyboardState != NULL)
+    {
+      if (SDL_SCANCODE_LSHIFT < scancodeCount &&
+          keyboardState[SDL_SCANCODE_LSHIFT])
+      {
+        result |= KeyboardModifiers_Shift;
+      }
+
+      if (SDL_SCANCODE_RSHIFT < scancodeCount &&
+          keyboardState[SDL_SCANCODE_RSHIFT])
+      {
+        result |= KeyboardModifiers_Shift;
+      }
+
+      if (SDL_SCANCODE_LCTRL < scancodeCount &&
+          keyboardState[SDL_SCANCODE_LCTRL])
+      {
+        result |= KeyboardModifiers_Control;
+      }
+
+      if (SDL_SCANCODE_RCTRL < scancodeCount &&
+          keyboardState[SDL_SCANCODE_RCTRL])
+      {
+        result |= KeyboardModifiers_Control;
+      }
+
+      if (SDL_SCANCODE_LALT < scancodeCount &&
+          keyboardState[SDL_SCANCODE_LALT])
+      {
+        result |= KeyboardModifiers_Alt;
+      }
+
+      if (SDL_SCANCODE_RALT < scancodeCount &&
+          keyboardState[SDL_SCANCODE_RALT])
+      {
+        result |= KeyboardModifiers_Alt;
+      }
+    }
+
+    return static_cast<KeyboardModifiers>(result);
+  }
+
+
+  static void GetPointerEvent(PointerEvent& p,
+                              const ICompositor& compositor,
+                              SDL_Event event,
+                              const uint8_t* keyboardState,
+                              const int scancodeCount)
+  {
+    KeyboardModifiers modifiers = GetKeyboardModifiers(keyboardState, scancodeCount);
+
+    switch (event.button.button)
+    {
+    case SDL_BUTTON_LEFT:
+      p.SetMouseButton(OrthancStone::MouseButton_Left);
+      break;
+
+    case SDL_BUTTON_RIGHT:
+      p.SetMouseButton(OrthancStone::MouseButton_Right);
+      break;
+
+    case SDL_BUTTON_MIDDLE:
+      p.SetMouseButton(OrthancStone::MouseButton_Middle);
+      break;
+
+    default:
+      p.SetMouseButton(OrthancStone::MouseButton_None);
+      break;
+    }
+
+    p.AddPosition(compositor.GetPixelCenterCoordinates(event.button.x, event.button.y));
+    p.SetAltModifier(modifiers & KeyboardModifiers_Alt);
+    p.SetControlModifier(modifiers & KeyboardModifiers_Control);
+    p.SetShiftModifier(modifiers & KeyboardModifiers_Shift);
+  }
+
+}
+
+/**
+ * IMPORTANT: The full arguments to "main()" are needed for SDL on
+ * Windows. Otherwise, one gets the linking error "undefined reference
+ * to `SDL_main'". https://wiki.libsdl.org/FAQWindows
+ **/
+int main(int argc, char* argv[])
+{
+  try
+  {
+    OrthancStone::StoneInitialize();
+    Orthanc::Logging::EnableInfoLevel(true);
+    //Orthanc::Logging::EnableTraceLevel(true);
+
+    {
+
+#if 1
+      boost::shared_ptr<OrthancStone::SdlViewport> viewport = 
+        OrthancStone::SdlOpenGLViewport::Create("Stone of Orthanc", 800, 600);
+#else
+      boost::shared_ptr<OrthancStone::SdlViewport> viewport =
+        OrthancStone::SdlCairoViewport::Create("Stone of Orthanc", 800, 600);
+#endif
+
+      OrthancStone::GenericLoadersContext context(1, 4, 1);
+      
+      context.StartOracle();
+
+      {
+
+        boost::shared_ptr<SdlSimpleViewerApplication> application(
+          SdlSimpleViewerApplication::Create(context, viewport));
+
+        OrthancStone::DicomSource source;
+
+        // Default and command-line parameters
+        const char* instanceId = "285dece8-e1956b38-cdc7d084-6ce3371e-536a9ffc";
+        unsigned int frameIndex = 0;
+
+        if (argc == 1)
+        {
+          LOG(ERROR) << "No instanceId supplied. The default of " << instanceId << " will be used. "
+            << "Please supply the Orthanc instance ID of the frame you wish to display then, optionally, "
+            << "the zero-based index of the frame (for multi-frame instances)";
+          // TODO: frame number as second argument...
+        }
+
+        if (argc >= 2)
+          instanceId = argv[1];
+
+        if (argc >= 3)
+          frameIndex = atoi(argv[1]);
+
+        if (argc > 3)
+        {
+          LOG(ERROR) << "Extra arguments ignored!";
+        }
+         
+
+        application->LoadOrthancFrame(source, instanceId, frameIndex);
+
+        OrthancStone::DefaultViewportInteractor interactor;
+
+        {
+          int scancodeCount = 0;
+          const uint8_t* keyboardState = SDL_GetKeyboardState(&scancodeCount);
+
+          bool stop = false;
+          while (!stop)
+          {
+            bool paint = false;
+            SDL_Event event;
+            while (SDL_PollEvent(&event))
+            {
+              if (event.type == SDL_QUIT)
+              {
+                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))
+              {
+                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;
+              }
+              else if (event.type == SDL_KEYDOWN &&
+                       event.key.repeat == 0 /* Ignore key bounce */)
+              {
+                switch (event.key.keysym.sym)
+                {
+                case SDLK_f:
+                  viewport->ToggleMaximize();
+                  break;
+
+                case SDLK_s:
+                  application->FitContent();
+                  break;
+
+                case SDLK_q:
+                  stop = true;
+                  break;
+
+                default:
+                  break;
+                }
+              }
+              else if (event.type == SDL_MOUSEBUTTONDOWN ||
+                       event.type == SDL_MOUSEMOTION ||
+                       event.type == SDL_MOUSEBUTTONUP)
+              {
+                std::auto_ptr<OrthancStone::IViewport::ILock> lock(viewport->Lock());
+                if (lock->HasCompositor())
+                {
+                  OrthancStone::PointerEvent p;
+                  OrthancStone::GetPointerEvent(p, lock->GetCompositor(),
+                                                event, keyboardState, scancodeCount);
+
+                  switch (event.type)
+                  {
+                  case SDL_MOUSEBUTTONDOWN:
+                    lock->GetController().HandleMousePress(interactor, p,
+                                                           lock->GetCompositor().GetCanvasWidth(),
+                                                           lock->GetCompositor().GetCanvasHeight());
+                    lock->Invalidate();
+                    break;
+
+                  case SDL_MOUSEMOTION:
+                    if (lock->GetController().HandleMouseMove(p))
+                    {
+                      lock->Invalidate();
+                    }
+                    break;
+
+                  case SDL_MOUSEBUTTONUP:
+                    lock->GetController().HandleMouseRelease(p);
+                    lock->Invalidate();
+                    break;
+
+                  default:
+                    throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+                  }
+                }
+              }
+            }
+
+            if (paint)
+            {
+              viewport->Paint();
+            }
+
+            // Small delay to avoid using 100% of CPU
+            SDL_Delay(1);
+          }
+        }
+
+        context.StopOracle();
+      }
+    }
+
+    OrthancStone::StoneFinalize();
+    return 0;
+  }
+  catch (Orthanc::OrthancException & e)
+  {
+    auto test = e.What();
+    fprintf(stdout, test);
+    LOG(ERROR) << "OrthancException: " << e.What();
+    return -1;
+  }
+  catch (OrthancStone::StoneException & e)
+  {
+    LOG(ERROR) << "StoneException: " << e.What();
+    return -1;
+  }
+  catch (std::runtime_error & e)
+  {
+    LOG(ERROR) << "Runtime error: " << e.what();
+    return -1;
+  }
+  catch (...)
+  {
+    LOG(ERROR) << "Native exception";
+    return -1;
+  }
+}
--- a/Samples/WebAssembly/SingleFrameViewer/CMakeLists.txt	Wed Apr 22 19:55:34 2020 +0200
+++ b/Samples/WebAssembly/SingleFrameViewer/CMakeLists.txt	Mon Apr 27 10:01:03 2020 +0200
@@ -1,5 +1,7 @@
 cmake_minimum_required(VERSION 2.8.3)
 
+project(SingleFrameViewer)
+
 # Configuration of the Emscripten compiler for WebAssembly target
 # ---------------------------------------------------------------
 set(USE_WASM ON CACHE BOOL "")