changeset 645:1e9ed656318e

Merge + ongoing measure work
author Benjamin Golinvaux <bgo@osimis.io>
date Mon, 13 May 2019 15:12:56 +0200
parents f939f449482c (diff) 7ca8dc7ec17b (current diff)
children b4fe9642e83b
files .hgignore Samples/Common/AngleMeasureTool.cpp Samples/Common/AngleMeasureTool.h Samples/Common/CreateAngleMeasureTracker.cpp Samples/Common/CreateAngleMeasureTracker.h Samples/Common/CreateLineMeasureTracker.cpp Samples/Common/CreateLineMeasureTracker.h Samples/Common/LineMeasureTool.cpp Samples/Common/LineMeasureTool.h Samples/Common/MeasureCommands.cpp Samples/Common/MeasureCommands.h Samples/Common/MeasureTools.cpp Samples/Common/MeasureTools.h Samples/Common/MeasureToolsToolbox.cpp Samples/Common/MeasureToolsToolbox.h Samples/Common/MeasureTrackers.cpp Samples/Common/MeasureTrackers.h Samples/Sdl/CMakeLists.txt Samples/Sdl/Loader.cpp Samples/Sdl/TrackerSample.cpp
diffstat 38 files changed, 2425 insertions(+), 1386 deletions(-) [+]
line wrap: on
line diff
--- a/.hgignore	Fri May 10 14:54:03 2019 +0200
+++ b/.hgignore	Mon May 13 15:12:56 2019 +0200
@@ -30,3 +30,5 @@
 Resources/CommandTool/protoc-tests/generated_ts/
 Resources/CommandTool/protoc-tests/node_modules/
 Samples/Sdl/ThirdPartyDownloads/
+Samples/Sdl/CMakeLists.txt.orig
+
--- a/Framework/Scene2D/IPointerTracker.h	Fri May 10 14:54:03 2019 +0200
+++ b/Framework/Scene2D/IPointerTracker.h	Mon May 13 15:12:56 2019 +0200
@@ -38,8 +38,8 @@
     virtual void Update(const PointerEvent& event) = 0;
 
     /**
-    This method will be called if the tracker is to be abandoned without
-    committing its result
+    This method will be called when the tracker should commit its result
+    before being destroyed.
     */
     virtual void Release() = 0;
   };
--- a/Framework/Scene2D/Scene2D.cpp	Fri May 10 14:54:03 2019 +0200
+++ b/Framework/Scene2D/Scene2D.cpp	Mon May 13 15:12:56 2019 +0200
@@ -102,8 +102,8 @@
   void Scene2D::SetLayer(int depth,
                          ISceneLayer* layer)  // Takes ownership
   {
-    LOG(INFO) << "SetLayer(" << depth << ", " <<
-      reinterpret_cast<intptr_t>(layer) << ")";
+    //LOG(INFO) << "SetLayer(" << depth << ", " <<
+    //  reinterpret_cast<intptr_t>(layer) << ")";
     std::auto_ptr<Item> item(new Item(layer, layerCounter_++));
 
     if (layer == NULL)
--- a/README.md	Fri May 10 14:54:03 2019 +0200
+++ b/README.md	Mon May 13 15:12:56 2019 +0200
@@ -244,3 +244,14 @@
 ```
 cmake -G "Visual Studio 15 2017 Win64" -DMSVC_MULTIPLE_PROCESSES=ON -DENABLE_OPENGL=ON -DSTATIC_BUILD=ON -DOPENSSL_NO_CAPIENG=ON -DORTHANC_FRAMEWORK_SOURCE=path -DORTHANC_FRAMEWORK_ROOT="$($pwd)\..\orthanc" -DALLOW_DOWNLOADS=ON -DENABLE_SDL=ON ../orthanc-stone/Samples/Sdl
 ```
+
+And under Ubuntu (note the /mnt/c/osi/dev/orthanc folder):
+```
+cmake -G "Ninja" -DENABLE_OPENGL=ON -DSTATIC_BUILD=OFF -DOPENSSL_NO_CAPIENG=ON -DORTHANC_FRAMEWORK_SOURCE=path -DORTHANC_FRAMEWORK_ROOT="/mnt/c/osi/dev/orthanc" -DALLOW_DOWNLOADS=ON -DENABLE_SDL=ON ../orthanc-stone/Samples/Sdl
+```
+
+TODO trackers:
+- text overlay 50% --> ColorTextureLayer 50%
+- angle tracker: draw arcs
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Common/AngleMeasureTool.cpp	Mon May 13 15:12:56 2019 +0200
@@ -0,0 +1,199 @@
+/**
+ * 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 "AngleMeasureTool.h"
+#include "MeasureToolsToolbox.h"
+
+#include <Core/Logging.h>
+
+#include <boost/math/constants/constants.hpp>
+
+namespace OrthancStone
+{
+  AngleMeasureTool::~AngleMeasureTool()
+  {
+    // this measuring tool is a RABI for the corresponding visual layers
+    // stored in the 2D scene
+    Disable();
+    RemoveFromScene();
+  }
+
+  void AngleMeasureTool::RemoveFromScene()
+  {
+    if (layersCreated)
+    {
+      assert(GetScene().HasLayer(polylineZIndex_));
+      assert(GetScene().HasLayer(textZIndex_));
+      GetScene().DeleteLayer(polylineZIndex_);
+      GetScene().DeleteLayer(textZIndex_);
+    }
+  }
+
+  void AngleMeasureTool::SetSide1End(ScenePoint2D pt)
+  {
+    side1End_ = pt;
+    RefreshScene();
+  }
+
+  void AngleMeasureTool::SetSide2End(ScenePoint2D pt)
+  {
+    side2End_ = pt;
+    RefreshScene();
+  }
+
+  void AngleMeasureTool::SetCenter(ScenePoint2D pt)
+  {
+    center_ = pt;
+    RefreshScene();
+  }
+  
+  PolylineSceneLayer* AngleMeasureTool::GetPolylineLayer()
+  {
+    assert(GetScene().HasLayer(polylineZIndex_));
+    ISceneLayer* layer = &(GetScene().GetLayer(polylineZIndex_));
+    PolylineSceneLayer* concreteLayer = dynamic_cast<PolylineSceneLayer*>(layer);
+    assert(concreteLayer != NULL);
+    return concreteLayer;
+  }
+
+  TextSceneLayer* AngleMeasureTool::GetTextLayer()
+  {
+    assert(GetScene().HasLayer(textZIndex_));
+    ISceneLayer* layer = &(GetScene().GetLayer(textZIndex_));
+    TextSceneLayer* concreteLayer = dynamic_cast<TextSceneLayer*>(layer);
+    assert(concreteLayer != NULL);
+    return concreteLayer;
+  }
+
+
+  void AngleMeasureTool::RefreshScene()
+  {
+    if (IsEnabled())
+    {
+
+      // get the scaling factor 
+      const double pixelToScene =
+        GetScene().GetCanvasToSceneTransform().ComputeZoom();
+
+      if (!layersCreated)
+      {
+        // Create the layers if need be
+
+        assert(textZIndex_ == -1);
+        {
+          polylineZIndex_ = GetScene().GetMaxDepth() + 100;
+          //LOG(INFO) << "set polylineZIndex_ to: " << polylineZIndex_;
+          std::auto_ptr<PolylineSceneLayer> layer(new PolylineSceneLayer());
+          GetScene().SetLayer(polylineZIndex_, layer.release());
+        }
+        {
+          textZIndex_ = GetScene().GetMaxDepth() + 100;
+          //LOG(INFO) << "set textZIndex_ to: " << textZIndex_;
+          std::auto_ptr<TextSceneLayer> layer(new TextSceneLayer());
+          GetScene().SetLayer(textZIndex_, layer.release());
+        }
+        layersCreated = true;
+      }
+      else
+      {
+        assert(GetScene().HasLayer(polylineZIndex_));
+        assert(GetScene().HasLayer(textZIndex_));
+      }
+      {
+        // Fill the polyline layer with the measurement line
+
+        PolylineSceneLayer* polylineLayer = GetPolylineLayer();
+        polylineLayer->ClearAllChains();
+        polylineLayer->SetColor(0, 223, 21);
+
+        // sides
+        {
+          {
+            PolylineSceneLayer::Chain chain;
+            chain.push_back(side1End_);
+            chain.push_back(center_);
+            polylineLayer->AddChain(chain, false);
+          }
+          {
+            PolylineSceneLayer::Chain chain;
+            chain.push_back(side2End_);
+            chain.push_back(center_);
+            polylineLayer->AddChain(chain, false);
+          }
+        }
+
+        // handles
+        {
+          //void AddSquare(PolylineSceneLayer::Chain& chain,const Scene2D& scene,const ScenePoint2D& centerS,const double& sideLength)
+
+          {
+            PolylineSceneLayer::Chain chain;
+            AddSquare(chain, GetScene(), side1End_, 10.0); //TODO: take DPI into account
+            polylineLayer->AddChain(chain, true);
+          }
+
+          {
+            PolylineSceneLayer::Chain chain;
+            AddSquare(chain, GetScene(), side2End_, 10.0); //TODO: take DPI into account
+            polylineLayer->AddChain(chain, true);
+          }
+        }
+
+        // arc
+        {
+          PolylineSceneLayer::Chain chain;
+
+          AddArc(chain, GetScene(), side1End_, center_, side2End_, 
+            20.0*pixelToScene, true); //TODO: true means always clockwise
+          polylineLayer->AddChain(chain, true);
+        }
+      }
+      {
+        // Set the text layer proporeties
+
+        // the angle is measured in a clockwise way between the points
+        double angleRad = MeasureAngle(side1End_, center_, side2End_);
+        double angleDeg = RadiansToDegrees(angleRad);
+               
+        TextSceneLayer* textLayer = GetTextLayer();
+
+        char buf[64];
+        sprintf(buf, "%0.02f deg", angleDeg);
+        textLayer->SetText(buf);
+        textLayer->SetColor(0, 223, 21);
+
+        ScenePoint2D textAnchor;
+        GetPositionOnBisectingLine(
+          textAnchor, side1End_, center_, side2End_, 40.0*pixelToScene);
+        textLayer->SetPosition(textAnchor.GetX(), textAnchor.GetY());
+      }
+    }
+    else
+    {
+      if (layersCreated)
+      {
+        RemoveFromScene();
+        layersCreated = false;
+      }
+    }
+  }
+
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Common/AngleMeasureTool.h	Mon May 13 15:12:56 2019 +0200
@@ -0,0 +1,74 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+#pragma once
+
+#include "MeasureTools.h"
+
+#include <Framework/Scene2D/Scene2D.h>
+#include <Framework/Scene2D/ScenePoint2D.h>
+#include <Framework/Scene2D/PolylineSceneLayer.h>
+#include <Framework/Scene2D/TextSceneLayer.h>
+
+#include <boost/shared_ptr.hpp>
+#include <boost/weak_ptr.hpp>
+
+#include <vector>
+#include <cmath>
+
+namespace OrthancStone
+{
+  class AngleMeasureTool : public MeasureTool
+  {
+  public:
+    AngleMeasureTool(Scene2D& scene)
+      : MeasureTool(scene)
+      , layersCreated(false)
+      , polylineZIndex_(-1)
+      , textZIndex_(-1)
+    {
+
+    }
+
+    ~AngleMeasureTool();
+
+    void SetSide1End(ScenePoint2D start);
+    void SetCenter(ScenePoint2D start);
+    void SetSide2End(ScenePoint2D start);
+
+  private:
+    PolylineSceneLayer* GetPolylineLayer();
+    TextSceneLayer*     GetTextLayer();
+    virtual void        RefreshScene() ORTHANC_OVERRIDE;
+    void                RemoveFromScene();
+
+  private:
+    ScenePoint2D side1End_;
+    ScenePoint2D side2End_;
+    ScenePoint2D center_;
+    bool         layersCreated;
+    int          polylineZIndex_;
+    int          textZIndex_;
+  };
+
+  typedef boost::shared_ptr<AngleMeasureTool> AngleMeasureToolPtr;
+}
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Common/CreateAngleMeasureTracker.cpp	Mon May 13 15:12:56 2019 +0200
@@ -0,0 +1,126 @@
+/**
+ * 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 "CreateAngleMeasureTracker.h"
+#include <Core/OrthancException.h>
+
+using namespace Orthanc;
+
+namespace OrthancStone
+{
+  CreateAngleMeasureTracker::CreateAngleMeasureTracker(
+    Scene2D&                        scene,
+    std::vector<TrackerCommandPtr>& undoStack,
+    std::vector<MeasureToolPtr>&    measureTools,
+    const PointerEvent&             e)
+    : CreateMeasureTracker(scene, undoStack, measureTools)
+    , state_(CreatingSide1)
+  {
+    command_.reset(
+      new CreateAngleMeasureCommand(
+        scene,
+        measureTools,
+        e.GetMainPosition().Apply(scene.GetCanvasToSceneTransform())));
+  }
+
+  CreateAngleMeasureTracker::~CreateAngleMeasureTracker()
+  {
+  }
+
+  void CreateAngleMeasureTracker::PointerMove(const PointerEvent& event)
+  {
+    if (!active_)
+    {
+      throw OrthancException(ErrorCode_InternalError,
+        "Internal error: wrong state in CreateAngleMeasureTracker::"
+        "PointerMove: active_ == false");
+    }
+
+    ScenePoint2D scenePos = event.GetMainPosition().Apply(
+      scene_.GetCanvasToSceneTransform());
+
+    switch (state_)
+    {
+    case CreatingSide1:
+      GetCommand()->SetCenter(scenePos);
+      break;
+    case CreatingSide2:
+      GetCommand()->SetSide2End(scenePos);
+      break;
+    default:
+      throw OrthancException(ErrorCode_InternalError,
+        "Wrong state in CreateAngleMeasureTracker::"
+        "PointerMove: state_ invalid");
+    }
+    //LOG(TRACE) << "scenePos.GetX() = " << scenePos.GetX() << "     " <<
+    //  "scenePos.GetY() = " << scenePos.GetY();
+  }
+
+  void CreateAngleMeasureTracker::PointerUp(const PointerEvent& e)
+  {
+    // TODO: the current app does not prevent multiple PointerDown AND
+    // PointerUp to be sent to the tracker.
+    // Unless we augment the PointerEvent structure with the button index, 
+    // we cannot really tell if this pointer up event matches the initial
+    // pointer down event. Let's make it simple for now.
+
+    switch (state_)
+    {
+    case CreatingSide1:
+      state_ = CreatingSide2;
+      break;
+    case CreatingSide2:
+      throw OrthancException(ErrorCode_InternalError,
+        "Wrong state in CreateAngleMeasureTracker::"
+        "PointerUp: state_ == CreatingSide2 ; this should not happen");
+      break;
+    default:
+      throw OrthancException(ErrorCode_InternalError,
+        "Wrong state in CreateAngleMeasureTracker::"
+        "PointerMove: state_ invalid");
+    }
+  }
+
+  void CreateAngleMeasureTracker::PointerDown(const PointerEvent& e)
+  {
+    switch (state_)
+    {
+    case CreatingSide1:
+      throw OrthancException(ErrorCode_InternalError,
+        "Wrong state in CreateAngleMeasureTracker::"
+        "PointerDown: state_ == CreatingSide1 ; this should not happen");
+      break;
+    case CreatingSide2:
+      // we are done
+      active_ = false;
+      break;
+    default:
+      throw OrthancException(ErrorCode_InternalError,
+        "Wrong state in CreateAngleMeasureTracker::"
+        "PointerMove: state_ invalid");
+    }
+  }
+
+  CreateAngleMeasureCommandPtr CreateAngleMeasureTracker::GetCommand()
+  {
+    return boost::dynamic_pointer_cast<CreateAngleMeasureCommand>(command_);
+  }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Common/CreateAngleMeasureTracker.h	Mon May 13 15:12:56 2019 +0200
@@ -0,0 +1,64 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+#pragma once
+
+#include "MeasureTrackers.h"
+#include "MeasureCommands.h"
+
+#include <vector>
+
+namespace OrthancStone
+{
+  class CreateAngleMeasureTracker : public CreateMeasureTracker
+  {
+  public:
+    /**
+    When you create this tracker, you need to supply it with the undo stack
+    where it will store the commands that perform the actual measure tool
+    creation and modification.
+    In turn, a container for these commands to store the actual measuring
+    must be supplied, too
+    */
+    CreateAngleMeasureTracker(
+      Scene2D&                        scene,
+      std::vector<TrackerCommandPtr>& undoStack,
+      std::vector<MeasureToolPtr>&    measureTools,
+      const PointerEvent&             e);
+
+    ~CreateAngleMeasureTracker();
+
+    virtual void PointerMove(const PointerEvent& e) ORTHANC_OVERRIDE;
+    virtual void PointerUp(const PointerEvent& e) ORTHANC_OVERRIDE;
+    virtual void PointerDown(const PointerEvent& e) ORTHANC_OVERRIDE;
+
+  private:
+    CreateAngleMeasureCommandPtr GetCommand();
+
+    enum State
+    {
+      CreatingSide1,
+      CreatingSide2,
+      Finished // just for debug
+    };
+    State state_;
+
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Common/CreateCircleMeasureTracker.cpp	Mon May 13 15:12:56 2019 +0200
@@ -0,0 +1,23 @@
+/**
+ * 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/>.
+ **/
+
+namespace OrthancStone
+{
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Common/CreateCircleMeasureTracker.h	Mon May 13 15:12:56 2019 +0200
@@ -0,0 +1,25 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+#pragma once
+
+namespace OrthancStone
+{
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Common/CreateLineMeasureTracker.cpp	Mon May 13 15:12:56 2019 +0200
@@ -0,0 +1,89 @@
+/**
+ * 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 "CreateLineMeasureTracker.h"
+#include <Core/OrthancException.h>
+
+using namespace Orthanc;
+
+namespace OrthancStone
+{
+  CreateLineMeasureTracker::CreateLineMeasureTracker(
+    Scene2D&                        scene,
+    std::vector<TrackerCommandPtr>& undoStack,
+    std::vector<MeasureToolPtr>&    measureTools,
+    const PointerEvent&             e)
+    : CreateMeasureTracker(scene, undoStack, measureTools)
+  {
+    command_.reset(
+      new CreateLineMeasureCommand(
+        scene,
+        measureTools,
+        e.GetMainPosition().Apply(scene.GetCanvasToSceneTransform())));
+  }
+
+  CreateLineMeasureTracker::~CreateLineMeasureTracker()
+  {
+
+  }
+
+  void CreateLineMeasureTracker::PointerMove(const PointerEvent& event)
+  {
+    if (!active_)
+    {
+      throw OrthancException(ErrorCode_InternalError,
+        "Internal error: wrong state in CreateLineMeasureTracker::"
+        "PointerMove: active_ == false");
+    }
+
+    ScenePoint2D scenePos = event.GetMainPosition().Apply(
+      scene_.GetCanvasToSceneTransform());
+
+    //LOG(TRACE) << "scenePos.GetX() = " << scenePos.GetX() << "     " <<
+    //  "scenePos.GetY() = " << scenePos.GetY();
+
+    CreateLineMeasureTracker* concreteThis =
+      dynamic_cast<CreateLineMeasureTracker*>(this);
+    assert(concreteThis != NULL);
+    GetCommand()->SetEnd(scenePos);
+  }
+
+  void CreateLineMeasureTracker::PointerUp(const PointerEvent& e)
+  {
+    // TODO: the current app does not prevent multiple PointerDown AND
+    // PointerUp to be sent to the tracker.
+    // Unless we augment the PointerEvent structure with the button index, 
+    // we cannot really tell if this pointer up event matches the initial
+    // pointer down event. Let's make it simple for now.
+    active_ = false;
+  }
+
+  void CreateLineMeasureTracker::PointerDown(const PointerEvent& e)
+  {
+    LOG(WARNING) << "Additional touches (fingers, pen, mouse buttons...) "
+      "are ignored when the line measure creation tracker is active";
+  }
+
+  CreateLineMeasureCommandPtr CreateLineMeasureTracker::GetCommand()
+  {
+    return boost::dynamic_pointer_cast<CreateLineMeasureCommand>(command_);
+  }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Common/CreateLineMeasureTracker.h	Mon May 13 15:12:56 2019 +0200
@@ -0,0 +1,52 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+#pragma once
+
+#include "MeasureTrackers.h"
+
+namespace OrthancStone
+{
+  class CreateLineMeasureTracker : public CreateMeasureTracker
+  {
+  public:
+    /**
+    When you create this tracker, you need to supply it with the undo stack
+    where it will store the commands that perform the actual measure tool
+    creation and modification.
+    In turn, a container for these commands to store the actual measuring
+    must be supplied, too
+    */
+    CreateLineMeasureTracker(
+      Scene2D&                        scene,
+      std::vector<TrackerCommandPtr>& undoStack,
+      std::vector<MeasureToolPtr>&    measureTools,
+      const PointerEvent&             e);
+
+    ~CreateLineMeasureTracker();
+
+    virtual void PointerMove(const PointerEvent& e) ORTHANC_OVERRIDE;
+    virtual void PointerUp(const PointerEvent& e) ORTHANC_OVERRIDE;
+    virtual void PointerDown(const PointerEvent& e) ORTHANC_OVERRIDE;
+
+  private:
+    CreateLineMeasureCommandPtr GetCommand();
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Common/CreateMeasureTracker.cpp	Mon May 13 15:12:56 2019 +0200
@@ -0,0 +1,20 @@
+/**
+ * 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/>.
+ **/
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Common/CreateMeasureTracker.h	Mon May 13 15:12:56 2019 +0200
@@ -0,0 +1,22 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+#pragma once
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Common/CreateSimpleTrackerAdapter.cpp	Mon May 13 15:12:56 2019 +0200
@@ -0,0 +1,77 @@
+/**
+ * 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 "IFlexiblePointerTracker.h"
+#include <Framework/Scene2D/IPointerTracker.h>
+
+
+namespace OrthancStone
+{
+  namespace 
+  {
+    class SimpleTrackerAdapter : public IFlexiblePointerTracker
+    {
+    public:
+      SimpleTrackerAdapter(PointerTrackerPtr wrappedTracker)
+        : wrappedTracker_(wrappedTracker)
+        , active_(true)
+      {
+      }
+
+      virtual void PointerMove(const PointerEvent& event) ORTHANC_OVERRIDE
+      {
+        if(active_)
+          wrappedTracker_->Update(event);
+      };
+      virtual void PointerUp(const PointerEvent& event) ORTHANC_OVERRIDE
+      {
+        if (wrappedTracker_)
+        {
+          wrappedTracker_->Release();
+          wrappedTracker_ = NULL;
+        }
+        active_ = false;
+      }
+      virtual void PointerDown(const PointerEvent& event) ORTHANC_OVERRIDE
+      {
+        // nothing to do atm
+      }
+      virtual bool IsActive() const ORTHANC_OVERRIDE
+      {
+        return active_;
+      }
+
+      virtual void Cancel() ORTHANC_OVERRIDE
+      {
+        wrappedTracker_ = NULL;
+        active_ = false;
+      }
+
+    private:
+      PointerTrackerPtr wrappedTracker_;
+      bool active_;
+    };
+  }
+
+  FlexiblePointerTrackerPtr CreateSimpleTrackerAdapter(PointerTrackerPtr t)
+  {
+    return FlexiblePointerTrackerPtr(new SimpleTrackerAdapter(t));
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Common/EditAngleMeasureTracker.cpp	Mon May 13 15:12:56 2019 +0200
@@ -0,0 +1,23 @@
+/**
+ * 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/>.
+ **/
+
+namespace OrthancStone
+{
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Common/EditAngleMeasureTracker.h	Mon May 13 15:12:56 2019 +0200
@@ -0,0 +1,25 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+#pragma once
+
+namespace OrthancStone
+{
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Common/EditCircleMeasureTracker.cpp	Mon May 13 15:12:56 2019 +0200
@@ -0,0 +1,23 @@
+/**
+ * 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/>.
+ **/
+
+namespace OrthancStone
+{
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Common/EditCircleMeasureTracker.h	Mon May 13 15:12:56 2019 +0200
@@ -0,0 +1,25 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+#pragma once
+
+namespace OrthancStone
+{
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Common/EditLineMeasureTracker.cpp	Mon May 13 15:12:56 2019 +0200
@@ -0,0 +1,23 @@
+/**
+ * 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/>.
+ **/
+
+namespace OrthancStone
+{
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Common/EditLineMeasureTracker.h	Mon May 13 15:12:56 2019 +0200
@@ -0,0 +1,25 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+#pragma once
+
+namespace OrthancStone
+{
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Common/IFlexiblePointerTracker.h	Mon May 13 15:12:56 2019 +0200
@@ -0,0 +1,84 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include <Framework/Scene2D/PointerEvent.h>
+#include <boost/shared_ptr.hpp>
+
+namespace OrthancStone
+{
+  class IPointerTracker;
+  typedef boost::shared_ptr<IPointerTracker> PointerTrackerPtr;
+
+  /**
+  This interface represents a flexible mouse tracker that can respond to 
+  several events and is not automatically deleted upon mouse up or when touch
+  interaction is suspended : for instance, a stateful tracker with a two-step 
+  interaction like: click & drag --> mouse up --> drag --> mouse click 
+  (for instance, for an angle measuring tracker or an ellipse tracker)
+  */
+  class IFlexiblePointerTracker : public boost::noncopyable
+  {
+  public:
+    virtual ~IFlexiblePointerTracker() {}
+
+    /**
+    This method will be repeatedly called during user interaction
+    */
+    virtual void PointerMove(const PointerEvent& event) = 0;
+
+    /**
+    This method will be called when a touch/pointer is removed (mouse up, 
+    pen lift, finger removed...)
+    */
+    virtual void PointerUp(const PointerEvent& event) = 0;
+
+    /**
+    This method will be called when a touch/pointer is added (mouse down, 
+    pen or finger press)
+    */
+    virtual void PointerDown(const PointerEvent& event) = 0;
+
+    /**
+    This method will be repeatedly called by the tracker owner (for instance,
+    the application) to check whether the tracker must keep on receiving 
+    interaction or if its job is done and it should be deleted.
+    */
+    virtual bool IsActive() const = 0;
+
+    /**
+    This will be called if the tracker needs to be dismissed without committing
+    its changes to the underlying model. If the model has been modified during
+    tracker lifetime, it must be restored to its initial value
+    */
+    virtual void Cancel() = 0;
+  };
+
+  typedef boost::shared_ptr<IFlexiblePointerTracker> FlexiblePointerTrackerPtr;
+
+  /**
+  This factory adopts the supplied simple tracker and creates a flexible 
+  tracker wrapper around it.
+  */
+  FlexiblePointerTrackerPtr CreateSimpleTrackerAdapter(PointerTrackerPtr);
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Common/LineMeasureTool.cpp	Mon May 13 15:12:56 2019 +0200
@@ -0,0 +1,200 @@
+/**
+ * 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 "LineMeasureTool.h"
+#include "MeasureToolsToolbox.h"
+
+#include <Core/Logging.h>
+
+
+namespace OrthancStone
+{
+  LineMeasureTool::~LineMeasureTool()
+  {
+    // this measuring tool is a RABI for the corresponding visual layers
+    // stored in the 2D scene
+    Disable();
+    RemoveFromScene();
+  }
+
+  void LineMeasureTool::RemoveFromScene()
+  {
+    if (layersCreated)
+    {
+      assert(GetScene().HasLayer(polylineZIndex_));
+      assert(GetScene().HasLayer(textZIndex_));
+      GetScene().DeleteLayer(polylineZIndex_);
+      GetScene().DeleteLayer(textZIndex_);
+    }
+  }
+
+
+  void LineMeasureTool::SetStart(ScenePoint2D start)
+  {
+    start_ = start;
+    RefreshScene();
+  }
+
+  void LineMeasureTool::SetEnd(ScenePoint2D end)
+  {
+    end_ = end;
+    RefreshScene();
+  }
+
+  void LineMeasureTool::Set(ScenePoint2D start, ScenePoint2D end)
+  {
+    start_ = start;
+    end_ = end;
+    RefreshScene();
+  }
+
+  PolylineSceneLayer* LineMeasureTool::GetPolylineLayer()
+  {
+    assert(GetScene().HasLayer(polylineZIndex_));
+    ISceneLayer* layer = &(GetScene().GetLayer(polylineZIndex_));
+    PolylineSceneLayer* concreteLayer = dynamic_cast<PolylineSceneLayer*>(layer);
+    assert(concreteLayer != NULL);
+    return concreteLayer;
+  }
+
+  TextSceneLayer* LineMeasureTool::GetTextLayer()
+  {
+    assert(GetScene().HasLayer(textZIndex_));
+    ISceneLayer* layer = &(GetScene().GetLayer(textZIndex_));
+    TextSceneLayer* concreteLayer = dynamic_cast<TextSceneLayer*>(layer);
+    assert(concreteLayer != NULL);
+    return concreteLayer;
+  }
+
+  void LineMeasureTool::RefreshScene()
+  {
+    if (IsEnabled())
+    {
+      if (!layersCreated)
+      {
+        // Create the layers if need be
+
+        assert(textZIndex_ == -1);
+        {
+          polylineZIndex_ = GetScene().GetMaxDepth() + 100;
+          //LOG(INFO) << "set polylineZIndex_ to: " << polylineZIndex_;
+          std::auto_ptr<PolylineSceneLayer> layer(new PolylineSceneLayer());
+          GetScene().SetLayer(polylineZIndex_, layer.release());
+        }
+        {
+          textZIndex_ = GetScene().GetMaxDepth() + 100;
+          //LOG(INFO) << "set textZIndex_ to: " << textZIndex_;
+          std::auto_ptr<TextSceneLayer> layer(new TextSceneLayer());
+          GetScene().SetLayer(textZIndex_, layer.release());
+        }
+        layersCreated = true;
+      }
+      else
+      {
+        assert(GetScene().HasLayer(polylineZIndex_));
+        assert(GetScene().HasLayer(textZIndex_));
+      }
+      {
+        // Fill the polyline layer with the measurement line
+
+        PolylineSceneLayer* polylineLayer = GetPolylineLayer();
+        polylineLayer->ClearAllChains();
+        polylineLayer->SetColor(0, 223, 21);
+
+        {
+          PolylineSceneLayer::Chain chain;
+          chain.push_back(start_);
+          chain.push_back(end_);
+          polylineLayer->AddChain(chain, false);
+        }
+
+        // handles
+        {
+          //void AddSquare(PolylineSceneLayer::Chain& chain,const Scene2D& scene,const ScenePoint2D& centerS,const double& sideLength)
+
+          {
+            PolylineSceneLayer::Chain chain;
+            AddSquare(chain, GetScene(), start_, 10.0); //TODO: take DPI into account
+            polylineLayer->AddChain(chain, true);
+          }
+
+          {
+            PolylineSceneLayer::Chain chain;
+            AddSquare(chain, GetScene(), end_, 10.0); //TODO: take DPI into account
+            polylineLayer->AddChain(chain, true);
+          }
+
+          //ScenePoint2D startC = start_.Apply(GetScene().GetSceneToCanvasTransform());
+          //double squareSize = 10.0; 
+          //double startHandleLX = startC.GetX() - squareSize/2;
+          //double startHandleTY = startC.GetY() - squareSize / 2;
+          //double startHandleRX = startC.GetX() + squareSize / 2;
+          //double startHandleBY = startC.GetY() + squareSize / 2;
+          //ScenePoint2D startLTC(startHandleLX, startHandleTY);
+          //ScenePoint2D startRTC(startHandleRX, startHandleTY);
+          //ScenePoint2D startRBC(startHandleRX, startHandleBY);
+          //ScenePoint2D startLBC(startHandleLX, startHandleBY);
+
+          //ScenePoint2D startLT = startLTC.Apply(GetScene().GetCanvasToSceneTransform());
+          //ScenePoint2D startRT = startRTC.Apply(GetScene().GetCanvasToSceneTransform());
+          //ScenePoint2D startRB = startRBC.Apply(GetScene().GetCanvasToSceneTransform());
+          //ScenePoint2D startLB = startLBC.Apply(GetScene().GetCanvasToSceneTransform());
+
+          //PolylineSceneLayer::Chain chain;
+          //chain.push_back(startLT);
+          //chain.push_back(startRT);
+          //chain.push_back(startRB);
+          //chain.push_back(startLB);
+          //polylineLayer->AddChain(chain, true);
+        }
+
+      }
+      {
+        // Set the text layer proporeties
+
+        TextSceneLayer* textLayer = GetTextLayer();
+        double deltaX = end_.GetX() - start_.GetX();
+        double deltaY = end_.GetY() - start_.GetY();
+        double squareDist = deltaX * deltaX + deltaY * deltaY;
+        double dist = sqrt(squareDist);
+        char buf[64];
+        sprintf(buf, "%0.02f units", dist);
+        textLayer->SetText(buf);
+        textLayer->SetColor(0, 223, 21);
+
+        // TODO: for now we simply position the text overlay at the middle
+        // of the measuring segment
+        double midX = 0.5*(end_.GetX() + start_.GetX());
+        double midY = 0.5*(end_.GetY() + start_.GetY());
+        textLayer->SetPosition(midX, midY);
+      }
+    }
+    else
+    {
+      if (layersCreated)
+      {
+        RemoveFromScene();
+        layersCreated = false;
+      }
+    }
+  }
+
+
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Common/LineMeasureTool.h	Mon May 13 15:12:56 2019 +0200
@@ -0,0 +1,72 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+#pragma once
+
+#include "MeasureTools.h"
+
+#include <Framework/Scene2D/Scene2D.h>
+#include <Framework/Scene2D/ScenePoint2D.h>
+#include <Framework/Scene2D/PolylineSceneLayer.h>
+#include <Framework/Scene2D/TextSceneLayer.h>
+
+#include <boost/shared_ptr.hpp>
+#include <boost/weak_ptr.hpp>
+
+#include <vector>
+#include <cmath>
+
+namespace OrthancStone
+{
+  class LineMeasureTool : public MeasureTool
+  {
+  public:
+    LineMeasureTool(Scene2D& scene)
+      : MeasureTool(scene)
+      , layersCreated(false)
+      , polylineZIndex_(-1)
+      , textZIndex_(-1)
+    {
+
+    }
+
+    ~LineMeasureTool();
+
+    void SetStart(ScenePoint2D start);
+    void SetEnd(ScenePoint2D end);
+    void Set(ScenePoint2D start, ScenePoint2D end);
+
+  private:
+    PolylineSceneLayer* GetPolylineLayer();
+    TextSceneLayer*     GetTextLayer();
+    virtual void        RefreshScene() ORTHANC_OVERRIDE;
+    void                RemoveFromScene();
+
+  private:
+    ScenePoint2D start_;
+    ScenePoint2D end_;
+    bool         layersCreated;
+    int          polylineZIndex_;
+    int          textZIndex_;
+  };
+
+  typedef boost::shared_ptr<LineMeasureTool> LineMeasureToolPtr;
+}
+
--- a/Samples/Common/MeasureCommands.cpp	Fri May 10 14:54:03 2019 +0200
+++ b/Samples/Common/MeasureCommands.cpp	Mon May 13 15:12:56 2019 +0200
@@ -52,14 +52,36 @@
     : CreateMeasureCommand(scene, measureTools)
     , measureTool_(new LineMeasureTool(scene))
   {
-    measureTool_ = LineMeasureToolPtr(new LineMeasureTool(scene));
     measureTools_.push_back(measureTool_);
     measureTool_->Set(point, point);
   }
 
-  void CreateLineMeasureCommand::Update(ScenePoint2D scenePos)
+  void CreateLineMeasureCommand::SetEnd(ScenePoint2D scenePos)
   {
     measureTool_->SetEnd(scenePos);
   }
 
+  CreateAngleMeasureCommand::CreateAngleMeasureCommand(
+    Scene2D& scene, MeasureToolList& measureTools, ScenePoint2D point)
+    : CreateMeasureCommand(scene, measureTools)
+    , measureTool_(new AngleMeasureTool(scene))
+  {
+    measureTools_.push_back(measureTool_);
+    measureTool_->SetSide1End(point);
+    measureTool_->SetCenter(point);
+    measureTool_->SetSide2End(point);
+  }
+
+  /** This method sets center*/
+  void CreateAngleMeasureCommand::SetCenter(ScenePoint2D scenePos)
+  {
+    measureTool_->SetCenter(scenePos);
+  }
+
+  /** This method sets end of side 2*/
+  void CreateAngleMeasureCommand::SetSide2End(ScenePoint2D scenePos)
+  {
+    measureTool_->SetSide2End(scenePos);
+  }
+
 }
--- a/Samples/Common/MeasureCommands.h	Fri May 10 14:54:03 2019 +0200
+++ b/Samples/Common/MeasureCommands.h	Mon May 13 15:12:56 2019 +0200
@@ -24,9 +24,16 @@
 
 // to be moved into Stone
 #include "MeasureTools.h"
+#include "LineMeasureTool.h"
+#include "AngleMeasureTool.h"
 
 namespace OrthancStone
 {
+  //class LineMeasureTool;
+  //typedef boost::shared_ptr<LineMeasureTool> LineMeasureToolPtr;
+  //class AngleMeasureTool;
+  //typedef boost::shared_ptr<AngleMeasureTool> AngleMeasureToolPtr;
+   
   class TrackerCommand
   {
   public:
@@ -36,8 +43,6 @@
     }
     virtual void Undo() = 0;
     virtual void Redo() = 0;
-    virtual void Update(ScenePoint2D scenePos) = 0;
-
     Scene2D& GetScene()
     {
       return scene_;
@@ -71,11 +76,11 @@
   class CreateLineMeasureCommand : public CreateMeasureCommand
   {
   public:
-    CreateLineMeasureCommand(Scene2D& scene, 
-                             MeasureToolList& measureTools, 
-                             ScenePoint2D point);
+    CreateLineMeasureCommand(
+      Scene2D& scene, MeasureToolList& measureTools, ScenePoint2D point);
     
-    void Update(ScenePoint2D scenePos) ORTHANC_OVERRIDE;
+    // the starting position is set in the ctor
+    void SetEnd(ScenePoint2D scenePos);
 
   private:
     virtual MeasureToolPtr GetMeasureTool() ORTHANC_OVERRIDE
@@ -83,10 +88,31 @@
       return measureTool_;
     }
     LineMeasureToolPtr measureTool_;
-    ScenePoint2D       start_;
-    ScenePoint2D       end_;
   };
 
   typedef boost::shared_ptr<CreateLineMeasureCommand> CreateLineMeasureCommandPtr;
+
+  class CreateAngleMeasureCommand : public CreateMeasureCommand
+  {
+  public:
+    /** Ctor sets end of side 1*/
+    CreateAngleMeasureCommand(
+      Scene2D& scene, MeasureToolList& measureTools, ScenePoint2D point);
+
+    /** This method sets center*/
+    void SetCenter(ScenePoint2D scenePos);
+
+    /** This method sets end of side 2*/
+    void SetSide2End(ScenePoint2D scenePos);
+
+  private:
+    virtual MeasureToolPtr GetMeasureTool() ORTHANC_OVERRIDE
+    {
+      return measureTool_;
+    }
+    AngleMeasureToolPtr measureTool_;
+  };
+
+  typedef boost::shared_ptr<CreateAngleMeasureCommand> CreateAngleMeasureCommandPtr;
 }
 
--- a/Samples/Common/MeasureTools.cpp	Fri May 10 14:54:03 2019 +0200
+++ b/Samples/Common/MeasureTools.cpp	Mon May 13 15:12:56 2019 +0200
@@ -18,8 +18,11 @@
  * along with this program. If not, see <http://www.gnu.org/licenses/>.
  **/
 
+#include "MeasureTools.h"
+
 #include <Core/Logging.h>
-#include "MeasureTools.h"
+
+#include <boost/math/constants/constants.hpp>
 
 namespace OrthancStone
 {
@@ -34,163 +37,5 @@
     enabled_ = false;
     RefreshScene();
   }
-
-  LineMeasureTool::~LineMeasureTool()
-  {
-    // this measuring tool is a RABI for the corresponding visual layers
-    // stored in the 2D scene
-    Disable();
-    RemoveFromScene();
-  }
-
-  void LineMeasureTool::RemoveFromScene()
-  {
-    if (layersCreated)
-    {
-      assert(GetScene().HasLayer(polylineZIndex_));
-      assert(GetScene().HasLayer(textZIndex_));
-      GetScene().DeleteLayer(polylineZIndex_);
-      GetScene().DeleteLayer(textZIndex_);
-    }
-  }
-
-
-  void LineMeasureTool::SetStart(ScenePoint2D start)
-  {
-    start_ = start;
-    RefreshScene();
-  }
-
-  void LineMeasureTool::SetEnd(ScenePoint2D end)
-  {
-    end_ = end;
-    RefreshScene();
-  }
-
-  void LineMeasureTool::Set(ScenePoint2D start, ScenePoint2D end)
-  {
-    start_ = start;
-    end_ = end;
-    RefreshScene();
-  }
-
-  PolylineSceneLayer* LineMeasureTool::GetPolylineLayer()
-  {
-    assert(GetScene().HasLayer(polylineZIndex_));
-    ISceneLayer* layer = &(GetScene().GetLayer(polylineZIndex_));
-    PolylineSceneLayer* concreteLayer = dynamic_cast<PolylineSceneLayer*>(layer);
-    assert(concreteLayer != NULL);
-    return concreteLayer;
-  }
-
-  TextSceneLayer* LineMeasureTool::GetTextLayer()
-  {
-    assert(GetScene().HasLayer(textZIndex_));
-    ISceneLayer* layer = &(GetScene().GetLayer(textZIndex_));
-    TextSceneLayer* concreteLayer = dynamic_cast<TextSceneLayer*>(layer);
-    assert(concreteLayer != NULL);
-    return concreteLayer;
-  }
-
-  void LineMeasureTool::RefreshScene()
-  {
-    if (IsEnabled())
-    {
-      if (!layersCreated)
-      {
-        // Create the layers if need be
-
-        assert(textZIndex_ == -1);
-        {
-          polylineZIndex_ = GetScene().GetMaxDepth() + 100;
-          LOG(INFO) << "set polylineZIndex_ to: " << polylineZIndex_;
-          std::auto_ptr<PolylineSceneLayer> layer(new PolylineSceneLayer());
-          GetScene().SetLayer(polylineZIndex_, layer.release());
-        }
-        {
-          textZIndex_ = GetScene().GetMaxDepth() + 100;
-          LOG(INFO) << "set textZIndex_ to: " << textZIndex_;
-          std::auto_ptr<TextSceneLayer> layer(new TextSceneLayer());
-          GetScene().SetLayer(textZIndex_, layer.release());
-        }
-        layersCreated = true;
-      }
-      else
-      {
-        assert(GetScene().HasLayer(polylineZIndex_));
-        assert(GetScene().HasLayer(textZIndex_));
-      }
-      {
-        // Fill the polyline layer with the measurement line
-
-        PolylineSceneLayer* polylineLayer = GetPolylineLayer();
-        polylineLayer->ClearAllChains();
-        polylineLayer->SetColor(0, 223, 21);
-
-        {
-          PolylineSceneLayer::Chain chain;
-          chain.push_back(start_);
-          chain.push_back(end_);
-          polylineLayer->AddChain(chain, false);
-        }
-
-        // handles
-        {
-          ScenePoint2D startC = start_.Apply(GetScene().GetSceneToCanvasTransform());
-          double squareSize = 10.0; //TODO: take DPI into account
-          double startHandleLX = startC.GetX() - squareSize/2;
-          double startHandleTY = startC.GetY() - squareSize / 2;
-          double startHandleRX = startC.GetX() + squareSize / 2;
-          double startHandleBY = startC.GetY() + squareSize / 2;
-          ScenePoint2D startLTC(startHandleLX, startHandleTY);
-          ScenePoint2D startRTC(startHandleRX, startHandleTY);
-          ScenePoint2D startRBC(startHandleRX, startHandleBY);
-          ScenePoint2D startLBC(startHandleLX, startHandleBY);
-
-          ScenePoint2D startLT = startLTC.Apply(GetScene().GetCanvasToSceneTransform());
-          ScenePoint2D startRT = startRTC.Apply(GetScene().GetCanvasToSceneTransform());
-          ScenePoint2D startRB = startRBC.Apply(GetScene().GetCanvasToSceneTransform());
-          ScenePoint2D startLB = startLBC.Apply(GetScene().GetCanvasToSceneTransform());
-
-          PolylineSceneLayer::Chain chain;
-          chain.push_back(startLT);
-          chain.push_back(startRT);
-          chain.push_back(startRB);
-          chain.push_back(startLB);
-          polylineLayer->AddChain(chain, true);
-        }
-
-
-        
-      }
-      {
-        // Set the text layer proporeties
-
-        TextSceneLayer* textLayer = GetTextLayer();
-        double deltaX = end_.GetX() - start_.GetX();
-        double deltaY = end_.GetY() - start_.GetY();
-        double squareDist = deltaX * deltaX + deltaY * deltaY;
-        double dist = sqrt(squareDist);
-        char buf[64];
-        sprintf(buf, "%0.02f units", dist);
-        textLayer->SetText(buf);
-        textLayer->SetColor(0, 223, 21);
-
-        // TODO: for now we simply position the text overlay at the middle
-        // of the measuring segment
-        double midX = 0.5*(end_.GetX() + start_.GetX());
-        double midY = 0.5*(end_.GetY() + start_.GetY());
-        textLayer->SetPosition(midX, midY);
-      }
-    }
-    else
-    {
-      if (layersCreated)
-      {
-        RemoveFromScene();
-        layersCreated = false;
-      }
-    }
-  }
 }
 
--- a/Samples/Common/MeasureTools.h	Fri May 10 14:54:03 2019 +0200
+++ b/Samples/Common/MeasureTools.h	Mon May 13 15:12:56 2019 +0200
@@ -17,6 +17,7 @@
  * You should have received a copy of the GNU Affero General Public License
  * along with this program. If not, see <http://www.gnu.org/licenses/>.
  **/
+
 #pragma once
 
 #include <Framework/Scene2D/Scene2D.h>
@@ -25,6 +26,7 @@
 #include <Framework/Scene2D/TextSceneLayer.h>
 
 #include <boost/shared_ptr.hpp>
+#include <boost/weak_ptr.hpp>
 
 #include <vector>
 #include <cmath>
@@ -50,7 +52,7 @@
     void Disable();
 
   protected:
-    MeasureTool(Scene2D& scene) 
+    MeasureTool(Scene2D& scene)
       : scene_(scene)
       , enabled_(true)
     {
@@ -65,7 +67,6 @@
     */
     virtual void RefreshScene() = 0;
 
-
     Scene2D& GetScene()
     {
       return scene_;
@@ -86,40 +87,6 @@
   };
 
   typedef boost::shared_ptr<MeasureTool> MeasureToolPtr;
-
-  class LineMeasureTool : public MeasureTool
-  {
-  public:
-    LineMeasureTool(Scene2D& scene)
-      : MeasureTool(scene)
-      , layersCreated(false)
-      , polylineZIndex_(-1)
-      , textZIndex_(-1)
-    {
-
-    }
-
-    ~LineMeasureTool();
-
-    void SetStart(ScenePoint2D start);
-    void SetEnd(ScenePoint2D end);
-    void Set(ScenePoint2D start, ScenePoint2D end);
-
-  private:
-    PolylineSceneLayer* GetPolylineLayer();
-    TextSceneLayer*     GetTextLayer();
-    virtual void        RefreshScene() ORTHANC_OVERRIDE;
-    void                RemoveFromScene();
-
-  private:
-    ScenePoint2D start_;
-    ScenePoint2D end_;
-    bool         layersCreated;
-    int          polylineZIndex_;
-    int          textZIndex_;
-  };
-
-  typedef boost::shared_ptr<LineMeasureTool> LineMeasureToolPtr;
   typedef std::vector<MeasureToolPtr> MeasureToolList;
 }
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Common/MeasureToolsToolbox.cpp	Mon May 13 15:12:56 2019 +0200
@@ -0,0 +1,229 @@
+/**
+ * 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 "MeasureToolsToolbox.h"
+
+#include <boost/math/constants/constants.hpp>
+
+namespace
+{
+  double g_pi = boost::math::constants::pi<double>();
+}
+
+namespace OrthancStone
+{
+
+  void AddSquare(PolylineSceneLayer::Chain& chain,
+    const Scene2D&      scene,
+    const ScenePoint2D& centerS,
+    const double&       sideLength)
+  {
+    chain.clear();
+    chain.reserve(4);
+    ScenePoint2D centerC = centerS.Apply(scene.GetSceneToCanvasTransform());
+    //TODO: take DPI into account
+    double handleLX = centerC.GetX() - sideLength / 2;
+    double handleTY = centerC.GetY() - sideLength / 2;
+    double handleRX = centerC.GetX() + sideLength / 2;
+    double handleBY = centerC.GetY() + sideLength / 2;
+    ScenePoint2D LTC(handleLX, handleTY);
+    ScenePoint2D RTC(handleRX, handleTY);
+    ScenePoint2D RBC(handleRX, handleBY);
+    ScenePoint2D LBC(handleLX, handleBY);
+
+    ScenePoint2D startLT = LTC.Apply(scene.GetCanvasToSceneTransform());
+    ScenePoint2D startRT = RTC.Apply(scene.GetCanvasToSceneTransform());
+    ScenePoint2D startRB = RBC.Apply(scene.GetCanvasToSceneTransform());
+    ScenePoint2D startLB = LBC.Apply(scene.GetCanvasToSceneTransform());
+
+    chain.push_back(startLT);
+    chain.push_back(startRT);
+    chain.push_back(startRB);
+    chain.push_back(startLB);
+  }
+
+  void AddArc(
+      PolylineSceneLayer::Chain& chain
+    , const Scene2D&      scene
+    , const ScenePoint2D& p1
+    , const ScenePoint2D& c
+    , const ScenePoint2D& p2
+    , const double&       radiusS
+    , const bool          clockwise
+    , const int           subdivisionsCount)
+  {
+    double p1cAngle = atan2(p1.GetY() - c.GetY(), p1.GetX() - c.GetX());
+    double p2cAngle = atan2(p2.GetY() - c.GetY(), p2.GetX() - c.GetX());
+    AddArc(
+      chain, scene, c, radiusS, p1cAngle, p2cAngle, 
+      clockwise, subdivisionsCount);
+  }
+
+  double RadiansToDegrees(double angleRad)
+  {
+    static const double factor = 180.0 / g_pi;
+    return angleRad * factor;
+  }
+
+  void GetPositionOnBisectingLine(
+    ScenePoint2D&       result
+    , const ScenePoint2D& p1
+    , const ScenePoint2D& c
+    , const ScenePoint2D& p2
+    , const double d)
+  {
+    // TODO: fix correct half-plane
+    double p1cAngle = atan2(p1.GetY() - c.GetY(), p1.GetX() - c.GetX());
+    double p2cAngle = atan2(p2.GetY() - c.GetY(), p2.GetX() - c.GetX());
+    double angle = 0.5*(p1cAngle + p2cAngle);
+    double unitVectorX = cos(angle);
+    double unitVectorY = sin(angle);
+    double posX = c.GetX() + d * unitVectorX;
+    double posY = c.GetX() + d * unitVectorY;
+    result = ScenePoint2D(posX, posY);
+  }
+   
+  void AddArc(
+      PolylineSceneLayer::Chain& chain
+    , const Scene2D&      scene
+    , const ScenePoint2D& centerS
+    , const double&       radiusS
+    , const double        startAngleRad
+    , const double        endAngleRad
+    , const bool          clockwise
+    , const int           subdivisionsCount)
+  {
+    double startAngleRadN = NormalizeAngle(startAngleRad);
+    double endAngleRadN = NormalizeAngle(endAngleRad);
+
+    double angle1Rad = std::min(startAngleRadN, endAngleRadN);
+    double angle2Rad = std::max(startAngleRadN, endAngleRadN);
+
+    // now we are sure angle1Rad < angle2Rad
+    // this means that if we draw from 1 to 2, it will be clockwise (
+    // increasing angles).
+    // let's fix this:
+    if (!clockwise)
+    {
+      angle2Rad -= 2 * g_pi;
+      // now we are sure angle2Rad < angle1Rad (since they were normalized) 
+      // and, thus, going from 1 to 2 means the angle values will DECREASE,
+      // which is the definition of anticlockwise
+    }
+
+    chain.clear();
+    chain.reserve(subdivisionsCount + 1);
+
+    double angleIncr = (angle2Rad - angle1Rad)
+      / static_cast<double>(subdivisionsCount);
+
+    double theta = angle1Rad;
+    for (int i = 0; i < subdivisionsCount + 1; ++i)
+    {
+      double offsetX = radiusS * cos(theta);
+      double offsetY = radiusS * sin(theta);
+      double pointX = centerS.GetX() + offsetX;
+      double pointY = centerS.GetY() + offsetY;
+      chain.push_back(ScenePoint2D(pointX, pointY));
+      theta += angleIncr;
+    }
+  }
+
+  void AddCircle(PolylineSceneLayer::Chain& chain,
+    const Scene2D&      scene,
+    const ScenePoint2D& centerS,
+    const double&       radiusS,
+    const int           numSubdivisions)
+  {
+    //ScenePoint2D centerC = centerS.Apply(scene.GetSceneToCanvasTransform());
+    //TODO: take DPI into account
+
+    // TODO: automatically compute the number for segments for smooth 
+    // display based on the radius in pixels.
+
+    chain.clear();
+    chain.reserve(numSubdivisions);
+
+    double angleIncr = (2.0 * g_pi)
+      / static_cast<double>(numSubdivisions);
+
+    double theta = 0;
+    for (int i = 0; i < numSubdivisions; ++i)
+    {
+      double offsetX = radiusS * cos(theta);
+      double offsetY = radiusS * sin(theta);
+      double pointX = centerS.GetX() + offsetX;
+      double pointY = centerS.GetY() + offsetY;
+      chain.push_back(ScenePoint2D(pointX, pointY));
+      theta += angleIncr;
+    }
+  }
+
+  double NormalizeAngle(double angle)
+  {
+    double retAngle = angle;
+    while (retAngle < 0)
+      retAngle += 2 * g_pi;
+    while (retAngle >= 2 * g_pi)
+      retAngle -= 2 * g_pi;
+    return retAngle;
+  }
+
+  double MeasureAngle(const ScenePoint2D& p1, const ScenePoint2D& c, const ScenePoint2D& p2)
+  {
+    double p1cAngle = atan2(p1.GetY() - c.GetY(), p1.GetX() - c.GetX());
+    double p2cAngle = atan2(p2.GetY() - c.GetY(), p2.GetX() - c.GetX());
+    double delta = p2cAngle - p1cAngle;
+    return NormalizeAngle(delta);
+  }
+
+
+#if 0
+  void AddEllipse(PolylineSceneLayer::Chain& chain,
+    const Scene2D&      scene,
+    const ScenePoint2D& centerS,
+    const double&       halfHAxis,
+    const double&       halfVAxis)
+  {
+    chain.clear();
+    chain.reserve(4);
+    ScenePoint2D centerC = centerS.Apply(scene.GetSceneToCanvasTransform());
+    //TODO: take DPI into account
+    double handleLX = centerC.GetX() - sideLength / 2;
+    double handleTY = centerC.GetY() - sideLength / 2;
+    double handleRX = centerC.GetX() + sideLength / 2;
+    double handleBY = centerC.GetY() + sideLength / 2;
+    ScenePoint2D LTC(handleLX, handleTY);
+    ScenePoint2D RTC(handleRX, handleTY);
+    ScenePoint2D RBC(handleRX, handleBY);
+    ScenePoint2D LBC(handleLX, handleBY);
+
+    ScenePoint2D startLT = LTC.Apply(scene.GetCanvasToSceneTransform());
+    ScenePoint2D startRT = RTC.Apply(scene.GetCanvasToSceneTransform());
+    ScenePoint2D startRB = RBC.Apply(scene.GetCanvasToSceneTransform());
+    ScenePoint2D startLB = LBC.Apply(scene.GetCanvasToSceneTransform());
+
+    chain.push_back(startLT);
+    chain.push_back(startRT);
+    chain.push_back(startRB);
+    chain.push_back(startLB);
+}
+#endif
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Common/MeasureToolsToolbox.h	Mon May 13 15:12:56 2019 +0200
@@ -0,0 +1,135 @@
+/**
+ * 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/Scene2D/PolylineSceneLayer.h>
+#include <Framework/Scene2D/Scene2D.h>
+
+namespace OrthancStone
+{
+
+  /**
+  This function will create a square around the center point supplied in
+  scene coordinates, with a side length given in canvas coordinates. The
+  square sides are parallel to the canvas boundaries.
+  */
+  void AddSquare(PolylineSceneLayer::Chain& chain,
+    const Scene2D&      scene,
+    const ScenePoint2D& centerS,
+    const double&       sideLength);
+
+  /**
+    Creates an arc centered pm c that goes
+    - from a point r1:
+      - so that r1 belongs to the p1,c line
+      - so that the distance from c to r1 equals radius
+    - to a point r2:
+      - so that r2 belongs to the p2,c line
+      - so that the distance from c to r2 equals radius
+
+    if clockwise is true, the arc is drawn from r1 to r2 with increasing 
+    angle values. Otherwise, the angle values decrease.
+
+    Warning: the existing chain content will be wiped out.
+  */
+
+  void AddArc(
+      PolylineSceneLayer::Chain& chain
+    , const Scene2D&      scene
+    , const ScenePoint2D& p1
+    , const ScenePoint2D& c
+    , const ScenePoint2D& p2
+    , const double&       radiusS
+    , const bool          clockwise
+    , const int           subdivisionsCount = 63);
+    
+  /**
+    Creates an arc (open curve) with "numSubdivisions"
+    (N + 1 points) from start angle to end angle.
+
+    if clockwise is true, the arc is drawn from start to end 
+    by increasing the angle values.
+
+    otherwise, the angle value decreases from start to end.
+
+    Warning: the existing chain content will be wiped out.
+  */
+  void AddArc(
+      PolylineSceneLayer::Chain& chain
+    , const Scene2D&      scene
+    , const ScenePoint2D& centerS
+    , const double&       radiusS
+    , const double        startAngleRad
+    , const double        endAngleRad
+    , const bool          clockwise
+    , const int           subdivisionsCount = 63);
+
+  /**
+    Creates a circle (closed curve) with "numSubdivisions"
+    (N points)
+
+    Warning: the existing chain content will be wiped out.
+  */
+  void AddCircle(PolylineSceneLayer::Chain& chain,
+    const Scene2D&      scene,
+    const ScenePoint2D& centerS,
+    const double&       radiusS,
+    const int           numSubdivisions = 63);
+
+  /**
+    Adds or subtracts 2*pi as many times as need to shift the specified
+    angle to a value such as:   0 <= value < 2*pi
+   */
+  double NormalizeAngle(double angle);
+
+  /**
+    Returns the angle magnitude between the p1,c and p2,c lines. 
+    The returned angle is between 0 and 2*pi
+
+    If the angle is between 0 and pi, this means that the shortest arc 
+    from p1 to p2 is clockwise.
+
+    If the angle is between pi and 2*pi, this means that the shortest arc
+    from p1 to p2 is COUNTERclockwise.
+
+  */
+  double MeasureAngle(
+      const ScenePoint2D& p1
+    , const ScenePoint2D& c
+    , const ScenePoint2D& p2);
+
+  /**
+  RadiansToDegrees
+  */
+  double RadiansToDegrees(double angleRad);
+
+  /**
+  This function will return the coordinates of a point that:
+  - belongs to the two bisecting lines of the p1 c p2 angle.
+  - is a distance d from c.
+  Among the four possible points, the one returned will be the one belonging
+  to the *smallest* half-plane defined by the [c,p1[ and [c,p2[ half-lines.
+  */
+  void GetPositionOnBisectingLine(
+      ScenePoint2D&       result
+    , const ScenePoint2D& p1
+    , const ScenePoint2D& c
+    , const ScenePoint2D& p2
+    , const double d);
+}
--- a/Samples/Common/MeasureTrackers.cpp	Fri May 10 14:54:03 2019 +0200
+++ b/Samples/Common/MeasureTrackers.cpp	Mon May 13 15:12:56 2019 +0200
@@ -31,12 +31,24 @@
     std::vector<TrackerCommandPtr>& undoStack,
     std::vector<MeasureToolPtr>&    measureTools)
     : scene_(scene)
+    , active_(true)
     , undoStack_(undoStack)
     , measureTools_(measureTools)
     , commitResult_(true)
   {
   }
 
+  void CreateMeasureTracker::Cancel()
+  {
+    commitResult_ = false;
+    active_ = false;
+  }
+
+  bool CreateMeasureTracker::IsActive() const
+  {
+    return active_;
+  }
+
   CreateMeasureTracker::~CreateMeasureTracker()
   {
     // if the tracker completes successfully, we add the command
@@ -48,45 +60,6 @@
     else
       command_->Undo();
   }
-  
-  CreateLineMeasureTracker::CreateLineMeasureTracker(
-    Scene2D&                        scene,
-    std::vector<TrackerCommandPtr>& undoStack,
-    std::vector<MeasureToolPtr>&    measureTools,
-    const PointerEvent&             e) 
-    : CreateMeasureTracker(scene, undoStack, measureTools)
-  {
-    command_.reset(
-      new CreateLineMeasureCommand(
-        scene,
-        measureTools,
-        e.GetMainPosition().Apply(scene.GetCanvasToSceneTransform())));
-  }
-
-  CreateLineMeasureTracker::~CreateLineMeasureTracker()
-  {
-
-  }
-
-  void CreateMeasureTracker::Update(const PointerEvent& event)
-  {
-    ScenePoint2D scenePos = event.GetMainPosition().Apply(
-      scene_.GetCanvasToSceneTransform());
-    
-    LOG(TRACE) << "scenePos.GetX() = " << scenePos.GetX() << "     " <<
-      "scenePos.GetY() = " << scenePos.GetY();
-
-    CreateLineMeasureTracker* concreteThis =
-      dynamic_cast<CreateLineMeasureTracker*>(this);
-    assert(concreteThis != NULL);
-    command_->Update(scenePos);
-  }
-
-  void CreateMeasureTracker::Release()
-  {
-    commitResult_ = false;
-  }
-
 }
 
 
--- a/Samples/Common/MeasureTrackers.h	Fri May 10 14:54:03 2019 +0200
+++ b/Samples/Common/MeasureTrackers.h	Mon May 13 15:12:56 2019 +0200
@@ -20,8 +20,9 @@
 
 #pragma once
 
-#include "../../Framework/Scene2D/IPointerTracker.h"
+#include "IFlexiblePointerTracker.h"
 #include "../../Framework/Scene2D/Scene2D.h"
+#include "../../Framework/Scene2D/PointerEvent.h"
 
 #include "MeasureTools.h"
 #include "MeasureCommands.h"
@@ -30,11 +31,11 @@
 
 namespace OrthancStone
 {
-  class CreateMeasureTracker : public IPointerTracker
+  class CreateMeasureTracker : public IFlexiblePointerTracker
   {
   public:
-    virtual void Update(const PointerEvent& e) ORTHANC_OVERRIDE;
-    virtual void Release() ORTHANC_OVERRIDE;
+    virtual void Cancel() ORTHANC_OVERRIDE;
+    virtual bool IsActive() const ORTHANC_OVERRIDE;
   protected:
     CreateMeasureTracker(
       Scene2D&                        scene,
@@ -43,32 +44,14 @@
 
     ~CreateMeasureTracker();
   
+  protected:
+    CreateMeasureCommandPtr         command_;
+    Scene2D&                        scene_;
+    bool                            active_;
   private:
-    Scene2D&                        scene_;
     std::vector<TrackerCommandPtr>& undoStack_;
     std::vector<MeasureToolPtr>&    measureTools_;
     bool                            commitResult_;
-
-  protected:
-    CreateMeasureCommandPtr         command_;
-  };
-
-  class CreateLineMeasureTracker : public CreateMeasureTracker
-  {
-  public:
-    /**
-    When you create this tracker, you need to supply it with the undo stack
-    where it will store the commands that perform the actual measure tool
-    creation and modification.
-    In turn, a container for these commands to store the actual measuring
-    must be supplied, too
-    */
-    CreateLineMeasureTracker(
-      Scene2D&                        scene,
-      std::vector<TrackerCommandPtr>& undoStack,
-      std::vector<MeasureToolPtr>&    measureTools,
-      const PointerEvent&             e);
-
-    ~CreateLineMeasureTracker();
   };
 }
+
--- a/Samples/Sdl/CMakeLists.txt	Fri May 10 14:54:03 2019 +0200
+++ b/Samples/Sdl/CMakeLists.txt	Mon May 13 15:12:56 2019 +0200
@@ -69,13 +69,51 @@
   LIST(APPEND TRACKERSAMPLE_SOURCE "../../../SDL-Console/SDL_Console.h")
 endif()
 
+LIST(APPEND TRACKERSAMPLE_SOURCE "../Common/AngleMeasureTool.cpp")
+LIST(APPEND TRACKERSAMPLE_SOURCE "../Common/AngleMeasureTool.h")
+
+LIST(APPEND TRACKERSAMPLE_SOURCE "../Common/CreateAngleMeasureTracker.cpp")
+LIST(APPEND TRACKERSAMPLE_SOURCE "../Common/CreateAngleMeasureTracker.h")
+LIST(APPEND TRACKERSAMPLE_SOURCE "../Common/CreateCircleMeasureTracker.cpp")
+LIST(APPEND TRACKERSAMPLE_SOURCE "../Common/CreateCircleMeasureTracker.h")
+LIST(APPEND TRACKERSAMPLE_SOURCE "../Common/CreateLineMeasureTracker.cpp")
+LIST(APPEND TRACKERSAMPLE_SOURCE "../Common/CreateLineMeasureTracker.h")
+
+LIST(APPEND TRACKERSAMPLE_SOURCE "../Common/CreateMeasureTracker.cpp")
+LIST(APPEND TRACKERSAMPLE_SOURCE "../Common/CreateMeasureTracker.h")
+
+LIST(APPEND TRACKERSAMPLE_SOURCE "../Common/CreateSimpleTrackerAdapter.cpp")
+
+LIST(APPEND TRACKERSAMPLE_SOURCE "../Common/EditAngleMeasureTracker.cpp")
+LIST(APPEND TRACKERSAMPLE_SOURCE "../Common/EditAngleMeasureTracker.h")
+LIST(APPEND TRACKERSAMPLE_SOURCE "../Common/EditCircleMeasureTracker.cpp")
+LIST(APPEND TRACKERSAMPLE_SOURCE "../Common/EditCircleMeasureTracker.h")
+LIST(APPEND TRACKERSAMPLE_SOURCE "../Common/EditLineMeasureTracker.cpp")
+LIST(APPEND TRACKERSAMPLE_SOURCE "../Common/EditLineMeasureTracker.h")
+
+LIST(APPEND TRACKERSAMPLE_SOURCE "../Common/IFlexiblePointerTracker.h")
+
+LIST(APPEND TRACKERSAMPLE_SOURCE "../Common/LineMeasureTool.cpp")
+LIST(APPEND TRACKERSAMPLE_SOURCE "../Common/LineMeasureTool.h")
+
 LIST(APPEND TRACKERSAMPLE_SOURCE "../Common/MeasureCommands.cpp")
 LIST(APPEND TRACKERSAMPLE_SOURCE "../Common/MeasureCommands.h")
 LIST(APPEND TRACKERSAMPLE_SOURCE "../Common/MeasureTools.cpp")
 LIST(APPEND TRACKERSAMPLE_SOURCE "../Common/MeasureTools.h")
+
+LIST(APPEND TRACKERSAMPLE_SOURCE "../Common/MeasureToolsToolbox.cpp")
+LIST(APPEND TRACKERSAMPLE_SOURCE "../Common/MeasureToolsToolbox.h")
+
 LIST(APPEND TRACKERSAMPLE_SOURCE "../Common/MeasureTrackers.cpp")
 LIST(APPEND TRACKERSAMPLE_SOURCE "../Common/MeasureTrackers.h")
+
 LIST(APPEND TRACKERSAMPLE_SOURCE "TrackerSample.cpp")
+LIST(APPEND TRACKERSAMPLE_SOURCE "TrackerSampleApp.cpp")
+LIST(APPEND TRACKERSAMPLE_SOURCE "TrackerSampleApp.h")
+
+if (MSVC AND MSVC_VERSION GREATER 1700)
+  LIST(APPEND TRACKERSAMPLE_SOURCE "cpp.hint")
+endif()
 
 add_executable(TrackerSample
   ${TRACKERSAMPLE_SOURCE}
--- a/Samples/Sdl/Loader.cpp	Fri May 10 14:54:03 2019 +0200
+++ b/Samples/Sdl/Loader.cpp	Mon May 13 15:12:56 2019 +0200
@@ -28,23 +28,17 @@
 #include "../../Framework/Volumes/ImageBuffer3D.h"
 
 // From Orthanc framework
-#include <Core/Compression/GzipCompressor.h>
-#include <Core/Compression/ZlibCompressor.h>
 #include <Core/DicomFormat/DicomArray.h>
 #include <Core/DicomFormat/DicomImageInformation.h>
 #include <Core/HttpClient.h>
 #include <Core/IDynamicObject.h>
 #include <Core/Images/Image.h>
 #include <Core/Images/ImageProcessing.h>
-#include <Core/Images/JpegReader.h>
-#include <Core/Images/PamReader.h>
-#include <Core/Images/PngReader.h>
 #include <Core/Images/PngWriter.h>
 #include <Core/Logging.h>
 #include <Core/MultiThreading/SharedMessageQueue.h>
 #include <Core/OrthancException.h>
 #include <Core/Toolbox.h>
-#include <Core/SystemToolbox.h>
 
 #include <json/reader.h>
 #include <json/value.h>
@@ -62,9 +56,7 @@
   public:
     enum Type
     {
-      Type_OrthancRestApi,
-      Type_GetOrthancImage,
-      Type_GetOrthancWebViewerJpeg
+      Type_OrthancApi
     };
 
     virtual ~IOracleCommand()
@@ -139,54 +131,20 @@
 
 
 
-  class OracleCommandExceptionMessage :
-    public OrthancStone::BaseMessage<OrthancStone::MessageType_OracleCommandExceptionMessage>
-  {
-  private:
-    const IOracleCommand&       command_;
-    Orthanc::OrthancException   exception_;
-
-  public:
-    OracleCommandExceptionMessage(const IOracleCommand& command,
-                                  const Orthanc::OrthancException& exception) :
-      command_(command),
-      exception_(exception)
-    {
-    }
-
-    OracleCommandExceptionMessage(const IOracleCommand& command,
-                                  const Orthanc::ErrorCode& error) :
-      command_(command),
-      exception_(error)
-    {
-    }
-
-    const IOracleCommand& GetCommand() const
-    {
-      return command_;
-    }
-    
-    const Orthanc::OrthancException& GetException() const
-    {
-      return exception_;
-    }
-  };
-  
-
   typedef std::map<std::string, std::string>  HttpHeaders;
 
-  class OrthancRestApiCommand : public OracleCommandWithPayload
+  class OrthancApiOracleCommand : public OracleCommandWithPayload
   {
   public:
-    class SuccessMessage : public OrthancStone::OriginMessage<OrthancStone::MessageType_OrthancRestApiCommand,
-                                                              OrthancRestApiCommand>
+    class SuccessMessage : public OrthancStone::OriginMessage<OrthancStone::MessageType_HttpRequestSuccess,   // TODO
+                                                              OrthancApiOracleCommand>
     {
     private:
       HttpHeaders   headers_;
       std::string   answer_;
 
     public:
-      SuccessMessage(const OrthancRestApiCommand& command,
+      SuccessMessage(const OrthancApiOracleCommand& command,
                      const HttpHeaders& answerHeaders,
                      std::string& answer  /* will be swapped to avoid a memcpy() */) :
         OriginMessage(command),
@@ -216,6 +174,27 @@
     };
 
 
+    class FailureMessage : public OrthancStone::OriginMessage<OrthancStone::MessageType_HttpRequestError,   // TODO
+                                                              OrthancApiOracleCommand>
+    {
+    private:
+      Orthanc::HttpStatus  status_;
+
+    public:
+      FailureMessage(const OrthancApiOracleCommand& command,
+                     Orthanc::HttpStatus status) :
+        OriginMessage(command),
+        status_(status)
+      {
+      }
+
+      Orthanc::HttpStatus GetHttpStatus() const
+      {
+        return status_;
+      }
+    };
+
+
   private:
     Orthanc::HttpMethod  method_;
     std::string          uri_;
@@ -224,10 +203,10 @@
     unsigned int         timeout_;
 
     std::auto_ptr< OrthancStone::MessageHandler<SuccessMessage> >  successCallback_;
-    std::auto_ptr< OrthancStone::MessageHandler<OracleCommandExceptionMessage> >  failureCallback_;
+    std::auto_ptr< OrthancStone::MessageHandler<FailureMessage> >  failureCallback_;
 
   public:
-    OrthancRestApiCommand() :
+    OrthancApiOracleCommand() :
       method_(Orthanc::HttpMethod_Get),
       uri_("/"),
       timeout_(10)
@@ -236,7 +215,7 @@
 
     virtual Type GetType() const
     {
-      return Type_OrthancRestApi;
+      return Type_OrthancApi;
     }
 
     void SetMethod(Orthanc::HttpMethod method)
@@ -260,11 +239,6 @@
       body_ = writer.write(json);
     }
 
-    void SetHttpHeaders(const HttpHeaders& headers)
-    {
-      headers_ = headers;
-    }
-
     void SetHttpHeader(const std::string& key,
                        const std::string& value)
     {
@@ -312,423 +286,6 @@
 
 
 
-
-  class GetOrthancImageCommand : public OracleCommandWithPayload
-  {
-  public:
-    class SuccessMessage : public OrthancStone::OriginMessage<OrthancStone::MessageType_GetOrthancImageCommand,
-                                                              GetOrthancImageCommand>
-    {
-    private:
-      std::auto_ptr<Orthanc::ImageAccessor>  image_;
-      Orthanc::MimeType                      mime_;
-
-    public:
-      SuccessMessage(const GetOrthancImageCommand& command,
-                     Orthanc::ImageAccessor* image,   // Takes ownership
-                     Orthanc::MimeType mime) :
-        OriginMessage(command),
-        image_(image),
-        mime_(mime)
-      {
-        if (image == NULL)
-        {
-          throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
-        }
-      }
-
-      const Orthanc::ImageAccessor& GetImage() const
-      {
-        return *image_;
-      }
-
-      Orthanc::MimeType GetMimeType() const
-      {
-        return mime_;
-      }
-    };
-
-
-  private:
-    std::string    uri_;
-    HttpHeaders    headers_;
-    unsigned int   timeout_;
-
-    std::auto_ptr< OrthancStone::MessageHandler<SuccessMessage> >  successCallback_;
-    std::auto_ptr< OrthancStone::MessageHandler<OracleCommandExceptionMessage> >  failureCallback_;
-
-  public:
-    GetOrthancImageCommand() :
-      uri_("/"),
-      timeout_(10)
-    {
-    }
-
-    virtual Type GetType() const
-    {
-      return Type_GetOrthancImage;
-    }
-
-    void SetUri(const std::string& uri)
-    {
-      uri_ = uri;
-    }
-
-    void SetHttpHeader(const std::string& key,
-                       const std::string& value)
-    {
-      headers_[key] = value;
-    }
-
-    const std::string& GetUri() const
-    {
-      return uri_;
-    }
-
-    const HttpHeaders& GetHttpHeaders() const
-    {
-      return headers_;
-    }
-
-    void SetTimeout(unsigned int seconds)
-    {
-      timeout_ = seconds;
-    }
-
-    unsigned int GetTimeout() const
-    {
-      return timeout_;
-    }
-
-    void ProcessHttpAnswer(IMessageEmitter& emitter,
-                           const OrthancStone::IObserver& receiver,
-                           const std::string& answer,
-                           const HttpHeaders& answerHeaders) const
-    {
-      Orthanc::MimeType contentType = Orthanc::MimeType_Binary;
-
-      for (HttpHeaders::const_iterator it = answerHeaders.begin(); 
-           it != answerHeaders.end(); ++it)
-      {
-        std::string s;
-        Orthanc::Toolbox::ToLowerCase(s, it->first);
-
-        if (s == "content-type")
-        {
-          contentType = Orthanc::StringToMimeType(it->second);
-          break;
-        }
-      }
-
-      std::auto_ptr<Orthanc::ImageAccessor> image;
-
-      switch (contentType)
-      {
-        case Orthanc::MimeType_Png:
-        {
-          image.reset(new Orthanc::PngReader);
-          dynamic_cast<Orthanc::PngReader&>(*image).ReadFromMemory(answer);
-          break;
-        }
-
-        case Orthanc::MimeType_Pam:
-        {
-          image.reset(new Orthanc::PamReader);
-          dynamic_cast<Orthanc::PamReader&>(*image).ReadFromMemory(answer);
-          break;
-        }
-
-        case Orthanc::MimeType_Jpeg:
-        {
-          image.reset(new Orthanc::JpegReader);
-          dynamic_cast<Orthanc::JpegReader&>(*image).ReadFromMemory(answer);
-          break;
-        }
-
-        default:
-          throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol,
-                                          "Unsupported HTTP Content-Type for an image: " + 
-                                          std::string(Orthanc::EnumerationToString(contentType)));
-      }
-
-      SuccessMessage message(*this, image.release(), contentType);
-      emitter.EmitMessage(receiver, message);
-    }
-  };
-
-
-
-  class GetOrthancWebViewerJpegCommand : public OracleCommandWithPayload
-  {
-  public:
-    class SuccessMessage : public OrthancStone::OriginMessage<OrthancStone::MessageType_GetOrthancWebViewerJpegCommand,
-                                                              GetOrthancWebViewerJpegCommand>
-    {
-    private:
-      std::auto_ptr<Orthanc::ImageAccessor>  image_;
-
-    public:
-      SuccessMessage(const GetOrthancWebViewerJpegCommand& command,
-                     Orthanc::ImageAccessor* image) :   // Takes ownership
-        OriginMessage(command),
-        image_(image)
-      {
-        if (image == NULL)
-        {
-          throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
-        }
-      }
-
-      const Orthanc::ImageAccessor& GetImage() const
-      {
-        return *image_;
-      }
-    };
-
-  private:
-    std::string           instanceId_;
-    unsigned int          frame_;
-    unsigned int          quality_;
-    HttpHeaders           headers_;
-    unsigned int          timeout_;
-    Orthanc::PixelFormat  expectedFormat_;
-
-    std::auto_ptr< OrthancStone::MessageHandler<SuccessMessage> >  successCallback_;
-    std::auto_ptr< OrthancStone::MessageHandler<OracleCommandExceptionMessage> >  failureCallback_;
-
-  public:
-    GetOrthancWebViewerJpegCommand() :
-      frame_(0),
-      quality_(95),
-      timeout_(10),
-      expectedFormat_(Orthanc::PixelFormat_Grayscale8)
-    {
-    }
-
-    virtual Type GetType() const
-    {
-      return Type_GetOrthancWebViewerJpeg;
-    }
-
-    void SetExpectedFormat(Orthanc::PixelFormat format)
-    {
-      expectedFormat_ = format;
-    }
-
-    void SetInstance(const std::string& instanceId)
-    {
-      instanceId_ = instanceId;
-    }
-
-    void SetFrame(unsigned int frame)
-    {
-      frame_ = frame;
-    }
-
-    void SetQuality(unsigned int quality)
-    {
-      if (quality <= 0 ||
-          quality > 100)
-      {
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
-      }
-      else
-      {
-        quality_ = quality;
-      }
-    }
-
-    void SetHttpHeader(const std::string& key,
-                       const std::string& value)
-    {
-      headers_[key] = value;
-    }
-
-    Orthanc::PixelFormat GetExpectedFormat() const
-    {
-      return expectedFormat_;
-    }
-
-    const std::string& GetInstanceId() const
-    {
-      return instanceId_;
-    }
-
-    unsigned int GetFrame() const
-    {
-      return frame_;
-    }
-
-    unsigned int GetQuality() const
-    {
-      return quality_;
-    }
-
-    const HttpHeaders& GetHttpHeaders() const
-    {
-      return headers_;
-    }
-
-    void SetTimeout(unsigned int seconds)
-    {
-      timeout_ = seconds;
-    }
-
-    unsigned int GetTimeout() const
-    {
-      return timeout_;
-    }
-
-    std::string GetUri() const
-    {
-      return ("/web-viewer/instances/jpeg" + boost::lexical_cast<std::string>(quality_) +
-              "-" + instanceId_ + "_" + boost::lexical_cast<std::string>(frame_));
-    }
-
-    void ProcessHttpAnswer(IMessageEmitter& emitter,
-                           const OrthancStone::IObserver& receiver,
-                           const std::string& answer) const
-    {
-      // This code comes from older "OrthancSlicesLoader::ParseSliceImageJpeg()"
-      
-      Json::Value encoded;
-
-      {
-        Json::Reader reader;
-        if (!reader.parse(answer, encoded))
-        {
-          throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
-        }
-      }
-
-      if (encoded.type() != Json::objectValue ||
-          !encoded.isMember("Orthanc") ||
-          encoded["Orthanc"].type() != Json::objectValue)
-      {
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
-      }
-    
-      const Json::Value& info = encoded["Orthanc"];
-      if (!info.isMember("PixelData") ||
-          !info.isMember("Stretched") ||
-          !info.isMember("Compression") ||
-          info["Compression"].type() != Json::stringValue ||
-          info["PixelData"].type() != Json::stringValue ||
-          info["Stretched"].type() != Json::booleanValue ||
-          info["Compression"].asString() != "Jpeg")
-      {
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
-      }
-    
-      bool isSigned = false;
-      bool isStretched = info["Stretched"].asBool();
-    
-      if (info.isMember("IsSigned"))
-      {
-        if (info["IsSigned"].type() != Json::booleanValue)
-        {
-          throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
-        }
-        else
-        {
-          isSigned = info["IsSigned"].asBool();
-        }
-      }
-    
-      std::auto_ptr<Orthanc::ImageAccessor> reader;
-    
-      {
-        std::string jpeg;
-        Orthanc::Toolbox::DecodeBase64(jpeg, info["PixelData"].asString());
-      
-        reader.reset(new Orthanc::JpegReader);
-        dynamic_cast<Orthanc::JpegReader&>(*reader).ReadFromMemory(jpeg);
-      }
-    
-      if (reader->GetFormat() == Orthanc::PixelFormat_RGB24)  // This is a color image
-      {
-        if (expectedFormat_ != Orthanc::PixelFormat_RGB24)
-        {
-          throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
-        }
-      
-        if (isSigned || isStretched)
-        {
-          throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
-        }
-        else
-        {
-          SuccessMessage message(*this, reader.release());
-          emitter.EmitMessage(receiver, message);
-          return;
-        }
-      }
-    
-      if (reader->GetFormat() != Orthanc::PixelFormat_Grayscale8)
-      {
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
-      }
-    
-      if (!isStretched)
-      {
-        if (expectedFormat_ != reader->GetFormat())
-        {
-          throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
-        }
-        else
-        {
-          SuccessMessage message(*this, reader.release());
-          emitter.EmitMessage(receiver, message);
-          return;
-        }
-      }
-    
-      int32_t stretchLow = 0;
-      int32_t stretchHigh = 0;
-    
-      if (!info.isMember("StretchLow") ||
-          !info.isMember("StretchHigh") ||
-          info["StretchLow"].type() != Json::intValue ||
-          info["StretchHigh"].type() != Json::intValue)
-      {
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
-      }
-    
-      stretchLow = info["StretchLow"].asInt();
-      stretchHigh = info["StretchHigh"].asInt();
-    
-      if (stretchLow < -32768 ||
-          stretchHigh > 65535 ||
-          (stretchLow < 0 && stretchHigh > 32767))
-      {
-        // This range cannot be represented with a uint16_t or an int16_t
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
-      }
-    
-      // Decode a grayscale JPEG 8bpp image coming from the Web viewer
-      std::auto_ptr<Orthanc::ImageAccessor> image
-        (new Orthanc::Image(expectedFormat_, reader->GetWidth(), reader->GetHeight(), false));
-
-      Orthanc::ImageProcessing::Convert(*image, *reader);
-      reader.reset();
-    
-      float scaling = static_cast<float>(stretchHigh - stretchLow) / 255.0f;
-    
-      if (!OrthancStone::LinearAlgebra::IsCloseToZero(scaling))
-      {
-        float offset = static_cast<float>(stretchLow) / scaling;
-        Orthanc::ImageProcessing::ShiftScale(*image, offset, scaling, true);
-      }
-    
-      SuccessMessage message(*this, image.release());
-      emitter.EmitMessage(receiver, message);
-    }
-  };
-
-
-
-
-
   class NativeOracle : public IOracle
   {
   private:
@@ -779,115 +336,53 @@
     std::vector<boost::thread*>    workers_;
 
 
-    void CopyHttpHeaders(Orthanc::HttpClient& client,
-                         const HttpHeaders& headers)
-    {
-      for (HttpHeaders::const_iterator it = headers.begin(); it != headers.end(); it++ )
-      {
-        client.AddHeader(it->first, it->second);
-      }
-    }
-
-
-    void DecodeAnswer(std::string& answer,
-                      const HttpHeaders& headers)
+    void Execute(const OrthancStone::IObserver& receiver,
+                 const OrthancApiOracleCommand& command)
     {
-      Orthanc::HttpCompression contentEncoding = Orthanc::HttpCompression_None;
-
-      for (HttpHeaders::const_iterator it = headers.begin(); 
-           it != headers.end(); ++it)
-      {
-        std::string s;
-        Orthanc::Toolbox::ToLowerCase(s, it->first);
-
-        if (s == "content-encoding")
-        {
-          if (it->second == "gzip")
-          {
-            contentEncoding = Orthanc::HttpCompression_Gzip;
-          }
-          else 
-          {
-            throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol,
-                                            "Unsupported HTTP Content-Encoding: " + it->second);
-          }
-
-          break;
-        }
-      }
-
-      if (contentEncoding == Orthanc::HttpCompression_Gzip)
-      {
-        std::string compressed;
-        answer.swap(compressed);
-          
-        Orthanc::GzipCompressor compressor;
-        compressor.Uncompress(answer, compressed.c_str(), compressed.size());
-      }
-    }
-
-
-    void Execute(const OrthancStone::IObserver& receiver,
-                 const OrthancRestApiCommand& command)
-    {
-      Orthanc::HttpClient client(orthanc_, command.GetUri());
+      Orthanc::HttpClient  client(orthanc_, command.GetUri());
       client.SetMethod(command.GetMethod());
       client.SetTimeout(command.GetTimeout());
 
-      CopyHttpHeaders(client, command.GetHttpHeaders());
-
       if (command.GetMethod() == Orthanc::HttpMethod_Post ||
           command.GetMethod() == Orthanc::HttpMethod_Put)
       {
         client.SetBody(command.GetBody());
       }
-
-      std::string answer;
-      HttpHeaders answerHeaders;
-      client.ApplyAndThrowException(answer, answerHeaders);
-
-      DecodeAnswer(answer, answerHeaders);
-
-      OrthancRestApiCommand::SuccessMessage message(command, answerHeaders, answer);
-      emitter_.EmitMessage(receiver, message);
-    }
-
-
-    void Execute(const OrthancStone::IObserver& receiver,
-                 const GetOrthancImageCommand& command)
-    {
-      Orthanc::HttpClient client(orthanc_, command.GetUri());
-      client.SetTimeout(command.GetTimeout());
-
-      CopyHttpHeaders(client, command.GetHttpHeaders());
+      
+      {
+        const HttpHeaders& headers = command.GetHttpHeaders();
+        for (HttpHeaders::const_iterator it = headers.begin(); it != headers.end(); it++ )
+        {
+          client.AddHeader(it->first, it->second);
+        }
+      }
 
       std::string answer;
       HttpHeaders answerHeaders;
-      client.ApplyAndThrowException(answer, answerHeaders);
+
+      bool success;
+      try
+      {
+        success = client.Apply(answer, answerHeaders);
+      }
+      catch (Orthanc::OrthancException& e)
+      {
+        success = false;
+      }
 
-      DecodeAnswer(answer, answerHeaders);
-
-      command.ProcessHttpAnswer(emitter_, receiver, answer, answerHeaders);
+      if (success)
+      {
+        OrthancApiOracleCommand::SuccessMessage message(command, answerHeaders, answer);
+        emitter_.EmitMessage(receiver, message);
+      }
+      else
+      {
+        OrthancApiOracleCommand::FailureMessage message(command, client.GetLastStatus());
+        emitter_.EmitMessage(receiver, message);
+      }
     }
 
 
-    void Execute(const OrthancStone::IObserver& receiver,
-                 const GetOrthancWebViewerJpegCommand& command)
-    {
-      Orthanc::HttpClient client(orthanc_, command.GetUri());
-      client.SetTimeout(command.GetTimeout());
-
-      CopyHttpHeaders(client, command.GetHttpHeaders());
-
-      std::string answer;
-      HttpHeaders answerHeaders;
-      client.ApplyAndThrowException(answer, answerHeaders);
-
-      DecodeAnswer(answer, answerHeaders);
-
-      command.ProcessHttpAnswer(emitter_, receiver, answer);
-    }
-
 
     void Step()
     {
@@ -895,27 +390,15 @@
 
       if (object.get() != NULL)
       {
-        printf("===========================> REQUEST\n");
-        
         const Item& item = dynamic_cast<Item&>(*object);
 
         try
         {
           switch (item.GetCommand().GetType())
           {
-            case IOracleCommand::Type_OrthancRestApi:
-              Execute(item.GetReceiver(), 
-                      dynamic_cast<const OrthancRestApiCommand&>(item.GetCommand()));
-              break;
-
-            case IOracleCommand::Type_GetOrthancImage:
+            case IOracleCommand::Type_OrthancApi:
               Execute(item.GetReceiver(), 
-                      dynamic_cast<const GetOrthancImageCommand&>(item.GetCommand()));
-              break;
-
-            case IOracleCommand::Type_GetOrthancWebViewerJpeg:
-              Execute(item.GetReceiver(), 
-                      dynamic_cast<const GetOrthancWebViewerJpegCommand&>(item.GetCommand()));
+                      dynamic_cast<const OrthancApiOracleCommand&>(item.GetCommand()));
               break;
 
             default:
@@ -925,13 +408,10 @@
         catch (Orthanc::OrthancException& e)
         {
           LOG(ERROR) << "Exception within the oracle: " << e.What();
-          emitter_.EmitMessage(item.GetReceiver(), OracleCommandExceptionMessage(item.GetCommand(), e));
         }
         catch (...)
         {
           LOG(ERROR) << "Native exception within the oracle";
-          emitter_.EmitMessage(item.GetReceiver(), OracleCommandExceptionMessage
-                               (item.GetCommand(), Orthanc::ErrorCode_InternalError));
         }
       }
     }
@@ -1082,15 +562,8 @@
     virtual void EmitMessage(const OrthancStone::IObserver& observer,
                              const OrthancStone::IMessage& message)
     {
-      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();
-      }
+      boost::unique_lock<boost::shared_mutex>  lock(mutex_);
+      oracleObservable_.EmitMessage(observer, message);
     }
 
 
@@ -1329,15 +802,12 @@
 
     OrthancStone::CoordinateSystem3D  GetFrameGeometry(unsigned int frame) const
     {
-      if (frame == 0)
-      {
-        return geometry_;
-      }
-      else if (frame >= imageInformation_.GetNumberOfFrames())
+      if (frame >= imageInformation_.GetNumberOfFrames())
       {
         throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
       }
-      else if (sopClassUid_ == OrthancStone::SopClassUid_RTDose)
+
+      if (sopClassUid_ == OrthancStone::SopClassUid_RTDose)
       {
         if (frame >= frameOffsets_.size())
         {
@@ -1349,10 +819,6 @@
           geometry_.GetAxisX(),
           geometry_.GetAxisY());
       }
-      else
-      {
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
-      }
     }
 
     bool FrameContainsPlane(unsigned int frame,
@@ -1455,10 +921,10 @@
     class MessageHandler : public Orthanc::IDynamicObject
     {
     public:
-      virtual void Handle(const OrthancRestApiCommand::SuccessMessage& message) const = 0;
+      virtual void Handle(const OrthancApiOracleCommand::SuccessMessage& message) const = 0;
     };
 
-    void Handle(const OrthancRestApiCommand::SuccessMessage& message)
+    void Handle(const OrthancApiOracleCommand::SuccessMessage& message)
     {
       dynamic_cast<const MessageHandler&>(message.GetOrigin().GetPayload()).Handle(message);
     }
@@ -1475,7 +941,7 @@
       {
       }
 
-      virtual void Handle(const OrthancRestApiCommand::SuccessMessage& message) const
+      virtual void Handle(const OrthancApiOracleCommand::SuccessMessage& message) const
       {
         Json::Value value;
         message.ParseJsonBody(value);
@@ -1509,7 +975,7 @@
       {
       }
 
-      virtual void Handle(const OrthancRestApiCommand::SuccessMessage& message) const
+      virtual void Handle(const OrthancApiOracleCommand::SuccessMessage& message) const
       {
         Json::Value value;
         message.ParseJsonBody(value);
@@ -1537,7 +1003,7 @@
       active_(false)
     {
       oracle.RegisterObserverCallback(
-        new OrthancStone::Callable<AxialVolumeOrthancLoader, OrthancRestApiCommand::SuccessMessage>
+        new OrthancStone::Callable<AxialVolumeOrthancLoader, OrthancApiOracleCommand::SuccessMessage>
         (*this, &AxialVolumeOrthancLoader::Handle));
     }
 
@@ -1551,7 +1017,7 @@
 
       active_ = true;
 
-      std::auto_ptr<Refactoring::OrthancRestApiCommand> command(new Refactoring::OrthancRestApiCommand);
+      std::auto_ptr<Refactoring::OrthancApiOracleCommand> command(new Refactoring::OrthancApiOracleCommand);
       command->SetUri("/series/" + seriesId + "/instances-tags");
       command->SetPayload(new LoadSeriesGeometryHandler(*this));
 
@@ -1573,7 +1039,7 @@
 
       // TODO => Should be part of a second call if needed
 
-      std::auto_ptr<Refactoring::OrthancRestApiCommand> command(new Refactoring::OrthancRestApiCommand);
+      std::auto_ptr<Refactoring::OrthancApiOracleCommand> command(new Refactoring::OrthancApiOracleCommand);
       command->SetUri("/instances/" + instanceId + "/tags?ignore-length=3004-000c");
       command->SetPayload(new LoadInstanceGeometryHandler(*this));
 
@@ -1588,7 +1054,7 @@
 class Toto : public OrthancStone::IObserver
 {
 private:
-  void Handle(const Refactoring::OrthancRestApiCommand::SuccessMessage& message)
+  void Handle(const Refactoring::OrthancApiOracleCommand::SuccessMessage& message)
   {
     Json::Value v;
     message.ParseJsonBody(v);
@@ -1596,30 +1062,9 @@
     printf("ICI [%s]\n", v.toStyledString().c_str());
   }
 
-  void Handle(const Refactoring::GetOrthancImageCommand::SuccessMessage& message)
-  {
-    printf("IMAGE %dx%d\n", message.GetImage().GetWidth(), message.GetImage().GetHeight());
-  }
-
-  void Handle(const Refactoring::GetOrthancWebViewerJpegCommand::SuccessMessage& message)
-  {
-    printf("WebViewer %dx%d\n", message.GetImage().GetWidth(), message.GetImage().GetHeight());
-  }
-
-  void Handle(const Refactoring::OracleCommandExceptionMessage& message)
+  void Handle(const Refactoring::OrthancApiOracleCommand::FailureMessage& message)
   {
-    printf("EXCEPTION: [%s] on command type %d\n", message.GetException().What(), message.GetCommand().GetType());
-
-    switch (message.GetCommand().GetType())
-    {
-      case Refactoring::IOracleCommand::Type_GetOrthancWebViewerJpeg:
-        printf("URI: [%s]\n", dynamic_cast<const Refactoring::GetOrthancWebViewerJpegCommand&>
-               (message.GetCommand()).GetUri().c_str());
-        break;
-      
-      default:
-        break;
-    }
+    printf("ERROR %d\n", message.GetHttpStatus());
   }
 
 public:
@@ -1628,19 +1073,7 @@
   {
     oracle.RegisterObserverCallback
       (new OrthancStone::Callable
-       <Toto, Refactoring::OrthancRestApiCommand::SuccessMessage>(*this, &Toto::Handle));
-
-    oracle.RegisterObserverCallback
-      (new OrthancStone::Callable
-       <Toto, Refactoring::GetOrthancImageCommand::SuccessMessage>(*this, &Toto::Handle));
-
-    oracle.RegisterObserverCallback
-      (new OrthancStone::Callable
-      <Toto, Refactoring::GetOrthancWebViewerJpegCommand::SuccessMessage>(*this, &Toto::Handle));
-
-    oracle.RegisterObserverCallback
-      (new OrthancStone::Callable
-       <Toto, Refactoring::OracleCommandExceptionMessage>(*this, &Toto::Handle));
+       <Toto, Refactoring::OrthancApiOracleCommand::SuccessMessage>(*this, &Toto::Handle));
   }
 };
 
@@ -1668,13 +1101,12 @@
 
   oracle.Start();
 
-  if (1)
   {
     Json::Value v = Json::objectValue;
     v["Level"] = "Series";
     v["Query"] = Json::objectValue;
 
-    std::auto_ptr<Refactoring::OrthancRestApiCommand>  command(new Refactoring::OrthancRestApiCommand);
+    std::auto_ptr<Refactoring::OrthancApiOracleCommand>  command(new Refactoring::OrthancApiOracleCommand);
     command->SetMethod(Orthanc::HttpMethod_Post);
     command->SetUri("/tools/find");
     command->SetBody(v);
@@ -1682,64 +1114,11 @@
     oracle.Schedule(*toto, command.release());
   }
   
-  if (1)
-  {
-    std::auto_ptr<Refactoring::GetOrthancImageCommand>  command(new Refactoring::GetOrthancImageCommand);
-    command->SetHttpHeader("Accept", std::string(Orthanc::EnumerationToString(Orthanc::MimeType_Jpeg)));
-    command->SetUri("/instances/6687cc73-07cae193-52ff29c8-f646cb16-0753ed92/preview");
-    oracle.Schedule(*toto, command.release());
-  }
-  
-  if (1)
-  {
-    std::auto_ptr<Refactoring::GetOrthancImageCommand>  command(new Refactoring::GetOrthancImageCommand);
-    command->SetHttpHeader("Accept", std::string(Orthanc::EnumerationToString(Orthanc::MimeType_Png)));
-    command->SetUri("/instances/6687cc73-07cae193-52ff29c8-f646cb16-0753ed92/preview");
-    oracle.Schedule(*toto, command.release());
-  }
-  
-  if (1)
-  {
-    std::auto_ptr<Refactoring::GetOrthancImageCommand>  command(new Refactoring::GetOrthancImageCommand);
-    command->SetHttpHeader("Accept", std::string(Orthanc::EnumerationToString(Orthanc::MimeType_Png)));
-    command->SetUri("/instances/6687cc73-07cae193-52ff29c8-f646cb16-0753ed92/image-uint16");
-    oracle.Schedule(*toto, command.release());
-  }
-  
-  if (1)
-  {
-    std::auto_ptr<Refactoring::GetOrthancImageCommand>  command(new Refactoring::GetOrthancImageCommand);
-    command->SetHttpHeader("Accept-Encoding", "gzip");
-    command->SetHttpHeader("Accept", std::string(Orthanc::EnumerationToString(Orthanc::MimeType_Pam)));
-    command->SetUri("/instances/6687cc73-07cae193-52ff29c8-f646cb16-0753ed92/image-uint16");
-    oracle.Schedule(*toto, command.release());
-  }
-  
-  if (1)
-  {
-    std::auto_ptr<Refactoring::GetOrthancImageCommand>  command(new Refactoring::GetOrthancImageCommand);
-    command->SetHttpHeader("Accept", std::string(Orthanc::EnumerationToString(Orthanc::MimeType_Pam)));
-    command->SetUri("/instances/6687cc73-07cae193-52ff29c8-f646cb16-0753ed92/image-uint16");
-    oracle.Schedule(*toto, command.release());
-  }
-
-  if (1)
-  {
-    std::auto_ptr<Refactoring::GetOrthancWebViewerJpegCommand>  command(new Refactoring::GetOrthancWebViewerJpegCommand);
-    command->SetHttpHeader("Accept-Encoding", "gzip");
-    command->SetInstance("e6c7c20b-c9f65d7e-0d76f2e2-830186f2-3e3c600e");
-    command->SetQuality(90);
-    oracle.Schedule(*toto, command.release());
-  }
-
-
   // 2017-11-17-Anonymized
   loader1->LoadSeries(oracle, "cb3ea4d1-d08f3856-ad7b6314-74d88d77-60b05618");  // CT
   loader2->LoadInstance(oracle, "41029085-71718346-811efac4-420e2c15-d39f99b6");  // RT-DOSE
 
-  LOG(WARNING) << "...Waiting for Ctrl-C...";
-  Orthanc::SystemToolbox::ServerBarrier();
-  //boost::this_thread::sleep(boost::posix_time::seconds(1));
+  boost::this_thread::sleep(boost::posix_time::seconds(1));
 
   oracle.Stop();
 }
--- a/Samples/Sdl/TrackerSample.cpp	Fri May 10 14:54:03 2019 +0200
+++ b/Samples/Sdl/TrackerSample.cpp	Mon May 13 15:12:56 2019 +0200
@@ -18,24 +18,18 @@
  * along with this program. If not, see <http://www.gnu.org/licenses/>.
  **/
 
+#include "TrackerSampleApp.h"
 
  // From Stone
 #include "../../Applications/Sdl/SdlOpenGLWindow.h"
 #include "../../Framework/Scene2D/CairoCompositor.h"
 #include "../../Framework/Scene2D/ColorTextureSceneLayer.h"
 #include "../../Framework/Scene2D/OpenGLCompositor.h"
-#include "../../Framework/Scene2D/PanSceneTracker.h"
-#include "../../Framework/Scene2D/RotateSceneTracker.h"
-#include "../../Framework/Scene2D/Scene2D.h"
-#include "../../Framework/Scene2D/ZoomSceneTracker.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/shared_ptr.hpp>
 #include <boost/weak_ptr.hpp>
@@ -43,11 +37,6 @@
 #include <SDL.h>
 #include <stdio.h>
 
-
-// to be moved into Stone
-#include "../Common/MeasureTrackers.h"
-#include "../Common/MeasureCommands.h"
-
 /*
 TODO:
 
@@ -63,369 +52,6 @@
 using namespace Orthanc;
 using namespace OrthancStone;
 
-namespace OrthancStone
-{
-  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 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];
-  }
-}
-
-class TrackerSampleApp
-{
-public:
-  // 12 because.
-  TrackerSampleApp() : currentTool_(GuiTool_Rotate)
-  {
-    TEXTURE_2x2_1_ZINDEX  = 1;
-    TEXTURE_1x1_ZINDEX    = 2;
-    TEXTURE_2x2_2_ZINDEX  = 3;
-    LINESET_1_ZINDEX      = 4;
-    LINESET_2_ZINDEX      = 5;
-    INFOTEXT_LAYER_ZINDEX = 6;
-  }
-  void PrepareScene();
-  void Run();
-
-private:
-  Scene2D& GetScene()
-  {
-    return scene_;
-  }
-
-  void 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 HandleApplicationEvent(
-    const OpenGLCompositor& compositor,
-    const SDL_Event& event,
-    std::auto_ptr<IPointerTracker>& activeTracker);
-
-  IPointerTracker* TrackerHitTest(const PointerEvent& e);
-
-  IPointerTracker* CreateSuitableTracker(
-    const SDL_Event& event,
-    const PointerEvent& e,
-    const OpenGLCompositor& compositor);
-  
-  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(TrackerCommandPtr cmd);
-  void Undo();
-  void Redo();
-  
-private:
-  static const unsigned int FONT_SIZE = 32;
-
-  std::vector<TrackerCommandPtr> undoStack_;
-  
-  // we store the measure tools here so that they don't get deleted
-  std::vector<MeasureToolPtr> measureTools_;
-
-  //static const int LAYER_POSITION = 150;
-#if 0
-  int TEXTURE_2x2_1_ZINDEX = 12;
-  int TEXTURE_1x1_ZINDEX = 13;
-  int TEXTURE_2x2_2_ZINDEX = 14;
-  int LINESET_1_ZINDEX = 50;
-  int LINESET_2_ZINDEX = 100;
-  int INFOTEXT_LAYER_ZINDEX = 150;
-#else
-  int TEXTURE_2x2_1_ZINDEX;
-  int TEXTURE_1x1_ZINDEX;
-  int TEXTURE_2x2_2_ZINDEX;
-  int LINESET_1_ZINDEX;
-  int LINESET_2_ZINDEX;
-  int INFOTEXT_LAYER_ZINDEX;
-#endif
-  Scene2D scene_;
-  GuiTool currentTool_;
-};
-
-
-void TrackerSampleApp::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;
-
-    scene_.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);
-    scene_.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);
-    scene_.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);
-
-    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);
-
-    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);
-
-    layer->SetColor(0, 255, 255);
-    scene_.SetLayer(LINESET_1_ZINDEX, layer.release());
-  }
-
-  // Some text
-  {
-    std::auto_ptr<TextSceneLayer> layer(new TextSceneLayer);
-    layer->SetText("Hello");
-    scene_.SetLayer(LINESET_2_ZINDEX, layer.release());
-  }
-}
-
-
-void TrackerSampleApp::TakeScreenshot(const std::string& target,
-  unsigned int canvasWidth,
-  unsigned int canvasHeight)
-{
-  // Take a screenshot, then save it as PNG file
-  CairoCompositor compositor(scene_, canvasWidth, canvasHeight);
-  compositor.SetFont(0, Orthanc::EmbeddedResources::UBUNTU_FONT, FONT_SIZE, 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);
-}
-
-
-IPointerTracker* TrackerSampleApp::TrackerHitTest(const PointerEvent& e)
-{
-  // std::vector<MeasureToolPtr> measureTools_;
-  return NULL;
-}
-
-IPointerTracker* TrackerSampleApp::CreateSuitableTracker(
-  const SDL_Event& event, 
-  const PointerEvent& e, 
-  const OpenGLCompositor& compositor)
-{
-  switch (event.button.button)
-  {
-  case SDL_BUTTON_MIDDLE:
-    return new PanSceneTracker(scene_, e);
-
-  case SDL_BUTTON_RIGHT:
-    return new ZoomSceneTracker(
-      scene_, e, compositor.GetCanvasHeight());
-
-  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?)
-    IPointerTracker* hitTestTracker = TrackerHitTest(e);
-    
-    if (hitTestTracker != NULL)
-    {
-      return hitTestTracker;
-    }
-    else
-    { 
-      switch (currentTool_)
-      {
-      case GuiTool_Rotate:
-        return new RotateSceneTracker(scene_, e);
-      case GuiTool_LineMeasure:
-        return new CreateLineMeasureTracker(
-          scene_, undoStack_, measureTools_, e);
-        //case GuiTool_AngleMeasure:
-        //  return new AngleMeasureTracker(scene_, measureTools_, undoStack_, e);
-        //case GuiTool_CircleMeasure:
-        //  return new CircleMeasureTracker(scene_, measureTools_, undoStack_, e);
-        //case GuiTool_EllipseMeasure:
-        //  return new EllipseMeasureTracker(scene_, measureTools_, undoStack_, e);
-      default:
-        throw OrthancException(ErrorCode_InternalError, "Wrong tool!");
-      }
-    }
-  }
-  default:
-    return NULL;
-  }
-}
-
-void TrackerSampleApp::HandleApplicationEvent(
-  const OpenGLCompositor& compositor,
-  const SDL_Event& event,
-  std::auto_ptr<IPointerTracker>& activeTracker)
-{
-  if (event.type == SDL_MOUSEMOTION)
-  {
-    int scancodeCount = 0;
-    const uint8_t* keyboardState = SDL_GetKeyboardState(&scancodeCount);
-
-    if (activeTracker.get() == NULL &&
-      SDL_SCANCODE_LCTRL < scancodeCount &&
-      keyboardState[SDL_SCANCODE_LCTRL])
-    {
-      // The "left-ctrl" key is down, while no tracker is present
-
-      PointerEvent e;
-      e.AddPosition(compositor.GetPixelCenterCoordinates(event.button.x, event.button.y));
-
-      ScenePoint2D p = e.GetMainPosition().Apply(scene_.GetCanvasToSceneTransform());
-
-      char buf[64];
-      sprintf(buf, "(%0.02f,%0.02f)", p.GetX(), p.GetY());
-
-      if (scene_.HasLayer(INFOTEXT_LAYER_ZINDEX))
-      {
-        TextSceneLayer& layer =
-          dynamic_cast<TextSceneLayer&>(scene_.GetLayer(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());
-        scene_.SetLayer(INFOTEXT_LAYER_ZINDEX, layer.release());
-      }
-    }
-    else
-    {
-      scene_.DeleteLayer(INFOTEXT_LAYER_ZINDEX);
-    }
-  }
-  else if (event.type == SDL_MOUSEBUTTONDOWN)
-  {
-    PointerEvent e;
-    e.AddPosition(compositor.GetPixelCenterCoordinates(event.button.x, event.button.y));
-
-    activeTracker.reset(CreateSuitableTracker(event, e, compositor));
-  }
-  else if (event.type == SDL_KEYDOWN &&
-    event.key.repeat == 0 /* Ignore key bounce */)
-  {
-    switch (event.key.keysym.sym)
-    {
-    case SDLK_s:
-      scene_.FitContent(compositor.GetCanvasWidth(),
-        compositor.GetCanvasHeight());
-      break;
-
-    case SDLK_c:
-      TakeScreenshot(
-        "screenshot.png",
-        compositor.GetCanvasWidth(),
-        compositor.GetCanvasHeight());
-      break;
-
-    default:
-      break;
-    }
-  }
-}
-
 
 static void GLAPIENTRY
 OpenGLMessageCallback(GLenum source,
@@ -446,23 +72,19 @@
 
 bool g_stopApplication = false;
 
-void TrackerSampleApp::Run()
+void Run(TrackerSampleApp* app)
 {
   SdlOpenGLWindow window("Hello", 1024, 768);
 
-  scene_.FitContent(window.GetCanvasWidth(), window.GetCanvasHeight());
+  app->GetScene().FitContent(window.GetCanvasWidth(), window.GetCanvasHeight());
 
   glEnable(GL_DEBUG_OUTPUT);
   glDebugMessageCallback(OpenGLMessageCallback, 0);
 
-  OpenGLCompositor compositor(window, scene_);
+  OpenGLCompositor compositor(window, app->GetScene());
   compositor.SetFont(0, Orthanc::EmbeddedResources::UBUNTU_FONT,
     FONT_SIZE, Orthanc::Encoding_Latin1);
 
-  // this will either be empty or contain the current tracker, if any
-  std::auto_ptr<IPointerTracker>  tracker;
-
- 
   while (!g_stopApplication)
   {
     compositor.Refresh();
@@ -475,29 +97,10 @@
         g_stopApplication = true;
         break;
       }
-      else if (event.type == SDL_MOUSEMOTION)
-      {
-        if (tracker.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;
-          tracker->Update(e);
-        }
-      }
-      else if (event.type == SDL_MOUSEBUTTONUP)
-      {
-        if (tracker.get() != NULL)
-        {
-          tracker->Release();
-          tracker.reset(NULL);
-        }
-      }
       else if (event.type == SDL_WINDOWEVENT &&
         event.window.event == SDL_WINDOWEVENT_SIZE_CHANGED)
       {
-        tracker.reset(NULL);
+        app->DisableTracker(); // was: tracker.reset(NULL);
         compositor.UpdateSize();
       }
       else if (event.type == SDL_KEYDOWN &&
@@ -512,16 +115,11 @@
         case SDLK_q:
           g_stopApplication = true;
           break;
-
-        case SDLK_t:
-          SelectNextTool();
-          break;
-
         default:
           break;
         }
       }
-      HandleApplicationEvent(compositor, event, tracker);
+      app->HandleApplicationEvent(compositor, event);
     }
     SDL_Delay(1);
   }
@@ -537,12 +135,14 @@
 {
   StoneInitialize();
   Orthanc::Logging::EnableInfoLevel(true);
+  Orthanc::Logging::EnableTraceLevel(true);
+
 
   try
   {
     TrackerSampleApp app;
     app.PrepareScene();
-    app.Run();
+    Run(&app);
   }
   catch (Orthanc::OrthancException& e)
   {
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Sdl/TrackerSampleApp.cpp	Mon May 13 15:12:56 2019 +0200
@@ -0,0 +1,421 @@
+/**
+ * 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 "TrackerSampleApp.h"
+
+#include "../Common/CreateLineMeasureTracker.h"
+
+#include "../../Applications/Sdl/SdlOpenGLWindow.h"
+
+#include "../../Framework/Scene2D/PanSceneTracker.h"
+#include "../../Framework/Scene2D/RotateSceneTracker.h"
+#include "../../Framework/Scene2D/Scene2D.h"
+#include "../../Framework/Scene2D/ZoomSceneTracker.h"
+#include "../../Framework/Scene2D/CairoCompositor.h"
+#include "../../Framework/Scene2D/ColorTextureSceneLayer.h"
+#include "../../Framework/Scene2D/OpenGLCompositor.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 <SDL.h>
+#include <stdio.h>
+
+using namespace Orthanc;
+
+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];
+  }
+
+  Scene2D& TrackerSampleApp::GetScene()
+  {
+    return scene_;
+  }
+
+  void TrackerSampleApp::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 TrackerSampleApp::DisplayInfoText(const PointerEvent& e)
+  {
+    ScenePoint2D p = e.GetMainPosition().Apply(scene_.GetCanvasToSceneTransform());
+
+    char buf[64];
+    sprintf(buf, "(%0.02f,%0.02f)", p.GetX(), p.GetY());
+
+    if (scene_.HasLayer(INFOTEXT_LAYER_ZINDEX))
+    {
+      TextSceneLayer& layer =
+        dynamic_cast<TextSceneLayer&>(scene_.GetLayer(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());
+      scene_.SetLayer(INFOTEXT_LAYER_ZINDEX, layer.release());
+    }
+  }
+
+  void TrackerSampleApp::HideInfoText()
+  {
+    scene_.DeleteLayer(INFOTEXT_LAYER_ZINDEX);
+  }
+
+  void TrackerSampleApp::HandleApplicationEvent(
+    const OpenGLCompositor & compositor,
+    const SDL_Event & event)
+  {
+    if (event.type == SDL_MOUSEMOTION)
+    {
+      int scancodeCount = 0;
+      const uint8_t* keyboardState = SDL_GetKeyboardState(&scancodeCount);
+
+      if (activeTracker_.get() == NULL &&
+        SDL_SCANCODE_LCTRL < scancodeCount &&
+        keyboardState[SDL_SCANCODE_LCTRL])
+      {
+        // 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));
+        DisplayInfoText(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_->IsActive())
+            activeTracker_ = NULL;
+        }
+      }
+    }
+    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_->IsActive())
+          activeTracker_ = NULL;
+      }
+    }
+    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_->IsActive())
+          activeTracker_ = NULL;
+      }
+      else
+      {
+        // we ATTEMPT to create a tracker if need be
+        activeTracker_ = CreateSuitableTracker(event, e, compositor);
+      }
+    }
+    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_->IsActive())
+            activeTracker_ = NULL;
+        }
+        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:
+        scene_.FitContent(compositor.GetCanvasWidth(),
+          compositor.GetCanvasHeight());
+        break;
+
+      case SDLK_c:
+        TakeScreenshot(
+          "screenshot.png",
+          compositor.GetCanvasWidth(),
+          compositor.GetCanvasHeight());
+        break;
+
+      default:
+        break;
+      }
+    }
+  }
+
+  FlexiblePointerTrackerPtr TrackerSampleApp::CreateSuitableTracker(
+    const SDL_Event & event,
+    const PointerEvent & e,
+    const OpenGLCompositor & compositor)
+  {
+    switch (event.button.button)
+    {
+    case SDL_BUTTON_MIDDLE:
+      return CreateSimpleTrackerAdapter(PointerTrackerPtr(
+        new PanSceneTracker(scene_, e)));
+
+    case SDL_BUTTON_RIGHT:
+      return CreateSimpleTrackerAdapter(PointerTrackerPtr(
+        new ZoomSceneTracker(scene_, 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?)
+      FlexiblePointerTrackerPtr hitTestTracker = TrackerHitTest(e);
+
+      if (hitTestTracker != NULL)
+      {
+        //LOG(TRACE) << "hitTestTracker != NULL";
+        return hitTestTracker;
+      }
+      else
+      {
+        switch (currentTool_)
+        {
+        case GuiTool_Rotate:
+          //LOG(TRACE) << "Creating RotateSceneTracker";
+          return CreateSimpleTrackerAdapter(PointerTrackerPtr(
+            new RotateSceneTracker(scene_, e)));
+        case GuiTool_LineMeasure:
+          return FlexiblePointerTrackerPtr(new CreateLineMeasureTracker(
+            scene_, undoStack_, measureTools_, e));
+        case GuiTool_Pan:
+          return CreateSimpleTrackerAdapter(PointerTrackerPtr(
+            new PanSceneTracker(scene_, e)));
+        case GuiTool_Zoom:
+          return CreateSimpleTrackerAdapter(PointerTrackerPtr(
+            new ZoomSceneTracker(scene_, e, compositor.GetCanvasHeight())));
+        //case GuiTool_AngleMeasure:
+        //  return new AngleMeasureTracker(scene_, measureTools_, undoStack_, e);
+        //case GuiTool_CircleMeasure:
+        //  return new CircleMeasureTracker(scene_, measureTools_, undoStack_, e);
+        //case GuiTool_EllipseMeasure:
+        //  return new EllipseMeasureTracker(scene_, measureTools_, undoStack_, e);
+        case GuiTool_AngleMeasure:
+          LOG(ERROR) << "Not implemented yet!";
+          return NULL;
+        case GuiTool_CircleMeasure:
+          LOG(ERROR) << "Not implemented yet!";
+          return NULL;
+        case GuiTool_EllipseMeasure:
+          LOG(ERROR) << "Not implemented yet!";
+          return NULL;
+        default:
+          throw OrthancException(ErrorCode_InternalError, "Wrong tool!");
+        }
+      }
+    }
+    default:
+      return NULL;
+    }
+  }
+
+
+  void TrackerSampleApp::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;
+
+      scene_.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);
+      scene_.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);
+      scene_.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);
+
+      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);
+
+      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);
+
+      layer->SetColor(0, 255, 255);
+      scene_.SetLayer(LINESET_1_ZINDEX, layer.release());
+    }
+
+    // Some text
+    {
+      std::auto_ptr<TextSceneLayer> layer(new TextSceneLayer);
+      layer->SetText("Hello");
+      scene_.SetLayer(LINESET_2_ZINDEX, layer.release());
+    }
+  }
+
+
+  void TrackerSampleApp::DisableTracker()
+  {
+    if (activeTracker_)
+    {
+      activeTracker_->Cancel();
+      activeTracker_ = NULL;
+    }
+  }
+
+  void TrackerSampleApp::TakeScreenshot(const std::string& target,
+    unsigned int canvasWidth,
+    unsigned int canvasHeight)
+  {
+    // Take a screenshot, then save it as PNG file
+    CairoCompositor compositor(scene_, canvasWidth, canvasHeight);
+    compositor.SetFont(0, Orthanc::EmbeddedResources::UBUNTU_FONT, FONT_SIZE, 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);
+  }
+
+
+  FlexiblePointerTrackerPtr TrackerSampleApp::TrackerHitTest(const PointerEvent & e)
+  {
+    // std::vector<MeasureToolPtr> measureTools_;
+    return nullptr;
+  }
+
+
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Sdl/TrackerSampleApp.h	Mon May 13 15:12:56 2019 +0200
@@ -0,0 +1,136 @@
+/**
+ * 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/Scene2D/OpenGLCompositor.h>
+
+#include "../Common/IFlexiblePointerTracker.h"
+#include "../Common/MeasureTools.h"
+
+#include <SDL.h>
+
+#include <boost/make_shared.hpp>
+#include <boost/shared_ptr.hpp>
+
+namespace OrthancStone
+{
+  class TrackerCommand;
+  typedef boost::shared_ptr<TrackerCommand> TrackerCommandPtr;
+
+  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 = 32;
+
+  class Scene2D;
+
+  class TrackerSampleApp
+  {
+  public:
+    // 12 because.
+    TrackerSampleApp() : currentTool_(GuiTool_Rotate)
+    {
+      TEXTURE_2x2_1_ZINDEX = 1;
+      TEXTURE_1x1_ZINDEX = 2;
+      TEXTURE_2x2_2_ZINDEX = 3;
+      LINESET_1_ZINDEX = 4;
+      LINESET_2_ZINDEX = 5;
+      INFOTEXT_LAYER_ZINDEX = 6;
+    }
+    void PrepareScene();
+
+    void DisableTracker();
+
+    Scene2D& GetScene();
+
+    void HandleApplicationEvent(
+      const OpenGLCompositor& compositor,
+      const SDL_Event& event);
+
+  private:
+    void SelectNextTool();
+
+
+    FlexiblePointerTrackerPtr TrackerHitTest(const PointerEvent& e);
+
+    FlexiblePointerTrackerPtr CreateSuitableTracker(
+      const SDL_Event& event,
+      const PointerEvent& e,
+      const OpenGLCompositor& compositor);
+
+    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(TrackerCommandPtr cmd);
+    void Undo();
+    void Redo();
+
+  private:
+    void DisplayInfoText(const PointerEvent& e);
+    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.
+    */
+    Scene2D scene_;
+
+    FlexiblePointerTrackerPtr activeTracker_;
+    std::vector<TrackerCommandPtr> undoStack_;
+
+    // we store the measure tools here so that they don't get deleted
+    std::vector<MeasureToolPtr> measureTools_;
+
+    //static const int LAYER_POSITION = 150;
+#if 0
+    int TEXTURE_2x2_1_ZINDEX = 12;
+    int TEXTURE_1x1_ZINDEX = 13;
+    int TEXTURE_2x2_2_ZINDEX = 14;
+    int LINESET_1_ZINDEX = 50;
+    int LINESET_2_ZINDEX = 100;
+    int INFOTEXT_LAYER_ZINDEX = 150;
+#else
+    int TEXTURE_2x2_1_ZINDEX;
+    int TEXTURE_1x1_ZINDEX;
+    int TEXTURE_2x2_2_ZINDEX;
+    int LINESET_1_ZINDEX;
+    int LINESET_2_ZINDEX;
+    int INFOTEXT_LAYER_ZINDEX;
+#endif
+    GuiTool currentTool_;
+  };
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Sdl/cpp.hint	Mon May 13 15:12:56 2019 +0200
@@ -0,0 +1,1 @@
+#define ORTHANC_OVERRIDE