changeset 632:500c3f70b6c2

- Added a ClearAllChains method to PolylineSceneLayer --> revision must change when calling it ==> BumpRevision has been added to base class - Added some docs = Added GetMinDepth + GetMaxDepth to Scene2D (to alleviate the need for app- specific "Z depth registry" : clients may simply add a new layer on top or at the bottom of the existing layer set. - Added the line tracker measurement tools, commands and trackers. Generic base classes + Line measure - started work on the line measure handles
author Benjamin Golinvaux <bgo@osimis.io>
date Thu, 09 May 2019 10:41:31 +0200
parents 0925b27e8750
children b0652595b62a
files Framework/Scene2D/ColorSceneLayer.h Framework/Scene2D/IPointerTracker.h Framework/Scene2D/PolylineSceneLayer.cpp Framework/Scene2D/PolylineSceneLayer.h Framework/Scene2D/Scene2D.cpp Framework/Scene2D/Scene2D.h Samples/Common/MeasureCommands.cpp Samples/Common/MeasureCommands.h Samples/Common/MeasureTools.cpp Samples/Common/MeasureTools.h Samples/Common/MeasureTrackers.cpp Samples/Common/MeasureTrackers.h Samples/Sdl/CMakeLists.txt Samples/Sdl/TrackerSample.cpp
diffstat 14 files changed, 1295 insertions(+), 8 deletions(-) [+]
line wrap: on
line diff
--- a/Framework/Scene2D/ColorSceneLayer.h	Thu May 02 18:58:46 2019 +0200
+++ b/Framework/Scene2D/ColorSceneLayer.h	Thu May 09 10:41:31 2019 +0200
@@ -22,6 +22,7 @@
 #pragma once
 
 #include "ISceneLayer.h"
+#include <Core/Enumerations.h>
 
 #include <stdint.h>
 
@@ -33,7 +34,13 @@
     uint8_t  red_;
     uint8_t  green_;
     uint8_t  blue_;
-
+    uint64_t revision_;
+  protected:
+    void BumpRevision()
+    {
+      // this is *not* thread-safe!!!
+      revision_++;
+    }
   public:
     ColorSceneLayer() :
       red_(255),
@@ -42,6 +49,11 @@
     {
     }
 
+    virtual uint64_t GetRevision() const ORTHANC_OVERRIDE
+    {
+      return revision_;
+    }
+
     void SetColor(uint8_t red,
                   uint8_t green,
                   uint8_t blue)
@@ -49,6 +61,7 @@
       red_ = red;
       green_ = green;
       blue_ = blue;
+      BumpRevision();
     }
 
     uint8_t GetRed() const
--- a/Framework/Scene2D/IPointerTracker.h	Thu May 02 18:58:46 2019 +0200
+++ b/Framework/Scene2D/IPointerTracker.h	Thu May 09 10:41:31 2019 +0200
@@ -32,8 +32,15 @@
     {
     }
 
+    /**
+    This method will be repeatedly called during user interaction
+    */
     virtual void Update(const PointerEvent& event) = 0;
 
+    /**
+    This method will be called if the tracker is to be abandoned without
+    committing its result
+    */
     virtual void Release() = 0;
   };
 }
--- a/Framework/Scene2D/PolylineSceneLayer.cpp	Thu May 02 18:58:46 2019 +0200
+++ b/Framework/Scene2D/PolylineSceneLayer.cpp	Thu May 09 10:41:31 2019 +0200
@@ -42,6 +42,7 @@
     else
     {
       thickness_ = thickness;
+      BumpRevision();
     }
   }
 
@@ -52,6 +53,7 @@
     chains_ = from.chains_;
     closed_ = from.closed_;
     thickness_ = from.thickness_;
+    BumpRevision();
   }
 
   
@@ -69,10 +71,18 @@
     {
       chains_.push_back(chain);
       closed_.push_back(isClosed);
+      BumpRevision();
     }
   }
 
 
+  void PolylineSceneLayer::ClearAllChains()
+  {
+    chains_.clear();
+    closed_.clear();
+    BumpRevision();
+  }
+
   const PolylineSceneLayer::Chain& PolylineSceneLayer::GetChain(size_t i) const
   {
     if (i < chains_.size())
--- a/Framework/Scene2D/PolylineSceneLayer.h	Thu May 02 18:58:46 2019 +0200
+++ b/Framework/Scene2D/PolylineSceneLayer.h	Thu May 09 10:41:31 2019 +0200
@@ -60,6 +60,8 @@
     void AddChain(const Chain& chain,
                   bool isClosed);
 
+    void ClearAllChains();
+
     size_t GetChainsCount() const
     {
       return chains_.size();
@@ -75,10 +77,6 @@
     }
 
     virtual bool GetBoundingBox(Extent2D& target) const;
-    
-    virtual uint64_t GetRevision() const
-    {
-      return 0;
-    }
+   
   };
 }
--- a/Framework/Scene2D/Scene2D.cpp	Thu May 02 18:58:46 2019 +0200
+++ b/Framework/Scene2D/Scene2D.cpp	Thu May 09 10:41:31 2019 +0200
@@ -102,6 +102,8 @@
   void Scene2D::SetLayer(int depth,
                          ISceneLayer* layer)  // Takes ownership
   {
+    LOG(INFO) << "SetLayer(" << depth << ", " <<
+      reinterpret_cast<intptr_t>(layer) << ")";
     std::auto_ptr<Item> item(new Item(layer, layerCounter_++));
 
     if (layer == NULL)
@@ -126,10 +128,12 @@
 
   void Scene2D::DeleteLayer(int depth)
   {
+
     Content::iterator found = content_.find(depth);
 
     if (found != content_.end())
     {
+      LOG(INFO) << "DeleteLayer --found-- (" << depth << ")";
       assert(found->second != NULL);
       delete found->second;
       content_.erase(found);
@@ -159,6 +163,23 @@
   }
 
   
+  int Scene2D::GetMinDepth() const
+  {
+    if (content_.size() == 0)
+      return 0;
+    else
+      return content_.begin()->first;
+  }
+
+
+  int Scene2D::GetMaxDepth() const
+  {
+    if (content_.size() == 0)
+      return 0;
+    else
+      return content_.rbegin()->first;
+  }
+
   ISceneLayer* Scene2D::ReleaseLayer(int depth)
   {
     Content::iterator found = content_.find(depth);
--- a/Framework/Scene2D/Scene2D.h	Thu May 02 18:58:46 2019 +0200
+++ b/Framework/Scene2D/Scene2D.h	Thu May 09 10:41:31 2019 +0200
@@ -23,7 +23,6 @@
 
 #include "ISceneLayer.h"
 #include "../Toolbox/AffineTransform2D.h"
-
 #include <map>
 
 namespace OrthancStone
@@ -71,12 +70,29 @@
     void SetLayer(int depth,
                   ISceneLayer* layer);  // Takes ownership
 
+    /**
+    Removes the layer at specified depth and deletes the underlying object
+    */
     void DeleteLayer(int depth);
 
     bool HasLayer(int depth) const;
 
     ISceneLayer& GetLayer(int depth) const;
 
+    /**
+    Returns the minimum depth among all layers or 0 if there are no layers
+    */
+    int GetMinDepth() const;
+
+    /**
+    Returns the minimum depth among all layers or 0 if there are no layers
+    */
+    int GetMaxDepth() const;
+
+    /**
+    Removes the layer at specified depth and transfers the object 
+    ownership to the caller
+    */
     ISceneLayer* ReleaseLayer(int depth);
 
     void Apply(IVisitor& visitor) const;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Common/MeasureCommands.cpp	Thu May 09 10:41:31 2019 +0200
@@ -0,0 +1,65 @@
+/**
+ * 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 "MeasureCommands.h"
+
+namespace OrthancStone
+{
+  void CreateMeasureCommand::Undo()
+  {
+    // simply disable the measure tool upon undo
+    GetMeasureTool()->Disable();
+  }
+
+  void CreateMeasureCommand::Redo()
+  {
+    GetMeasureTool()->Enable();
+  }
+
+  CreateMeasureCommand::CreateMeasureCommand(
+    Scene2D& scene, MeasureToolList& measureTools)
+    : TrackerCommand(scene)
+    , measureTools_(measureTools)
+  {
+
+  }
+
+  CreateMeasureCommand::~CreateMeasureCommand()
+  {
+    // deleting the command should not change the model state
+    // we thus leave it as is
+  }
+
+  CreateLineMeasureCommand::CreateLineMeasureCommand(
+    Scene2D& scene, MeasureToolList& measureTools, ScenePoint2D point)
+    : 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)
+  {
+    measureTool_->SetEnd(scenePos);
+  }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Common/MeasureCommands.h	Thu May 09 10:41:31 2019 +0200
@@ -0,0 +1,91 @@
+/**
+ * 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/Scene2D.h>
+#include <boost/shared_ptr.hpp>
+
+// to be moved into Stone
+#include "MeasureTools.h"
+
+namespace OrthancStone
+{
+  class TrackerCommand
+  {
+  public:
+    TrackerCommand(Scene2D& scene) : scene_(scene)
+    {
+
+    }
+    virtual void Undo() = 0;
+    virtual void Redo() = 0;
+    virtual void Update(ScenePoint2D scenePos) = 0;
+
+    Scene2D& GetScene()
+    {
+      return scene_;
+    }
+
+  protected:
+    Scene2D& scene_;
+  private:
+    TrackerCommand(const TrackerCommand&);
+    TrackerCommand& operator=(const TrackerCommand&);
+  };
+
+  typedef boost::shared_ptr<TrackerCommand> TrackerCommandPtr;
+  
+  class CreateMeasureCommand : public TrackerCommand
+  {
+  public:
+    CreateMeasureCommand(Scene2D& scene, MeasureToolList& measureTools);
+    ~CreateMeasureCommand();
+    virtual void Undo() ORTHANC_OVERRIDE;
+    virtual void Redo() ORTHANC_OVERRIDE;
+  protected:
+    MeasureToolList& measureTools_;
+  private:
+    /** Must be implemented by the subclasses that create the actual tool */
+    virtual MeasureToolPtr GetMeasureTool() = 0;
+  };
+
+  typedef boost::shared_ptr<CreateMeasureCommand> CreateMeasureCommandPtr;
+
+  class CreateLineMeasureCommand : public CreateMeasureCommand
+  {
+  public:
+    CreateLineMeasureCommand::CreateLineMeasureCommand(
+      Scene2D& scene, MeasureToolList& measureTools, ScenePoint2D point);
+    
+    void Update(ScenePoint2D scenePos) ORTHANC_OVERRIDE;
+
+  private:
+    virtual MeasureToolPtr GetMeasureTool() ORTHANC_OVERRIDE
+    {
+      return measureTool_;
+    }
+    LineMeasureToolPtr measureTool_;
+    ScenePoint2D       start_;
+    ScenePoint2D       end_;
+  };
+
+  typedef boost::shared_ptr<CreateLineMeasureCommand> CreateLineMeasureCommandPtr;
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Common/MeasureTools.cpp	Thu May 09 10:41:31 2019 +0200
@@ -0,0 +1,196 @@
+/**
+ * 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 <Core/Logging.h>
+#include "MeasureTools.h"
+
+namespace OrthancStone
+{
+  void MeasureTool::Enable()
+  {
+    enabled_ = true;
+    RefreshScene();
+  }
+
+  void MeasureTool::Disable()
+  {
+    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
+        {
+          auto startC = start_.Apply(GetScene().GetSceneToCanvasTransform());
+          auto squareSize = 10; //TODO: take DPI into account
+          auto startHandleLX = startC.GetX() - squareSize/2;
+          auto startHandleTY = startC.GetY() - squareSize / 2;
+          auto startHandleRX = startC.GetX() + squareSize / 2;
+          auto startHandleBY = startC.GetY() + squareSize / 2;
+          auto startLTC = ScenePoint2D(startHandleLX, startHandleTY);
+          auto startRTC = ScenePoint2D(startHandleRX, startHandleTY);
+          auto startRBC = ScenePoint2D(startHandleRX, startHandleBY);
+          auto startLBC = ScenePoint2D(startHandleLX, startHandleBY);
+
+          auto startLT = startLTC.Apply(GetScene().GetCanvasToSceneTransform());
+          auto startRT = startRTC.Apply(GetScene().GetCanvasToSceneTransform());
+          auto startRB = startRBC.Apply(GetScene().GetCanvasToSceneTransform());
+          auto 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;
+      }
+    }
+  }
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Common/MeasureTools.h	Thu May 09 10:41:31 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/>.
+ **/
+#pragma once
+
+#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 <vector>
+#include <cmath>
+
+namespace OrthancStone
+{
+  class MeasureTool
+  {
+  public:
+    virtual ~MeasureTool() {}
+
+    /**
+    Enabled tools are rendered in the scene.
+    */
+    void Enable();
+
+    /**
+    Disabled tools are not rendered in the scene. This is useful to be able
+    to use them as their own memento in command stacks (when a measure tool
+    creation command has been undone, the measure remains alive in the
+    command object but is disabled so that it can be redone later on easily)
+    */
+    void Disable();
+
+  protected:
+    MeasureTool(Scene2D& scene) 
+      : scene_(scene)
+      , enabled_(true)
+    {
+    }
+  
+
+
+    /**
+    This is the meat of the tool: this method must [create (if needed) and]
+    update the layers and their data according to the measure tool kind and
+    current state. This is repeatedly called during user interaction
+    */
+    virtual void RefreshScene() = 0;
+
+
+    Scene2D& GetScene()
+    {
+      return scene_;
+    }
+
+    /**
+    enabled_ is not accessible by subclasses because there is a state machine
+    that we do not wanna mess with
+    */
+    bool IsEnabled() const
+    {
+      return enabled_;
+    }
+
+  private:
+    Scene2D& scene_;
+    bool     enabled_;
+  };
+
+  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/MeasureTrackers.cpp	Thu May 09 10:41:31 2019 +0200
@@ -0,0 +1,92 @@
+/**
+ * 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 "MeasureTrackers.h"
+#include <Core/OrthancException.h>
+
+using namespace Orthanc;
+
+namespace OrthancStone
+{
+
+  CreateMeasureTracker::CreateMeasureTracker(
+    Scene2D&                        scene,
+    std::vector<TrackerCommandPtr>& undoStack,
+    std::vector<MeasureToolPtr>&    measureTools)
+    : scene_(scene)
+    , undoStack_(undoStack)
+    , measureTools_(measureTools)
+    , commitResult_(true)
+  {
+  }
+
+  CreateMeasureTracker::~CreateMeasureTracker()
+  {
+    // if the tracker completes successfully, we add the command
+    // to the undo stack
+
+    // otherwise, we simply undo it
+    if (commitResult_)
+      undoStack_.push_back(command_);
+    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;
+  }
+
+}
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Common/MeasureTrackers.h	Thu May 09 10:41:31 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 "../../Framework/Scene2D/IPointerTracker.h"
+#include "../../Framework/Scene2D/Scene2D.h"
+
+#include "MeasureTools.h"
+#include "MeasureCommands.h"
+
+#include <vector>
+
+namespace OrthancStone
+{
+  class CreateMeasureTracker : public IPointerTracker
+  {
+  public:
+    virtual void Update(const PointerEvent& e) ORTHANC_OVERRIDE;
+    virtual void Release() ORTHANC_OVERRIDE;
+  protected:
+    CreateMeasureTracker(
+      Scene2D&                        scene,
+      std::vector<TrackerCommandPtr>& undoStack,
+      std::vector<MeasureToolPtr>&    measureTools);
+
+    ~CreateMeasureTracker();
+  
+  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	Thu May 02 18:58:46 2019 +0200
+++ b/Samples/Sdl/CMakeLists.txt	Thu May 09 10:41:31 2019 +0200
@@ -37,6 +37,7 @@
   UBUNTU_FONT  ${CMAKE_BINARY_DIR}/ubuntu-font-family-0.83/Ubuntu-R.ttf
   )
 
+SET(ENABLE_SDL_CONSOLE OFF CACHE BOOL "Enable the use of the MIT-licensed SDL_Console")
 SET(ENABLE_GOOGLE_TEST OFF)
 SET(ENABLE_LOCALE ON)
 SET(ENABLE_SDL ON)
@@ -46,7 +47,6 @@
 
 include(${CMAKE_SOURCE_DIR}/../../Resources/CMake/OrthancStoneConfiguration.cmake)
 
-
 #####################################################################
 ## Build the samples
 #####################################################################
@@ -60,3 +60,26 @@
   )
 
 target_link_libraries(BasicScene OrthancStone)
+
+if(ENABLE_SDL_CONSOLE)
+  add_definitions(
+    -DENABLE_SDL_CONSOLE=1
+    )
+  LIST(APPEND TRACKERSAMPLE_SOURCE "../../../SDL-Console/SDL_Console.c")
+  LIST(APPEND TRACKERSAMPLE_SOURCE "../../../SDL-Console/SDL_Console.h")
+endif()
+
+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/MeasureTrackers.cpp")
+LIST(APPEND TRACKERSAMPLE_SOURCE "../Common/MeasureTrackers.h")
+LIST(APPEND TRACKERSAMPLE_SOURCE "TrackerSample.cpp")
+
+add_executable(TrackerSample
+  ${TRACKERSAMPLE_SOURCE}
+  )
+
+target_link_libraries(TrackerSample OrthancStone)
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Sdl/TrackerSample.cpp	Thu May 09 10:41:31 2019 +0200
@@ -0,0 +1,555 @@
+/**
+ * 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/>.
+ **/
+
+
+ // 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>
+
+#include <SDL.h>
+#include <stdio.h>
+
+
+// to be moved into Stone
+#include "../Common/MeasureTrackers.h"
+#include "../Common/MeasureCommands.h"
+
+/*
+TODO:
+
+- to decouple the trackers from the sample, we need to supply them with
+  the scene rather than the app
+
+- in order to do that, we need a GetNextFreeZIndex function (or something 
+  along those lines) in the scene object
+
+*/
+
+
+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* TrackerSampleApp::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 nullptr;
+}
+
+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,
+  GLenum type,
+  GLuint id,
+  GLenum severity,
+  GLsizei length,
+  const GLchar* message,
+  const void* userParam)
+{
+  if (severity != GL_DEBUG_SEVERITY_NOTIFICATION)
+  {
+    fprintf(stderr, "GL CALLBACK: %s type = 0x%x, severity = 0x%x, message = %s\n",
+      (type == GL_DEBUG_TYPE_ERROR ? "** GL ERROR **" : ""),
+      type, severity, message);
+  }
+}
+
+bool g_stopApplication = false;
+
+void TrackerSampleApp::Run()
+{
+  SdlOpenGLWindow window("Hello", 1024, 768);
+
+  scene_.FitContent(window.GetCanvasWidth(), window.GetCanvasHeight());
+
+  glEnable(GL_DEBUG_OUTPUT);
+  glDebugMessageCallback(OpenGLMessageCallback, 0);
+
+  OpenGLCompositor compositor(window, scene_);
+  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();
+
+    SDL_Event event;
+    while (!g_stopApplication && SDL_PollEvent(&event))
+    {
+      if (event.type == SDL_QUIT)
+      {
+        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);
+        compositor.UpdateSize();
+      }
+      else if (event.type == SDL_KEYDOWN &&
+        event.key.repeat == 0 /* Ignore key bounce */)
+      {
+        switch (event.key.keysym.sym)
+        {
+        case SDLK_f:
+          window.GetWindow().ToggleMaximize();
+          break;
+
+        case SDLK_q:
+          g_stopApplication = true;
+          break;
+
+        case SDLK_t:
+          SelectNextTool();
+          break;
+
+        default:
+          break;
+        }
+      }
+      HandleApplicationEvent(compositor, event, tracker);
+    }
+    SDL_Delay(1);
+  }
+}
+
+
+/**
+ * IMPORTANT: The full arguments to "main()" are needed for SDL on
+ * Windows. Otherwise, one gets the linking error "undefined reference
+ * to `SDL_main'". https://wiki.libsdl.org/FAQWindows
+ **/
+int main(int argc, char* argv[])
+{
+  StoneInitialize();
+  Orthanc::Logging::EnableInfoLevel(true);
+
+  try
+  {
+    TrackerSampleApp app;
+    app.PrepareScene();
+    app.Run();
+  }
+  catch (Orthanc::OrthancException& e)
+  {
+    LOG(ERROR) << "EXCEPTION: " << e.What();
+  }
+
+  StoneFinalize();
+
+  return 0;
+}