changeset 506:801d2697a1b1 bgo-commands-codegen

Merge with am-touch-events
author Benjamin Golinvaux <bgo@osimis.io>
date Tue, 26 Feb 2019 21:33:16 +0100
parents 4cc7bb55bd49 (current diff) 77e0eb83ff63 (diff)
children ce49eae4c887
files Applications/Commands/ICommand.h Applications/Samples/CMakeLists.txt Applications/Samples/SimpleViewer/MainWidgetInteractor.h Applications/Samples/SingleFrameEditorApplication.h Applications/Samples/build-wasm.sh Platforms/Wasm/wasm-viewport.ts README.md Resources/CMake/OrthancStoneConfiguration.cmake
diffstat 73 files changed, 1613 insertions(+), 346 deletions(-) [+]
line wrap: on
line diff
--- a/Applications/Commands/BaseCommandBuilder.cpp	Tue Feb 26 21:26:47 2019 +0100
+++ b/Applications/Commands/BaseCommandBuilder.cpp	Tue Feb 26 21:33:16 2019 +0100
@@ -34,13 +34,12 @@
 
     if (commandJson["commandType"].isString() && commandJson["commandType"].asString() == "generic-no-arg-command")
     {
-        printf("creating a simple command\n");
-        return new GenericNoArgCommand(commandJson["command"].asString().c_str());
+      return new GenericNoArgCommand(commandJson["command"].asString().c_str());
     }
     else if (commandJson["commandType"].isString() && commandJson["commandType"].asString() == "generic-one-string-arg-command")
     {
-        printf("creating a simple command\n");
-        return new GenericNoArgCommand(commandJson["command"].asString().c_str());
+      // TODO: we should create a command with a string arg !
+      return new GenericNoArgCommand(commandJson["command"].asString().c_str());
     }
 
     return NULL;
--- a/Applications/Commands/ICommand.h	Tue Feb 26 21:26:47 2019 +0100
+++ b/Applications/Commands/ICommand.h	Tue Feb 26 21:33:16 2019 +0100
@@ -38,6 +38,7 @@
     virtual ~ICommand() 
     {}
     virtual void Execute() = 0;
+    virtual ~ICommand() {}
 //    virtual void Configure(const Json::Value& arguments) = 0;
     const std::string& GetName() const
     {
--- a/Applications/Qt/QCairoWidget.cpp	Tue Feb 26 21:26:47 2019 +0100
+++ b/Applications/Qt/QCairoWidget.cpp	Tue Feb 26 21:33:16 2019 +0100
@@ -125,7 +125,7 @@
 
   {
     OrthancStone::NativeStoneApplicationContext::GlobalMutexLocker locker(*context_);
-    locker.GetCentralViewport().MouseDown(button, event->pos().x(), event->pos().y(), stoneModifiers);
+    locker.GetCentralViewport().MouseDown(button, event->pos().x(), event->pos().y(), stoneModifiers, std::vector<OrthancStone::Touch>());
   }
 }
 
@@ -140,7 +140,7 @@
 void QCairoWidget::mouseMoveEvent(QMouseEvent* event)
 {
   OrthancStone::NativeStoneApplicationContext::GlobalMutexLocker locker(*context_);
-  locker.GetCentralViewport().MouseMove(event->pos().x(), event->pos().y());
+  locker.GetCentralViewport().MouseMove(event->pos().x(), event->pos().y(), std::vector<OrthancStone::Touch>());
 }
 
 
--- a/Applications/Samples/SimpleViewer/MainWidgetInteractor.cpp	Tue Feb 26 21:26:47 2019 +0100
+++ b/Applications/Samples/SimpleViewer/MainWidgetInteractor.cpp	Tue Feb 26 21:33:16 2019 +0100
@@ -32,7 +32,8 @@
                                                                     int viewportY,
                                                                     double x,
                                                                     double y,
-                                                                    IStatusBar* statusBar)
+                                                                    IStatusBar* statusBar,
+                                                                    const std::vector<Touch>& displayTouches)
   {
     if (button == MouseButton_Left)
     {
--- a/Applications/Samples/SimpleViewer/MainWidgetInteractor.h	Tue Feb 26 21:26:47 2019 +0100
+++ b/Applications/Samples/SimpleViewer/MainWidgetInteractor.h	Tue Feb 26 21:33:16 2019 +0100
@@ -50,7 +50,8 @@
                                                         int viewportY,
                                                         double x,
                                                         double y,
-                                                        IStatusBar* statusBar);
+                                                        IStatusBar* statusBar,
+                                                        const std::vector<Touch>& displayTouches);
 
     virtual void MouseOver(CairoContext& context,
                            WorldSceneWidget& widget,
--- a/Applications/Samples/SimpleViewer/ThumbnailInteractor.cpp	Tue Feb 26 21:26:47 2019 +0100
+++ b/Applications/Samples/SimpleViewer/ThumbnailInteractor.cpp	Tue Feb 26 21:33:16 2019 +0100
@@ -32,7 +32,8 @@
                                                                    int viewportY,
                                                                    double x,
                                                                    double y,
-                                                                   IStatusBar* statusBar)
+                                                                   IStatusBar* statusBar,
+                                                                   const std::vector<Touch>& displayTouches)
   {
     if (button == MouseButton_Left)
     {
--- a/Applications/Samples/SimpleViewer/ThumbnailInteractor.h	Tue Feb 26 21:26:47 2019 +0100
+++ b/Applications/Samples/SimpleViewer/ThumbnailInteractor.h	Tue Feb 26 21:33:16 2019 +0100
@@ -47,7 +47,8 @@
                                                         int viewportY,
                                                         double x,
                                                         double y,
-                                                        IStatusBar* statusBar);
+                                                        IStatusBar* statusBar,
+                                                        const std::vector<Touch>& displayTouches);
 
     virtual void MouseOver(CairoContext& context,
                            WorldSceneWidget& widget,
--- a/Applications/Samples/SimpleViewerApplicationSingleFile.h	Tue Feb 26 21:26:47 2019 +0100
+++ b/Applications/Samples/SimpleViewerApplicationSingleFile.h	Tue Feb 26 21:33:16 2019 +0100
@@ -66,7 +66,8 @@
                                                             int viewportY,
                                                             double x,
                                                             double y,
-                                                            IStatusBar* statusBar)
+                                                            IStatusBar* statusBar,
+                                                            const std::vector<Touch>& displayTouches)
         {
           if (button == MouseButton_Left)
           {
@@ -121,7 +122,8 @@
                                                             int viewportY,
                                                             double x,
                                                             double y,
-                                                            IStatusBar* statusBar)
+                                                            IStatusBar* statusBar,
+                                                            const std::vector<Touch>& displayTouches)
         {
           if (button == MouseButton_Left)
           {
--- a/Applications/Samples/SingleFrameApplication.h	Tue Feb 26 21:26:47 2019 +0100
+++ b/Applications/Samples/SingleFrameApplication.h	Tue Feb 26 21:33:16 2019 +0100
@@ -60,7 +60,8 @@
                                                             int viewportY,
                                                             double x,
                                                             double y,
-                                                            IStatusBar* statusBar)
+                                                            IStatusBar* statusBar,
+                                                            const std::vector<Touch>& displayTouches)
         {
           return NULL;
         }
--- a/Applications/Sdl/SdlEngine.cpp	Tue Feb 26 21:26:47 2019 +0100
+++ b/Applications/Sdl/SdlEngine.cpp	Tue Feb 26 21:33:16 2019 +0100
@@ -146,15 +146,15 @@
           switch (event.button.button)
           {
           case SDL_BUTTON_LEFT:
-            locker.GetCentralViewport().MouseDown(MouseButton_Left, event.button.x, event.button.y, modifiers);
+            locker.GetCentralViewport().MouseDown(MouseButton_Left, event.button.x, event.button.y, modifiers, std::vector<Touch>());
             break;
             
           case SDL_BUTTON_RIGHT:
-            locker.GetCentralViewport().MouseDown(MouseButton_Right, event.button.x, event.button.y, modifiers);
+            locker.GetCentralViewport().MouseDown(MouseButton_Right, event.button.x, event.button.y, modifiers, std::vector<Touch>());
             break;
             
           case SDL_BUTTON_MIDDLE:
-            locker.GetCentralViewport().MouseDown(MouseButton_Middle, event.button.x, event.button.y, modifiers);
+            locker.GetCentralViewport().MouseDown(MouseButton_Middle, event.button.x, event.button.y, modifiers, std::vector<Touch>());
             break;
 
           default:
@@ -163,7 +163,7 @@
         }
         else if (event.type == SDL_MOUSEMOTION)
         {
-          locker.GetCentralViewport().MouseMove(event.button.x, event.button.y);
+          locker.GetCentralViewport().MouseMove(event.button.x, event.button.y, std::vector<Touch>());
         }
         else if (event.type == SDL_MOUSEBUTTONUP)
         {
--- a/Framework/Layers/CircleMeasureTracker.cpp	Tue Feb 26 21:26:47 2019 +0100
+++ b/Framework/Layers/CircleMeasureTracker.cpp	Tue Feb 26 21:33:16 2019 +0100
@@ -91,7 +91,9 @@
   void CircleMeasureTracker::MouseMove(int displayX,
                                        int displayY,
                                        double x,
-                                       double y)
+                                       double y,
+                                       const std::vector<Touch>& displayTouches,
+                                       const std::vector<Touch>& sceneTouches)
   {
     x2_ = x;
     y2_ = y;
--- a/Framework/Layers/CircleMeasureTracker.h	Tue Feb 26 21:26:47 2019 +0100
+++ b/Framework/Layers/CircleMeasureTracker.h	Tue Feb 26 21:33:16 2019 +0100
@@ -72,6 +72,8 @@
     virtual void MouseMove(int displayX,
                            int displayY,
                            double x,
-                           double y);
+                           double y,
+                           const std::vector<Touch>& displayTouches,
+                           const std::vector<Touch>& sceneTouches);
   };
 }
--- a/Framework/Layers/LineMeasureTracker.cpp	Tue Feb 26 21:26:47 2019 +0100
+++ b/Framework/Layers/LineMeasureTracker.cpp	Tue Feb 26 21:33:16 2019 +0100
@@ -87,7 +87,9 @@
   void LineMeasureTracker::MouseMove(int displayX,
                                      int displayY,
                                      double x,
-                                     double y)
+                                     double y,
+                                     const std::vector<Touch>& displayTouches,
+                                     const std::vector<Touch>& sceneTouches)
   {
     x2_ = x;
     y2_ = y;
--- a/Framework/Layers/LineMeasureTracker.h	Tue Feb 26 21:26:47 2019 +0100
+++ b/Framework/Layers/LineMeasureTracker.h	Tue Feb 26 21:33:16 2019 +0100
@@ -71,6 +71,8 @@
     virtual void MouseMove(int displayX,
                            int displayY,
                            double x,
-                           double y);
+                           double y,
+                           const std::vector<Touch>& displayTouches,
+                           const std::vector<Touch>& sceneTouches);
   };
 }
--- a/Framework/Radiography/RadiographyAlphaLayer.cpp	Tue Feb 26 21:26:47 2019 +0100
+++ b/Framework/Radiography/RadiographyAlphaLayer.cpp	Tue Feb 26 21:33:16 2019 +0100
@@ -45,6 +45,8 @@
 
     SetSize(image->GetWidth(), image->GetHeight());
     alpha_ = raii;
+
+    EmitMessage(RadiographyLayer::LayerEditedMessage(*this));
   }
 
   void RadiographyAlphaLayer::Render(Orthanc::ImageAccessor& buffer,
@@ -84,7 +86,7 @@
     if (useWindowing_)
     {
       float center, width;
-      if (scene_.GetWindowing(center, width))
+      if (GetScene().GetWindowing(center, width))
       {
         value = center + width / 2.0f;  // set it to the maximum pixel value of the image
       }
--- a/Framework/Radiography/RadiographyAlphaLayer.h	Tue Feb 26 21:26:47 2019 +0100
+++ b/Framework/Radiography/RadiographyAlphaLayer.h	Tue Feb 26 21:33:16 2019 +0100
@@ -33,14 +33,13 @@
   class RadiographyAlphaLayer : public RadiographyLayer
   {
   private:
-    const RadiographyScene&                scene_;
     std::auto_ptr<Orthanc::ImageAccessor>  alpha_;      // Grayscale8
     bool                                   useWindowing_;
     float                                  foreground_;
 
   public:
-    RadiographyAlphaLayer(const RadiographyScene& scene) :
-      scene_(scene),
+    RadiographyAlphaLayer(MessageBroker& broker, const RadiographyScene& scene) :
+      RadiographyLayer(broker, scene),
       useWindowing_(true),
       foreground_(0)
     {
--- a/Framework/Radiography/RadiographyDicomLayer.cpp	Tue Feb 26 21:26:47 2019 +0100
+++ b/Framework/Radiography/RadiographyDicomLayer.cpp	Tue Feb 26 21:33:16 2019 +0100
@@ -102,6 +102,8 @@
 
     source_ = raii;
     ApplyConverter();
+
+    EmitMessage(RadiographyLayer::LayerEditedMessage(*this));
   }
 
   void RadiographyDicomLayer::Render(Orthanc::ImageAccessor& buffer,
--- a/Framework/Radiography/RadiographyDicomLayer.h	Tue Feb 26 21:26:47 2019 +0100
+++ b/Framework/Radiography/RadiographyDicomLayer.h	Tue Feb 26 21:33:16 2019 +0100
@@ -41,6 +41,11 @@
     void ApplyConverter();
 
   public:
+    RadiographyDicomLayer(MessageBroker& broker, const RadiographyScene& scene)
+      : RadiographyLayer(broker, scene)
+    {
+    }
+
     void SetInstance(const std::string& instanceId, unsigned int frame)
     {
       instanceId_ = instanceId;
--- a/Framework/Radiography/RadiographyLayer.cpp	Tue Feb 26 21:26:47 2019 +0100
+++ b/Framework/Radiography/RadiographyLayer.cpp	Tue Feb 26 21:33:16 2019 +0100
@@ -76,58 +76,14 @@
                                      double x,
                                      double y) const
   {
-    transform_.Apply(x, y);
+    GetTransform().Apply(x, y);
     extent.AddPoint(x, y);
   }
 
-
-  void RadiographyLayer::GetCornerInternal(double& x,
-                                           double& y,
-                                           Corner corner,
-                                           unsigned int cropX,
-                                           unsigned int cropY,
-                                           unsigned int cropWidth,
-                                           unsigned int cropHeight) const
-  {
-    double dx = static_cast<double>(cropX);
-    double dy = static_cast<double>(cropY);
-    double dwidth = static_cast<double>(cropWidth);
-    double dheight = static_cast<double>(cropHeight);
-
-    switch (corner)
-    {
-    case Corner_TopLeft:
-      x = dx;
-      y = dy;
-      break;
-
-    case Corner_TopRight:
-      x = dx + dwidth;
-      y = dy;
-      break;
-
-    case Corner_BottomLeft:
-      x = dx;
-      y = dy + dheight;
-      break;
-
-    case Corner_BottomRight:
-      x = dx + dwidth;
-      y = dy + dheight;
-      break;
-
-    default:
-      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
-    }
-
-    transform_.Apply(x, y);
-  }
-
-
   bool RadiographyLayer::Contains(double x,
                                   double y) const
   {
-    transformInverse_.Apply(x, y);
+    GetTransformInverse().Apply(x, y);
 
     unsigned int cropX, cropY, cropWidth, cropHeight;
     GetCrop(cropX, cropY, cropWidth, cropHeight);
@@ -140,53 +96,35 @@
   void RadiographyLayer::DrawBorders(CairoContext& context,
                                      double zoom)
   {
-    unsigned int cx, cy, width, height;
-    GetCrop(cx, cy, width, height);
-
-    double dx = static_cast<double>(cx);
-    double dy = static_cast<double>(cy);
-    double dwidth = static_cast<double>(width);
-    double dheight = static_cast<double>(height);
+    if (GetControlPointCount() < 3 )
+      return;
 
     cairo_t* cr = context.GetObject();
     cairo_set_line_width(cr, 2.0 / zoom);
 
-    double x, y;
-    x = dx;
-    y = dy;
-    transform_.Apply(x, y);
-    cairo_move_to(cr, x, y);
-
-    x = dx + dwidth;
-    y = dy;
-    transform_.Apply(x, y);
-    cairo_line_to(cr, x, y);
+    ControlPoint cp;
+    GetControlPoint(cp, 0);
+    cairo_move_to(cr, cp.x, cp.y);
 
-    x = dx + dwidth;
-    y = dy + dheight;
-    transform_.Apply(x, y);
-    cairo_line_to(cr, x, y);
+    for (size_t i = 0; i < GetControlPointCount(); i++)
+    {
+      GetControlPoint(cp, i);
+      cairo_line_to(cr, cp.x, cp.y);
+    }
 
-    x = dx;
-    y = dy + dheight;
-    transform_.Apply(x, y);
-    cairo_line_to(cr, x, y);
-
-    x = dx;
-    y = dy;
-    transform_.Apply(x, y);
-    cairo_line_to(cr, x, y);
-
+    cairo_close_path(cr);
     cairo_stroke(cr);
   }
 
 
-  RadiographyLayer::RadiographyLayer() :
+  RadiographyLayer::RadiographyLayer(MessageBroker& broker, const RadiographyScene& scene) :
+    IObservable(broker),
     index_(0),
     hasSize_(false),
     width_(0),
     height_(0),
-    prefferedPhotometricDisplayMode_(PhotometricDisplayMode_Default)
+    prefferedPhotometricDisplayMode_(PhotometricDisplayMode_Default),
+    scene_(scene)
   {
     UpdateTransform();
   }
@@ -197,6 +135,13 @@
     UpdateTransform();
   }
 
+  void RadiographyLayer::SetPreferredPhotomotricDisplayMode(PhotometricDisplayMode  prefferedPhotometricDisplayMode)
+  {
+    prefferedPhotometricDisplayMode_ = prefferedPhotometricDisplayMode;
+
+    EmitMessage(RadiographyLayer::LayerEditedMessage(*this));
+  }
+
   void RadiographyLayer::SetCrop(unsigned int x,
                                  unsigned int y,
                                  unsigned int width,
@@ -215,6 +160,8 @@
 
     geometry_.SetCrop(x, y, width, height);
     UpdateTransform();
+
+    EmitMessage(RadiographyLayer::LayerEditedMessage(*this));
   }
 
   void RadiographyLayer::SetGeometry(const Geometry& geometry)
@@ -225,6 +172,8 @@
     {
       UpdateTransform();
     }
+
+    EmitMessage(RadiographyLayer::LayerEditedMessage(*this));
   }
 
 
@@ -237,7 +186,7 @@
     {
       GetGeometry().GetCrop(x, y, width, height);
     }
-    else 
+    else
     {
       x = 0;
       y = 0;
@@ -251,6 +200,8 @@
   {
     geometry_.SetAngle(angle);
     UpdateTransform();
+
+    EmitMessage(RadiographyLayer::LayerEditedMessage(*this));
   }
 
 
@@ -269,6 +220,7 @@
     height_ = height;
 
     UpdateTransform();
+    EmitMessage(RadiographyLayer::LayerEditedMessage(*this));
   }
 
 
@@ -305,7 +257,7 @@
     }
     else
     {
-      transformInverse_.Apply(sceneX, sceneY);
+      GetTransformInverse().Apply(sceneX, sceneY);
 
       int x = static_cast<int>(std::floor(sceneX));
       int y = static_cast<int>(std::floor(sceneY));
@@ -346,6 +298,7 @@
   {
     geometry_.SetPan(x, y);
     UpdateTransform();
+    EmitMessage(RadiographyLayer::LayerEditedMessage(*this));
   }
 
 
@@ -354,6 +307,7 @@
   {
     geometry_.SetPixelSpacing(x, y);
     UpdateTransform();
+    EmitMessage(RadiographyLayer::LayerEditedMessage(*this));
   }
 
 
@@ -362,48 +316,65 @@
   {
     centerX = static_cast<double>(width_) / 2.0;
     centerY = static_cast<double>(height_) / 2.0;
-    transform_.Apply(centerX, centerY);
-  }
-
-
-  void RadiographyLayer::GetCorner(double& x /* out */,
-                                   double& y /* out */,
-                                   Corner corner) const
-  {
-    unsigned int cropX, cropY, cropWidth, cropHeight;
-    GetCrop(cropX, cropY, cropWidth, cropHeight);
-    GetCornerInternal(x, y, corner, cropX, cropY, cropWidth, cropHeight);
+    GetTransform().Apply(centerX, centerY);
   }
 
 
-  bool RadiographyLayer::LookupCorner(Corner& corner /* out */,
-                                      double x,
-                                      double y,
-                                      double zoom,
-                                      double viewportDistance) const
+
+  size_t RadiographyLayer::GetControlPointCount() const {return 4;}
+
+  void RadiographyLayer::GetControlPoint(ControlPoint& cpScene /* out in scene coordinates */,
+                                         size_t index) const
   {
-    static const Corner CORNERS[] = {
-      Corner_TopLeft,
-      Corner_TopRight,
-      Corner_BottomLeft,
-      Corner_BottomRight
-    };
-
     unsigned int cropX, cropY, cropWidth, cropHeight;
     GetCrop(cropX, cropY, cropWidth, cropHeight);
 
+    ControlPoint cp;
+    switch (index)
+    {
+    case ControlPoint_TopLeftCorner:
+      cp = ControlPoint(cropX, cropY, ControlPoint_TopLeftCorner);
+      break;
+
+    case ControlPoint_TopRightCorner:
+      cp = ControlPoint(cropX + cropWidth, cropY, ControlPoint_TopRightCorner);
+      break;
+
+    case ControlPoint_BottomLeftCorner:
+      cp = ControlPoint(cropX, cropY + cropHeight, ControlPoint_BottomLeftCorner);
+      break;
+
+    case ControlPoint_BottomRightCorner:
+      cp = ControlPoint(cropX + cropWidth, cropY + cropHeight, ControlPoint_BottomRightCorner);
+      break;
+
+    default:
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+
+    // transforms image coordinates into scene coordinates
+    GetTransform().Apply(cp.x, cp.y);
+    cpScene = cp;
+  }
+
+  bool RadiographyLayer::LookupControlPoint(ControlPoint& cpScene /* out */,
+                                            double x,
+                                            double y,
+                                            double zoom,
+                                            double viewportDistance) const
+  {
     double threshold = Square(viewportDistance / zoom);
 
-    for (size_t i = 0; i < 4; i++)
+    for (size_t i = 0; i < GetControlPointCount(); i++)
     {
-      double cx, cy;
-      GetCornerInternal(cx, cy, CORNERS[i], cropX, cropY, cropWidth, cropHeight);
+      ControlPoint cp;
+      GetControlPoint(cp, i);
 
-      double d = Square(cx - x) + Square(cy - y);
+      double d = Square(cp.x - x) + Square(cp.y - y);
 
       if (d <= threshold)
       {
-        corner = CORNERS[i];
+        cpScene = cp;
         return true;
       }
     }
--- a/Framework/Radiography/RadiographyLayer.h	Tue Feb 26 21:26:47 2019 +0100
+++ b/Framework/Radiography/RadiographyLayer.h	Tue Feb 26 21:33:16 2019 +0100
@@ -24,14 +24,50 @@
 #include "../Toolbox/AffineTransform2D.h"
 #include "../Toolbox/Extent2D.h"
 #include "../Viewport/CairoContext.h"
+#include "../Messages/IMessage.h"
+#include "../Messages/IObservable.h"
 
 namespace OrthancStone
 {
-  class RadiographyLayer : public boost::noncopyable
+  class RadiographyScene;
+
+  struct ControlPoint
+  {
+    double x;
+    double y;
+    size_t index;
+
+    ControlPoint(double x, double y, size_t index)
+      : x(x),
+        y(y),
+        index(index)
+    {}
+
+    ControlPoint()
+      : x(0),
+        y(0),
+        index(std::numeric_limits<size_t>::max())
+    {}
+  };
+
+  class RadiographyLayer : public IObservable
   {
     friend class RadiographyScene;
 
   public:
+    class LayerEditedMessage :
+        public OriginMessage<MessageType_RadiographyLayer_Edited, RadiographyLayer>
+    {
+    private:
+
+    public:
+      LayerEditedMessage(const RadiographyLayer& origin) :
+        OriginMessage(origin)
+      {
+      }
+    };
+
+
     class Geometry
     {
       bool               hasCrop_;
@@ -141,47 +177,41 @@
     AffineTransform2D  transformInverse_;
     Geometry           geometry_;
     PhotometricDisplayMode  prefferedPhotometricDisplayMode_;
-
+    const RadiographyScene&   scene_;
 
   protected:
-    const AffineTransform2D& GetTransform() const
+    virtual const AffineTransform2D& GetTransform() const
     {
       return transform_;
     }
 
-    void SetPreferredPhotomotricDisplayMode(PhotometricDisplayMode  prefferedPhotometricDisplayMode)
+    virtual const AffineTransform2D& GetTransformInverse() const
     {
-      prefferedPhotometricDisplayMode_ = prefferedPhotometricDisplayMode;
+      return transformInverse_;
     }
 
+    void SetPreferredPhotomotricDisplayMode(PhotometricDisplayMode  prefferedPhotometricDisplayMode);
+
   private:
     void UpdateTransform();
-      
+
     void AddToExtent(Extent2D& extent,
                      double x,
                      double y) const;
 
-    void GetCornerInternal(double& x,
-                           double& y,
-                           Corner corner,
-                           unsigned int cropX,
-                           unsigned int cropY,
-                           unsigned int cropWidth,
-                           unsigned int cropHeight) const;
-
     void SetIndex(size_t index)
     {
       index_ = index;
     }
-      
+
     bool Contains(double x,
                   double y) const;
-      
+
     void DrawBorders(CairoContext& context,
                      double zoom);
 
   public:
-    RadiographyLayer();
+    RadiographyLayer(MessageBroker& broker, const RadiographyScene& scene);
 
     virtual ~RadiographyLayer()
     {
@@ -192,6 +222,11 @@
       return index_;
     }
 
+    const RadiographyScene& GetScene() const
+    {
+      return scene_;
+    }
+
     const Geometry& GetGeometry() const
     {
       return geometry_;
@@ -232,19 +267,19 @@
     unsigned int GetWidth() const
     {
       return width_;
-    }        
+    }
 
     unsigned int GetHeight() const
     {
       return height_;
-    }       
+    }
 
     Extent2D GetExtent() const;
 
-    bool GetPixel(unsigned int& imageX,
-                  unsigned int& imageY,
-                  double sceneX,
-                  double sceneY) const;
+    virtual bool GetPixel(unsigned int& imageX,
+                          unsigned int& imageY,
+                          double sceneX,
+                          double sceneY) const;
 
     void SetPixelSpacing(double x,
                          double y);
@@ -252,15 +287,16 @@
     void GetCenter(double& centerX,
                    double& centerY) const;
 
-    void GetCorner(double& x /* out */,
-                   double& y /* out */,
-                   Corner corner) const;
-      
-    bool LookupCorner(Corner& corner /* out */,
-                      double x,
-                      double y,
-                      double zoom,
-                      double viewportDistance) const;
+    virtual void GetControlPoint(ControlPoint& cpScene /* out in scene coordinates */,
+                                 size_t index) const;
+
+    virtual size_t GetControlPointCount() const;
+
+    bool LookupControlPoint(ControlPoint& cpScene /* out */,
+                            double x,
+                            double y,
+                            double zoom,
+                            double viewportDistance) const;
 
     virtual bool GetDefaultWindowing(float& center,
                                      float& width) const = 0;
@@ -276,5 +312,7 @@
 
     virtual bool GetRange(float& minValue,
                           float& maxValue) const = 0;
-  }; 
+
+    friend class RadiographyMaskLayer; // because it needs to GetTransform on the dicomLayer it relates to
+  };
 }
--- a/Framework/Radiography/RadiographyLayerCropTracker.cpp	Tue Feb 26 21:26:47 2019 +0100
+++ b/Framework/Radiography/RadiographyLayerCropTracker.cpp	Tue Feb 26 21:33:16 2019 +0100
@@ -68,12 +68,10 @@
                                                            RadiographyScene& scene,
                                                            const ViewportGeometry& view,
                                                            size_t layer,
-                                                           double x,
-                                                           double y,
-                                                           Corner corner) :
+                                                           const ControlPoint& startControlPoint) :
     undoRedoStack_(undoRedoStack),
     accessor_(scene, layer),
-    corner_(corner)
+    startControlPoint_(startControlPoint)
   {
     if (accessor_.IsValid())
     {
@@ -101,7 +99,9 @@
   void RadiographyLayerCropTracker::MouseMove(int displayX,
                                               int displayY,
                                               double sceneX,
-                                              double sceneY)
+                                              double sceneY,
+                                              const std::vector<Touch>& displayTouches,
+                                              const std::vector<Touch>& sceneTouches)
   {
     if (accessor_.IsValid())
     {
@@ -112,8 +112,8 @@
       {
         unsigned int targetX, targetWidth;
 
-        if (corner_ == Corner_TopLeft ||
-            corner_ == Corner_BottomLeft)
+        if (startControlPoint_.index == ControlPoint_TopLeftCorner ||
+            startControlPoint_.index == ControlPoint_BottomLeftCorner)
         {
           targetX = std::min(x, cropX_ + cropWidth_);
           targetWidth = cropX_ + cropWidth_ - targetX;
@@ -126,8 +126,8 @@
 
         unsigned int targetY, targetHeight;
 
-        if (corner_ == Corner_TopLeft ||
-            corner_ == Corner_TopRight)
+        if (startControlPoint_.index == ControlPoint_TopLeftCorner ||
+            startControlPoint_.index == ControlPoint_TopRightCorner)
         {
           targetY = std::min(y, cropY_ + cropHeight_);
           targetHeight = cropY_ + cropHeight_ - targetY;
--- a/Framework/Radiography/RadiographyLayerCropTracker.h	Tue Feb 26 21:26:47 2019 +0100
+++ b/Framework/Radiography/RadiographyLayerCropTracker.h	Tue Feb 26 21:33:16 2019 +0100
@@ -35,7 +35,7 @@
 
     UndoRedoStack&                   undoRedoStack_;
     RadiographyScene::LayerAccessor  accessor_;
-    Corner                           corner_;
+    ControlPoint                     startControlPoint_;
     unsigned int                     cropX_;
     unsigned int                     cropY_;
     unsigned int                     cropWidth_;
@@ -46,9 +46,7 @@
                                 RadiographyScene& scene,
                                 const ViewportGeometry& view,
                                 size_t layer,
-                                double x,
-                                double y,
-                                Corner corner);
+                                const ControlPoint& startControlPoint);
 
     virtual bool HasRender() const
     {
@@ -63,6 +61,8 @@
     virtual void MouseMove(int displayX,
                            int displayY,
                            double sceneX,
-                           double sceneY);
+                           double sceneY,
+                           const std::vector<Touch>& displayTouches,
+                           const std::vector<Touch>& sceneTouches);
   };
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Radiography/RadiographyLayerMaskTracker.cpp	Tue Feb 26 21:33:16 2019 +0100
@@ -0,0 +1,140 @@
+/**
+ * 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 "RadiographyLayerMaskTracker.h"
+#include "RadiographyMaskLayer.h"
+
+#include "RadiographySceneCommand.h"
+
+#include <Core/OrthancException.h>
+
+namespace OrthancStone
+{
+  class RadiographyLayerMaskTracker::UndoRedoCommand : public RadiographySceneCommand
+  {
+  private:
+    ControlPoint sourceSceneCp_;
+    ControlPoint targetSceneCp_;
+
+  protected:
+    virtual void UndoInternal(RadiographyLayer& layer) const
+    {
+      RadiographyMaskLayer* maskLayer = dynamic_cast<RadiographyMaskLayer*>(&layer);
+      if (maskLayer == NULL)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+      }
+      unsigned int ix, iy; // image coordinates
+      if (maskLayer->GetPixel(ix, iy, sourceSceneCp_.x, sourceSceneCp_.y))
+      {
+        maskLayer->SetCorner(Orthanc::ImageProcessing::ImagePoint((int32_t)ix, (int32_t)iy), sourceSceneCp_.index);
+      }
+    }
+
+    virtual void RedoInternal(RadiographyLayer& layer) const
+    {
+      RadiographyMaskLayer* maskLayer = dynamic_cast<RadiographyMaskLayer*>(&layer);
+      if (maskLayer == NULL)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+      }
+      unsigned int ix, iy; // image coordinates
+      if (maskLayer->GetPixel(ix, iy, targetSceneCp_.x, targetSceneCp_.y))
+      {
+        maskLayer->SetCorner(Orthanc::ImageProcessing::ImagePoint((int32_t)ix, (int32_t)iy), targetSceneCp_.index);
+      }
+    }
+
+  public:
+    UndoRedoCommand(const RadiographyLayerMaskTracker& tracker) :
+      RadiographySceneCommand(tracker.accessor_),
+      sourceSceneCp_(tracker.startSceneCp_),
+      targetSceneCp_(tracker.endSceneCp_)
+    {
+      RadiographyMaskLayer* maskLayer = dynamic_cast<RadiographyMaskLayer*>(&(tracker.accessor_.GetLayer()));
+      if (maskLayer == NULL)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+      }
+      unsigned int ix, iy; // image coordinates
+      if (maskLayer->GetPixel(ix, iy, targetSceneCp_.x, targetSceneCp_.y))
+      {
+        maskLayer->SetCorner(Orthanc::ImageProcessing::ImagePoint((int32_t)ix, (int32_t)iy), targetSceneCp_.index);
+      }
+    }
+  };
+
+
+  RadiographyLayerMaskTracker::RadiographyLayerMaskTracker(UndoRedoStack& undoRedoStack,
+                                                           RadiographyScene& scene,
+                                                           const ViewportGeometry& view,
+                                                           size_t layer,
+                                                           const ControlPoint& startSceneControlPoint) :
+    undoRedoStack_(undoRedoStack),
+    accessor_(scene, layer),
+    startSceneCp_(startSceneControlPoint),
+    endSceneCp_(startSceneControlPoint)
+  {
+  }
+
+
+  void RadiographyLayerMaskTracker::Render(CairoContext& context,
+                                           double zoom)
+  {
+    throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+  }
+
+
+  void RadiographyLayerMaskTracker::MouseUp()
+  {
+    if (accessor_.IsValid() && startSceneCp_.x != endSceneCp_.x && startSceneCp_.y != endSceneCp_.y)
+    {
+      undoRedoStack_.Add(new UndoRedoCommand(*this));
+    }
+  }
+
+  
+  void RadiographyLayerMaskTracker::MouseMove(int displayX,
+                                              int displayY,
+                                              double sceneX,
+                                              double sceneY,
+                                              const std::vector<Touch>& displayTouches,
+                                              const std::vector<Touch>& sceneTouches)
+  {
+    if (accessor_.IsValid())
+    {
+      unsigned int ix, iy; // image coordinates
+
+      RadiographyLayer& layer = accessor_.GetLayer();
+      if (layer.GetPixel(ix, iy, sceneX, sceneY))
+      {
+        endSceneCp_ = ControlPoint(sceneX, sceneY, startSceneCp_.index);
+
+        RadiographyMaskLayer* maskLayer = dynamic_cast<RadiographyMaskLayer*>(&layer);
+        if (maskLayer == NULL)
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+        }
+        maskLayer->SetCorner(Orthanc::ImageProcessing::ImagePoint((int32_t)ix, (int32_t)iy), startSceneCp_.index);
+      }
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Radiography/RadiographyLayerMaskTracker.h	Tue Feb 26 21:33:16 2019 +0100
@@ -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/>.
+ **/
+
+
+#pragma once
+
+#include "../Toolbox/UndoRedoStack.h"
+#include "../Toolbox/ViewportGeometry.h"
+#include "../Widgets/IWorldSceneMouseTracker.h"
+#include "RadiographyScene.h"
+
+namespace OrthancStone
+{
+  class RadiographyLayerMaskTracker : public IWorldSceneMouseTracker
+  {
+  private:
+    class UndoRedoCommand;
+
+    UndoRedoStack&                   undoRedoStack_;
+    RadiographyScene::LayerAccessor  accessor_;
+    ControlPoint                     startSceneCp_;
+    ControlPoint                     endSceneCp_;
+
+  public:
+    RadiographyLayerMaskTracker(UndoRedoStack& undoRedoStack,
+                                RadiographyScene& scene,
+                                const ViewportGeometry& view,
+                                size_t layer,
+                                const ControlPoint& startSceneControlPoint);
+
+    virtual bool HasRender() const
+    {
+      return false;
+    }
+
+    virtual void Render(CairoContext& context,
+                        double zoom);
+
+    virtual void MouseUp();
+
+    virtual void MouseMove(int displayX,
+                           int displayY,
+                           double sceneX,
+                           double sceneY,
+                           const std::vector<Touch>& displayTouches,
+                           const std::vector<Touch>& sceneTouches);
+  };
+}
--- a/Framework/Radiography/RadiographyLayerMoveTracker.cpp	Tue Feb 26 21:26:47 2019 +0100
+++ b/Framework/Radiography/RadiographyLayerMoveTracker.cpp	Tue Feb 26 21:33:16 2019 +0100
@@ -97,7 +97,9 @@
   void RadiographyLayerMoveTracker::MouseMove(int displayX,
                                               int displayY,
                                               double sceneX,
-                                              double sceneY)
+                                              double sceneY,
+                                              const std::vector<Touch>& displayTouches,
+                                              const std::vector<Touch>& sceneTouches)
   {
     if (accessor_.IsValid())
     {
--- a/Framework/Radiography/RadiographyLayerMoveTracker.h	Tue Feb 26 21:26:47 2019 +0100
+++ b/Framework/Radiography/RadiographyLayerMoveTracker.h	Tue Feb 26 21:33:16 2019 +0100
@@ -61,6 +61,8 @@
     virtual void MouseMove(int displayX,
                            int displayY,
                            double sceneX,
-                           double sceneY);
+                           double sceneY,
+                           const std::vector<Touch>& displayTouches,
+                           const std::vector<Touch>& sceneTouches);
   };
 }
--- a/Framework/Radiography/RadiographyLayerResizeTracker.cpp	Tue Feb 26 21:26:47 2019 +0100
+++ b/Framework/Radiography/RadiographyLayerResizeTracker.cpp	Tue Feb 26 21:33:16 2019 +0100
@@ -85,9 +85,7 @@
   RadiographyLayerResizeTracker::RadiographyLayerResizeTracker(UndoRedoStack& undoRedoStack,
                                                                RadiographyScene& scene,
                                                                size_t layer,
-                                                               double x,
-                                                               double y,
-                                                               Corner corner,
+                                                               const ControlPoint& startControlPoint,
                                                                bool roundScaling) :
     undoRedoStack_(undoRedoStack),
     accessor_(scene, layer),
@@ -101,31 +99,32 @@
       originalPanX_ = accessor_.GetLayer().GetGeometry().GetPanX();
       originalPanY_ = accessor_.GetLayer().GetGeometry().GetPanY();
 
-      switch (corner)
+      size_t oppositeControlPointType;
+      switch (startControlPoint.index)
       {
-        case Corner_TopLeft:
-          oppositeCorner_ = Corner_BottomRight;
+        case ControlPoint_TopLeftCorner:
+          oppositeControlPointType = ControlPoint_BottomRightCorner;
           break;
 
-        case Corner_TopRight:
-          oppositeCorner_ = Corner_BottomLeft;
+        case ControlPoint_TopRightCorner:
+          oppositeControlPointType = ControlPoint_BottomLeftCorner;
           break;
 
-        case Corner_BottomLeft:
-          oppositeCorner_ = Corner_TopRight;
+        case ControlPoint_BottomLeftCorner:
+          oppositeControlPointType = ControlPoint_TopRightCorner;
           break;
 
-        case Corner_BottomRight:
-          oppositeCorner_ = Corner_TopLeft;
+        case ControlPoint_BottomRightCorner:
+          oppositeControlPointType = ControlPoint_TopLeftCorner;
           break;
 
         default:
           throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
       }
 
-      accessor_.GetLayer().GetCorner(oppositeX_, oppositeY_, oppositeCorner_);
+      accessor_.GetLayer().GetControlPoint(startOppositeControlPoint_, oppositeControlPointType);
 
-      double d = ComputeDistance(x, y, oppositeX_, oppositeY_);
+      double d = ComputeDistance(startControlPoint.x, startControlPoint.y, startOppositeControlPoint_.x, startOppositeControlPoint_.y);
       if (d >= std::numeric_limits<float>::epsilon())
       {
         baseScaling_ = 1.0 / d;
@@ -159,14 +158,16 @@
   void RadiographyLayerResizeTracker::MouseMove(int displayX,
                                                 int displayY,
                                                 double sceneX,
-                                                double sceneY)
+                                                double sceneY,
+                                                const std::vector<Touch>& displayTouches,
+                                                const std::vector<Touch>& sceneTouches)
   {
     static const double ROUND_SCALING = 0.1;
         
     if (accessor_.IsValid() &&
         accessor_.GetLayer().GetGeometry().IsResizeable())
     {
-      double scaling = ComputeDistance(oppositeX_, oppositeY_, sceneX, sceneY) * baseScaling_;
+      double scaling = ComputeDistance(startOppositeControlPoint_.x, startOppositeControlPoint_.y, sceneX, sceneY) * baseScaling_;
 
       if (roundScaling_)
       {
@@ -178,10 +179,10 @@
                             scaling * originalSpacingY_);
 
       // Keep the opposite corner at a fixed location
-      double ox, oy;
-      layer.GetCorner(ox, oy, oppositeCorner_);
-      layer.SetPan(layer.GetGeometry().GetPanX() + oppositeX_ - ox,
-                   layer.GetGeometry().GetPanY() + oppositeY_ - oy);
+      ControlPoint currentOppositeCorner;
+      layer.GetControlPoint(currentOppositeCorner, startOppositeControlPoint_.index);
+      layer.SetPan(layer.GetGeometry().GetPanX() + startOppositeControlPoint_.x - currentOppositeCorner.x,
+                   layer.GetGeometry().GetPanY() + startOppositeControlPoint_.y - currentOppositeCorner.y);
     }
   }
 }
--- a/Framework/Radiography/RadiographyLayerResizeTracker.h	Tue Feb 26 21:26:47 2019 +0100
+++ b/Framework/Radiography/RadiographyLayerResizeTracker.h	Tue Feb 26 21:33:16 2019 +0100
@@ -39,18 +39,14 @@
     double                           originalSpacingY_;
     double                           originalPanX_;
     double                           originalPanY_;
-    Corner                           oppositeCorner_;
-    double                           oppositeX_;
-    double                           oppositeY_;
+    ControlPoint                     startOppositeControlPoint_;
     double                           baseScaling_;
 
   public:
     RadiographyLayerResizeTracker(UndoRedoStack& undoRedoStack,
                                   RadiographyScene& scene,
                                   size_t layer,
-                                  double x,
-                                  double y,
-                                  Corner corner,
+                                  const ControlPoint& startControlPoint,
                                   bool roundScaling);
 
     virtual bool HasRender() const
@@ -66,6 +62,8 @@
     virtual void MouseMove(int displayX,
                            int displayY,
                            double sceneX,
-                           double sceneY);
+                           double sceneY,
+                           const std::vector<Touch>& displayTouches,
+                           const std::vector<Touch>& sceneTouches);
   };
 }
--- a/Framework/Radiography/RadiographyLayerRotateTracker.cpp	Tue Feb 26 21:26:47 2019 +0100
+++ b/Framework/Radiography/RadiographyLayerRotateTracker.cpp	Tue Feb 26 21:33:16 2019 +0100
@@ -132,7 +132,9 @@
   void RadiographyLayerRotateTracker::MouseMove(int displayX,
                                                 int displayY,
                                                 double sceneX,
-                                                double sceneY)
+                                                double sceneY,
+                                                const std::vector<Touch>& displayTouches,
+                                                const std::vector<Touch>& sceneTouches)
   {
     static const double ROUND_ANGLE = 15.0 / 180.0 * boost::math::constants::pi<double>(); 
         
--- a/Framework/Radiography/RadiographyLayerRotateTracker.h	Tue Feb 26 21:26:47 2019 +0100
+++ b/Framework/Radiography/RadiographyLayerRotateTracker.h	Tue Feb 26 21:33:16 2019 +0100
@@ -68,6 +68,8 @@
     virtual void MouseMove(int displayX,
                            int displayY,
                            double sceneX,
-                           double sceneY);
+                           double sceneY,
+                           const std::vector<Touch>& displayTouches,
+                           const std::vector<Touch>& sceneTouches);
   };
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Radiography/RadiographyMaskLayer.cpp	Tue Feb 26 21:33:16 2019 +0100
@@ -0,0 +1,143 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 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 "RadiographyMaskLayer.h"
+#include "RadiographyDicomLayer.h"
+
+#include "RadiographyScene.h"
+#include "Core/Images/Image.h"
+#include "Core/Images/ImageProcessing.h"
+#include <Core/OrthancException.h>
+
+namespace OrthancStone
+{
+  const unsigned char IN_MASK_VALUE = 0x00;
+  const unsigned char OUT_MASK_VALUE = 0xFF;
+
+  const AffineTransform2D& RadiographyMaskLayer::GetTransform() const
+  {
+    return dicomLayer_.GetTransform();
+  }
+
+  const AffineTransform2D& RadiographyMaskLayer::GetTransformInverse() const
+  {
+    return dicomLayer_.GetTransformInverse();
+  }
+
+  bool RadiographyMaskLayer::GetPixel(unsigned int& imageX,
+                        unsigned int& imageY,
+                        double sceneX,
+                        double sceneY) const
+  {
+    return dicomLayer_.GetPixel(imageX, imageY, sceneX, sceneY);
+  }
+
+  std::string RadiographyMaskLayer::GetInstanceId() const
+  {
+    return dicomLayer_.GetInstanceId();
+  }
+
+  void RadiographyMaskLayer::SetCorner(const Orthanc::ImageProcessing::ImagePoint& corner, size_t index)
+  {
+    if (index < corners_.size())
+      corners_[index] = corner;
+    else
+      corners_.push_back(corner);
+    invalidated_ = true;
+
+    EmitMessage(RadiographyLayer::LayerEditedMessage(*this));
+  }
+
+  void RadiographyMaskLayer::SetCorners(const std::vector<Orthanc::ImageProcessing::ImagePoint>& corners)
+  {
+    corners_ = corners;
+    invalidated_ = true;
+
+    EmitMessage(RadiographyLayer::LayerEditedMessage(*this));
+  }
+
+  void RadiographyMaskLayer::Render(Orthanc::ImageAccessor& buffer,
+                                    const AffineTransform2D& viewTransform,
+                                    ImageInterpolation interpolation) const
+  {
+    if (dicomLayer_.GetWidth() == 0) // nothing to do if the DICOM layer is not displayed (or not loaded)
+      return;
+
+    if (invalidated_)
+    {
+      mask_.reset(new Orthanc::Image(Orthanc::PixelFormat_Grayscale8, dicomLayer_.GetWidth(), dicomLayer_.GetHeight(), false));
+
+      DrawMask();
+
+      invalidated_ = false;
+    }
+
+    {// rendering
+      if (buffer.GetFormat() != Orthanc::PixelFormat_Float32)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageFormat);
+      }
+
+      unsigned int cropX, cropY, cropWidth, cropHeight;
+      dicomLayer_.GetCrop(cropX, cropY, cropWidth, cropHeight);
+
+      const AffineTransform2D t = AffineTransform2D::Combine(
+            viewTransform, dicomLayer_.GetTransform(),
+            AffineTransform2D::CreateOffset(cropX, cropY));
+
+      Orthanc::ImageAccessor cropped;
+      mask_->GetRegion(cropped, cropX, cropY, cropWidth, cropHeight);
+
+      Orthanc::Image tmp(Orthanc::PixelFormat_Grayscale8, buffer.GetWidth(), buffer.GetHeight(), false);
+
+      t.Apply(tmp, cropped, interpolation, true /* clear */);
+
+      // Blit
+      const unsigned int width = buffer.GetWidth();
+      const unsigned int height = buffer.GetHeight();
+
+      for (unsigned int y = 0; y < height; y++)
+      {
+        float *q = reinterpret_cast<float*>(buffer.GetRow(y));
+        const uint8_t *p = reinterpret_cast<uint8_t*>(tmp.GetRow(y));
+
+        for (unsigned int x = 0; x < width; x++, p++, q++)
+        {
+          if (*p == OUT_MASK_VALUE)
+            *q = foreground_;
+          // else keep the underlying pixel value
+        }
+      }
+
+    }
+  }
+
+  void RadiographyMaskLayer::DrawMask() const
+  {
+    // first fill the complete image
+    Orthanc::ImageProcessing::Set(*mask_, OUT_MASK_VALUE);
+
+    // fill mask
+    Orthanc::ImageProcessing::FillPolygon(*mask_, corners_, IN_MASK_VALUE);
+
+  }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Radiography/RadiographyMaskLayer.h	Tue Feb 26 21:33:16 2019 +0100
@@ -0,0 +1,127 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 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 "RadiographyLayer.h"
+#include "Core/Images/Image.h"
+#include "Core/Images/ImageProcessing.h"
+
+namespace OrthancStone
+{
+  class RadiographyScene;
+  class RadiographyDicomLayer;
+
+  class RadiographyMaskLayer : public RadiographyLayer
+  {
+  private:
+    std::vector<Orthanc::ImageProcessing::ImagePoint>            corners_;
+    const RadiographyDicomLayer&      dicomLayer_;
+    mutable bool                      invalidated_;
+    float                             foreground_;
+
+    mutable std::auto_ptr<Orthanc::ImageAccessor>  mask_;
+  public:
+    RadiographyMaskLayer(MessageBroker& broker, const RadiographyScene& scene, const RadiographyDicomLayer& dicomLayer,
+                         float foreground) :
+      RadiographyLayer(broker, scene),
+      dicomLayer_(dicomLayer),
+      invalidated_(true),
+      foreground_(foreground)
+    {
+    }
+
+    void SetCorners(const std::vector<Orthanc::ImageProcessing::ImagePoint>& corners);
+    void SetCorner(const Orthanc::ImageProcessing::ImagePoint& corner, size_t index);
+
+    const std::vector<Orthanc::ImageProcessing::ImagePoint>& GetCorners() const
+    {
+      return corners_;
+    }
+
+    float GetForeground() const
+    {
+      return foreground_;
+    }
+
+    virtual void Render(Orthanc::ImageAccessor& buffer,
+                        const AffineTransform2D& viewTransform,
+                        ImageInterpolation interpolation) const;
+
+    std::string GetInstanceId() const;
+
+    virtual size_t GetControlPointCount() const
+    {
+      return corners_.size();
+    }
+
+    virtual void GetControlPoint(ControlPoint& cpScene,
+                                         size_t index) const
+    {
+      ControlPoint cp(corners_[index].GetX(), corners_[index].GetY(), index);
+
+      // transforms image coordinates into scene coordinates
+      GetTransform().Apply(cp.x, cp.y);
+      cpScene = cp;
+    }
+
+    virtual bool GetDefaultWindowing(float& center,
+                                     float& width) const
+    {
+      return false;
+    }
+
+    virtual bool GetRange(float& minValue,
+                          float& maxValue) const
+    {
+      minValue = 0;
+      maxValue = 0;
+
+      if (foreground_ < 0)
+      {
+        minValue = foreground_;
+      }
+
+      if (foreground_ > 0)
+      {
+        maxValue = foreground_;
+      }
+
+      return true;
+
+    }
+
+    virtual bool GetPixel(unsigned int& imageX,
+                          unsigned int& imageY,
+                          double sceneX,
+                          double sceneY) const;
+
+  protected:
+    virtual const AffineTransform2D& GetTransform() const;
+
+    virtual const AffineTransform2D& GetTransformInverse() const;
+
+
+  private:
+    void DrawMask() const;
+
+  };
+}
--- a/Framework/Radiography/RadiographyScene.cpp	Tue Feb 26 21:26:47 2019 +0100
+++ b/Framework/Radiography/RadiographyScene.cpp	Tue Feb 26 21:33:16 2019 +0100
@@ -24,6 +24,7 @@
 #include "RadiographyAlphaLayer.h"
 #include "RadiographyDicomLayer.h"
 #include "RadiographyTextLayer.h"
+#include "RadiographyMaskLayer.h"
 #include "../Toolbox/DicomFrameConverter.h"
 
 #include <Core/Images/Image.h>
@@ -141,10 +142,15 @@
 
     EmitMessage(GeometryChangedMessage(*this, *layer));
     EmitMessage(ContentChangedMessage(*this, *layer));
+    layer->RegisterObserverCallback(new Callable<RadiographyScene, RadiographyLayer::LayerEditedMessage>(*this, &RadiographyScene::OnLayerEdited));
 
     return *layer;
   }
 
+  void RadiographyScene::OnLayerEdited(const RadiographyLayer::LayerEditedMessage& message)
+  {
+    EmitMessage(RadiographyScene::LayerEditedMessage(*this, message.GetOrigin()));
+  }
 
   RadiographyScene::RadiographyScene(MessageBroker& broker) :
     IObserver(broker),
@@ -246,6 +252,8 @@
     hasWindowing_ = true;
     windowingCenter_ = center;
     windowingWidth_ = width;
+
+    EmitMessage(RadiographyScene::WindowingChangedMessage(*this));
   }
 
 
@@ -253,7 +261,7 @@
                                                const std::string& utf8,
                                                RadiographyLayer::Geometry* geometry)
   {
-    std::auto_ptr<RadiographyTextLayer>  alpha(new RadiographyTextLayer(*this));
+    std::auto_ptr<RadiographyTextLayer>  alpha(new RadiographyTextLayer(IObservable::GetBroker(), *this));
     alpha->LoadText(font, utf8);
     if (geometry != NULL)
     {
@@ -292,9 +300,25 @@
     return LoadAlphaBitmap(block.release(), geometry);
   }
 
+  RadiographyLayer& RadiographyScene::LoadMask(const std::vector<Orthanc::ImageProcessing::ImagePoint>& corners,
+                                               const RadiographyDicomLayer& dicomLayer,
+                                               float foreground,
+                                               RadiographyLayer::Geometry* geometry)
+  {
+    std::auto_ptr<RadiographyMaskLayer>  mask(new RadiographyMaskLayer(IObservable::GetBroker(), *this, dicomLayer, foreground));
+    mask->SetCorners(corners);
+    if (geometry != NULL)
+    {
+      mask->SetGeometry(*geometry);
+    }
+
+    return RegisterLayer(mask.release());
+  }
+
+
   RadiographyLayer& RadiographyScene::LoadAlphaBitmap(Orthanc::ImageAccessor* bitmap, RadiographyLayer::Geometry *geometry)
   {
-    std::auto_ptr<RadiographyAlphaLayer>  alpha(new RadiographyAlphaLayer(*this));
+    std::auto_ptr<RadiographyAlphaLayer>  alpha(new RadiographyAlphaLayer(IObservable::GetBroker(), *this));
     alpha->SetAlpha(bitmap);
     if (geometry != NULL)
     {
@@ -310,7 +334,7 @@
                                                      bool httpCompression,
                                                      RadiographyLayer::Geometry* geometry)
   {
-    RadiographyDicomLayer& layer = dynamic_cast<RadiographyDicomLayer&>(RegisterLayer(new RadiographyDicomLayer));
+    RadiographyDicomLayer& layer = dynamic_cast<RadiographyDicomLayer&>(RegisterLayer(new RadiographyDicomLayer(IObservable::GetBroker(), *this)));
     layer.SetInstance(instance, frame);
 
     if (geometry != NULL)
@@ -354,7 +378,7 @@
 
   RadiographyLayer& RadiographyScene::LoadDicomWebFrame(IWebService& web)
   {
-    RadiographyLayer& layer = RegisterLayer(new RadiographyDicomLayer);
+    RadiographyLayer& layer = RegisterLayer(new RadiographyDicomLayer(IObservable::GetBroker(), *this));
 
 
     return layer;
@@ -522,41 +546,11 @@
     }
   }
 
-
-  void RadiographyScene::ExportDicom(OrthancApiClient& orthanc,
-                                     const Orthanc::DicomMap& dicom,
-                                     const std::string& parentOrthancId,
-                                     double pixelSpacingX,
-                                     double pixelSpacingY,
-                                     bool invert,
-                                     ImageInterpolation interpolation,
-                                     bool usePam)
-  {
-    Json::Value createDicomRequestContent;
-
-    ExportToCreateDicomRequest(createDicomRequestContent, dicom, pixelSpacingX, pixelSpacingY, invert, interpolation, usePam);
-
-    if (!parentOrthancId.empty())
-    {
-      createDicomRequestContent["Parent"] = parentOrthancId;
-    }
-
-    orthanc.PostJsonAsyncExpectJson(
-          "/tools/create-dicom", createDicomRequestContent,
-          new Callable<RadiographyScene, OrthancApiClient::JsonResponseReadyMessage>
-          (*this, &RadiographyScene::OnDicomExported),
-          NULL, NULL);
-  }
-
-  // Export using PAM is faster than using PNG, but requires Orthanc
-  // core >= 1.4.3
-  void RadiographyScene::ExportToCreateDicomRequest(Json::Value& createDicomRequestContent,
-                                const Orthanc::DicomMap& dicom,
-                                double pixelSpacingX,
-                                double pixelSpacingY,
-                                bool invert,
-                                ImageInterpolation interpolation,
-                                bool usePam)
+  Orthanc::Image* RadiographyScene::ExportToImage(double pixelSpacingX,
+                                                  double pixelSpacingY,
+                                                  ImageInterpolation interpolation,
+                                                  bool invert,
+                                                  int64_t maxValue /* for inversion */)
   {
     if (pixelSpacingX <= 0 ||
         pixelSpacingY <= 0)
@@ -589,9 +583,27 @@
 
     Render(layers, view, interpolation);
 
-    Orthanc::Image rendered(Orthanc::PixelFormat_Grayscale16,
-                            layers.GetWidth(), layers.GetHeight(), false);
-    Orthanc::ImageProcessing::Convert(rendered, layers);
+    std::auto_ptr<Orthanc::Image> rendered(new Orthanc::Image(Orthanc::PixelFormat_Grayscale16,
+                                                               layers.GetWidth(), layers.GetHeight(), false));
+
+    Orthanc::ImageProcessing::Convert(*rendered, layers);
+    if (invert)
+      Orthanc::ImageProcessing::Invert(*rendered, maxValue);
+
+    return rendered.release();
+  }
+
+
+  void RadiographyScene::ExportToCreateDicomRequest(Json::Value& createDicomRequestContent,
+                                                    const Json::Value& dicomTags,
+                                                    const std::string& parentOrthancId,
+                                                    double pixelSpacingX,
+                                                    double pixelSpacingY,
+                                                    bool invert,
+                                                    ImageInterpolation interpolation,
+                                                    bool usePam)
+  {
+    std::auto_ptr<Orthanc::Image> rendered(ExportToImage(pixelSpacingX, pixelSpacingY, interpolation)); // note: we don't invert the image in the pixels data because we'll set the PhotometricDisplayMode correctly in the DICOM tags
 
     std::string base64;
 
@@ -601,42 +613,28 @@
       if (usePam)
       {
         Orthanc::PamWriter writer;
-        writer.WriteToMemory(content, rendered);
+        writer.WriteToMemory(content, *rendered);
       }
       else
       {
         Orthanc::PngWriter writer;
-        writer.WriteToMemory(content, rendered);
+        writer.WriteToMemory(content, *rendered);
       }
 
       Orthanc::Toolbox::EncodeBase64(base64, content);
     }
 
-    std::set<Orthanc::DicomTag> tags;
-    dicom.GetTags(tags);
-
-    createDicomRequestContent["Tags"] = Json::objectValue;
-
-    for (std::set<Orthanc::DicomTag>::const_iterator
-         tag = tags.begin(); tag != tags.end(); ++tag)
-    {
-      const Orthanc::DicomValue& value = dicom.GetValue(*tag);
-      if (!value.IsNull() &&
-          !value.IsBinary())
-      {
-        createDicomRequestContent["Tags"][tag->Format()] = value.GetContent();
-      }
-    }
+    createDicomRequestContent["Tags"] = dicomTags;
 
     PhotometricDisplayMode photometricMode = GetPreferredPhotomotricDisplayMode();
     if ((invert && photometricMode != PhotometricDisplayMode_Monochrome2) ||
         (!invert && photometricMode == PhotometricDisplayMode_Monochrome1))
     {
-      createDicomRequestContent["Tags"][Orthanc::DICOM_TAG_PHOTOMETRIC_INTERPRETATION.Format()] = "MONOCHROME1";
+      createDicomRequestContent["Tags"]["PhotometricInterpretation"] = "MONOCHROME1";
     }
     else
     {
-      createDicomRequestContent["Tags"][Orthanc::DICOM_TAG_PHOTOMETRIC_INTERPRETATION.Format()] = "MONOCHROME2";
+      createDicomRequestContent["Tags"]["PhotometricInterpretation"] = "MONOCHROME2";
     }
 
     // WARNING: The order of PixelSpacing is Y/X. We use "%0.8f" to
@@ -646,15 +644,15 @@
     char buf[32];
     sprintf(buf, "%0.8f\\%0.8f", pixelSpacingY, pixelSpacingX);
 
-    createDicomRequestContent["Tags"][Orthanc::DICOM_TAG_PIXEL_SPACING.Format()] = buf;
+    createDicomRequestContent["Tags"]["PixelSpacing"] = buf;
 
     float center, width;
     if (GetWindowing(center, width))
     {
-      createDicomRequestContent["Tags"][Orthanc::DICOM_TAG_WINDOW_CENTER.Format()] =
+      createDicomRequestContent["Tags"]["WindowCenter"] =
           boost::lexical_cast<std::string>(boost::math::iround(center));
 
-      createDicomRequestContent["Tags"][Orthanc::DICOM_TAG_WINDOW_WIDTH.Format()] =
+      createDicomRequestContent["Tags"]["WindowWidth"] =
           boost::lexical_cast<std::string>(boost::math::iround(width));
     }
 
@@ -663,9 +661,67 @@
     createDicomRequestContent["Content"] = ("data:" +
                                             std::string(usePam ? Orthanc::MIME_PAM : Orthanc::MIME_PNG) +
                                             ";base64," + base64);
+
+    if (!parentOrthancId.empty())
+    {
+      createDicomRequestContent["Parent"] = parentOrthancId;
+    }
+
+  }
+
+
+  void RadiographyScene::ExportDicom(OrthancApiClient& orthanc,
+                                     const Json::Value& dicomTags,
+                                     const std::string& parentOrthancId,
+                                     double pixelSpacingX,
+                                     double pixelSpacingY,
+                                     bool invert,
+                                     ImageInterpolation interpolation,
+                                     bool usePam)
+  {
+    Json::Value createDicomRequestContent;
+
+    ExportToCreateDicomRequest(createDicomRequestContent, dicomTags, parentOrthancId, pixelSpacingX, pixelSpacingY, invert, interpolation, usePam);
+
+    orthanc.PostJsonAsyncExpectJson(
+          "/tools/create-dicom", createDicomRequestContent,
+          new Callable<RadiographyScene, OrthancApiClient::JsonResponseReadyMessage>
+          (*this, &RadiographyScene::OnDicomExported),
+          NULL, NULL);
+
   }
 
 
+  // Export using PAM is faster than using PNG, but requires Orthanc
+  // core >= 1.4.3
+  void RadiographyScene::ExportDicom(OrthancApiClient& orthanc,
+                                     const Orthanc::DicomMap& dicom,
+                                     const std::string& parentOrthancId,
+                                     double pixelSpacingX,
+                                     double pixelSpacingY,
+                                     bool invert,
+                                     ImageInterpolation interpolation,
+                                     bool usePam)
+  {
+    std::set<Orthanc::DicomTag> tags;
+    dicom.GetTags(tags);
+
+    Json::Value jsonTags = Json::objectValue;
+
+    for (std::set<Orthanc::DicomTag>::const_iterator
+         tag = tags.begin(); tag != tags.end(); ++tag)
+    {
+      const Orthanc::DicomValue& value = dicom.GetValue(*tag);
+      if (!value.IsNull() &&
+          !value.IsBinary())
+      {
+        jsonTags[tag->Format()] = value.GetContent();
+      }
+    }
+
+    ExportDicom(orthanc, jsonTags, parentOrthancId, pixelSpacingX, pixelSpacingY, invert, interpolation, usePam);
+  }
+
   void RadiographyScene::OnDicomExported(const OrthancApiClient::JsonResponseReadyMessage& message)
   {
     LOG(INFO) << "DICOM export was successful: "
--- a/Framework/Radiography/RadiographyScene.h	Tue Feb 26 21:26:47 2019 +0100
+++ b/Framework/Radiography/RadiographyScene.h	Tue Feb 26 21:33:16 2019 +0100
@@ -24,16 +24,20 @@
 #include "RadiographyLayer.h"
 #include "../Toolbox/OrthancApiClient.h"
 #include "Framework/StoneEnumerations.h"
+#include "Core/Images/Image.h"
+#include "Core/Images/ImageProcessing.h"
 
 namespace OrthancStone
 {
+  class RadiographyDicomLayer;
+
   class RadiographyScene :
       public IObserver,
       public IObservable
   {
   public:
     class GeometryChangedMessage :
-        public OriginMessage<MessageType_Scene_GeometryChanged, RadiographyScene>
+        public OriginMessage<MessageType_RadiographyScene_GeometryChanged, RadiographyScene>
     {
     private:
       RadiographyLayer&        layer_;
@@ -53,7 +57,7 @@
     };
 
     class ContentChangedMessage :
-        public OriginMessage<MessageType_Scene_ContentChanged, RadiographyScene>
+        public OriginMessage<MessageType_RadiographyScene_ContentChanged, RadiographyScene>
     {
     private:
       RadiographyLayer&        layer_;
@@ -72,6 +76,37 @@
       }
     };
 
+    class LayerEditedMessage :
+        public OriginMessage<MessageType_RadiographyScene_LayerEdited, RadiographyScene>
+    {
+    private:
+      const RadiographyLayer&        layer_;
+
+    public:
+      LayerEditedMessage(const RadiographyScene& origin,
+                         const RadiographyLayer& layer) :
+        OriginMessage(origin),
+        layer_(layer)
+      {
+      }
+
+      const RadiographyLayer& GetLayer() const
+      {
+        return layer_;
+      }
+
+    };
+
+    class WindowingChangedMessage :
+        public OriginMessage<MessageType_RadiographyScene_WindowingChanged, RadiographyScene>
+    {
+
+    public:
+      WindowingChangedMessage(const RadiographyScene& origin) :
+        OriginMessage(origin)
+      {
+      }
+    };
 
     class LayerAccessor : public boost::noncopyable
     {
@@ -126,6 +161,7 @@
 
     void OnDicomWebReceived(const IWebService::HttpRequestSuccessMessage& message);
 
+    void OnLayerEdited(const RadiographyLayer::LayerEditedMessage& message);
   public:
     RadiographyScene(MessageBroker& broker);
     
@@ -150,6 +186,11 @@
                                     unsigned int height,
                                     RadiographyLayer::Geometry* geometry);
 
+    RadiographyLayer& LoadMask(const std::vector<Orthanc::ImageProcessing::ImagePoint>& corners,
+                               const RadiographyDicomLayer& dicomLayer,
+                               float foreground,
+                               RadiographyLayer::Geometry* geometry);
+
     RadiographyLayer& LoadAlphaBitmap(Orthanc::ImageAccessor* bitmap,  // takes ownership
                                       RadiographyLayer::Geometry* geometry);
 
@@ -165,6 +206,54 @@
 
     const RadiographyLayer& GetLayer(size_t layerIndex) const;
 
+    template <typename TypeLayer>
+    TypeLayer* GetLayer(size_t index = 0)
+    {
+      std::vector<size_t> layerIndexes;
+      GetLayersIndexes(layerIndexes);
+
+      size_t count = 0;
+
+      for (size_t i = 0; i < layerIndexes.size(); ++i)
+      {
+        TypeLayer* typedLayer = dynamic_cast<TypeLayer*>(layers_[layerIndexes[i]]);
+        if (typedLayer != NULL)
+        {
+          if (count == index)
+          {
+            return typedLayer;
+          }
+          count++;
+        }
+      }
+
+      return NULL;
+    }
+
+    template <typename TypeLayer>
+    const TypeLayer* GetLayer(size_t index = 0) const
+    {
+      std::vector<size_t> layerIndexes;
+      GetLayersIndexes(layerIndexes);
+
+      size_t count = 0;
+
+      for (size_t i = 0; i < layerIndexes.size(); ++i)
+      {
+        const TypeLayer* typedLayer = dynamic_cast<const TypeLayer*>(layers_.at(layerIndexes[i]));
+        if (typedLayer != NULL)
+        {
+          if (count == index)
+          {
+            return typedLayer;
+          }
+          count++;
+        }
+      }
+
+      return NULL;
+    }
+
     void GetLayersIndexes(std::vector<size_t>& output) const;
 
     Extent2D GetSceneExtent() const;
@@ -195,13 +284,35 @@
                      ImageInterpolation interpolation,
                      bool usePam);
 
-    // temporary version used by VSOL because we need to send the same request at another url
+    void ExportDicom(OrthancApiClient& orthanc,
+                     const Json::Value& dicomTags,
+                     const std::string& parentOrthancId,
+                     double pixelSpacingX,
+                     double pixelSpacingY,
+                     bool invert,
+                     ImageInterpolation interpolation,
+                     bool usePam);
+
     void ExportToCreateDicomRequest(Json::Value& createDicomRequestContent,
-                                    const Orthanc::DicomMap& dicom,
+                                    const Json::Value& dicomTags,
+                                    const std::string& parentOrthancId,
                                     double pixelSpacingX,
                                     double pixelSpacingY,
                                     bool invert,
                                     ImageInterpolation interpolation,
                                     bool usePam);
+
+    Orthanc::Image* ExportToImage(double pixelSpacingX,
+                                  double pixelSpacingY,
+                                  ImageInterpolation interpolation)
+    {
+      return ExportToImage(pixelSpacingX, pixelSpacingY, interpolation, false, 0);
+    }
+
+    Orthanc::Image* ExportToImage(double pixelSpacingX,
+                                  double pixelSpacingY,
+                                  ImageInterpolation interpolation,
+                                  bool invert,
+                                  int64_t maxValue /* for inversion */);
   };
 }
--- a/Framework/Radiography/RadiographySceneReader.cpp	Tue Feb 26 21:26:47 2019 +0100
+++ b/Framework/Radiography/RadiographySceneReader.cpp	Tue Feb 26 21:33:16 2019 +0100
@@ -35,6 +35,7 @@
     if (version != 1)
       throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
 
+    RadiographyDicomLayer* dicomLayer = NULL;
     for(size_t layerIndex = 0; layerIndex < input["layers"].size(); layerIndex++)
     {
       const Json::Value& jsonLayer = input["layers"][(int)layerIndex];
@@ -43,7 +44,26 @@
       if (jsonLayer["type"].asString() == "dicom")
       {
         ReadLayerGeometry(geometry, jsonLayer);
-        scene_.LoadDicomFrame(orthancApiClient_, jsonLayer["instanceId"].asString(), jsonLayer["frame"].asUInt(), false, &geometry);
+        dicomLayer = dynamic_cast<RadiographyDicomLayer*>(&(scene_.LoadDicomFrame(orthancApiClient_, jsonLayer["instanceId"].asString(), jsonLayer["frame"].asUInt(), false, &geometry)));
+      }
+      else if (jsonLayer["type"].asString() == "mask")
+      {
+        if (dicomLayer == NULL)
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); // we always assumed the dicom layer was read before the mask
+        }
+        ReadLayerGeometry(geometry, jsonLayer);
+
+        float foreground = jsonLayer["foreground"].asFloat();
+        std::vector<Orthanc::ImageProcessing::ImagePoint> corners;
+        for (size_t i = 0; i < jsonLayer["corners"].size(); i++)
+        {
+          Orthanc::ImageProcessing::ImagePoint corner(jsonLayer["corners"][(int)i]["x"].asInt(),
+              jsonLayer["corners"][(int)i]["y"].asInt());
+          corners.push_back(corner);
+        }
+
+        scene_.LoadMask(corners, *dicomLayer, foreground, &geometry);
       }
       else if (jsonLayer["type"].asString() == "text")
       {
--- a/Framework/Radiography/RadiographySceneReader.h	Tue Feb 26 21:26:47 2019 +0100
+++ b/Framework/Radiography/RadiographySceneReader.h	Tue Feb 26 21:33:16 2019 +0100
@@ -24,6 +24,7 @@
 #include "RadiographyScene.h"
 #include "RadiographyAlphaLayer.h"
 #include "RadiographyDicomLayer.h"
+#include "RadiographyMaskLayer.h"
 #include "RadiographyTextLayer.h"
 #include <json/value.h>
 #include <Core/Images/FontRegistry.h>
--- a/Framework/Radiography/RadiographySceneWriter.cpp	Tue Feb 26 21:26:47 2019 +0100
+++ b/Framework/Radiography/RadiographySceneWriter.cpp	Tue Feb 26 21:33:16 2019 +0100
@@ -57,6 +57,22 @@
     output["fontName"] = layer.GetFontName();
   }
 
+  void RadiographySceneWriter::WriteLayer(Json::Value& output, const RadiographyMaskLayer& layer)
+  {
+    output["type"] = "mask";
+    output["instanceId"] = layer.GetInstanceId(); // the dicom layer it's being linked to
+    output["foreground"] = layer.GetForeground();
+    output["corners"] = Json::arrayValue;
+    const std::vector<Orthanc::ImageProcessing::ImagePoint>& corners = layer.GetCorners();
+    for (size_t i = 0; i < corners.size(); i++)
+    {
+      Json::Value corner;
+      corner["x"] = corners[i].GetX();
+      corner["y"] = corners[i].GetY();
+      output["corners"].append(corner);
+    }
+  }
+
   void RadiographySceneWriter::WriteLayer(Json::Value& output, const RadiographyAlphaLayer& layer)
   {
     output["type"] = "alpha";
@@ -128,6 +144,10 @@
     {
       WriteLayer(output, dynamic_cast<const RadiographyAlphaLayer&>(layer));
     }
+    else if (dynamic_cast<const RadiographyMaskLayer*>(&layer) != NULL)
+    {
+      WriteLayer(output, dynamic_cast<const RadiographyMaskLayer&>(layer));
+    }
     else
     {
       throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
--- a/Framework/Radiography/RadiographySceneWriter.h	Tue Feb 26 21:26:47 2019 +0100
+++ b/Framework/Radiography/RadiographySceneWriter.h	Tue Feb 26 21:33:16 2019 +0100
@@ -25,6 +25,7 @@
 #include "RadiographyAlphaLayer.h"
 #include "RadiographyDicomLayer.h"
 #include "RadiographyTextLayer.h"
+#include "RadiographyMaskLayer.h"
 #include <json/value.h>
 
 namespace OrthancStone
@@ -46,5 +47,6 @@
     void WriteLayer(Json::Value& output, const RadiographyDicomLayer& layer);
     void WriteLayer(Json::Value& output, const RadiographyTextLayer& layer);
     void WriteLayer(Json::Value& output, const RadiographyAlphaLayer& layer);
+    void WriteLayer(Json::Value& output, const RadiographyMaskLayer& layer);
   };
 }
--- a/Framework/Radiography/RadiographyTextLayer.h	Tue Feb 26 21:26:47 2019 +0100
+++ b/Framework/Radiography/RadiographyTextLayer.h	Tue Feb 26 21:33:16 2019 +0100
@@ -34,8 +34,8 @@
     std::string                fontName_;
 
   public:
-    RadiographyTextLayer(const RadiographyScene& scene) :
-      RadiographyAlphaLayer(scene)
+    RadiographyTextLayer(MessageBroker& broker, const RadiographyScene& scene) :
+      RadiographyAlphaLayer(broker, scene)
     {
     }
 
--- a/Framework/Radiography/RadiographyWidget.cpp	Tue Feb 26 21:26:47 2019 +0100
+++ b/Framework/Radiography/RadiographyWidget.cpp	Tue Feb 26 21:33:16 2019 +0100
@@ -25,6 +25,7 @@
 #include <Core/Images/Image.h>
 #include <Core/Images/ImageProcessing.h>
 
+#include "RadiographyMaskLayer.h"
 
 namespace OrthancStone
 {
@@ -194,6 +195,28 @@
     selectedLayer_ = layer;
   }
 
+  bool RadiographyWidget::SelectMaskLayer(size_t index)
+  {
+    std::vector<size_t> layerIndexes;
+    size_t count = 0;
+    scene_->GetLayersIndexes(layerIndexes);
+
+    for (size_t i = 0; i < layerIndexes.size(); ++i)
+    {
+      const RadiographyMaskLayer* maskLayer = dynamic_cast<const RadiographyMaskLayer*>(&(scene_->GetLayer(layerIndexes[i])));
+      if (maskLayer != NULL)
+      {
+        if (count == index)
+        {
+          Select(layerIndexes[i]);
+          return true;
+        }
+        count++;
+      }
+    }
+
+    return false;
+  }
 
   bool RadiographyWidget::LookupSelectedLayer(size_t& layer)
   {
--- a/Framework/Radiography/RadiographyWidget.h	Tue Feb 26 21:26:47 2019 +0100
+++ b/Framework/Radiography/RadiographyWidget.h	Tue Feb 26 21:33:16 2019 +0100
@@ -27,6 +27,8 @@
 
 namespace OrthancStone
 {
+  class RadiographyMaskLayer;
+
   class RadiographyWidget :
     public WorldSceneWidget,
     public IObserver
@@ -76,6 +78,8 @@
 
     void Select(size_t layer);
 
+    bool SelectMaskLayer(size_t index = 0);
+
     bool LookupSelectedLayer(size_t& layer);
 
     void OnGeometryChanged(const RadiographyScene::GeometryChangedMessage& message);
--- a/Framework/Radiography/RadiographyWindowingTracker.cpp	Tue Feb 26 21:26:47 2019 +0100
+++ b/Framework/Radiography/RadiographyWindowingTracker.cpp	Tue Feb 26 21:33:16 2019 +0100
@@ -163,7 +163,9 @@
   void RadiographyWindowingTracker::MouseMove(int displayX,
                                               int displayY,
                                               double sceneX,
-                                              double sceneY)
+                                              double sceneY,
+                                              const std::vector<Touch>& displayTouches,
+                                              const std::vector<Touch>& sceneTouches)
   {
     // This follows the behavior of the Osimis Web viewer:
     // https://bitbucket.org/osimis/osimis-webviewer-plugin/src/master/frontend/src/app/viewport/image-plugins/windowing-viewport-tool.class.js
--- a/Framework/Radiography/RadiographyWindowingTracker.h	Tue Feb 26 21:26:47 2019 +0100
+++ b/Framework/Radiography/RadiographyWindowingTracker.h	Tue Feb 26 21:33:16 2019 +0100
@@ -82,6 +82,8 @@
     virtual void MouseMove(int displayX,
                            int displayY,
                            double sceneX,
-                           double sceneY);
+                           double sceneY,
+                           const std::vector<Touch>& displayTouches,
+                           const std::vector<Touch>& sceneTouches);
   };
 }
--- a/Framework/StoneEnumerations.h	Tue Feb 26 21:26:47 2019 +0100
+++ b/Framework/StoneEnumerations.h	Tue Feb 26 21:33:16 2019 +0100
@@ -157,8 +157,12 @@
     MessageType_OrthancApi_GenericHttpError_Ready,
     MessageType_OrthancApi_GenericEmptyResponse_Ready,
 
-    MessageType_Scene_GeometryChanged,
-    MessageType_Scene_ContentChanged,
+    MessageType_RadiographyScene_GeometryChanged,
+    MessageType_RadiographyScene_ContentChanged,
+    MessageType_RadiographyScene_LayerEdited,
+    MessageType_RadiographyScene_WindowingChanged,
+
+    MessageType_RadiographyLayer_Edited,
 
     MessageType_ViewportChanged,
 
@@ -172,12 +176,12 @@
   };
 
   
-  enum Corner
+  enum ControlPointType
   {
-    Corner_TopLeft,
-    Corner_TopRight,
-    Corner_BottomLeft,
-    Corner_BottomRight
+    ControlPoint_TopLeftCorner = 0,
+    ControlPoint_TopRightCorner = 1,
+    ControlPoint_BottomRightCorner = 2,
+    ControlPoint_BottomLeftCorner = 3
   };
 
   enum PhotometricDisplayMode
--- a/Framework/Toolbox/OrthancApiClient.cpp	Tue Feb 26 21:26:47 2019 +0100
+++ b/Framework/Toolbox/OrthancApiClient.cpp	Tue Feb 26 21:33:16 2019 +0100
@@ -248,6 +248,21 @@
     web_.PostAsync(baseUrl_ + uri, IWebService::HttpHeaders(), body, NULL, NULL, NULL);
   }
 
+  void OrthancApiClient::PostBinaryAsync(
+      const std::string& uri,
+      const std::string& body,
+      MessageHandler<EmptyResponseReadyMessage>* successCallback,
+      MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback,
+      Orthanc::IDynamicObject* payload   /* takes ownership */)
+  {
+    web_.PostAsync(baseUrl_ + uri, IWebService::HttpHeaders(), body,
+                   new WebServicePayload(successCallback, failureCallback, payload),
+                   new Callable<OrthancApiClient, IWebService::HttpRequestSuccessMessage>
+                   (*this, &OrthancApiClient::NotifyHttpSuccess),
+                   new Callable<OrthancApiClient, IWebService::HttpRequestErrorMessage>
+                   (*this, &OrthancApiClient::NotifyHttpError));
+  }
+
   void OrthancApiClient::PostJsonAsyncExpectJson(
       const std::string& uri,
       const Json::Value& data,
@@ -269,6 +284,18 @@
     return PostBinaryAsync(uri, body);
   }
 
+  void OrthancApiClient::PostJsonAsync(
+      const std::string& uri,
+      const Json::Value& data,
+      MessageHandler<EmptyResponseReadyMessage>* successCallback,
+      MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback,
+      Orthanc::IDynamicObject* payload   /* takes ownership */)
+  {
+    std::string body;
+    MessagingToolbox::JsonToString(body, data);
+    return PostBinaryAsync(uri, body, successCallback, failureCallback, payload);
+  }
+
   void OrthancApiClient::DeleteAsync(
       const std::string& uri,
       MessageHandler<EmptyResponseReadyMessage>* successCallback,
--- a/Framework/Toolbox/OrthancApiClient.h	Tue Feb 26 21:26:47 2019 +0100
+++ b/Framework/Toolbox/OrthancApiClient.h	Tue Feb 26 21:33:16 2019 +0100
@@ -201,10 +201,25 @@
     void PostJsonAsync(const std::string& uri,
                        const Json::Value& data);
 
+    // schedule a POST request and don't expect any response.
+    void PostJsonAsync(const std::string& uri,
+                       const Json::Value& data,
+                       MessageHandler<EmptyResponseReadyMessage>* successCallback,
+                       MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback = NULL,
+                       Orthanc::IDynamicObject* payload = NULL   /* takes ownership */);
+
+
     // schedule a POST request and don't mind the response.
     void PostBinaryAsync(const std::string& uri,
                          const std::string& body);
 
+    // schedule a POST request and don't expect any response.
+    void PostBinaryAsync(const std::string& uri,
+                         const std::string& body,
+                         MessageHandler<EmptyResponseReadyMessage>* successCallback,
+                         MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback = NULL,
+                         Orthanc::IDynamicObject* payload = NULL   /* takes ownership */);
+
     // schedule a DELETE request expecting an empty response.
     void DeleteAsync(const std::string& uri,
                      MessageHandler<EmptyResponseReadyMessage>* successCallback,
--- a/Framework/Toolbox/ViewportGeometry.cpp	Tue Feb 26 21:26:47 2019 +0100
+++ b/Framework/Toolbox/ViewportGeometry.cpp	Tue Feb 26 21:33:16 2019 +0100
@@ -121,6 +121,18 @@
   }
 
 
+  void ViewportGeometry::MapPixelCenterToScene(std::vector<Touch>& sceneTouches /* out */,
+                                               const std::vector<Touch>& displayTouches) const
+  {
+    double sceneX, sceneY;
+    sceneTouches.clear();
+    for (size_t t = 0; t < displayTouches.size(); t++)
+    {
+      MapPixelCenterToScene(sceneX, sceneY, displayTouches[t].x, displayTouches[t].y);
+      sceneTouches.push_back(Touch((float)sceneX, (float)sceneY));
+    }
+  }
+
   void ViewportGeometry::MapPixelCenterToScene(double& sceneX,
                                                double& sceneY,
                                                int x,
--- a/Framework/Toolbox/ViewportGeometry.h	Tue Feb 26 21:26:47 2019 +0100
+++ b/Framework/Toolbox/ViewportGeometry.h	Tue Feb 26 21:33:16 2019 +0100
@@ -24,6 +24,7 @@
 #include "../Viewport/CairoContext.h"
 #include "Extent2D.h"
 #include "LinearAlgebra.h"
+#include "../Viewport/IMouseTracker.h"  // to include "Touch" definition
 
 namespace OrthancStone
 {
@@ -69,6 +70,9 @@
                                int x,
                                int y) const;
 
+    void MapPixelCenterToScene(std::vector<Touch>& sceneTouches /* out */,
+                               const std::vector<Touch>& displayTouches) const;
+
     void MapSceneToDisplay(int& displayX /* out */,
                            int& displayY /* out */,
                            double x,
--- a/Framework/Viewport/IMouseTracker.h	Tue Feb 26 21:26:47 2019 +0100
+++ b/Framework/Viewport/IMouseTracker.h	Tue Feb 26 21:33:16 2019 +0100
@@ -22,9 +22,28 @@
 #pragma once
 
 #include "CairoSurface.h"
+#include <vector>
 
 namespace OrthancStone
 {
+  struct Touch
+  {
+    float x;
+    float y;
+
+    Touch(float x, float y)
+    : x(x),
+      y(y)
+    {
+    }
+    Touch()
+      : x(0.0f),
+        y(0.0f)
+    {
+    }
+  };
+
+
   // this is tracking a mouse in screen coordinates/pixels unlike
   // the IWorldSceneMouseTracker that is tracking a mouse
   // in scene coordinates/mm.
@@ -41,6 +60,9 @@
 
     // Returns "true" iff. the background scene must be repainted
     virtual void MouseMove(int x, 
-                           int y) = 0;
+                           int y,
+                           const std::vector<Touch>& displayTouches) = 0;
+
+    virtual bool IsTouchTracker() const {return false;}
   };
 }
--- a/Framework/Viewport/IViewport.h	Tue Feb 26 21:26:47 2019 +0100
+++ b/Framework/Viewport/IViewport.h	Tue Feb 26 21:33:16 2019 +0100
@@ -26,6 +26,7 @@
 #include "../Messages/IObservable.h"
 
 #include <Core/Images/ImageAccessor.h>
+#include "../Viewport/IMouseTracker.h" // only to get the "Touch" definition
 
 namespace OrthancStone
 {
@@ -58,12 +59,14 @@
     virtual void MouseDown(MouseButton button,
                            int x,
                            int y,
-                           KeyboardModifiers modifiers) = 0;
+                           KeyboardModifiers modifiers,
+                           const std::vector<Touch>& touches) = 0;
 
     virtual void MouseUp() = 0;
 
     virtual void MouseMove(int x, 
-                           int y) = 0;
+                           int y,
+                           const std::vector<Touch>& displayTouches) = 0;
 
     virtual void MouseEnter() = 0;
 
--- a/Framework/Viewport/WidgetViewport.cpp	Tue Feb 26 21:26:47 2019 +0100
+++ b/Framework/Viewport/WidgetViewport.cpp	Tue Feb 26 21:33:16 2019 +0100
@@ -137,18 +137,36 @@
     return true;
   }
 
+  void WidgetViewport::TouchStart(const std::vector<Touch>& displayTouches)
+  {
+    MouseDown(MouseButton_Left, (int)displayTouches[0].x, (int)displayTouches[0].y, KeyboardModifiers_None, displayTouches); // one touch is equivalent to a mouse tracker without left button -> set the mouse coordinates to the first touch coordinates
+  }
+      
+  void WidgetViewport::TouchMove(const std::vector<Touch>& displayTouches)
+  {
+    MouseMove((int)displayTouches[0].x, (int)displayTouches[0].y, displayTouches); // one touch is equivalent to a mouse tracker without left button -> set the mouse coordinates to the first touch coordinates
+  }
+      
+  void WidgetViewport::TouchEnd(const std::vector<Touch>& displayTouches)
+  {
+    // note: TouchEnd is not triggered when a single touch gesture ends (it is only triggered when
+    // going from 2 touches to 1 touch, ...)
+    MouseUp();
+  }
 
   void WidgetViewport::MouseDown(MouseButton button,
                                  int x,
                                  int y,
-                                 KeyboardModifiers modifiers)
+                                 KeyboardModifiers modifiers,
+                                 const std::vector<Touch>& displayTouches
+                                 )
   {
     lastMouseX_ = x;
     lastMouseY_ = y;
 
     if (centralWidget_.get() != NULL)
     {
-      mouseTracker_.reset(centralWidget_->CreateMouseTracker(button, x, y, modifiers));
+      mouseTracker_.reset(centralWidget_->CreateMouseTracker(button, x, y, modifiers, displayTouches));
     }
     else
     {
@@ -171,7 +189,8 @@
 
 
   void WidgetViewport::MouseMove(int x, 
-                                 int y) 
+                                 int y,
+                                 const std::vector<Touch>& displayTouches)
   {
     if (centralWidget_.get() == NULL)
     {
@@ -185,7 +204,7 @@
     
     if (mouseTracker_.get() != NULL)
     {
-      mouseTracker_->MouseMove(x, y);
+      mouseTracker_->MouseMove(x, y, displayTouches);
       repaint = true;
     }
     else
--- a/Framework/Viewport/WidgetViewport.h	Tue Feb 26 21:26:47 2019 +0100
+++ b/Framework/Viewport/WidgetViewport.h	Tue Feb 26 21:33:16 2019 +0100
@@ -59,17 +59,25 @@
     virtual void MouseDown(MouseButton button,
                            int x,
                            int y,
-                           KeyboardModifiers modifiers);
+                           KeyboardModifiers modifiers,
+                           const std::vector<Touch>& displayTouches);
 
     virtual void MouseUp();
 
     virtual void MouseMove(int x, 
-                           int y);
+                           int y,
+                           const std::vector<Touch>& displayTouches);
 
     virtual void MouseEnter();
 
     virtual void MouseLeave();
 
+    virtual void TouchStart(const std::vector<Touch>& touches);
+    
+    virtual void TouchMove(const std::vector<Touch>& touches);
+    
+    virtual void TouchEnd(const std::vector<Touch>& touches);
+
     virtual void MouseWheel(MouseWheelDirection direction,
                             int x,
                             int y,
--- a/Framework/Widgets/EmptyWidget.h	Tue Feb 26 21:26:47 2019 +0100
+++ b/Framework/Widgets/EmptyWidget.h	Tue Feb 26 21:33:16 2019 +0100
@@ -76,7 +76,8 @@
     virtual IMouseTracker* CreateMouseTracker(MouseButton button,
                                               int x,
                                               int y,
-                                              KeyboardModifiers modifiers)
+                                              KeyboardModifiers modifiers,
+                                              const std::vector<Touch>& touches)
     {
       return NULL;
     }
--- a/Framework/Widgets/IWidget.h	Tue Feb 26 21:26:47 2019 +0100
+++ b/Framework/Widgets/IWidget.h	Tue Feb 26 21:33:16 2019 +0100
@@ -52,7 +52,8 @@
     virtual IMouseTracker* CreateMouseTracker(MouseButton button,
                                               int x,
                                               int y,
-                                              KeyboardModifiers modifiers) = 0;
+                                              KeyboardModifiers modifiers,
+                                              const std::vector<Touch>& touches) = 0;
 
     virtual void RenderMouseOver(Orthanc::ImageAccessor& target,
                                  int x,
--- a/Framework/Widgets/IWorldSceneInteractor.h	Tue Feb 26 21:26:47 2019 +0100
+++ b/Framework/Widgets/IWorldSceneInteractor.h	Tue Feb 26 21:33:16 2019 +0100
@@ -46,7 +46,8 @@
                                                             int viewportY,
                                                             double x,
                                                             double y,
-                                                            IStatusBar* statusBar) = 0;
+                                                            IStatusBar* statusBar,
+                                                            const std::vector<Touch>& touches) = 0;
 
         virtual void MouseOver(CairoContext& context,
                                WorldSceneWidget& widget,
--- a/Framework/Widgets/IWorldSceneMouseTracker.h	Tue Feb 26 21:26:47 2019 +0100
+++ b/Framework/Widgets/IWorldSceneMouseTracker.h	Tue Feb 26 21:33:16 2019 +0100
@@ -22,6 +22,7 @@
 #pragma once
 
 #include "../Viewport/CairoContext.h"
+#include "../Viewport/IMouseTracker.h" // only to get the "Touch" definition
 
 namespace OrthancStone
 {
@@ -46,6 +47,8 @@
     virtual void MouseMove(int displayX,
                            int displayY,
                            double sceneX,
-                           double sceneY) = 0;
+                           double sceneY,
+                           const std::vector<Touch>& displayTouches,
+                           const std::vector<Touch>& sceneTouches) = 0;
   };
 }
--- a/Framework/Widgets/LayoutWidget.cpp	Tue Feb 26 21:26:47 2019 +0100
+++ b/Framework/Widgets/LayoutWidget.cpp	Tue Feb 26 21:33:16 2019 +0100
@@ -68,9 +68,16 @@
     }
 
     virtual void MouseMove(int x, 
-                           int y)
+                           int y,
+                           const std::vector<Touch>& displayTouches)
     {
-      tracker_->MouseMove(x - left_, y - top_);
+      std::vector<Touch> relativeTouches;
+      for (size_t t = 0; t < displayTouches.size(); t++)
+      {
+        relativeTouches.push_back(Touch((int)displayTouches[t].x - left_, (int)displayTouches[t].y - top_));
+      }
+
+      tracker_->MouseMove(x - left_, y - top_, relativeTouches);
     }
   };
 
@@ -150,14 +157,16 @@
     IMouseTracker* CreateMouseTracker(MouseButton button,
                                       int x,
                                       int y,
-                                      KeyboardModifiers modifiers)
+                                      KeyboardModifiers modifiers,
+                                      const std::vector<Touch>& touches)
     {
       if (Contains(x, y))
       {
         IMouseTracker* tracker = widget_->CreateMouseTracker(button, 
                                                              x - left_, 
                                                              y - top_, 
-                                                             modifiers);
+                                                             modifiers,
+                                                             touches);
         if (tracker)
         {
           return new LayoutMouseTracker(tracker, left_, top_, width_, height_);
@@ -413,11 +422,12 @@
   IMouseTracker* LayoutWidget::CreateMouseTracker(MouseButton button,
                                                   int x,
                                                   int y,
-                                                  KeyboardModifiers modifiers)
+                                                  KeyboardModifiers modifiers,
+                                                  const std::vector<Touch>& touches)
   {
     for (size_t i = 0; i < children_.size(); i++)
     {
-      IMouseTracker* tracker = children_[i]->CreateMouseTracker(button, x, y, modifiers);
+      IMouseTracker* tracker = children_[i]->CreateMouseTracker(button, x, y, modifiers, touches);
       if (tracker != NULL)
       {
         return tracker;
--- a/Framework/Widgets/LayoutWidget.h	Tue Feb 26 21:26:47 2019 +0100
+++ b/Framework/Widgets/LayoutWidget.h	Tue Feb 26 21:33:16 2019 +0100
@@ -106,7 +106,8 @@
     virtual IMouseTracker* CreateMouseTracker(MouseButton button,
                                               int x,
                                               int y,
-                                              KeyboardModifiers modifiers);
+                                              KeyboardModifiers modifiers,
+                                              const std::vector<Touch>& touches);
 
     virtual void RenderMouseOver(Orthanc::ImageAccessor& target,
                                  int x,
--- a/Framework/Widgets/PanMouseTracker.cpp	Tue Feb 26 21:26:47 2019 +0100
+++ b/Framework/Widgets/PanMouseTracker.cpp	Tue Feb 26 21:33:16 2019 +0100
@@ -46,7 +46,9 @@
   void PanMouseTracker::MouseMove(int displayX,
                                   int displayY,
                                   double x,
-                                  double y)
+                                  double y,
+                                  const std::vector<Touch>& displayTouches,
+                                  const std::vector<Touch>& sceneTouches)
   {
     ViewportGeometry view = that_.GetView();
     view.SetPan(originalPanX_ + (x - downX_) * view.GetZoom(),
--- a/Framework/Widgets/PanMouseTracker.h	Tue Feb 26 21:26:47 2019 +0100
+++ b/Framework/Widgets/PanMouseTracker.h	Tue Feb 26 21:33:16 2019 +0100
@@ -54,6 +54,8 @@
     virtual void MouseMove(int displayX,
                            int displayY,
                            double x,
-                           double y);
+                           double y,
+                           const std::vector<Touch>& displayTouches,
+                           const std::vector<Touch>& sceneTouches);
   };
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Widgets/PanZoomMouseTracker.cpp	Tue Feb 26 21:33:16 2019 +0100
@@ -0,0 +1,137 @@
+/**
+ * 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 "PanZoomMouseTracker.h"
+
+#include <Core/Logging.h>
+#include <Core/OrthancException.h>
+#include <math.h>
+
+namespace OrthancStone
+{
+  Touch GetCenter(const std::vector<Touch>& touches)
+  {
+    return Touch((touches[0].x + touches[1].x) / 2.0f, (touches[0].y + touches[1].y) / 2.0f);
+  }
+
+  double GetDistance(const std::vector<Touch>& touches)
+  {
+    float dx = touches[0].x - touches[1].x;
+    float dy = touches[0].y - touches[1].y;
+    return sqrt((double)(dx * dx) + (double)(dy * dy));
+  }
+
+
+  PanZoomMouseTracker::PanZoomMouseTracker(WorldSceneWidget& that,
+                                           const std::vector<Touch>& startTouches)
+    : that_(that),
+      originalZoom_(that.GetView().GetZoom())
+  {
+    that.GetView().GetPan(originalPanX_, originalPanY_);
+    that.GetView().MapPixelCenterToScene(originalSceneTouches_, startTouches);
+
+    originalDisplayCenter_ = GetCenter(startTouches);
+    originalSceneCenter_ = GetCenter(originalSceneTouches_);
+    originalDisplayDistanceBetweenTouches_ = GetDistance(startTouches);
+
+//    printf("original Pan %f %f\n", originalPanX_, originalPanY_);
+//    printf("original Zoom %f \n", originalZoom_);
+//    printf("original distance %f \n", (float)originalDisplayDistanceBetweenTouches_);
+//    printf("original display touches 0 %f %f\n", startTouches[0].x, startTouches[0].y);
+//    printf("original display touches 1 %f %f\n", startTouches[1].x, startTouches[1].y);
+//    printf("original Scene center %f %f\n", originalSceneCenter_.x, originalSceneCenter_.y);
+
+    unsigned int height = that.GetView().GetDisplayHeight();
+
+    if (height <= 3)
+    {
+      idle_ = true;
+      LOG(WARNING) << "image is too small to zoom (current height = " << height << ")";
+    }
+    else
+    {
+      idle_ = false;
+      normalization_ = 1.0 / static_cast<double>(height - 1);
+    }
+
+  }
+
+
+  void PanZoomMouseTracker::Render(CairoContext& context,
+                                   double zoom)
+  {
+    throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+  }
+
+
+  void PanZoomMouseTracker::MouseMove(int displayX,
+                                      int displayY,
+                                      double x,
+                                      double y,
+                                      const std::vector<Touch>& displayTouches,
+                                      const std::vector<Touch>& sceneTouches)
+  {
+    ViewportGeometry view = that_.GetView();
+
+//    printf("Display touches 0 %f %f\n", displayTouches[0].x, displayTouches[0].y);
+//    printf("Display touches 1 %f %f\n", displayTouches[1].x, displayTouches[1].y);
+//    printf("Scene touches 0 %f %f\n", sceneTouches[0].x, sceneTouches[0].y);
+//    printf("Scene touches 1 %f %f\n", sceneTouches[1].x, sceneTouches[1].y);
+
+//    printf("zoom = %f\n", view.GetZoom());
+    Touch currentSceneCenter = GetCenter(sceneTouches);
+    double panX = originalPanX_ + (currentSceneCenter.x - originalSceneCenter_.x) * view.GetZoom();
+    double panY = originalPanY_ + (currentSceneCenter.y - originalSceneCenter_.y) * view.GetZoom();
+
+    view.SetPan(panX, panY);
+
+    static const double MIN_ZOOM = -4;
+    static const double MAX_ZOOM = 4;
+
+    if (!idle_)
+    {
+      double currentDistanceBetweenTouches = GetDistance(displayTouches);
+
+      double dy = static_cast<double>(currentDistanceBetweenTouches - originalDisplayDistanceBetweenTouches_) * normalization_;  // In the range [-1,1]
+      double z;
+
+      // Linear interpolation from [-1, 1] to [MIN_ZOOM, MAX_ZOOM]
+      if (dy < -1.0)
+      {
+        z = MIN_ZOOM;
+      }
+      else if (dy > 1.0)
+      {
+        z = MAX_ZOOM;
+      }
+      else
+      {
+        z = MIN_ZOOM + (MAX_ZOOM - MIN_ZOOM) * (dy + 1.0) / 2.0;
+      }
+
+      z = pow(2.0, z);
+
+      view.SetZoom(z * originalZoom_);
+    }
+
+    that_.SetView(view);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Widgets/PanZoomMouseTracker.h	Tue Feb 26 21:33:16 2019 +0100
@@ -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/>.
+ **/
+
+
+#pragma once
+
+#include "WorldSceneWidget.h"
+
+namespace OrthancStone
+{
+  class PanZoomMouseTracker : public IWorldSceneMouseTracker
+  {
+  private:
+    WorldSceneWidget&  that_;
+    std::vector<Touch> originalSceneTouches_;
+    Touch              originalSceneCenter_;
+    Touch              originalDisplayCenter_;
+    double             originalPanX_;
+    double             originalPanY_;
+    double             originalZoom_;
+    double             originalDisplayDistanceBetweenTouches_;
+    bool               idle_;
+    double             normalization_;
+
+  public:
+    PanZoomMouseTracker(WorldSceneWidget& that,
+                        const std::vector<Touch>& startTouches);
+    
+    virtual bool HasRender() const
+    {
+      return false;
+    }
+
+    virtual void MouseUp()
+    {
+    }
+
+    virtual void Render(CairoContext& context,
+                        double zoom);
+
+    virtual void MouseMove(int displayX,
+                           int displayY,
+                           double x,
+                           double y,
+                           const std::vector<Touch>& displayTouches,
+                           const std::vector<Touch>& sceneTouches);
+  };
+}
--- a/Framework/Widgets/TestCairoWidget.cpp	Tue Feb 26 21:26:47 2019 +0100
+++ b/Framework/Widgets/TestCairoWidget.cpp	Tue Feb 26 21:33:16 2019 +0100
@@ -99,7 +99,8 @@
     IMouseTracker* TestCairoWidget::CreateMouseTracker(MouseButton button,
                                                        int x,
                                                        int y,
-                                                       KeyboardModifiers modifiers)
+                                                       KeyboardModifiers modifiers,
+                                                       const std::vector<Touch>& touches)
     {
       UpdateStatusBar("Click");
       return NULL;
--- a/Framework/Widgets/TestCairoWidget.h	Tue Feb 26 21:26:47 2019 +0100
+++ b/Framework/Widgets/TestCairoWidget.h	Tue Feb 26 21:33:16 2019 +0100
@@ -51,7 +51,8 @@
       virtual IMouseTracker* CreateMouseTracker(MouseButton button,
                                                 int x,
                                                 int y,
-                                                KeyboardModifiers modifiers);
+                                                KeyboardModifiers modifiers,
+                                                const std::vector<Touch>& touches);
 
       virtual void MouseWheel(MouseWheelDirection direction,
                               int x,
--- a/Framework/Widgets/TestWorldSceneWidget.cpp	Tue Feb 26 21:26:47 2019 +0100
+++ b/Framework/Widgets/TestWorldSceneWidget.cpp	Tue Feb 26 21:33:16 2019 +0100
@@ -41,7 +41,8 @@
                                                           int viewportY,
                                                           double x,
                                                           double y,
-                                                          IStatusBar* statusBar)
+                                                          IStatusBar* statusBar,
+                                                          const std::vector<Touch>& touches)
       {
         if (statusBar)
         {
--- a/Framework/Widgets/WorldSceneWidget.cpp	Tue Feb 26 21:26:47 2019 +0100
+++ b/Framework/Widgets/WorldSceneWidget.cpp	Tue Feb 26 21:33:16 2019 +0100
@@ -23,6 +23,7 @@
 
 #include "PanMouseTracker.h"
 #include "ZoomMouseTracker.h"
+#include "PanZoomMouseTracker.h"
 
 #include <Core/Logging.h>
 #include <Core/OrthancException.h>
@@ -72,11 +73,20 @@
     }
 
     virtual void MouseMove(int x,
-                           int y)
+                           int y,
+                           const std::vector<Touch>& displayTouches)
     {
       double sceneX, sceneY;
       view_.MapPixelCenterToScene(sceneX, sceneY, x, y);
-      tracker_->MouseMove(x, y, sceneX, sceneY);
+
+      std::vector<Touch> sceneTouches;
+      for (size_t t = 0; t < displayTouches.size(); t++)
+      {
+        double sx, sy;
+        view_.MapPixelCenterToScene(sx, sy, (int)displayTouches[t].x, (int)displayTouches[t].y);
+        sceneTouches.push_back(Touch(sx, sy));
+      }
+      tracker_->MouseMove(x, y, sceneX, sceneY, displayTouches, sceneTouches);
     }
   };
 
@@ -145,7 +155,8 @@
   IMouseTracker* WorldSceneWidget::CreateMouseTracker(MouseButton button,
                                                       int x,
                                                       int y,
-                                                      KeyboardModifiers modifiers)
+                                                      KeyboardModifiers modifiers,
+                                                      const std::vector<Touch>& touches)
   {
     double sceneX, sceneY;
     view_.MapPixelCenterToScene(sceneX, sceneY, x, y);
@@ -155,7 +166,7 @@
 
     if (interactor_)
     {
-      tracker.reset(interactor_->CreateMouseTracker(*this, view_, button, modifiers, x, y, sceneX, sceneY, GetStatusBar()));
+      tracker.reset(interactor_->CreateMouseTracker(*this, view_, button, modifiers, x, y, sceneX, sceneY, GetStatusBar(), touches));
     }
     
     if (tracker.get() != NULL)
@@ -164,17 +175,26 @@
     }
     else if (hasDefaultMouseEvents_)
     {
-      switch (button)
+      printf("has default mouse events\n");
+      if (touches.size() == 2)
       {
-        case MouseButton_Middle:
-          return new SceneMouseTracker(view_, new PanMouseTracker(*this, x, y));
+        printf("2 touches !\n");
+        return new SceneMouseTracker(view_, new PanZoomMouseTracker(*this, touches));
+      }
+      else
+      {
+        switch (button)
+        {
+          case MouseButton_Middle:
+            return new SceneMouseTracker(view_, new PanMouseTracker(*this, x, y));
 
-        case MouseButton_Right:
-          return new SceneMouseTracker(view_, new ZoomMouseTracker(*this, x, y));
+          case MouseButton_Right:
+            return new SceneMouseTracker(view_, new ZoomMouseTracker(*this, x, y));
 
-        default:
-          return NULL;
-      }      
+          default:
+            return NULL;
+        }
+      }
     }
     else
     {
--- a/Framework/Widgets/WorldSceneWidget.h	Tue Feb 26 21:26:47 2019 +0100
+++ b/Framework/Widgets/WorldSceneWidget.h	Tue Feb 26 21:33:16 2019 +0100
@@ -88,7 +88,8 @@
     virtual IMouseTracker* CreateMouseTracker(MouseButton button,
                                               int x,
                                               int y,
-                                              KeyboardModifiers modifiers);
+                                              KeyboardModifiers modifiers,
+                                              const std::vector<Touch>& touches);
 
     virtual void MouseWheel(MouseWheelDirection direction,
                             int x,
--- a/Framework/Widgets/ZoomMouseTracker.cpp	Tue Feb 26 21:26:47 2019 +0100
+++ b/Framework/Widgets/ZoomMouseTracker.cpp	Tue Feb 26 21:33:16 2019 +0100
@@ -61,7 +61,9 @@
   void ZoomMouseTracker::MouseMove(int displayX,
                                    int displayY,
                                    double x,
-                                   double y)
+                                   double y,
+                                   const std::vector<Touch>& displayTouches,
+                                   const std::vector<Touch>& sceneTouches)
   {
     static const double MIN_ZOOM = -4;
     static const double MAX_ZOOM = 4;
--- a/Framework/Widgets/ZoomMouseTracker.h	Tue Feb 26 21:26:47 2019 +0100
+++ b/Framework/Widgets/ZoomMouseTracker.h	Tue Feb 26 21:33:16 2019 +0100
@@ -57,6 +57,8 @@
     virtual void MouseMove(int displayX,
                            int displayY,
                            double x,
-                           double y);
+                           double y,
+                           const std::vector<Touch>& displayTouches,
+                           const std::vector<Touch>& sceneTouches);
   };
 }
--- a/Platforms/Wasm/Defaults.cpp	Tue Feb 26 21:26:47 2019 +0100
+++ b/Platforms/Wasm/Defaults.cpp	Tue Feb 26 21:33:16 2019 +0100
@@ -49,7 +49,7 @@
 
     viewports_.push_back(viewport);
 
-    printf("There are now %d viewports in C++\n", viewports_.size());
+    printf("There are now %lu viewports in C++\n", viewports_.size());
 
     viewport->SetStatusBar(statusBar_);
 
@@ -64,7 +64,7 @@
   void EMSCRIPTEN_KEEPALIVE ReleaseCppViewport(ViewportHandle viewport) {
     viewports_.remove_if([viewport](const std::shared_ptr<OrthancStone::WidgetViewport>& v) { return v.get() == viewport;});
 
-    printf("There are now %d viewports in C++\n", viewports_.size());
+    printf("There are now %lu viewports in C++\n", viewports_.size());
   }
 
   void EMSCRIPTEN_KEEPALIVE CreateWasmApplication(ViewportHandle viewport) {
@@ -191,7 +191,7 @@
         return;  // Unknown button
     }
 
-    viewport->MouseDown(button, x, y, OrthancStone::KeyboardModifiers_None /* TODO */);
+    viewport->MouseDown(button, x, y, OrthancStone::KeyboardModifiers_None, std::vector<OrthancStone::Touch>());
   }
   
 
@@ -222,9 +222,82 @@
                                               int x,
                                               int y)
   {
-    viewport->MouseMove(x, y);
+    viewport->MouseMove(x, y, std::vector<OrthancStone::Touch>());
+  }
+
+  void GetTouchVector(std::vector<OrthancStone::Touch>& output,
+                      int touchCount,
+                      float x0,
+                      float y0,
+                      float x1,
+                      float y1,
+                      float x2,
+                      float y2)
+  {
+    // TODO: it might be nice to try to pass all the x0,y0 coordinates as arrays but that's not so easy to pass array between JS and C++
+    if (touchCount > 0)
+    {
+      output.push_back(OrthancStone::Touch(x0, y0));
+    }
+    if (touchCount > 1)
+    {
+      output.push_back(OrthancStone::Touch(x1, y1));
+    }
+    if (touchCount > 2)
+    {
+      output.push_back(OrthancStone::Touch(x2, y2));
+    }
+
   }
-  
+
+  void EMSCRIPTEN_KEEPALIVE ViewportTouchStart(ViewportHandle viewport,
+                                              int touchCount,
+                                              float x0,
+                                              float y0,
+                                              float x1,
+                                              float y1,
+                                              float x2,
+                                              float y2)
+  {
+    printf("touch start with %d touches\n", touchCount);
+
+    std::vector<OrthancStone::Touch> touches;
+    GetTouchVector(touches, touchCount, x0, y0, x1, y1, x2, y2);
+    viewport->TouchStart(touches);
+  }
+
+  void EMSCRIPTEN_KEEPALIVE ViewportTouchMove(ViewportHandle viewport,
+                                              int touchCount,
+                                              float x0,
+                                              float y0,
+                                              float x1,
+                                              float y1,
+                                              float x2,
+                                              float y2)
+  {
+    printf("touch move with %d touches\n", touchCount);
+
+    std::vector<OrthancStone::Touch> touches;
+    GetTouchVector(touches, touchCount, x0, y0, x1, y1, x2, y2);
+    viewport->TouchMove(touches);
+  }
+
+  void EMSCRIPTEN_KEEPALIVE ViewportTouchEnd(ViewportHandle viewport,
+                                              int touchCount,
+                                              float x0,
+                                              float y0,
+                                              float x1,
+                                              float y1,
+                                              float x2,
+                                              float y2)
+  {
+    printf("touch end with %d touches remaining\n", touchCount);
+
+    std::vector<OrthancStone::Touch> touches;
+    GetTouchVector(touches, touchCount, x0, y0, x1, y1, x2, y2);
+    viewport->TouchEnd(touches);
+  }
+
   void EMSCRIPTEN_KEEPALIVE ViewportKeyPressed(ViewportHandle viewport,
                                                int key,
                                                const char* keyChar, 
@@ -273,15 +346,13 @@
   {
     static std::string output; // we don't want the string to be deallocated when we return to JS code so we always use the same string (this is fine since JS is single-thread)
 
-    printf("SendMessageToStoneApplication\n");
-    printf("%s", message);
+    printf("SendMessageToStoneApplication (JS -> C++): %s\n", message);
 
     if (applicationWasmAdapter.get() != NULL) {
-      printf("sending message to C++\n");
       applicationWasmAdapter->HandleMessageFromWeb(output, std::string(message));
       return output.c_str();
     }
-    printf("This stone application does not have a Web Adapter");
+    printf("This stone application does not have a Web Adapter\n");
     return NULL;
   }
 
--- a/Platforms/Wasm/wasm-viewport.ts	Tue Feb 26 21:26:47 2019 +0100
+++ b/Platforms/Wasm/wasm-viewport.ts	Tue Feb 26 21:33:16 2019 +0100
@@ -65,6 +65,9 @@
     private ViewportMouseLeave : Function;
     private ViewportMouseWheel : Function;
     private ViewportKeyPressed : Function;
+    private ViewportTouchStart : Function;
+    private ViewportTouchMove : Function;
+    private ViewportTouchEnd : Function;
 
     private pimpl_ : any; // Private pointer to the underlying WebAssembly C++ object
 
@@ -91,6 +94,9 @@
       this.ViewportMouseLeave = this.module_.cwrap('ViewportMouseLeave', null, [ 'number' ]);
       this.ViewportMouseWheel = this.module_.cwrap('ViewportMouseWheel', null, [ 'number', 'number', 'number', 'number', 'number' ]);
       this.ViewportKeyPressed = this.module_.cwrap('ViewportKeyPressed', null, [ 'number', 'number', 'string', 'number', 'number' ]);
+      this.ViewportTouchStart = this.module_.cwrap('ViewportTouchStart', null, [ 'number', 'number', 'number', 'number', 'number', 'number', 'number' ]);
+      this.ViewportTouchMove = this.module_.cwrap('ViewportTouchMove', null, [ 'number', 'number', 'number', 'number', 'number', 'number', 'number' ]);
+      this.ViewportTouchEnd = this.module_.cwrap('ViewportTouchEnd', null, [ 'number', 'number', 'number', 'number', 'number', 'number', 'number' ]);
     }
 
     public GetCppViewport() : number {
@@ -197,7 +203,8 @@
       this.htmlCanvas_.addEventListener('mousedown', function(event) {
         var x = event.pageX - this.offsetLeft;
         var y = event.pageY - this.offsetTop;
-        that.ViewportMouseDown(that.pimpl_, event.button, x, y, 0 /* TODO detect modifier keys*/);    
+
+       that.ViewportMouseDown(that.pimpl_, event.button, x, y, 0 /* TODO detect modifier keys*/);    
       });
     
       this.htmlCanvas_.addEventListener('mousemove', function(event) {
@@ -229,12 +236,32 @@
         event.preventDefault();
       });
 
-      this.htmlCanvas_.addEventListener('touchstart', function(event) {
+      this.htmlCanvas_.addEventListener('touchstart', function(event: TouchEvent) {
         // don't propagate events to the whole body (this could zoom the entire page instead of zooming the viewport)
         event.preventDefault();
         event.stopPropagation();
 
-        that.ResetTouch();
+        // TODO: find a way to pass the coordinates as an array between JS and C++
+        var x0 = 0;
+        var y0 = 0;
+        var x1 = 0;
+        var y1 = 0;
+        var x2 = 0;
+        var y2 = 0;
+        if (event.targetTouches.length > 0) {
+          x0 = event.targetTouches[0].pageX;
+          y0 = event.targetTouches[0].pageY;
+        }
+        if (event.targetTouches.length > 1) {
+          x1 = event.targetTouches[1].pageX;
+          y1 = event.targetTouches[1].pageY;
+        }
+        if (event.targetTouches.length > 2) {
+          x2 = event.targetTouches[2].pageX;
+          y2 = event.targetTouches[2].pageY;
+        }
+
+        that.ViewportTouchStart(that.pimpl_, event.targetTouches.length, x0, y0, x1, y1, x2, y2);
       });
     
       this.htmlCanvas_.addEventListener('touchend', function(event) {
@@ -242,7 +269,27 @@
         event.preventDefault();
         event.stopPropagation();
 
-        that.ResetTouch();
+        // TODO: find a way to pass the coordinates as an array between JS and C++
+        var x0 = 0;
+        var y0 = 0;
+        var x1 = 0;
+        var y1 = 0;
+        var x2 = 0;
+        var y2 = 0;
+        if (event.targetTouches.length > 0) {
+          x0 = event.targetTouches[0].pageX;
+          y0 = event.targetTouches[0].pageY;
+        }
+        if (event.targetTouches.length > 1) {
+          x1 = event.targetTouches[1].pageX;
+          y1 = event.targetTouches[1].pageY;
+        }
+        if (event.targetTouches.length > 2) {
+          x2 = event.targetTouches[2].pageX;
+          y2 = event.targetTouches[2].pageY;
+        }
+
+        that.ViewportTouchEnd(that.pimpl_, event.targetTouches.length, x0, y0, x1, y1, x2, y2);
       });
     
       this.htmlCanvas_.addEventListener('touchmove', function(event: TouchEvent) {
@@ -251,6 +298,30 @@
         event.preventDefault();
         event.stopPropagation();
 
+
+        // TODO: find a way to pass the coordinates as an array between JS and C++
+        var x0 = 0;
+        var y0 = 0;
+        var x1 = 0;
+        var y1 = 0;
+        var x2 = 0;
+        var y2 = 0;
+        if (event.targetTouches.length > 0) {
+          x0 = event.targetTouches[0].pageX;
+          y0 = event.targetTouches[0].pageY;
+        }
+        if (event.targetTouches.length > 1) {
+          x1 = event.targetTouches[1].pageX;
+          y1 = event.targetTouches[1].pageY;
+        }
+        if (event.targetTouches.length > 2) {
+          x2 = event.targetTouches[2].pageX;
+          y2 = event.targetTouches[2].pageY;
+        }
+
+        that.ViewportTouchMove(that.pimpl_, event.targetTouches.length, x0, y0, x1, y1, x2, y2);
+        return;
+
         // if (!that.touchGestureInProgress_) {
         //   // starting a new gesture
         //   that.touchCount_ = event.targetTouches.length;
--- a/Resources/CMake/OrthancStoneConfiguration.cmake	Tue Feb 26 21:26:47 2019 +0100
+++ b/Resources/CMake/OrthancStoneConfiguration.cmake	Tue Feb 26 21:33:16 2019 +0100
@@ -30,6 +30,7 @@
 
 include(${ORTHANC_ROOT}/Resources/CMake/OrthancFrameworkConfiguration.cmake)
 include_directories(${ORTHANC_ROOT})
+include_directories(${ORTHANC_ROOT}/Core/Images) # hack for the numerous #include "../Enumerations.h" in Orthanc to work
 
 
 #####################################################################
@@ -255,9 +256,11 @@
   ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographyDicomLayer.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographyLayer.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographyLayerCropTracker.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographyLayerMaskTracker.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographyLayerMoveTracker.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographyLayerResizeTracker.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographyLayerRotateTracker.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographyMaskLayer.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographyScene.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographySceneCommand.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographySceneReader.cpp
@@ -294,6 +297,7 @@
   ${ORTHANC_STONE_ROOT}/Framework/Toolbox/ViewportGeometry.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Viewport/CairoContext.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Viewport/CairoSurface.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Viewport/IMouseTracker.h
   ${ORTHANC_STONE_ROOT}/Framework/Viewport/IStatusBar.h
   ${ORTHANC_STONE_ROOT}/Framework/Viewport/IViewport.h
   ${ORTHANC_STONE_ROOT}/Framework/Viewport/WidgetViewport.cpp
@@ -307,6 +311,7 @@
   ${ORTHANC_STONE_ROOT}/Framework/Widgets/IWorldSceneMouseTracker.h
   ${ORTHANC_STONE_ROOT}/Framework/Widgets/LayoutWidget.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Widgets/PanMouseTracker.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Widgets/PanZoomMouseTracker.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Widgets/SliceViewerWidget.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Widgets/TestCairoWidget.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Widgets/TestWorldSceneWidget.cpp
--- a/UnitTestsSources/TestMessageBroker.cpp	Tue Feb 26 21:26:47 2019 +0100
+++ b/UnitTestsSources/TestMessageBroker.cpp	Tue Feb 26 21:33:16 2019 +0100
@@ -312,7 +312,7 @@
   ASSERT_EQ(0, testCounter);
 }
 
-#if __cplusplus >= 201103L
+#if 0 //__cplusplus >= 201103L
 
 TEST(MessageBroker, TestLambdaSimpleUseCase)
 {