changeset 848:80829436ce0c am-mainline

starting to re-implement radiography editor with latest framework
author Alain Mazy <alain@mazy.be>
date Thu, 13 Jun 2019 16:47:02 +0200
parents 2b245953b44b
children 31319fe867b9 e0d47bb62253
files Framework/Scene2D/CairoCompositor.h Framework/Scene2D/Internals/CairoInfoPanelRenderer.cpp Framework/Scene2D/Internals/CompositorHelper.h Framework/Scene2D/Internals/ICairoContextProvider.h Framework/Scene2D/OpenGLCompositor.cpp Framework/Scene2D/OpenGLCompositor.h Samples/Sdl/BasicScene.cpp Samples/Sdl/CMakeLists.txt Samples/Sdl/FusionMprSdl.cpp Samples/Sdl/RadiographyEditor.cpp Samples/Sdl/TrackerSampleApp.cpp Samples/Shared/RadiographyEditorApp.cpp Samples/Shared/RadiographyEditorApp.h
diffstat 13 files changed, 1303 insertions(+), 56 deletions(-) [+]
line wrap: on
line diff
--- a/Framework/Scene2D/CairoCompositor.h	Wed Jun 12 09:52:25 2019 +0200
+++ b/Framework/Scene2D/CairoCompositor.h	Thu Jun 13 16:47:02 2019 +0200
@@ -29,6 +29,7 @@
 namespace OrthancStone
 {
   class CairoCompositor :
+    public ICompositor,
     private Internals::CompositorHelper::IRendererFactory,
     private Internals::ICairoContextProvider
   {
@@ -44,16 +45,6 @@
 
     virtual cairo_t* GetCairoContext();
 
-    virtual unsigned int GetCairoWidth()
-    {
-      return canvas_.GetWidth();
-    }
-
-    virtual unsigned int GetCairoHeight()
-    {
-      return canvas_.GetHeight();
-    }
-    
     virtual Internals::CompositorHelper::ILayerRenderer* Create(const ISceneLayer& layer);
 
   public:
@@ -63,6 +54,16 @@
     
     ~CairoCompositor();
 
+    virtual unsigned int GetWidth() const
+    {
+      return canvas_.GetWidth();
+    }
+
+    virtual unsigned int GetHeight() const
+    {
+      return canvas_.GetHeight();
+    }
+
     const CairoSurface& GetCanvas() const
     {
       return canvas_;
@@ -78,7 +79,7 @@
                  Orthanc::Encoding codepage);
 #endif
 
-    void Refresh();
+    virtual void Refresh();
 
     Orthanc::ImageAccessor* RenderText(size_t fontIndex,
                                        const std::string& utf8) const;
--- a/Framework/Scene2D/Internals/CairoInfoPanelRenderer.cpp	Wed Jun 12 09:52:25 2019 +0200
+++ b/Framework/Scene2D/Internals/CairoInfoPanelRenderer.cpp	Thu Jun 13 16:47:02 2019 +0200
@@ -42,7 +42,7 @@
       int dx, dy;
       InfoPanelSceneLayer::ComputeAnchorLocation(
         dx, dy, anchor_, texture_.GetWidth(), texture_.GetHeight(),
-        target_.GetCairoWidth(), target_.GetCairoHeight());
+        target_.GetWidth(), target_.GetHeight());
 
       cairo_t* cr = target_.GetCairoContext();
 
--- a/Framework/Scene2D/Internals/CompositorHelper.h	Wed Jun 12 09:52:25 2019 +0200
+++ b/Framework/Scene2D/Internals/CompositorHelper.h	Thu Jun 13 16:47:02 2019 +0200
@@ -22,13 +22,29 @@
 #pragma once
 
 #include "../Scene2D.h"
-
+#include "../ScenePoint2D.h"
 #include <boost/noncopyable.hpp>
 
 #include <map>
 
 namespace OrthancStone
 {
+  class ICompositor
+  {
+  public:
+    virtual unsigned int GetWidth() const = 0;
+    virtual unsigned int GetHeight() const = 0;
+    virtual void Refresh() = 0;
+
+    ScenePoint2D GetPixelCenterCoordinates(int x, int y) const
+    {
+      return ScenePoint2D(
+        static_cast<double>(x) + 0.5 - static_cast<double>(GetWidth()) / 2.0,
+        static_cast<double>(y) + 0.5 - static_cast<double>(GetHeight()) / 2.0);
+    }
+
+  };
+
   namespace Internals
   {
     class CompositorHelper : protected Scene2D::IVisitor
--- a/Framework/Scene2D/Internals/ICairoContextProvider.h	Wed Jun 12 09:52:25 2019 +0200
+++ b/Framework/Scene2D/Internals/ICairoContextProvider.h	Thu Jun 13 16:47:02 2019 +0200
@@ -38,9 +38,9 @@
 
       virtual cairo_t* GetCairoContext() = 0;
 
-      virtual unsigned int GetCairoWidth() = 0;
+      virtual unsigned int GetWidth() const = 0;
 
-      virtual unsigned int GetCairoHeight() = 0;
+      virtual unsigned int GetHeight() const = 0;
     };
   }
 }
--- a/Framework/Scene2D/OpenGLCompositor.cpp	Wed Jun 12 09:52:25 2019 +0200
+++ b/Framework/Scene2D/OpenGLCompositor.cpp	Thu Jun 13 16:47:02 2019 +0200
@@ -207,12 +207,4 @@
     SetFont(index, dict);
   }
 #endif
-
-
-  ScenePoint2D OpenGLCompositor::GetPixelCenterCoordinates(int x, int y) const
-  {
-    return ScenePoint2D(
-      static_cast<double>(x) + 0.5 - static_cast<double>(canvasWidth_) / 2.0,
-      static_cast<double>(y) + 0.5 - static_cast<double>(canvasHeight_) / 2.0);
-  }
 }
--- a/Framework/Scene2D/OpenGLCompositor.h	Wed Jun 12 09:52:25 2019 +0200
+++ b/Framework/Scene2D/OpenGLCompositor.h	Thu Jun 13 16:47:02 2019 +0200
@@ -29,7 +29,9 @@
 
 namespace OrthancStone
 {
-  class OpenGLCompositor : private Internals::CompositorHelper::IRendererFactory
+  class OpenGLCompositor :
+      public ICompositor,
+      private Internals::CompositorHelper::IRendererFactory
   {
   private:
     class Font;
@@ -58,7 +60,7 @@
 
     void UpdateSize();
 
-    void Refresh();
+    virtual void Refresh();
 
     void SetFont(size_t index,
                  const GlyphBitmapAlphabet& dict);
@@ -70,16 +72,15 @@
                  Orthanc::Encoding codepage);
 #endif
 
-    unsigned int GetCanvasWidth() const
+    virtual unsigned int GetWidth() const
     {
       return canvasWidth_;
     }
 
-    unsigned int GetCanvasHeight() const
+    unsigned int GetHeight() const
     {
       return canvasHeight_;
     }
 
-    ScenePoint2D GetPixelCenterCoordinates(int x, int y) const;
   };
 }
--- a/Samples/Sdl/BasicScene.cpp	Wed Jun 12 09:52:25 2019 +0200
+++ b/Samples/Sdl/BasicScene.cpp	Thu Jun 13 16:47:02 2019 +0200
@@ -224,7 +224,7 @@
 
       case SDL_BUTTON_RIGHT:
         activeTracker = boost::make_shared<ZoomSceneTracker>(controller, 
-          e, compositor.GetCanvasHeight());
+          e, compositor.GetHeight());
         break;
 
       case SDL_BUTTON_LEFT:
@@ -241,14 +241,14 @@
     switch (event.key.keysym.sym)
     {
       case SDLK_s:
-        controller->FitContent(compositor.GetCanvasWidth(), 
-                         compositor.GetCanvasHeight());
+        controller->FitContent(compositor.GetWidth(),
+                         compositor.GetHeight());
         break;
               
       case SDLK_c:
         TakeScreenshot("screenshot.png", scene, 
-                       compositor.GetCanvasWidth(), 
-                       compositor.GetCanvasHeight());
+                       compositor.GetWidth(),
+                       compositor.GetHeight());
         break;
               
       default:
--- a/Samples/Sdl/CMakeLists.txt	Wed Jun 12 09:52:25 2019 +0200
+++ b/Samples/Sdl/CMakeLists.txt	Thu Jun 13 16:47:02 2019 +0200
@@ -66,7 +66,7 @@
 target_link_libraries(BasicScene OrthancStone)
 
 #
-# BasicScene
+# TrackerSample
 # 
 
 LIST(APPEND TRACKERSAMPLE_SOURCE "TrackerSample.cpp")
@@ -103,3 +103,21 @@
 )
 
 target_link_libraries(FusionMprSdl OrthancStone)
+
+#
+# RadiographyEditor
+#
+
+LIST(APPEND RADIOGRAPHY_EDITOR_SOURCE "../Shared/RadiographyEditorApp.cpp")
+LIST(APPEND RADIOGRAPHY_EDITOR_SOURCE "../Shared/RadiographyEditorApp.h")
+LIST(APPEND RADIOGRAPHY_EDITOR_SOURCE "RadiographyEditor.cpp")
+
+if (MSVC AND MSVC_VERSION GREATER 1700)
+  LIST(APPEND RADIOGRAPHY_EDITOR_SOURCE "cpp.hint")
+endif()
+
+add_executable(RadiographyEditor
+  ${RADIOGRAPHY_EDITOR_SOURCE}
+  )
+
+target_link_libraries(RadiographyEditor OrthancStone)
--- a/Samples/Sdl/FusionMprSdl.cpp	Wed Jun 12 09:52:25 2019 +0200
+++ b/Samples/Sdl/FusionMprSdl.cpp	Thu Jun 13 16:47:02 2019 +0200
@@ -134,8 +134,8 @@
     }
     // 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);
+    double cX = compositor_->GetWidth() * (-0.5);
+    double cY = compositor_->GetHeight() * (-0.5);
     GetScene()->GetCanvasToSceneTransform().Apply(cX,cY);
     layerP->SetPosition(cX, cY);
   }
@@ -269,8 +269,8 @@
         }
         break;
       case SDLK_s:
-        controller_->FitContent(compositor_->GetCanvasWidth(),
-          compositor_->GetCanvasHeight());
+        controller_->FitContent(compositor_->GetWidth(),
+          compositor_->GetHeight());
         break;
 
       case SDLK_z:
@@ -308,8 +308,8 @@
       case SDLK_c:
         TakeScreenshot(
           "screenshot.png",
-          compositor_->GetCanvasWidth(),
-          compositor_->GetCanvasHeight());
+          compositor_->GetWidth(),
+          compositor_->GetHeight());
         break;
 
       default:
@@ -339,7 +339,7 @@
 
     case SDL_BUTTON_RIGHT:
       return boost::shared_ptr<IFlexiblePointerTracker>(new ZoomSceneTracker
-        (controller_, e, compositor_->GetCanvasHeight()));
+        (controller_, e, compositor_->GetHeight()));
 
     case SDL_BUTTON_LEFT:
     {
@@ -371,7 +371,7 @@
             controller_, e));
         case FusionMprGuiTool_Zoom:
           return boost::shared_ptr<IFlexiblePointerTracker>(new ZoomSceneTracker(
-            controller_, e, compositor_->GetCanvasHeight()));
+            controller_, e, compositor_->GetHeight()));
         //case GuiTool_AngleMeasure:
         //  return new AngleMeasureTracker(GetScene(), e);
         //case GuiTool_CircleMeasure:
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Sdl/RadiographyEditor.cpp	Thu Jun 13 16:47:02 2019 +0200
@@ -0,0 +1,267 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+#include "../Shared/RadiographyEditorApp.h"
+
+// From Stone
+#include "../../Framework/Oracle/SleepOracleCommand.h"
+#include "../../Framework/Oracle/ThreadedOracle.h"
+#include "../../Applications/Sdl/SdlOpenGLWindow.h"
+#include "../../Framework/Scene2D/OpenGLCompositor.h"
+#include "../../Framework/Scene2D/CairoCompositor.h"
+#include "../../Framework/Scene2D/ColorTextureSceneLayer.h"
+#include "../../Framework/Scene2D/OpenGLCompositor.h"
+#include "../../Framework/StoneInitialization.h"
+
+#include <Core/Logging.h>
+#include <Core/OrthancException.h>
+
+
+#include <boost/shared_ptr.hpp>
+#include <boost/weak_ptr.hpp>
+
+#include <SDL.h>
+#include <stdio.h>
+
+using namespace OrthancStone;
+
+namespace OrthancStone
+{
+  class NativeApplicationContext : public IMessageEmitter
+  {
+  private:
+    boost::shared_mutex  mutex_;
+    MessageBroker        broker_;
+    IObservable          oracleObservable_;
+
+  public:
+    NativeApplicationContext() :
+      oracleObservable_(broker_)
+    {
+    }
+
+
+    virtual void EmitMessage(const 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();
+      }
+    }
+
+
+    class ReaderLock : public boost::noncopyable
+    {
+    private:
+      NativeApplicationContext&                that_;
+      boost::shared_lock<boost::shared_mutex>  lock_;
+
+    public:
+      ReaderLock(NativeApplicationContext& that) :
+        that_(that),
+        lock_(that.mutex_)
+      {
+      }
+    };
+
+
+    class WriterLock : public boost::noncopyable
+    {
+    private:
+      NativeApplicationContext&                that_;
+      boost::unique_lock<boost::shared_mutex>  lock_;
+
+    public:
+      WriterLock(NativeApplicationContext& that) :
+        that_(that),
+        lock_(that.mutex_)
+      {
+      }
+
+      MessageBroker& GetBroker()
+      {
+        return that_.broker_;
+      }
+
+      IObservable& GetOracleObservable()
+      {
+        return that_.oracleObservable_;
+      }
+    };
+  };
+}
+
+class OpenGlSdlCompositorFactory : public ICompositorFactory
+{
+  OpenGL::IOpenGLContext& openGlContext_;
+
+public:
+  OpenGlSdlCompositorFactory(OpenGL::IOpenGLContext& openGlContext) :
+    openGlContext_(openGlContext)
+  {}
+
+  ICompositor* GetCompositor(const Scene2D& scene)
+  {
+
+    OpenGLCompositor* compositor = new OpenGLCompositor(openGlContext_, scene);
+    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);
+    return compositor;
+  }
+};
+
+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);
+  }
+}
+
+
+/**
+ * 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
+  {
+    OrthancStone::NativeApplicationContext context;
+    OrthancStone::NativeApplicationContext::WriterLock lock(context);
+    OrthancStone::ThreadedOracle oracle(context);
+
+    // False means we do NOT let Windows treat this as a legacy application
+    // that needs to be scaled
+    SdlOpenGLWindow window("Hello", 1024, 1024, false);
+
+    glEnable(GL_DEBUG_OUTPUT);
+    glDebugMessageCallback(OpenGLMessageCallback, 0);
+
+    std::auto_ptr<OpenGlSdlCompositorFactory> compositorFactory(new OpenGlSdlCompositorFactory(window));
+    boost::shared_ptr<RadiographyEditorApp> app(new RadiographyEditorApp(oracle, lock.GetOracleObservable(), compositorFactory.release()));
+    app->PrepareScene();
+    app->FitContent(window.GetCanvasWidth(), window.GetCanvasHeight());
+
+    bool stopApplication = false;
+
+    while (!stopApplication)
+    {
+      app->Refresh();
+
+      SDL_Event event;
+      while (!stopApplication && SDL_PollEvent(&event))
+      {
+        OrthancStone::KeyboardModifiers modifiers = OrthancStone::KeyboardModifiers_None;
+        if (event.key.keysym.mod & KMOD_CTRL)
+          modifiers = static_cast<OrthancStone::KeyboardModifiers>(static_cast<int>(modifiers) | static_cast<int>(OrthancStone::KeyboardModifiers_Control));
+        if (event.key.keysym.mod & KMOD_ALT)
+          modifiers = static_cast<OrthancStone::KeyboardModifiers>(static_cast<int>(modifiers) | static_cast<int>(OrthancStone::KeyboardModifiers_Alt));
+        if (event.key.keysym.mod & KMOD_SHIFT)
+          modifiers = static_cast<OrthancStone::KeyboardModifiers>(static_cast<int>(modifiers) | static_cast<int>(OrthancStone::KeyboardModifiers_Shift));
+
+        OrthancStone::MouseButton button;
+        if (event.button.button == SDL_BUTTON_LEFT)
+          button = OrthancStone::MouseButton_Left;
+        else if (event.button.button == SDL_BUTTON_MIDDLE)
+          button = OrthancStone::MouseButton_Middle;
+        else if (event.button.button == SDL_BUTTON_RIGHT)
+          button = OrthancStone::MouseButton_Right;
+
+        if (event.type == SDL_QUIT)
+        {
+          stopApplication = true;
+          break;
+        }
+        else if (event.type == SDL_WINDOWEVENT &&
+                 event.window.event == SDL_WINDOWEVENT_SIZE_CHANGED)
+        {
+          app->DisableTracker(); // was: tracker.reset(NULL);
+          app->UpdateSize();
+        }
+        else if (event.type == SDL_KEYDOWN &&
+                 event.key.repeat == 0 /* Ignore key bounce */)
+        {
+          switch (event.key.keysym.sym)
+          {
+          case SDLK_f:
+            window.GetWindow().ToggleMaximize();
+            break;
+
+          case SDLK_q:
+            stopApplication = true;
+            break;
+          default:
+          {
+            app->OnKeyPressed(event.key.keysym.sym, modifiers);
+           }
+          }
+        }
+        else if (event.type == SDL_MOUSEBUTTONDOWN)
+        {
+          app->OnMouseDown(event.button.x, event.button.y, modifiers, button);
+        }
+        else if (event.type == SDL_MOUSEMOTION)
+        {
+          app->OnMouseMove(event.button.x, event.button.y, modifiers);
+        }
+        else if (event.type == SDL_MOUSEBUTTONUP)
+        {
+          app->OnMouseUp(event.button.x, event.button.y, modifiers, button);
+        }
+      }
+      SDL_Delay(1);
+    }
+  }
+  catch (Orthanc::OrthancException& e)
+  {
+    LOG(ERROR) << "EXCEPTION: " << e.What();
+  }
+
+  StoneFinalize();
+
+  return 0;
+}
+
+
--- a/Samples/Sdl/TrackerSampleApp.cpp	Wed Jun 12 09:52:25 2019 +0200
+++ b/Samples/Sdl/TrackerSampleApp.cpp	Thu Jun 13 16:47:02 2019 +0200
@@ -120,8 +120,8 @@
     }
     // 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);
+    double cX = compositor_->GetWidth() * (-0.5);
+    double cY = compositor_->GetHeight() * (-0.5);
     GetScene()->GetCanvasToSceneTransform().Apply(cX,cY);
     layerP->SetPosition(cX, cY);
   }
@@ -161,12 +161,12 @@
 
   ScenePoint2D TrackerSampleApp::GetRandomPointInScene() const
   {
-    unsigned int w = compositor_->GetCanvasWidth();
-    LOG(TRACE) << "compositor_->GetCanvasWidth() = " << 
-      compositor_->GetCanvasWidth();
-    unsigned int h = compositor_->GetCanvasHeight();
-    LOG(TRACE) << "compositor_->GetCanvasHeight() = " << 
-      compositor_->GetCanvasHeight();
+    unsigned int w = compositor_->GetWidth();
+    LOG(TRACE) << "compositor_->GetWidth() = " <<
+      compositor_->GetWidth();
+    unsigned int h = compositor_->GetHeight();
+    LOG(TRACE) << "compositor_->GetHeight() = " <<
+      compositor_->GetHeight();
 
     if ((w >= RAND_MAX) || (h >= RAND_MAX))
       LOG(WARNING) << "Canvas is too big : tools will not be randomly placed";
@@ -324,8 +324,8 @@
         CreateRandomMeasureTool();
         break;
       case SDLK_s:
-        controller_->FitContent(compositor_->GetCanvasWidth(),
-          compositor_->GetCanvasHeight());
+        controller_->FitContent(compositor_->GetWidth(),
+          compositor_->GetHeight());
         break;
 
       case SDLK_z:
@@ -363,8 +363,8 @@
       case SDLK_c:
         TakeScreenshot(
           "screenshot.png",
-          compositor_->GetCanvasWidth(),
-          compositor_->GetCanvasHeight());
+          compositor_->GetWidth(),
+          compositor_->GetHeight());
         break;
 
       default:
@@ -394,7 +394,7 @@
 
     case SDL_BUTTON_RIGHT:
       return boost::shared_ptr<IFlexiblePointerTracker>(new ZoomSceneTracker
-        (controller_, e, compositor_->GetCanvasHeight()));
+        (controller_, e, compositor_->GetHeight()));
 
     case SDL_BUTTON_LEFT:
     {
@@ -426,7 +426,7 @@
             controller_, e));
         case GuiTool_Zoom:
           return boost::shared_ptr<IFlexiblePointerTracker>(new ZoomSceneTracker(
-            controller_, e, compositor_->GetCanvasHeight()));
+            controller_, e, compositor_->GetHeight()));
         //case GuiTool_AngleMeasure:
         //  return new AngleMeasureTracker(GetScene(), e);
         //case GuiTool_CircleMeasure:
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Shared/RadiographyEditorApp.cpp	Thu Jun 13 16:47:02 2019 +0200
@@ -0,0 +1,784 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+#include "RadiographyEditorApp.h"
+
+#include "../../Applications/Sdl/SdlOpenGLWindow.h"
+
+#include "../../Framework/Scene2D/CairoCompositor.h"
+#include "../../Framework/Scene2D/OpenGLCompositor.h"
+#include "../../Framework/Scene2D/ColorTextureSceneLayer.h"
+#include "../../Framework/Scene2D/PanSceneTracker.h"
+#include "../../Framework/Scene2D/RotateSceneTracker.h"
+#include "../../Framework/Scene2D/Scene2D.h"
+#include "../../Framework/Scene2D/ZoomSceneTracker.h"
+#include "../../Framework/Scene2DViewport/CreateAngleMeasureTracker.h"
+#include "../../Framework/Scene2DViewport/CreateLineMeasureTracker.h"
+#include "../../Framework/StoneInitialization.h"
+
+// From Orthanc framework
+#include <Core/Logging.h>
+#include <Core/OrthancException.h>
+#include <Core/Images/Image.h>
+#include <Core/Images/ImageProcessing.h>
+#include <Core/Images/PngWriter.h>
+
+#include <boost/ref.hpp>
+#include <boost/make_shared.hpp>
+#include <SDL.h>
+
+#include <stdio.h>
+
+namespace OrthancStone
+{
+  const char* MeasureToolToString(size_t i)
+  {
+    static const char* descs[] = {
+      "GuiTool_Rotate",
+      "GuiTool_Pan",
+      "GuiTool_Zoom",
+      "GuiTool_LineMeasure",
+      "GuiTool_CircleMeasure",
+      "GuiTool_AngleMeasure",
+      "GuiTool_EllipseMeasure",
+      "GuiTool_LAST"
+    };
+    if (i >= GuiTool_LAST)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError, "Wrong tool index");
+    }
+    return descs[i];
+  }
+
+  boost::shared_ptr<Scene2D> RadiographyEditorApp::GetScene()
+  {
+    return controller_->GetScene();
+  }
+
+  boost::shared_ptr<const Scene2D> RadiographyEditorApp::GetScene() const
+  {
+    return controller_->GetScene();
+  }
+
+  void RadiographyEditorApp::SelectNextTool()
+  {
+    currentTool_ = static_cast<GuiTool>(currentTool_ + 1);
+    if (currentTool_ == GuiTool_LAST)
+      currentTool_ = static_cast<GuiTool>(0);;
+    printf("Current tool is now: %s\n", MeasureToolToString(currentTool_));
+  }
+
+  void RadiographyEditorApp::DisplayInfoText()
+  {
+    // do not try to use stuff too early!
+    if (compositor_.get() == NULL)
+      return;
+
+    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 (GetScene()->HasLayer(FIXED_INFOTEXT_LAYER_ZINDEX))
+    {
+      TextSceneLayer& layer = dynamic_cast<TextSceneLayer&>(
+            GetScene()->GetLayer(FIXED_INFOTEXT_LAYER_ZINDEX));
+      layerP = &layer;
+    }
+    else
+    {
+      std::auto_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);
+      GetScene()->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_->GetWidth() * (-0.5);
+    double cY = compositor_->GetHeight() * (-0.5);
+    GetScene()->GetCanvasToSceneTransform().Apply(cX,cY);
+    layerP->SetPosition(cX, cY);
+  }
+
+  void RadiographyEditorApp::DisplayFloatingCtrlInfoText(const PointerEvent& e)
+  {
+    ScenePoint2D p = e.GetMainPosition().Apply(GetScene()->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 (GetScene()->HasLayer(FLOATING_INFOTEXT_LAYER_ZINDEX))
+    {
+      TextSceneLayer& layer =
+          dynamic_cast<TextSceneLayer&>(GetScene()->GetLayer(FLOATING_INFOTEXT_LAYER_ZINDEX));
+      layer.SetText(buf);
+      layer.SetPosition(p.GetX(), p.GetY());
+    }
+    else
+    {
+      std::auto_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());
+      GetScene()->SetLayer(FLOATING_INFOTEXT_LAYER_ZINDEX, layer.release());
+    }
+  }
+
+  void RadiographyEditorApp::HideInfoText()
+  {
+    GetScene()->DeleteLayer(FLOATING_INFOTEXT_LAYER_ZINDEX);
+  }
+
+  ScenePoint2D RadiographyEditorApp::GetRandomPointInScene() const
+  {
+    unsigned int w = compositor_->GetWidth();
+    LOG(TRACE) << "compositor_->GetWidth() = " <<
+                  compositor_->GetWidth();
+    unsigned int h = compositor_->GetHeight();
+    LOG(TRACE) << "compositor_->GetHeight() = " <<
+                  compositor_->GetHeight();
+
+    if ((w >= RAND_MAX) || (h >= RAND_MAX))
+      LOG(WARNING) << "Canvas is too big : tools will not be randomly placed";
+
+    int x = rand() % w;
+    int y = rand() % h;
+    LOG(TRACE) << "random x = " << x << "random y = " << y;
+
+    ScenePoint2D p = compositor_->GetPixelCenterCoordinates(x, y);
+    LOG(TRACE) << "--> p.GetX() = " << p.GetX() << " p.GetY() = " << p.GetY();
+
+    ScenePoint2D r = p.Apply(GetScene()->GetCanvasToSceneTransform());
+    LOG(TRACE) << "--> r.GetX() = " << r.GetX() << " r.GetY() = " << r.GetY();
+    return r;
+  }
+
+  void RadiographyEditorApp::CreateRandomMeasureTool()
+  {
+    static bool srandCalled = false;
+    if (!srandCalled)
+    {
+      srand(42);
+      srandCalled = true;
+    }
+
+    int i = rand() % 2;
+    LOG(TRACE) << "random i = " << i;
+    switch (i)
+    {
+    case 0:
+      // line measure
+    {
+      boost::shared_ptr<CreateLineMeasureCommand> cmd =
+          boost::make_shared<CreateLineMeasureCommand>(
+            boost::ref(IObserver::GetBroker()),
+            controller_,
+            GetRandomPointInScene());
+      cmd->SetEnd(GetRandomPointInScene());
+      controller_->PushCommand(cmd);
+    }
+      break;
+    case 1:
+      // angle measure
+    {
+      boost::shared_ptr<CreateAngleMeasureCommand> cmd =
+          boost::make_shared<CreateAngleMeasureCommand>(
+            boost::ref(IObserver::GetBroker()),
+            controller_,
+            GetRandomPointInScene());
+      cmd->SetCenter(GetRandomPointInScene());
+      cmd->SetSide2End(GetRandomPointInScene());
+      controller_->PushCommand(cmd);
+    }
+      break;
+    }
+  }
+
+  void RadiographyEditorApp::OnMouseMove(int x, int y, OrthancStone::KeyboardModifiers modifiers)
+  {
+    DisplayInfoText();
+    if (activeTracker_.get() == NULL && (modifiers & OrthancStone::KeyboardModifiers_Alt))
+    {
+      // The "left-ctrl" key is down, while no tracker is present
+      // Let's display the info text
+      PointerEvent e;
+      e.AddPosition(compositor_->GetPixelCenterCoordinates(x, 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(x, 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();
+      }
+    }
+  }
+
+  void RadiographyEditorApp::OnKeyPressed(char keyChar, OrthancStone::KeyboardModifiers modifiers)
+  {
+    DisplayInfoText();
+
+    switch (keyChar)
+    {
+    case '\033': // escape
+    {
+      if (activeTracker_)
+      {
+        activeTracker_->Cancel();
+        if (!activeTracker_->IsAlive())
+          activeTracker_.reset();
+      }
+    };break;
+    case 't':
+    {
+      if (!activeTracker_)
+        SelectNextTool();
+      else
+      {
+        LOG(WARNING) << "You cannot change the active tool when an interaction"
+                        " is taking place";
+      }
+    };break;
+    case 'm':
+      CreateRandomMeasureTool();
+      break;
+    case 's':
+      controller_->FitContent(compositor_->GetWidth(),
+                              compositor_->GetHeight());
+      break;
+    case 'z':
+      LOG(TRACE) << "z has been pressed. modifier = " << modifiers;
+      if (modifiers & OrthancStone::KeyboardModifiers_Control)
+      {
+        if (controller_->CanUndo())
+        {
+          LOG(TRACE) << "Undoing...";
+          controller_->Undo();
+        }
+        else
+        {
+          LOG(WARNING) << "Nothing to undo!!!";
+        }
+      }
+      break;
+
+    case 'y':
+      LOG(TRACE) << "y has been pressed. modifier = " << modifiers;
+      if (modifiers & OrthancStone::KeyboardModifiers_Control)
+      {
+        if (controller_->CanRedo())
+        {
+          LOG(TRACE) << "Redoing...";
+          controller_->Redo();
+        }
+        else
+        {
+          LOG(WARNING) << "Nothing to redo!!!";
+        }
+      }
+      break;
+
+    case 'c':
+      TakeScreenshot(
+            "screenshot.png",
+            compositor_->GetWidth(),
+            compositor_->GetHeight());
+      break;
+
+    }
+  }
+
+  void RadiographyEditorApp::OnMouseDown(int x, int y, OrthancStone::KeyboardModifiers modifiers, OrthancStone::MouseButton button)
+  {
+    DisplayInfoText();
+    PointerEvent e;
+    e.AddPosition(compositor_->GetPixelCenterCoordinates(x, y));
+    // TODO: set modifiers in e
+
+    if (activeTracker_)
+    {
+      activeTracker_->PointerDown(e);
+      if (!activeTracker_->IsAlive())
+        activeTracker_.reset();
+    }
+    else
+    {
+      // we ATTEMPT to create a tracker if need be
+      activeTracker_ = CreateSuitableTracker(button, e);
+    }
+  }
+
+  void RadiographyEditorApp::OnMouseUp(int x, int y, OrthancStone::KeyboardModifiers modifiers, OrthancStone::MouseButton button)
+  {
+    DisplayInfoText();
+    if (activeTracker_)
+    {
+      PointerEvent e;
+      e.AddPosition(compositor_->GetPixelCenterCoordinates(x, y));
+      // TODO: set modifiers in e
+
+      activeTracker_->PointerUp(e);
+      if (!activeTracker_->IsAlive())
+        activeTracker_.reset();
+    }
+  }
+
+  void RadiographyEditorApp::HandleApplicationEvent(
+      const SDL_Event & event)
+  {
+    DisplayInfoText();
+
+    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));
+        // TODO: set modifiers in e
+
+        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));
+          // TODO: set modifiers in e
+
+          //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));
+        // TODO: set modifiers in e
+        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));
+      // TODO: set modifiers in e
+      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_m:
+        CreateRandomMeasureTool();
+        break;
+      case SDLK_s:
+        controller_->FitContent(compositor_->GetWidth(),
+                                compositor_->GetHeight());
+        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_->GetWidth(),
+              compositor_->GetHeight());
+        break;
+
+      default:
+        break;
+      }
+    }
+  }
+
+
+  void RadiographyEditorApp::OnSceneTransformChanged(
+      const ViewportController::SceneTransformChanged& message)
+  {
+    DisplayInfoText();
+  }
+
+  boost::shared_ptr<IFlexiblePointerTracker> RadiographyEditorApp::CreateSuitableTracker(
+      OrthancStone::MouseButton button,
+      const PointerEvent & e)
+  {
+    using namespace Orthanc;
+
+    switch (button)
+    {
+    case OrthancStone::MouseButton_Middle:
+      return boost::shared_ptr<IFlexiblePointerTracker>(new PanSceneTracker
+                                                        (controller_, e));
+
+    case OrthancStone::MouseButton_Right:
+      return boost::shared_ptr<IFlexiblePointerTracker>(new ZoomSceneTracker
+                                                        (controller_, e, compositor_->GetHeight()));
+
+    case OrthancStone::MouseButton_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 GuiTool_Rotate:
+          //LOG(TRACE) << "Creating RotateSceneTracker";
+          return boost::shared_ptr<IFlexiblePointerTracker>(new RotateSceneTracker(
+                                                              controller_, e));
+        case GuiTool_Pan:
+          return boost::shared_ptr<IFlexiblePointerTracker>(new PanSceneTracker(
+                                                              controller_, e));
+        case GuiTool_Zoom:
+          return boost::shared_ptr<IFlexiblePointerTracker>(new ZoomSceneTracker(
+                                                              controller_, e, compositor_->GetHeight()));
+          //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 GuiTool_LineMeasure:
+          return boost::shared_ptr<IFlexiblePointerTracker>(new CreateLineMeasureTracker(
+                                                              IObserver::GetBroker(), controller_, e));
+        case GuiTool_AngleMeasure:
+          return boost::shared_ptr<IFlexiblePointerTracker>(new CreateAngleMeasureTracker(
+                                                              IObserver::GetBroker(), controller_, e));
+        case GuiTool_CircleMeasure:
+          LOG(ERROR) << "Not implemented yet!";
+          return boost::shared_ptr<IFlexiblePointerTracker>();
+        case GuiTool_EllipseMeasure:
+          LOG(ERROR) << "Not implemented yet!";
+          return boost::shared_ptr<IFlexiblePointerTracker>();
+        default:
+          throw OrthancException(ErrorCode_InternalError, "Wrong tool!");
+        }
+      }
+    }
+    default:
+      return boost::shared_ptr<IFlexiblePointerTracker>();
+    }
+  }
+
+
+  RadiographyEditorApp::RadiographyEditorApp(OrthancStone::IOracle& oracle,
+                                             IObservable& oracleObservable,
+                                             ICompositorFactory* compositorFactory) :
+    IObserver(oracleObservable.GetBroker()),
+    oracle_(oracle),
+    compositorFactory_(compositorFactory),
+    currentTool_(GuiTool_Rotate)
+  {
+    controller_ = boost::shared_ptr<ViewportController>(new ViewportController(IObserver::GetBroker()));
+
+    controller_->RegisterObserverCallback(
+          new Callable<RadiographyEditorApp, ViewportController::SceneTransformChanged>
+          (*this, &RadiographyEditorApp::OnSceneTransformChanged));
+
+    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 RadiographyEditorApp::PrepareScene()
+  {
+    // 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;
+
+      GetScene()->SetLayer(TEXTURE_2x2_1_ZINDEX, new ColorTextureSceneLayer(i));
+
+      std::auto_ptr<ColorTextureSceneLayer> l(new ColorTextureSceneLayer(i));
+      l->SetOrigin(-3, 2);
+      l->SetPixelSpacing(1.5, 1);
+      l->SetAngle(20.0 / 180.0 * M_PI);
+      GetScene()->SetLayer(TEXTURE_2x2_2_ZINDEX, l.release());
+    }
+
+    // Texture of 1x1 size
+    {
+      Orthanc::Image i(Orthanc::PixelFormat_RGB24, 1, 1, false);
+
+      uint8_t* p = reinterpret_cast<uint8_t*>(i.GetRow(0));
+      p[0] = 255;
+      p[1] = 0;
+      p[2] = 0;
+
+      std::auto_ptr<ColorTextureSceneLayer> l(new ColorTextureSceneLayer(i));
+      l->SetOrigin(-2, 1);
+      l->SetAngle(20.0 / 180.0 * M_PI);
+      GetScene()->SetLayer(TEXTURE_1x1_ZINDEX, l.release());
+    }
+
+    // Some lines
+    {
+      std::auto_ptr<PolylineSceneLayer> layer(new PolylineSceneLayer);
+
+      layer->SetThickness(1);
+
+      PolylineSceneLayer::Chain chain;
+      chain.push_back(ScenePoint2D(0 - 0.5, 0 - 0.5));
+      chain.push_back(ScenePoint2D(0 - 0.5, 2 - 0.5));
+      chain.push_back(ScenePoint2D(2 - 0.5, 2 - 0.5));
+      chain.push_back(ScenePoint2D(2 - 0.5, 0 - 0.5));
+      layer->AddChain(chain, true, 255, 0, 0);
+
+      chain.clear();
+      chain.push_back(ScenePoint2D(-5, -5));
+      chain.push_back(ScenePoint2D(5, -5));
+      chain.push_back(ScenePoint2D(5, 5));
+      chain.push_back(ScenePoint2D(-5, 5));
+      layer->AddChain(chain, true, 0, 255, 0);
+
+      double dy = 1.01;
+      chain.clear();
+      chain.push_back(ScenePoint2D(-4, -4));
+      chain.push_back(ScenePoint2D(4, -4 + dy));
+      chain.push_back(ScenePoint2D(-4, -4 + 2.0 * dy));
+      chain.push_back(ScenePoint2D(4, 2));
+      layer->AddChain(chain, false, 0, 0, 255);
+
+      GetScene()->SetLayer(LINESET_1_ZINDEX, layer.release());
+    }
+
+    // Some text
+    {
+      std::auto_ptr<TextSceneLayer> layer(new TextSceneLayer);
+      layer->SetText("Hello");
+      GetScene()->SetLayer(LINESET_2_ZINDEX, layer.release());
+    }
+  }
+
+
+  void RadiographyEditorApp::DisableTracker()
+  {
+    if (activeTracker_)
+    {
+      activeTracker_->Cancel();
+      activeTracker_.reset();
+    }
+  }
+
+  void RadiographyEditorApp::TakeScreenshot(const std::string& target,
+                                            unsigned int canvasWidth,
+                                            unsigned int canvasHeight)
+  {
+    CairoCompositor compositor(*GetScene(), canvasWidth, canvasHeight);
+    compositor.SetFont(0, Orthanc::EmbeddedResources::UBUNTU_FONT, FONT_SIZE_0, Orthanc::Encoding_Latin1);
+    compositor.Refresh();
+
+    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> RadiographyEditorApp::TrackerHitTest(const PointerEvent & e)
+  {
+    // std::vector<boost::shared_ptr<MeasureTool>> measureTools_;
+    return boost::shared_ptr<IFlexiblePointerTracker>();
+  }
+
+
+  void RadiographyEditorApp::FitContent(unsigned int width, unsigned int height)
+  {
+    controller_->FitContent(width, height);
+  }
+
+  void RadiographyEditorApp::UpdateSize()
+  {
+    if (dynamic_cast<OpenGLCompositor*>(compositor_.get()) != NULL)
+    {
+      dynamic_cast<OpenGLCompositor*>(compositor_.get())->UpdateSize();
+    }
+  }
+
+  void RadiographyEditorApp::Refresh()
+  {
+    compositor_.reset(compositorFactory_->GetCompositor(*GetScene()));
+    compositor_->Refresh();
+
+    // the following is paramount because the compositor holds a reference
+    // to the scene and we do not want this reference to become dangling
+    // TODO ???? compositor_.reset(NULL);
+  }
+
+  void RadiographyEditorApp::SetInfoDisplayMessage(
+      std::string key, std::string value)
+  {
+    if (value == "")
+      infoTextMap_.erase(key);
+    else
+      infoTextMap_[key] = value;
+    DisplayInfoText();
+  }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Shared/RadiographyEditorApp.h	Thu Jun 13 16:47:02 2019 +0200
@@ -0,0 +1,168 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "../../Framework/Messages/IObserver.h"
+#include "../../Framework/Oracle/IOracle.h"
+#include "../../Framework/Scene2D/OpenGLCompositor.h"
+#include "../../Framework/Scene2D/Internals/CompositorHelper.h"
+#include "../../Framework/Scene2DViewport/IFlexiblePointerTracker.h"
+#include "../../Framework/Scene2DViewport/MeasureTool.h"
+#include "../../Framework/Scene2DViewport/PredeclaredTypes.h"
+#include "../../Framework/Scene2DViewport/ViewportController.h"
+
+#include <SDL.h>
+
+#include <boost/make_shared.hpp>
+#include <boost/shared_ptr.hpp>
+#include <boost/enable_shared_from_this.hpp>
+
+
+namespace OrthancStone
+{
+  class ICompositorFactory
+  {
+  public:
+    virtual OrthancStone::ICompositor* GetCompositor(const OrthancStone::Scene2D& scene) = 0;
+  };
+
+  class IInteractor
+  {
+  public:
+    virtual void OnMouseDown(int x, int y, OrthancStone::KeyboardModifiers modifiers, OrthancStone::MouseButton button) = 0;
+    virtual void OnMouseMove(int x, int y, OrthancStone::KeyboardModifiers modifiers) = 0;
+    virtual void OnMouseUp(int x, int y, OrthancStone::KeyboardModifiers modifiers, OrthancStone::MouseButton button) = 0;
+    virtual void OnKeyPressed(char keyChar, OrthancStone::KeyboardModifiers modifiers) = 0;
+  };
+
+
+
+
+  enum GuiTool
+  {
+    GuiTool_Rotate = 0,
+    GuiTool_Pan,
+    GuiTool_Zoom,
+    GuiTool_LineMeasure,
+    GuiTool_CircleMeasure,
+    GuiTool_AngleMeasure,
+    GuiTool_EllipseMeasure,
+    GuiTool_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 RadiographyEditorApp : public IObserver
+    , public IInteractor, public boost::enable_shared_from_this<RadiographyEditorApp>
+  {
+    OrthancStone::IOracle&            oracle_;
+    std::auto_ptr<ICompositorFactory> compositorFactory_;
+    std::auto_ptr<ICompositor>        compositor_;
+
+  public:
+    // 12 because.
+    RadiographyEditorApp(OrthancStone::IOracle& oracle, IObservable& oracleObservable, ICompositorFactory* compositorFactory);
+
+    void PrepareScene();
+    void FitContent(unsigned int width, unsigned int height);
+    void Refresh();
+    void UpdateSize();
+    void SetInfoDisplayMessage(std::string key, std::string value);
+    void DisableTracker();
+
+    virtual void OnMouseMove(int x, int y, OrthancStone::KeyboardModifiers modifiers);
+    virtual void OnKeyPressed(char keyChar, OrthancStone::KeyboardModifiers modifiers);
+    virtual void OnMouseDown(int x, int y, OrthancStone::KeyboardModifiers modifiers, OrthancStone::MouseButton button);
+    virtual void OnMouseUp(int x, int y, OrthancStone::KeyboardModifiers modifiers, OrthancStone::MouseButton button);
+
+    boost::shared_ptr<Scene2D> GetScene();
+    boost::shared_ptr<const Scene2D> GetScene() const;
+
+    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);
+
+  private:
+    void SelectNextTool();
+    void CreateRandomMeasureTool();
+
+    /**
+    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(
+      OrthancStone::MouseButton button,
+      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();
+
+  private:
+    void DisplayFloatingCtrlInfoText(const PointerEvent& e);
+    void DisplayInfoText();
+    void HideInfoText();
+
+  private:
+    /**
+    WARNING: the measuring tools do store a reference to the scene, and it
+    paramount that the scene gets destroyed AFTER the measurement tools.
+    */
+    boost::shared_ptr<ViewportController> controller_;
+
+    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;
+
+    GuiTool currentTool_;
+  };
+
+}