changeset 2003:963f28eb40cb deep-learning

integration default->deep-learning
author Sebastien Jodogne <s.jodogne@gmail.com>
date Wed, 02 Nov 2022 15:14:56 +0100
parents 2034ae383cfd (current diff) 1bb0a9716876 (diff)
children 37d6805b80ee
files Applications/StoneWebViewer/WebApplication/app.js Applications/StoneWebViewer/WebApplication/index.html Applications/StoneWebViewer/WebAssembly/StoneWebViewer.cpp
diffstat 58 files changed, 2951 insertions(+), 614 deletions(-) [+]
line wrap: on
line diff
--- a/Applications/Resources/RunCppCheck.sh	Fri Oct 28 07:47:55 2022 +0200
+++ b/Applications/Resources/RunCppCheck.sh	Wed Nov 02 15:14:56 2022 +0100
@@ -29,13 +29,14 @@
 fi
 
 cat <<EOF > /tmp/cppcheck-suppressions.txt
+constParameter:../../RenderingPlugin/Sources/Plugin.cpp:778
 stlFindInsert:../../Applications/Samples/WebAssembly/SingleFrameViewer/SingleFrameViewerApplication.h
-stlFindInsert:../../Applications/StoneWebViewer/WebAssembly/StoneWebViewer.cpp:508
-stlFindInsert:../../Applications/StoneWebViewer/WebAssembly/StoneWebViewer.cpp:1151
+stlFindInsert:../../Applications/StoneWebViewer/WebAssembly/StoneWebViewer.cpp:1166
+stlFindInsert:../../Applications/StoneWebViewer/WebAssembly/StoneWebViewer.cpp:523
 unpreciseMathCall:../../OrthancStone/Sources/Scene2D/Internals/CairoFloatTextureRenderer.cpp
 unpreciseMathCall:../../OrthancStone/Sources/Scene2D/LookupTableTextureSceneLayer.cpp
-unreadVariable:../../OrthancStone/Sources/Viewport/SdlViewport.cpp:143
-unreadVariable:../../OrthancStone/Sources/Viewport/SdlViewport.cpp:197
+unreadVariable:../../OrthancStone/Sources/Platforms/Sdl/SdlViewport.cpp:159
+unreadVariable:../../OrthancStone/Sources/Platforms/Sdl/SdlViewport.cpp:213
 unusedFunction
 EOF
 
--- a/Applications/Samples/Common/RtViewerApp.cpp	Fri Oct 28 07:47:55 2022 +0200
+++ b/Applications/Samples/Common/RtViewerApp.cpp	Wed Nov 02 15:14:56 2022 +0100
@@ -99,7 +99,12 @@
   {
     if (activeTracker_)
     {
-      activeTracker_->Cancel();
+      // Creating "dummyScene" is a HACK: It won't work with trackers
+      // that probe the values of the textures. For such trackers, the
+      // actual underlying scene should be provided.
+      Scene2D dummyScene;
+      activeTracker_->Cancel(dummyScene);
+      
       activeTracker_.reset();
     }
   }
--- a/Applications/Samples/Sdl/SingleFrameViewer/SdlSimpleViewer.cpp	Fri Oct 28 07:47:55 2022 +0200
+++ b/Applications/Samples/Sdl/SingleFrameViewer/SdlSimpleViewer.cpp	Wed Nov 02 15:14:56 2022 +0100
@@ -80,8 +80,11 @@
             << "  a\tCreate angle annotations" << std::endl
             << "  c\tCreate circle annotations" << std::endl
             << "  d\tDelete mode for annotations" << std::endl
-            << "  e\tEdit mode, don't create annotation (default)" << std::endl
-            << "  l\tCreate line annotations" << std::endl
+            << "  e\tCreate ellipse probe" << std::endl
+            << "  l\tCreate length annotations" << std::endl
+            << "  m\tModification/edit mode, don't create annotation (default)" << std::endl
+            << "  p\tCreate pixel probe" << std::endl
+            << "  r\tCreate rectangle probe" << std::endl
 #else
             << "  a\tEnable/disable the angle annotation tool" << std::endl
             << "  l\tEnable/disable the line annotation tool" << std::endl
@@ -194,6 +197,7 @@
 #if SAMPLE_USE_ANNOTATIONS_LAYER == 1
         OrthancStone::AnnotationsSceneLayer annotations(10);
         annotations.SetActiveTool(OrthancStone::AnnotationsSceneLayer::Tool_Edit);
+        annotations.SetProbedLayer(0);
 
 #else
         ActiveTool activeTool = ActiveTool_None;
@@ -300,7 +304,7 @@
 #endif
 
 #if SAMPLE_USE_ANNOTATIONS_LAYER == 1
-                  case SDLK_e:
+                  case SDLK_m:
                     annotations.SetActiveTool(OrthancStone::AnnotationsSceneLayer::Tool_Edit);
                     break;
 #endif
@@ -313,7 +317,7 @@
 
                   case SDLK_l:
 #if SAMPLE_USE_ANNOTATIONS_LAYER == 1
-                    annotations.SetActiveTool(OrthancStone::AnnotationsSceneLayer::Tool_Segment);
+                    annotations.SetActiveTool(OrthancStone::AnnotationsSceneLayer::Tool_Length);
 #else
                     if (activeTool == ActiveTool_Line)
                     {
@@ -380,6 +384,24 @@
 #endif
                     break;
 
+#if SAMPLE_USE_ANNOTATIONS_LAYER == 1
+                  case SDLK_p:
+                    annotations.SetActiveTool(OrthancStone::AnnotationsSceneLayer::Tool_PixelProbe);
+                    break;
+#endif
+
+#if SAMPLE_USE_ANNOTATIONS_LAYER == 1
+                  case SDLK_e:
+                    annotations.SetActiveTool(OrthancStone::AnnotationsSceneLayer::Tool_EllipseProbe);
+                    break;
+#endif
+
+#if SAMPLE_USE_ANNOTATIONS_LAYER == 1
+                  case SDLK_r:
+                    annotations.SetActiveTool(OrthancStone::AnnotationsSceneLayer::Tool_RectangleProbe);
+                    break;
+#endif
+
                   default:
                     break;
                 }
--- a/Applications/Samples/Sdl/SingleFrameViewer/SdlSimpleViewerApplication.h	Fri Oct 28 07:47:55 2022 +0200
+++ b/Applications/Samples/Sdl/SingleFrameViewer/SdlSimpleViewerApplication.h	Wed Nov 02 15:14:56 2022 +0100
@@ -130,7 +130,8 @@
 
     std::unique_ptr<TextureBaseSceneLayer> layer(
       message.GetInstanceParameters().CreateTexture(message.GetImage()));
-    layer->SetLinearInterpolation(true);
+    //layer->SetLinearInterpolation(true);
+    layer->SetLinearInterpolation(false);
 
     {
       std::unique_ptr<IViewport::ILock> lock(viewport_->Lock());
--- a/Applications/StoneWebViewer/NEWS	Fri Oct 28 07:47:55 2022 +0200
+++ b/Applications/StoneWebViewer/NEWS	Wed Nov 02 15:14:56 2022 +0100
@@ -1,15 +1,32 @@
 Pending changes in the mainline
 ===============================
 
+* New types of annotations:
+  - Text annotation
+  - Pixel probe
+  - Rectangle probe
+  - Ellipse probe
+* Added left/right rotation buttons
+* Added magnifying glass
+* Added vertical slider showing position of the current frame inside the series
 * Display of orientation markers
+* The text field with the instance number is editable to go to a specific instance
+* Linear interpolation of images can be turned off in the user preferences
 * New configuration options:
   - "ShowInfoPanelAtStartup" to control the info panel at startup
   - "ShowUserPreferencesButton" to show the button for setting preferences
   - "ShowNotForDiagnosticUsageDisclaimer" to show disclaimer about diagnostic usage
   - "DicomWebHttpHeaders" to set HTTP headers in DICOMweb requests
+
+Maintenance
+-----------
+
+* Fix issues with drag-and-drop
+* Fix handling of "token": The authorization header was not set in QIDO-RS requests
+* Start at the first frame for series presumably not containing 3D images (e.g. US)
 * More tolerance wrt. bad values of the Pixel Spacing (0028,0030) tag
 * Support of DICOM images without the Study Date (0008,0020) tag
-* Fix handling of "token": The authorization header was not set in QIDO-RS requests
+* Upgraded Vue.js to 2.6.14
 
 
 Version 2.3 (2022-03-24)
--- a/Applications/StoneWebViewer/WebApplication/app-fixes.css	Fri Oct 28 07:47:55 2022 +0200
+++ b/Applications/StoneWebViewer/WebApplication/app-fixes.css	Wed Nov 02 15:14:56 2022 +0100
@@ -43,3 +43,31 @@
     border-radius: 5px;
     padding: 7px;
 }
+
+
+.wvVerticalScrollbar {
+    position: absolute;
+    top: 0;
+    bottom: 0;
+    right: 0;
+    width: 10px;
+    background-color: #1b663e;
+}
+
+.wvInfoRightMargin {
+    right: 10px !important;  /* must match the "width" of "wvVerticalScrollbar" */
+}
+
+.wvVerticalScrollbarHighlight {
+    position: absolute;
+    left: 0;
+    right: 0;
+    height: 5%;
+    background-color: #00ff00;
+}
+
+.wvInputInstanceNumber {
+    width: 6ch;  /* width of 6 characters */
+    border: 2px solid rgba(255, 202, 128, 0.24);
+    background-color: transparent;
+}
--- a/Applications/StoneWebViewer/WebApplication/app.js	Fri Oct 28 07:47:55 2022 +0200
+++ b/Applications/StoneWebViewer/WebApplication/app.js	Wed Nov 02 15:14:56 2022 +0100
@@ -42,10 +42,15 @@
 var MOUSE_TOOL_ZOOM = 2;
 var MOUSE_TOOL_PAN = 3;
 var MOUSE_TOOL_CROSSHAIR = 4;
-var MOUSE_TOOL_CREATE_SEGMENT = 5;
+var MOUSE_TOOL_CREATE_LENGTH = 5;
 var MOUSE_TOOL_CREATE_ANGLE = 6;
 var MOUSE_TOOL_CREATE_CIRCLE = 7;
 var MOUSE_TOOL_REMOVE_MEASURE = 8;
+var MOUSE_TOOL_CREATE_PIXEL_PROBE = 9;       // New in 2.4
+var MOUSE_TOOL_CREATE_ELLIPSE_PROBE = 10;    // New in 2.4
+var MOUSE_TOOL_CREATE_RECTANGLE_PROBE = 11;  // New in 2.4
+var MOUSE_TOOL_CREATE_TEXT_ANNOTATION = 12;  // New in 2.4
+var MOUSE_TOOL_MAGNIFYING_GLASS = 13;        // New in 2.4
 
 
 function getParameterFromUrl(key) {
@@ -109,6 +114,9 @@
   else if (config == "Crosshair") {
     return stone.WebViewerAction.CROSSHAIR;
   }
+  else if (config == "MagnifyingGlass") {
+    return stone.WebViewerAction.MAGNIFYING_GLASS;
+  }
   else {
     alert('Unsupported mouse action in the configuration file: ' + config);
     return stone.WebViewerAction.PAN;
@@ -141,6 +149,7 @@
       stone: stone,  // To access global object "stone" from "index.html"
       status: 'waiting',
       currentFrame: 0,
+      currentFrameFromUser: 0,
       numberOfFrames: 0,
       quality: '',
       cineControls: false,
@@ -158,12 +167,24 @@
   },
   watch: {
     currentFrame: function(newVal, oldVal) {
-      /**
-       * The "FrameUpdated" event has been received, which indicates
-       * that the schedule frame has been displayed: The cine loop can
-       * proceed to the next frame (check out "CineCallback()").
-       **/
-      this.cineLoadingFrame = false;
+      this.currentFrameFromUser = newVal + 1;
+      if (this.cineLoadingFrame) {
+        /**
+         * The "FrameUpdated" event has been received, which indicates
+         * that the schedule frame has been displayed: The cine loop can
+         * proceed to the next frame (check out "CineCallback()").
+         **/
+        this.cineLoadingFrame = false;
+      } else {
+        stone.SetFrame(this.canvasId, newVal);
+      }
+    },
+    currentFrameFromUser: function(newVal, oldVal) {
+      if (parseInt(newVal, 10) !== NaN &&
+          newVal >= 1 &&
+          newVal <= this.numberOfFrames) {
+        this.currentFrame = this.currentFrameFromUser - 1;
+      }
     },
     content: function(newVal, oldVal) {
       this.status = 'loading';
@@ -424,6 +445,7 @@
       // User preferences (stored in the local storage)
       settingNotDiagnostic: true,
       settingSoftwareRendering: false,
+      settingLinearInterpolation: true,
 
       layoutCountX: 1,
       layoutCountY: 1,
@@ -505,6 +527,9 @@
     },
     settingSoftwareRendering: function(newVal, oldVal) {
       localStorage.settingSoftwareRendering = (newVal ? '1' : '0');
+    },
+    settingLinearInterpolation: function(newVal, oldVal) {
+      localStorage.settingLinearInterpolation = (newVal ? '1' : '0');
     }
   },
   methods: {
@@ -885,9 +910,25 @@
       }
     },
 
+    RotateLeft: function() {
+      var canvas = this.GetActiveCanvas();
+      if (canvas != '') {
+        stone.RotateLeft(canvas);
+      }
+    },
+
+    RotateRight: function() {
+      var canvas = this.GetActiveCanvas();
+      if (canvas != '') {
+        stone.RotateRight(canvas);
+      }
+    },
+
     ApplyPreferences: function() {
       this.modalPreferences = false;
 
+      stone.SetLinearInterpolation(localStorage.settingLinearInterpolation);
+
       if ((stone.IsSoftwareRendering() != 0) != this.settingSoftwareRendering) {
         document.location.reload();
       }
@@ -1114,6 +1155,8 @@
   },
   
   mounted: function() {
+    // Warning: In this function, the "stone" global object is not initialized yet!
+    
     this.SetViewportLayout('1x1');
 
     if (localStorage.settingNotDiagnostic) {
@@ -1124,6 +1167,10 @@
       this.settingSoftwareRendering = (localStorage.settingSoftwareRendering == '1');
     }
 
+    if (localStorage.settingLinearInterpolation) {
+      this.settingLinearInterpolation = (localStorage.settingLinearInterpolation == '1');
+    }
+
     var that = this;
     
     window.addEventListener('VirtualSeriesThumbnailLoaded', function(args) {
@@ -1149,6 +1196,15 @@
     window.addEventListener('StoneAnnotationRemoved', function() {
       // Ignore
     });
+
+    window.addEventListener('TextAnnotationRequired', function(args) {
+      var label = prompt('Enter your annotation:', '');
+      if (label !== null) {
+        stone.AddTextAnnotation(args.detail.canvasId, label,
+                                args.detail.pointedX, args.detail.pointedY,
+                                args.detail.labelX, args.detail.labelY);
+      }
+    });
   }
 });
 
@@ -1163,7 +1219,8 @@
   stone.Setup(Module);
   stone.SetDicomWebRoot(app.globalConfiguration.DicomWebRoot,
                         true /* assume "/rendered" is available in DICOMweb (could be a configuration option) */);
-  stone.SetSoftwareRendering(localStorage.settingSoftwareRendering == '1');
+  stone.SetSoftwareRendering(app.settingSoftwareRendering);
+  stone.SetLinearInterpolation(app.settingLinearInterpolation);
 
   if ('DicomCacheSize' in app.globalConfiguration) {
     stone.SetDicomCacheSize(app.globalConfiguration.DicomCacheSize);
--- a/Applications/StoneWebViewer/WebApplication/configuration.json	Fri Oct 28 07:47:55 2022 +0200
+++ b/Applications/StoneWebViewer/WebApplication/configuration.json	Wed Nov 02 15:14:56 2022 +0100
@@ -37,7 +37,8 @@
      * windowing, zoom and pan from a single mouse configuration. The
      * behaviour of the combined tool is defined in
      * CombinedToolBehaviour. The available mouse actions are
-     * "Crosshair", "Windowing", "Pan", "Rotate" and "Zoom".
+     * "Crosshair", "Windowing", "Pan", "Rotate", "Zoom" and
+     * "MagnifyingGlass".
      **/
     "CombinedToolEnabled" : true,
     "CombinedToolBehaviour" : {
--- a/Applications/StoneWebViewer/WebApplication/index.html	Fri Oct 28 07:47:55 2022 +0200
+++ b/Applications/StoneWebViewer/WebApplication/index.html	Wed Nov 02 15:14:56 2022 +0100
@@ -85,7 +85,11 @@
               </label>
               <br>
             </div>
-            <label>Use software rendering (will reload the viewer)
+            <label>Enable linear interpolation
+              <input type="checkbox" style="margin-left: 20px" v-model="settingLinearInterpolation">
+            </label>
+            <br>
+            <label>Use software rendering (slower, will reload the viewer)
               <input type="checkbox" style="margin-left: 20px" v-model="settingSoftwareRendering">
             </label>
             <br><br>
@@ -497,6 +501,31 @@
 
             <div class="ng-scope inline-object">
               <button class="wvButton--underline text-center"
+                      data-toggle="tooltip" data-title="Magnifying glass"
+                      v-bind:class="{ 'active' : mouseTool == MOUSE_TOOL_MAGNIFYING_GLASS }"
+                      v-on:click="SetMouseButtonActions(MOUSE_TOOL_MAGNIFYING_GLASS, stone.WebViewerAction.MAGNIFYING_GLASS, stone.WebViewerAction.PAN, stone.WebViewerAction.ZOOM)">
+                <i class="fas fa-search-plus"></i>
+              </button>
+            </div>
+
+            <div class="ng-scope inline-object">
+              <button class="wvButton--underline text-center"
+                      data-toggle="tooltip" data-title="Rotate to the left"
+                      v-on:click="RotateLeft()">
+                <i class="fas fa-undo"></i>
+              </button>
+            </div>
+
+            <div class="ng-scope inline-object">
+              <button class="wvButton--underline text-center"
+                      data-toggle="tooltip" data-title="Rotate to the right"
+                      v-on:click="RotateRight()">
+                <i class="fas fa-undo fa-flip-horizontal"></i>
+              </button>
+            </div>
+            
+            <div class="ng-scope inline-object">
+              <button class="wvButton--underline text-center"
                       data-toggle="tooltip" data-title="Flip horizontally"
                       v-on:click="FlipX()">
                 <i class="fas fa-exchange-alt"></i>
@@ -513,10 +542,10 @@
             
             <div class="ng-scope inline-object">
               <button class="wvButton--underline text-center"
-                      v-bind:class="{ 'active' : mouseTool == MOUSE_TOOL_CREATE_SEGMENT }"
-                      v-on:click="SetLeftMouseButtonAction(MOUSE_TOOL_CREATE_SEGMENT, stone.WebViewerAction.CREATE_SEGMENT)"
+                      v-bind:class="{ 'active' : mouseTool == MOUSE_TOOL_CREATE_LENGTH }"
+                      v-on:click="SetLeftMouseButtonAction(MOUSE_TOOL_CREATE_LENGTH, stone.WebViewerAction.CREATE_LENGTH)"
                       data-toggle="tooltip" data-title="Measure length">
-                <i class="fas fa-arrows-alt-h"></i>
+                <i class="fas fa-ruler"></i>
               </button>
             </div>
 
@@ -525,7 +554,7 @@
                       v-bind:class="{ 'active' : mouseTool == MOUSE_TOOL_CREATE_ANGLE }"
                       v-on:click="SetLeftMouseButtonAction(MOUSE_TOOL_CREATE_ANGLE, stone.WebViewerAction.CREATE_ANGLE)"
                       data-toggle="tooltip" data-title="Measure angle">
-                <i class="fas fa-angle-left fa-lg"></i>
+                <i class="fas fa-drafting-compass"></i>
               </button>
             </div>
 
@@ -540,9 +569,45 @@
 
             <div class="ng-scope inline-object">
               <button class="wvButton--underline text-center"
+                      v-bind:class="{ 'active' : mouseTool == MOUSE_TOOL_CREATE_PIXEL_PROBE }"
+                      v-on:click="SetLeftMouseButtonAction(MOUSE_TOOL_CREATE_PIXEL_PROBE, stone.WebViewerAction.CREATE_PIXEL_PROBE)"
+                      data-toggle="tooltip" data-title="Pixel probe">
+                <i class="fas fa-microscope"></i>
+              </button>
+            </div>
+
+            <div class="ng-scope inline-object">
+              <button class="wvButton--underline text-center"
+                      v-bind:class="{ 'active' : mouseTool == MOUSE_TOOL_CREATE_RECTANGLE_PROBE }"
+                      v-on:click="SetLeftMouseButtonAction(MOUSE_TOOL_CREATE_RECTANGLE_PROBE, stone.WebViewerAction.CREATE_RECTANGLE_PROBE)"
+                      data-toggle="tooltip" data-title="Rectangle probe">
+                <i class="fas fa-plus-square"></i>
+              </button>
+            </div>
+
+            <div class="ng-scope inline-object">
+              <button class="wvButton--underline text-center"
+                      v-bind:class="{ 'active' : mouseTool == MOUSE_TOOL_CREATE_ELLIPSE_PROBE }"
+                      v-on:click="SetLeftMouseButtonAction(MOUSE_TOOL_CREATE_ELLIPSE_PROBE, stone.WebViewerAction.CREATE_ELLIPSE_PROBE)"
+                      data-toggle="tooltip" data-title="Ellipse probe">
+                <i class="fas fa-plus-circle"></i>
+              </button>
+            </div>
+
+            <div class="ng-scope inline-object">
+              <button class="wvButton--underline text-center"
+                      v-bind:class="{ 'active' : mouseTool == MOUSE_TOOL_CREATE_TEXT_ANNOTATION }"
+                      v-on:click="SetLeftMouseButtonAction(MOUSE_TOOL_CREATE_TEXT_ANNOTATION, stone.WebViewerAction.CREATE_TEXT_ANNOTATION)"
+                      data-toggle="tooltip" data-title="Add text annotation">
+                <i class="fas fa-comment-dots"></i>
+              </button>
+            </div>
+
+            <div class="ng-scope inline-object">
+              <button class="wvButton--underline text-center"
                       v-bind:class="{ 'active' : mouseTool == MOUSE_TOOL_REMOVE_MEASURE }"
                       v-on:click="SetLeftMouseButtonAction(MOUSE_TOOL_REMOVE_MEASURE, stone.WebViewerAction.REMOVE_MEASURE)"
-                      data-toggle="tooltip" data-title="Delete measurement">
+                      data-toggle="tooltip" data-title="Delete annotation">
                 <i class="fas fa-trash"></i>
               </button>
             </div>
@@ -674,7 +739,8 @@
                            'wvSplitpane__cellBorder--yellow' : content.series.color == 'yellow', 
                            'wvSplitpane__cellBorder--violet' : content.series.color == 'violet'
                            }" 
-             ondragover="event.preventDefault()"
+             v-on:dragenter="$event.preventDefault()"
+             v-on:dragover="$event.preventDefault()"
              v-on:drop="DragDrop($event)"
              style="width:100%;height:100%">
           <div class="wvSplitpane__cell" 
@@ -687,13 +753,21 @@
                       oncontextmenu="return false"></canvas>
 
               <div v-show="showInfo">
+                <div v-if="numberOfFrames > 1" class="wvVerticalScrollbar"
+                       v-on:click="var offset = $event.currentTarget.getClientRects()[0]; var y = $event.clientY - offset.top; var height = $event.currentTarget.offsetHeight; currentFrame = Math.min(numberOfFrames - 1, Math.floor(y * numberOfFrames / (height - 1)));">
+                  <div class="wvVerticalScrollbarHighlight"
+                       v-bind:style="{ top: (currentFrame / (numberOfFrames - 1) * 95.0) + '%' }">
+                  </div>
+                </div>
+                
                 <div class="wv-overlay">
                   <div v-if="'tags' in content.series" class="wv-overlay-topleft">
                     {{ content.series.tags[PATIENT_NAME] }}<br/>
                     {{ content.series.tags[PATIENT_ID] }}<br/>
                     {{ app.FormatDate(content.series.tags[PATIENT_BIRTH_DATE]) }}
                   </div>
-                  <div v-if="'tags' in content.series" class="wv-overlay-topright">
+                  <div v-if="'tags' in content.series" class="wv-overlay-topright"
+                       v-bind:class="{ 'wvInfoRightMargin' : numberOfFrames > 1 }">
                     {{ content.series.tags[STUDY_DESCRIPTION] }}<br/>
                     <span v-if="contentDate !== undefined && contentDate != ''">{{ app.FormatDate(contentDate) }} <span v-show="contentTime != ''">{{ app.FormatTime(contentTime) }}</span><br/></span>
                     <span v-else="contentDate === undefined || contentDate == ''">{{ app.FormatDate(content.series.tags[STUDY_DATE]) }}<br/></span>
@@ -723,7 +797,9 @@
                         </button>
                       </div>
                       <span data-toggle="tooltip" data-title="Current frame number">
-                        &nbsp;&nbsp;{{ currentFrame + 1 }} / {{ numberOfFrames }}&nbsp;&nbsp;
+                        &nbsp;&nbsp;
+                        <input type="text" v-model="currentFrameFromUser" class="wvInputInstanceNumber"> / {{ numberOfFrames }}
+                        &nbsp;&nbsp;
                       </span>
                       <div class="btn-group btn-group-sm" role="group">                        
                         <button class="btn btn-primary" @click="IncrementFrame()">
@@ -746,7 +822,8 @@
                       </div>
                     </span>
                   </div>
-                  <div class="wv-overlay-bottomright wvPrintExclude" style="bottom: 0px">
+                  <div class="wv-overlay-bottomright wvPrintExclude" style="bottom: 0px"
+                       v-bind:class="{ 'wvInfoRightMargin' : numberOfFrames > 1 }">
                     <div v-if="windowingWidth != 0">
                       ww/wc: {{ windowingWidth }} / {{ windowingCenter }}
                     </div>
--- a/Applications/StoneWebViewer/WebAssembly/JavaScriptLibraries.cmake	Fri Oct 28 07:47:55 2022 +0200
+++ b/Applications/StoneWebViewer/WebAssembly/JavaScriptLibraries.cmake	Wed Nov 02 15:14:56 2022 +0100
@@ -31,9 +31,9 @@
   "${CMAKE_CURRENT_BINARY_DIR}/bootstrap-3.4.1-dist")
 
 DownloadPackage(
-  "8242afdc5bd44105d9dc9e6535315484"
-  "${BASE_URL}/dicom-web/vuejs-2.6.10.tar.gz"
-  "${CMAKE_CURRENT_BINARY_DIR}/vue-2.6.10")
+  "ca84d906dcaecd4c66553bf49b547f65"
+  "${BASE_URL}/dicom-web/vue-2.6.14.tar.gz"
+  "${CMAKE_CURRENT_BINARY_DIR}/vue-2.6.14")
 
 DownloadPackage(
   "3e2b4e1522661f7fcf8ad49cb933296c"
@@ -69,7 +69,7 @@
   FILES
   ${CMAKE_CURRENT_BINARY_DIR}/bootstrap-3.4.1-dist/js/bootstrap.min.js
   ${CMAKE_SOURCE_DIR}/ThirdPartyDownloads/jquery-3.4.1.min.js
-  ${CMAKE_CURRENT_BINARY_DIR}/vue-2.6.10/dist/vue.min.js
+  ${CMAKE_CURRENT_BINARY_DIR}/vue-2.6.14/dist/vue.min.js
   ${CMAKE_CURRENT_BINARY_DIR}/axios-0.19.0/dist/axios.min.js
   ${CMAKE_CURRENT_BINARY_DIR}/axios-0.19.0/dist/axios.min.map
   ${CMAKE_CURRENT_BINARY_DIR}/pdfjs/pdf.js
--- a/Applications/StoneWebViewer/WebAssembly/ParseWebAssemblyExports.py	Fri Oct 28 07:47:55 2022 +0200
+++ b/Applications/StoneWebViewer/WebAssembly/ParseWebAssemblyExports.py	Wed Nov 02 15:14:56 2022 +0100
@@ -174,6 +174,8 @@
                         arg['type'] = "'int'"
                     elif argType == 'const char *':
                         arg['type'] = "'string'"
+                    elif argType == 'double':
+                        arg['type'] = "'double'"
                     else:
                         raise Exception('Unknown type for argument "%s" in function "%s()": %s' %
                                         (child.displayname, node.spelling, argType))
--- a/Applications/StoneWebViewer/WebAssembly/StoneWebViewer.cpp	Fri Oct 28 07:47:55 2022 +0200
+++ b/Applications/StoneWebViewer/WebAssembly/StoneWebViewer.cpp	Wed Nov 02 15:14:56 2022 +0100
@@ -96,10 +96,13 @@
 #include "../../../OrthancStone/Sources/Platforms/WebAssembly/WebGLViewport.h"
 
 
-#include <boost/math/special_functions/round.hpp>
+#include <algorithm>
 #include <boost/make_shared.hpp>
+#include <boost/math/constants/constants.hpp>
+#include <boost/math/special_functions/round.hpp>
 #include <stdio.h>
-#include <algorithm>
+
+static const double PI = boost::math::constants::pi<double>();
 
 #if !defined(STONE_WEB_VIEWER_EXPORT)
 // We are not running ParseWebAssemblyExports.py, but we're compiling the wasm
@@ -138,11 +141,16 @@
     WebViewerAction_Pan,
     WebViewerAction_Rotate,
     WebViewerAction_Crosshair,
+    WebViewerAction_MagnifyingGlass,       // New in 2.4
     
     WebViewerAction_CreateAngle,
     WebViewerAction_CreateCircle,
-    WebViewerAction_CreateSegment,
-    WebViewerAction_RemoveMeasure
+    WebViewerAction_CreateLength,
+    WebViewerAction_RemoveMeasure,
+    WebViewerAction_CreatePixelProbe,      // New in 2.4
+    WebViewerAction_CreateEllipseProbe,    // New in 2.4
+    WebViewerAction_CreateRectangleProbe,  // New in 2.4
+    WebViewerAction_CreateTextAnnotation   // New in 2.4
     };
   
 
@@ -163,12 +171,19 @@
     case WebViewerAction_Rotate:
       return OrthancStone::MouseAction_Rotate;
       
+    case WebViewerAction_MagnifyingGlass:
+      return OrthancStone::MouseAction_MagnifyingGlass;
+      
     case WebViewerAction_None:
     case WebViewerAction_Crosshair:
     case WebViewerAction_CreateAngle:
     case WebViewerAction_CreateCircle:
-    case WebViewerAction_CreateSegment:
+    case WebViewerAction_CreateLength:
     case WebViewerAction_RemoveMeasure:
+    case WebViewerAction_CreatePixelProbe:
+    case WebViewerAction_CreateEllipseProbe:
+    case WebViewerAction_CreateRectangleProbe:
+    case WebViewerAction_CreateTextAnnotation:
       return OrthancStone::MouseAction_None;
 
     default:
@@ -1198,9 +1213,10 @@
   }
   
 public:
-  explicit SeriesCursor(size_t framesCount) :
+  explicit SeriesCursor(size_t framesCount,
+                        bool startAtMiddle /* Whether to start at the middle frame */) :
     framesCount_(framesCount),
-    currentFrame_(framesCount / 2),  // Start at the middle frame    
+    currentFrame_(startAtMiddle ? framesCount / 2 : 0),
     isCircularPrefetch_(false),
     lastAction_(Action_None)
   {
@@ -1581,16 +1597,20 @@
     virtual void SignalStoneAnnotationAdded(const ViewerViewport& viewport) = 0;
 
     virtual void SignalStoneAnnotationRemoved(const ViewerViewport& viewport) = 0;
+
+    virtual void SignalStoneTextAnnotationRequired(const ViewerViewport& viewport,
+                                                   const OrthancStone::ScenePoint2D& pointedPosition,
+                                                   const OrthancStone::ScenePoint2D& labelPosition) = 0;
   };
 
 private:
   static const int LAYER_TEXTURE = 0;
   static const int LAYER_OVERLAY = 1;
-  static const int LAYER_ORIENTATION_MARKERS = 2;
-  static const int LAYER_REFERENCE_LINES = 3;
-  static const int LAYER_ANNOTATIONS_OSIRIX = 4;
-  static const int LAYER_ANNOTATIONS_STONE = 5;
-  static const int LAYER_DEEP_LEARNING = 6;
+  static const int LAYER_DEEP_LEARNING = 2;
+  static const int LAYER_ORIENTATION_MARKERS = 3;
+  static const int LAYER_REFERENCE_LINES = 4;
+  static const int LAYER_ANNOTATIONS_OSIRIX = 5;
+  static const int LAYER_ANNOTATIONS_STONE = 6;
 
   
   class ICommand : public Orthanc::IDynamicObject
@@ -1985,8 +2005,6 @@
   std::vector<float>                           windowingPresetWidths_;
   unsigned int                                 cineRate_;
   bool                                         inverted_;
-  bool                                         flipX_;
-  bool                                         flipY_;
   bool                                         fitNextContent_;
   std::list<PrefetchItem>                      prefetchQueue_;
   bool                                         serverSideTranscoding_;
@@ -2008,11 +2026,12 @@
   // coordinates of the current texture, with (0,0) corresponding to
   // the center of the top-left pixel
   boost::shared_ptr<OrthancStone::AnnotationsSceneLayer>  stoneAnnotations_;
+  
+  bool linearInterpolation_;
 
   boost::shared_ptr<Orthanc::ImageAccessor>  deepLearningMask_;
   std::string deepLearningSopInstanceUid_;
   unsigned int deepLearningFrameNumber_;
-  
 
   void ScheduleNextPrefetch()
   {
@@ -2150,9 +2169,7 @@
 
     assert(layer.get() != NULL);
 
-    layer->SetLinearInterpolation(true);
-    layer->SetFlipX(flipX_);
-    layer->SetFlipY(flipY_);
+    layer->SetLinearInterpolation(linearInterpolation_);
 
     double pixelSpacingX, pixelSpacingY;
 
@@ -2206,8 +2223,7 @@
       if (accessor.IsValid())
       {
         overlay.reset(accessor.CreateTexture());
-        overlay->SetFlipX(flipX_);
-        overlay->SetFlipY(flipY_);
+        overlay->SetLinearInterpolation(false);
       }
     }
 
@@ -2252,8 +2268,6 @@
       deepLearningLayer.reset(new OrthancStone::LookupTableTextureSceneLayer(*deepLearningMask_));
       deepLearningLayer->SetLookupTable(lut);
       deepLearningLayer->SetPixelSpacing(pixelSpacingX, pixelSpacingY);
-      deepLearningLayer->SetFlipX(flipX_);
-      deepLearningLayer->SetFlipY(flipY_);
     }
 
     StoneAnnotationsRegistry::GetInstance().Load(*stoneAnnotations_, instance.GetSopInstanceUid(), frameIndex);
@@ -2523,25 +2537,6 @@
           lock->GetController().GetScene().GetLayer(LAYER_TEXTURE)).
           SetCustomWindowing(windowingCenter_, windowingWidth_);
       }
-
-      {
-        OrthancStone::TextureBaseSceneLayer& layer = 
-          dynamic_cast<OrthancStone::TextureBaseSceneLayer&>(
-            lock->GetController().GetScene().GetLayer(LAYER_TEXTURE));
-
-        layer.SetFlipX(flipX_);
-        layer.SetFlipY(flipY_);
-      }
-
-      if (lock->GetController().GetScene().HasLayer(LAYER_OVERLAY))
-      {
-        OrthancStone::TextureBaseSceneLayer& layer = 
-          dynamic_cast<OrthancStone::TextureBaseSceneLayer&>(
-            lock->GetController().GetScene().GetLayer(LAYER_OVERLAY));
-
-        layer.SetFlipX(flipX_);
-        layer.SetFlipY(flipY_);
-      }
         
       lock->Invalidate();
     }
@@ -2552,13 +2547,12 @@
                  const OrthancStone::DicomSource& source,
                  const std::string& canvas,
                  boost::shared_ptr<FramesCache> cache,
-                 bool softwareRendering) :
+                 bool softwareRendering,
+                 bool linearInterpolation) :
     context_(context),
     source_(source),
     framesCache_(cache),
     fitNextContent_(true),
-    flipX_(false),
-    flipY_(false),
     hasFocusOnInstance_(false),
     focusFrameNumber_(0),
     synchronizationOffset_(OrthancStone::LinearAlgebra::CreateVector(0, 0, 0)),
@@ -2566,7 +2560,8 @@
     centralPhysicalWidth_(1),
     centralPhysicalHeight_(1),
     centralPixelSpacingX_(1),
-    centralPixelSpacingY_(1)
+    centralPixelSpacingY_(1),
+    linearInterpolation_(linearInterpolation)
   {
     if (!framesCache_)
     {
@@ -2596,6 +2591,7 @@
     SetWindowingPreset();
 
     stoneAnnotations_.reset(new OrthancStone::AnnotationsSceneLayer(LAYER_ANNOTATIONS_STONE));
+    stoneAnnotations_->SetProbedLayer(LAYER_TEXTURE);
   }
 
 
@@ -2733,6 +2729,14 @@
     }
   }
 
+  void Handle(const OrthancStone::AnnotationsSceneLayer::TextAnnotationRequiredMessage& message)
+  {
+    if (observer_.get() != NULL)
+    {
+      observer_->SignalStoneTextAnnotationRequired(*this, message.GetPointedPosition(), message.GetLabelPosition());
+    }
+  }
+
 public:
   virtual ~ViewerViewport()
   {
@@ -2746,10 +2750,11 @@
                                                   const OrthancStone::DicomSource& source,
                                                   const std::string& canvas,
                                                   boost::shared_ptr<FramesCache> cache,
-                                                  bool softwareRendering)
+                                                  bool softwareRendering,
+                                                  bool linearInterpolation)
   {
     boost::shared_ptr<ViewerViewport> viewport(
-      new ViewerViewport(context, source, canvas, cache, softwareRendering));
+      new ViewerViewport(context, source, canvas, cache, softwareRendering, linearInterpolation));
 
     {
       std::unique_ptr<OrthancStone::ILoadersContext::ILock> lock(context.Lock());
@@ -2772,6 +2777,9 @@
 
       viewport->Register<OrthancStone::AnnotationsSceneLayer::AnnotationRemovedMessage>(
         *viewport->stoneAnnotations_, &ViewerViewport::Handle);
+
+      viewport->Register<OrthancStone::AnnotationsSceneLayer::TextAnnotationRequiredMessage>(
+        *viewport->stoneAnnotations_, &ViewerViewport::Handle);
     }
 
     {
@@ -2790,8 +2798,6 @@
       throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
     }
 
-    flipX_ = false;
-    flipY_ = false;
     fitNextContent_ = true;
     cineRate_ = DEFAULT_CINE_RATE;
     inverted_ = false;
@@ -2799,7 +2805,28 @@
     OrthancStone::LinearAlgebra::AssignVector(synchronizationOffset_, 0, 0, 0);
 
     frames_.reset(frames);
-    cursor_.reset(new SeriesCursor(frames_->GetFramesCount()));
+    cursor_.reset(new SeriesCursor(frames_->GetFramesCount(), false));
+    
+    if (frames_->GetFramesCount() != 0)
+    {
+      const OrthancStone::DicomInstanceParameters& firstInstance = frames_->GetInstanceOfFrame(0);
+      std::string modality;
+      if (firstInstance.GetTags().LookupStringValue(modality, Orthanc::DICOM_TAG_MODALITY, false))
+      {
+        if (modality == "MR" ||
+            modality == "CT" ||
+            modality == "NM" ||
+            modality == "OPT" ||
+            modality == "PT" ||
+            modality == "RTDOSE" ||
+            modality == "XA")
+        {
+          // For series that might correspond to 3D images, use their
+          // central frame as the first frame to be displayed
+          cursor_.reset(new SeriesCursor(frames_->GetFramesCount(), true));
+        }
+      }
+    }
 
     LOG(INFO) << "Number of frames in series: " << frames_->GetFramesCount();
 
@@ -3102,14 +3129,42 @@
 
   void FlipX()
   {
-    flipX_ = !flipX_;
-    UpdateCurrentTextureParameters();
+    {
+      std::unique_ptr<OrthancStone::IViewport::ILock> lock(viewport_->Lock());
+      lock->GetController().GetScene().FlipViewportX(
+        lock->GetCompositor().GetCanvasWidth(), lock->GetCompositor().GetCanvasHeight());
+      lock->Invalidate();
+    }    
   }
 
   void FlipY()
   {
-    flipY_ = !flipY_;
-    UpdateCurrentTextureParameters();
+    {
+      std::unique_ptr<OrthancStone::IViewport::ILock> lock(viewport_->Lock());
+      lock->GetController().GetScene().FlipViewportY(
+        lock->GetCompositor().GetCanvasWidth(), lock->GetCompositor().GetCanvasHeight());
+      lock->Invalidate();
+    }
+  }
+
+  void RotateLeft()
+  {
+    {
+      std::unique_ptr<OrthancStone::IViewport::ILock> lock(viewport_->Lock());
+      lock->GetController().GetScene().RotateViewport(
+        -PI / 2.0, lock->GetCompositor().GetCanvasWidth(), lock->GetCompositor().GetCanvasHeight());
+      lock->Invalidate();
+    }    
+  }
+
+  void RotateRight()
+  {
+    {
+      std::unique_ptr<OrthancStone::IViewport::ILock> lock(viewport_->Lock());
+      lock->GetController().GetScene().RotateViewport(
+        PI / 2.0, lock->GetCompositor().GetCanvasWidth(), lock->GetCompositor().GetCanvasHeight());
+      lock->Invalidate();
+    }
   }
 
   void Invert()
@@ -3224,14 +3279,30 @@
               viewer_.stoneAnnotations_->SetActiveTool(OrthancStone::AnnotationsSceneLayer::Tool_Circle);
               break;
               
-            case WebViewerAction_CreateSegment:
-              viewer_.stoneAnnotations_->SetActiveTool(OrthancStone::AnnotationsSceneLayer::Tool_Segment);
+            case WebViewerAction_CreateLength:
+              viewer_.stoneAnnotations_->SetActiveTool(OrthancStone::AnnotationsSceneLayer::Tool_Length);
               break;
 
             case WebViewerAction_RemoveMeasure:
               viewer_.stoneAnnotations_->SetActiveTool(OrthancStone::AnnotationsSceneLayer::Tool_Remove);
               break;
 
+            case WebViewerAction_CreatePixelProbe:
+              viewer_.stoneAnnotations_->SetActiveTool(OrthancStone::AnnotationsSceneLayer::Tool_PixelProbe);
+              break;
+
+            case WebViewerAction_CreateEllipseProbe:
+              viewer_.stoneAnnotations_->SetActiveTool(OrthancStone::AnnotationsSceneLayer::Tool_EllipseProbe);
+              break;
+
+            case WebViewerAction_CreateRectangleProbe:
+              viewer_.stoneAnnotations_->SetActiveTool(OrthancStone::AnnotationsSceneLayer::Tool_RectangleProbe);
+              break;
+
+            case WebViewerAction_CreateTextAnnotation:
+              viewer_.stoneAnnotations_->SetActiveTool(OrthancStone::AnnotationsSceneLayer::Tool_TextAnnotation);
+              break;
+
             default:
               viewer_.stoneAnnotations_->SetActiveTool(OrthancStone::AnnotationsSceneLayer::Tool_Edit);
               break;
@@ -3405,6 +3476,25 @@
   }
 
 
+  void SetLinearInterpolation(bool linearInterpolation)
+  {
+    if (linearInterpolation_ != linearInterpolation)
+    {
+      linearInterpolation_ = linearInterpolation;
+      Redraw();
+    }
+  }
+
+  
+  void AddTextAnnotation(const std::string& label,
+                         const OrthancStone::ScenePoint2D& pointedPosition,
+                         const OrthancStone::ScenePoint2D& labelPosition)
+  {
+    stoneAnnotations_->AddTextAnnotation(label, pointedPosition, labelPosition);
+    Redraw();
+  }
+
+
   bool GetCurrentFrame(std::string& sopInstanceUid /* out */,
                        unsigned int& frameNumber /* out */) const
   {
@@ -3700,6 +3790,27 @@
       },
       viewport.GetCanvasId().c_str());
   }
+
+  virtual void SignalStoneTextAnnotationRequired(const ViewerViewport& viewport,
+                                                 const OrthancStone::ScenePoint2D& pointedPosition,
+                                                 const OrthancStone::ScenePoint2D& labelPosition) ORTHANC_OVERRIDE
+  {
+    EM_ASM({
+        const customEvent = document.createEvent("CustomEvent");
+        customEvent.initCustomEvent("TextAnnotationRequired", false, false,
+                                    { "canvasId" : UTF8ToString($0),
+                                      "pointedX" : $1,
+                                      "pointedY" : $2,
+                                      "labelX" : $3,
+                                      "labelY" : $4 });
+        window.dispatchEvent(customEvent);
+      },
+      viewport.GetCanvasId().c_str(),
+      pointedPosition.GetX(),
+      pointedPosition.GetY(),
+      labelPosition.GetX(),
+      labelPosition.GetY() );
+  }
 };
 
 
@@ -3709,6 +3820,7 @@
 static boost::shared_ptr<OrthancStone::WebAssemblyLoadersContext> context_;
 static std::string stringBuffer_;
 static bool softwareRendering_ = false;
+static bool linearInterpolation_ = true;
 static WebViewerAction leftButtonAction_ = WebViewerAction_Windowing;
 static WebViewerAction middleButtonAction_ = WebViewerAction_Pan;
 static WebViewerAction rightButtonAction_ = WebViewerAction_Zoom;
@@ -3755,7 +3867,7 @@
   if (found == allViewports_.end())
   {
     boost::shared_ptr<ViewerViewport> viewport(
-      ViewerViewport::Create(*context_, source_, canvas, framesCache_, softwareRendering_));
+      ViewerViewport::Create(*context_, source_, canvas, framesCache_, softwareRendering_, linearInterpolation_));
     viewport->SetMouseButtonActions(leftButtonAction_, middleButtonAction_, rightButtonAction_);
     viewport->AcquireObserver(new WebAssemblyObserver);
     viewport->SetOsiriXAnnotations(osiriXAnnotations_);
@@ -4494,6 +4606,28 @@
   
 
   EMSCRIPTEN_KEEPALIVE
+  void RotateLeft(const char* canvas)
+  {
+    try
+    {
+      GetViewport(canvas)->RotateLeft();
+    }
+    EXTERN_CATCH_EXCEPTIONS;
+  }  
+
+
+  EMSCRIPTEN_KEEPALIVE
+  void RotateRight(const char* canvas)
+  {
+    try
+    {
+      GetViewport(canvas)->RotateRight();
+    }
+    EXTERN_CATCH_EXCEPTIONS;
+  }  
+  
+
+  EMSCRIPTEN_KEEPALIVE
   void SetSoftwareRendering(int softwareRendering)
   {
     softwareRendering_ = softwareRendering;
@@ -4508,6 +4642,23 @@
 
 
   EMSCRIPTEN_KEEPALIVE
+  void SetLinearInterpolation(int linearInterpolation)
+  {
+    linearInterpolation_ = linearInterpolation;
+
+    try
+    {
+      for (Viewports::iterator it = allViewports_.begin(); it != allViewports_.end(); ++it)
+      {
+        assert(it->second != NULL);
+        it->second->SetLinearInterpolation(linearInterpolation);
+      }
+    }
+    EXTERN_CATCH_EXCEPTIONS;
+  }  
+
+
+  EMSCRIPTEN_KEEPALIVE
   void SetMouseButtonActions(int leftAction,
                              int middleAction,
                              int rightAction)
@@ -4718,4 +4869,21 @@
     EXTERN_CATCH_EXCEPTIONS;
     return false;
   }
+
+
+  EMSCRIPTEN_KEEPALIVE
+  void AddTextAnnotation(const char* canvas,
+                         const char* label,
+                         double pointedX,
+                         double pointedY,
+                         double labelX,
+                         double labelY)
+  {
+    try
+    {
+      GetViewport(canvas)->AddTextAnnotation(label, OrthancStone::ScenePoint2D(pointedX, pointedY),
+                                             OrthancStone::ScenePoint2D(labelX, labelY));
+    }
+    EXTERN_CATCH_EXCEPTIONS;
+  }
 }
--- a/OrthancStone/Resources/CMake/OrthancStoneConfiguration.cmake	Fri Oct 28 07:47:55 2022 +0200
+++ b/OrthancStone/Resources/CMake/OrthancStoneConfiguration.cmake	Wed Nov 02 15:14:56 2022 +0100
@@ -304,6 +304,8 @@
   ${ORTHANC_STONE_ROOT}/Scene2D/LookupTableTextureSceneLayer.h
   ${ORTHANC_STONE_ROOT}/Scene2D/MacroSceneLayer.cpp
   ${ORTHANC_STONE_ROOT}/Scene2D/MacroSceneLayer.h
+  ${ORTHANC_STONE_ROOT}/Scene2D/MagnifyingGlassTracker.cpp
+  ${ORTHANC_STONE_ROOT}/Scene2D/MagnifyingGlassTracker.h
   ${ORTHANC_STONE_ROOT}/Scene2D/NullLayer.h
   ${ORTHANC_STONE_ROOT}/Scene2D/PanSceneTracker.cpp
   ${ORTHANC_STONE_ROOT}/Scene2D/PanSceneTracker.h
@@ -445,8 +447,9 @@
   ${ORTHANC_STONE_ROOT}/Toolbox/UnionOfRectangles.cpp
   ${ORTHANC_STONE_ROOT}/Toolbox/UnionOfRectangles.h
   
+  ${ORTHANC_STONE_ROOT}/Viewport/DefaultViewportInteractor.cpp
   ${ORTHANC_STONE_ROOT}/Viewport/IViewport.h
-  ${ORTHANC_STONE_ROOT}/Viewport/DefaultViewportInteractor.cpp
+  ${ORTHANC_STONE_ROOT}/Viewport/ViewportLocker.cpp
   
   ${ORTHANC_STONE_ROOT}/Volumes/IGeometryProvider.h
   ${ORTHANC_STONE_ROOT}/Volumes/IVolumeSlicer.cpp
--- a/OrthancStone/Resources/SyncOrthancFolder.py	Fri Oct 28 07:47:55 2022 +0200
+++ b/OrthancStone/Resources/SyncOrthancFolder.py	Wed Nov 02 15:14:56 2022 +0100
@@ -29,7 +29,13 @@
 import multiprocessing
 import os
 import stat
-import urllib2
+import sys
+
+if sys.version_info[0] < 3:
+    from urllib import urlopen
+else:
+    from urllib.request import urlopen
+
 
 TARGET = os.path.join(os.path.dirname(__file__), 'Orthanc')
 REPOSITORY = 'https://hg.orthanc-server.com/orthanc/raw-file'
@@ -54,7 +60,7 @@
     branch = x[0]
     source = x[1]
     target = os.path.join(TARGET, x[2])
-    print target
+    print(target)
 
     try:
         os.makedirs(os.path.dirname(target))
@@ -63,8 +69,8 @@
 
     url = '%s/%s/%s' % (REPOSITORY, branch, source)
 
-    with open(target, 'w') as f:
-        f.write(urllib2.urlopen(url).read())
+    with open(target, 'wb') as f:
+        f.write(urlopen(url).read())
 
 
 commands = []
--- a/OrthancStone/Sources/Scene2D/AnnotationsSceneLayer.cpp	Fri Oct 28 07:47:55 2022 +0200
+++ b/OrthancStone/Sources/Scene2D/AnnotationsSceneLayer.cpp	Wed Nov 02 15:14:56 2022 +0100
@@ -26,17 +26,24 @@
 #include "MacroSceneLayer.h"
 #include "PolylineSceneLayer.h"
 #include "TextSceneLayer.h"
-
+#include "TextureBaseSceneLayer.h"
+
+#include <Images/ImageTraits.h>
 #include <OrthancException.h>
 
 #include <boost/math/constants/constants.hpp>
 #include <list>
 
+static const double PI = boost::math::constants::pi<double>();
+
 static const double HANDLE_SIZE = 10.0;
-static const double PI = boost::math::constants::pi<double>();
+static const double ARROW_LENGTH = 1.5 * HANDLE_SIZE;
+static const double ARROW_ANGLE = 20.0 * PI / 180.0;
 
 static const char* const KEY_ANNOTATIONS = "annotations";
 static const char* const KEY_TYPE = "type";
+static const char* const KEY_X = "x";
+static const char* const KEY_Y = "y";
 static const char* const KEY_X1 = "x1";
 static const char* const KEY_Y1 = "y1";
 static const char* const KEY_X2 = "x2";
@@ -44,12 +51,17 @@
 static const char* const KEY_X3 = "x3";
 static const char* const KEY_Y3 = "y3";
 static const char* const KEY_UNITS = "units";
+static const char* const KEY_LABEL = "label";
 
 static const char* const VALUE_ANGLE = "angle";
 static const char* const VALUE_CIRCLE = "circle";
-static const char* const VALUE_SEGMENT = "segment";
+static const char* const VALUE_LENGTH = "length";
 static const char* const VALUE_MILLIMETERS = "millimeters";
 static const char* const VALUE_PIXELS = "pixels";
+static const char* const VALUE_PIXEL_PROBE = "pixel-probe";
+static const char* const VALUE_RECTANGLE_PROBE = "rectangle-probe";
+static const char* const VALUE_ELLIPSE_PROBE = "ellipse-probe";
+static const char* const VALUE_TEXT_ANNOTATION = "text";
 
 #if 0
 static OrthancStone::Color COLOR_PRIMITIVES(192, 192, 192);
@@ -145,6 +157,11 @@
     {
       return hoverColor_;
     }
+    
+    Color GetActiveColor() const
+    {
+      return (IsHover() ? GetHoverColor() : GetColor());
+    }
 
     virtual bool IsHit(const ScenePoint2D& p,
                        const Scene2D& scene) const = 0;
@@ -157,9 +174,11 @@
     virtual void RenderOtherLayers(MacroSceneLayer& macro,
                                    const Scene2D& scene) = 0;
 
-    virtual void MovePreview(const ScenePoint2D& delta) = 0;
-
-    virtual void MoveDone(const ScenePoint2D& delta) = 0;
+    virtual void MovePreview(const ScenePoint2D& delta,
+                             const Scene2D& scene) = 0;
+
+    virtual void MoveDone(const ScenePoint2D& delta,
+                          const Scene2D& scene) = 0;
   };
     
 
@@ -170,13 +189,10 @@
       
     AnnotationsSceneLayer&  that_;
     GeometricPrimitives     primitives_;
-    Units                   units_;
       
   public:
-    explicit Annotation(AnnotationsSceneLayer& that,
-                        Units units) :
-      that_(that),
-      units_(units)
+    explicit Annotation(AnnotationsSceneLayer& that) :
+      that_(that)
     {
       that.AddAnnotation(this);
     }
@@ -189,9 +205,9 @@
       }
     }
 
-    Units GetUnits() const
+    AnnotationsSceneLayer& GetParentLayer() const
     {
-      return units_;
+      return that_;
     }
 
     GeometricPrimitive* AddPrimitive(GeometricPrimitive* primitive)
@@ -216,7 +232,14 @@
       return *primitive;
     }
 
-    virtual void SignalMove(GeometricPrimitive& primitive) = 0;
+    virtual unsigned int GetHandlesCount() const = 0;
+
+    virtual Handle& GetHandle(unsigned int index) const = 0;
+
+    virtual void SignalMove(GeometricPrimitive& primitive,
+                            const Scene2D& scene) = 0;
+
+    virtual void UpdateProbe(const Scene2D& scene) = 0;
 
     virtual void Serialize(Json::Value& target) = 0;
   };
@@ -224,14 +247,38 @@
 
   class AnnotationsSceneLayer::Handle : public GeometricPrimitive
   {
+  public:
+    enum Shape {
+      Shape_Square,
+      Shape_CrossedSquare,
+      Shape_Circle,
+      Shape_CrossedCircle,
+      Shape_Invisible  /* to use in conjunction with arrows */
+    };
+    
   private:
+    Shape         shape_;
     ScenePoint2D  center_;
     ScenePoint2D  delta_;
 
+    void AddCross(PolylineSceneLayer& polyline,
+                  double x1,
+                  double y1,
+                  double x2,
+                  double y2)
+    {
+      const double halfX = (x1 + x2) / 2.0;
+      const double halfY = (y1 + y2) / 2.0;
+      polyline.AddSegment(x1, halfY, x2, halfY, GetActiveColor());
+      polyline.AddSegment(halfX, y1, halfX, y2, GetActiveColor());
+    }
+
   public:
     explicit Handle(Annotation& parentAnnotation,
+                    Shape shape,
                     const ScenePoint2D& center) :
       GeometricPrimitive(parentAnnotation, 0),  // Highest priority
+      shape_(shape),
       center_(center),
       delta_(0, 0)
     {
@@ -249,6 +296,12 @@
       delta_ = ScenePoint2D(0, 0);
     }
 
+    void SetCenter(double x,
+                   double y)
+    {
+      SetCenter(ScenePoint2D(x, y));
+    }
+
     ScenePoint2D GetCenter() const
     {
       return center_ + delta_;
@@ -269,28 +322,44 @@
     virtual void RenderPolylineLayer(PolylineSceneLayer& polyline,
                                      const Scene2D& scene) ORTHANC_OVERRIDE
     {
+      static unsigned int NUM_SEGMENTS = 16;
+
       const double zoom = scene.GetSceneToCanvasTransform().ComputeZoom();
 
-      // TODO: take DPI into account 
-      double x1 = center_.GetX() + delta_.GetX() - (HANDLE_SIZE / 2.0) / zoom;
-      double y1 = center_.GetY() + delta_.GetY() - (HANDLE_SIZE / 2.0) / zoom;
-      double x2 = center_.GetX() + delta_.GetX() + (HANDLE_SIZE / 2.0) / zoom;
-      double y2 = center_.GetY() + delta_.GetY() + (HANDLE_SIZE / 2.0) / zoom;
-
-      PolylineSceneLayer::Chain chain;
-      chain.reserve(4);
-      chain.push_back(ScenePoint2D(x1, y1));
-      chain.push_back(ScenePoint2D(x2, y1));
-      chain.push_back(ScenePoint2D(x2, y2));
-      chain.push_back(ScenePoint2D(x1, y2));
-
-      if (IsHover())
+      // TODO: take DPI into account
+      const double unzoomedHandleSize = (HANDLE_SIZE / 2.0) / zoom;
+      const double x = center_.GetX() + delta_.GetX();
+      const double y = center_.GetY() + delta_.GetY();
+      const double x1 = x - unzoomedHandleSize;
+      const double y1 = y - unzoomedHandleSize;
+      const double x2 = x + unzoomedHandleSize;
+      const double y2 = y + unzoomedHandleSize;
+
+      switch (shape_)
       {
-        polyline.AddChain(chain, true /* closed */, GetHoverColor());
-      }
-      else
-      {
-        polyline.AddChain(chain, true /* closed */, GetColor());
+        case Shape_Square:
+          polyline.AddRectangle(x1, y1, x2, y2, GetActiveColor());
+          break;
+          
+        case Shape_CrossedSquare:
+          polyline.AddRectangle(x1, y1, x2, y2, GetActiveColor());
+          AddCross(polyline, x1, y1, x2, y2);
+          break;
+
+        case Shape_Circle:
+          polyline.AddCircle(x, y, unzoomedHandleSize, GetActiveColor(), NUM_SEGMENTS);
+          break;
+          
+        case Shape_CrossedCircle:
+          polyline.AddCircle(x, y, unzoomedHandleSize, GetActiveColor(), NUM_SEGMENTS);
+          AddCross(polyline, x1, y1, x2, y2);
+          break;
+
+        case Shape_Invisible:
+          break;
+          
+        default:
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
       }
     }
       
@@ -299,19 +368,21 @@
     {
     }
 
-    virtual void MovePreview(const ScenePoint2D& delta) ORTHANC_OVERRIDE
+    virtual void MovePreview(const ScenePoint2D& delta,
+                             const Scene2D& scene) ORTHANC_OVERRIDE
     {
       SetModified(true);
       delta_ = delta;
-      GetParentAnnotation().SignalMove(*this);
+      GetParentAnnotation().SignalMove(*this, scene);
     }
 
-    virtual void MoveDone(const ScenePoint2D& delta) ORTHANC_OVERRIDE
+    virtual void MoveDone(const ScenePoint2D& delta,
+                          const Scene2D& scene) ORTHANC_OVERRIDE
     {
       SetModified(true);
       center_ = center_ + delta;
       delta_ = ScenePoint2D(0, 0);
-      GetParentAnnotation().SignalMove(*this);
+      GetParentAnnotation().SignalMove(*this, scene);
     }
   };
 
@@ -322,6 +393,8 @@
     ScenePoint2D  p1_;
     ScenePoint2D  p2_;
     ScenePoint2D  delta_;
+    bool          hasStartArrow_;
+    bool          hasEndArrow_;
       
   public:
     Segment(Annotation& parentAnnotation,
@@ -330,7 +403,23 @@
       GeometricPrimitive(parentAnnotation, 1),  // Can only be selected if no handle matches
       p1_(p1),
       p2_(p2),
-      delta_(0, 0)
+      delta_(0, 0),
+      hasStartArrow_(false),
+      hasEndArrow_(false)
+    {
+    }
+
+    Segment(Annotation& parentAnnotation,
+            double x1,
+            double y1,
+            double x2,
+            double y2) :
+      GeometricPrimitive(parentAnnotation, 1),  // Can only be selected if no handle matches
+      p1_(x1, y1),
+      p2_(x2, y2),
+      delta_(0, 0),
+      hasStartArrow_(false),
+      hasEndArrow_(false)
     {
     }
 
@@ -343,6 +432,17 @@
       delta_ = ScenePoint2D(0, 0);
     }
 
+    void SetPosition(double x1,
+                     double y1,
+                     double x2,
+                     double y2)
+    {
+      SetModified(true);
+      p1_ = ScenePoint2D(x1, y1);
+      p2_ = ScenePoint2D(x2, y2);
+      delta_ = ScenePoint2D(0, 0);
+    }
+
     ScenePoint2D GetPosition1() const
     {
       return p1_ + delta_;
@@ -353,6 +453,28 @@
       return p2_ + delta_;
     }
 
+    void SetStartArrow(bool enabled)
+    {
+      SetModified(true);
+      hasStartArrow_ = enabled;
+    }
+
+    bool HasStartArrow() const
+    {
+      return hasStartArrow_;
+    }
+
+    void SetEndArrow(bool enabled)
+    {
+      SetModified(true);
+      hasEndArrow_ = enabled;
+    }
+
+    bool HasEndArrow() const
+    {
+      return hasEndArrow_;
+    }
+
     virtual bool IsHit(const ScenePoint2D& p,
                        const Scene2D& scene) const ORTHANC_OVERRIDE
     {
@@ -364,18 +486,37 @@
     virtual void RenderPolylineLayer(PolylineSceneLayer& polyline,
                                      const Scene2D& scene) ORTHANC_OVERRIDE
     {
-      PolylineSceneLayer::Chain chain;
-      chain.reserve(2);
-      chain.push_back(p1_ + delta_);
-      chain.push_back(p2_ + delta_);
-
-      if (IsHover())
+      const Color color = GetActiveColor();
+      const ScenePoint2D a(p1_ + delta_);
+      const ScenePoint2D b(p2_ + delta_);
+      
+      polyline.AddSegment(a, b, color);
+      
+      if (hasStartArrow_ ||
+          hasEndArrow_)
       {
-        polyline.AddChain(chain, false /* closed */, GetHoverColor());
-      }
-      else
-      {
-        polyline.AddChain(chain, false /* closed */, GetColor());
+        const double length = ARROW_LENGTH / scene.GetSceneToCanvasTransform().ComputeZoom();
+        const double angle = atan2(b.GetY() - a.GetY(), b.GetX() - a.GetX());
+
+        if (hasStartArrow_)
+        {
+          polyline.AddSegment(a, a + ScenePoint2D(
+                                length * cos(angle + ARROW_ANGLE),
+                              length * sin(angle + ARROW_ANGLE)), color);
+          polyline.AddSegment(a, a + ScenePoint2D(
+                                length * cos(angle - ARROW_ANGLE),
+                                length * sin(angle - ARROW_ANGLE)), color);
+        }
+
+        if (hasEndArrow_)
+        {
+          polyline.AddSegment(b, b + ScenePoint2D(
+                                length * cos(angle + ARROW_ANGLE + PI),
+                                length * sin(angle + ARROW_ANGLE + PI)), color);
+          polyline.AddSegment(b, b + ScenePoint2D(
+                                length * cos(angle - ARROW_ANGLE + PI),
+                                length * sin(angle - ARROW_ANGLE + PI)), color);
+        }
       }
     }
       
@@ -384,20 +525,22 @@
     {
     }
 
-    virtual void MovePreview(const ScenePoint2D& delta) ORTHANC_OVERRIDE
+    virtual void MovePreview(const ScenePoint2D& delta,
+                             const Scene2D& scene) ORTHANC_OVERRIDE
     {
       SetModified(true);
       delta_ = delta;
-      GetParentAnnotation().SignalMove(*this);
+      GetParentAnnotation().SignalMove(*this, scene);
     }
 
-    virtual void MoveDone(const ScenePoint2D& delta) ORTHANC_OVERRIDE
+    virtual void MoveDone(const ScenePoint2D& delta,
+                          const Scene2D& scene) ORTHANC_OVERRIDE
     {
       SetModified(true);
       p1_ = p1_ + delta;
       p2_ = p2_ + delta;
       delta_ = ScenePoint2D(0, 0);
-      GetParentAnnotation().SignalMove(*this);
+      GetParentAnnotation().SignalMove(*this, scene);
     }
   };
 
@@ -458,32 +601,13 @@
     {
       static unsigned int NUM_SEGMENTS = 128;
 
-      ScenePoint2D middle((p1_.GetX() + p2_.GetX()) / 2.0,
+      ScenePoint2D center((p1_.GetX() + p2_.GetX()) / 2.0,
                           (p1_.GetY() + p2_.GetY()) / 2.0);
         
-      const double radius = ScenePoint2D::DistancePtPt(middle, p1_);
-
-      double increment = 2.0 * PI / static_cast<double>(NUM_SEGMENTS - 1);
-
-      PolylineSceneLayer::Chain chain;
-      chain.reserve(NUM_SEGMENTS);
-
-      double theta = 0;
-      for (unsigned int i = 0; i < NUM_SEGMENTS; i++)
-      {
-        chain.push_back(ScenePoint2D(delta_.GetX() + middle.GetX() + radius * cos(theta),
-                                     delta_.GetY() + middle.GetY() + radius * sin(theta)));
-        theta += increment;
-      }
-        
-      if (IsHover())
-      {
-        polyline.AddChain(chain, false /* closed */, GetHoverColor());
-      }
-      else
-      {
-        polyline.AddChain(chain, false /* closed */, GetColor());
-      }
+      const double radius = ScenePoint2D::DistancePtPt(center, p1_);
+
+      polyline.AddCircle(center.GetX() + delta_.GetX(), center.GetY() + delta_.GetY(),
+                         radius, GetActiveColor(), NUM_SEGMENTS);
     }
       
     virtual void RenderOtherLayers(MacroSceneLayer& macro,
@@ -491,20 +615,22 @@
     {
     }
 
-    virtual void MovePreview(const ScenePoint2D& delta) ORTHANC_OVERRIDE
+    virtual void MovePreview(const ScenePoint2D& delta,
+                             const Scene2D& scene) ORTHANC_OVERRIDE
     {
       SetModified(true);
       delta_ = delta;
-      GetParentAnnotation().SignalMove(*this);
+      GetParentAnnotation().SignalMove(*this, scene);
     }
 
-    virtual void MoveDone(const ScenePoint2D& delta) ORTHANC_OVERRIDE
+    virtual void MoveDone(const ScenePoint2D& delta,
+                          const Scene2D& scene) ORTHANC_OVERRIDE
     {
       SetModified(true);
       p1_ = p1_ + delta;
       p2_ = p2_ + delta;
       delta_ = ScenePoint2D(0, 0);
-      GetParentAnnotation().SignalMove(*this);
+      GetParentAnnotation().SignalMove(*this, scene);
     }
   };
 
@@ -598,27 +724,7 @@
       double fullAngle, startAngle, endAngle;
       ComputeAngles(fullAngle, startAngle, endAngle);
 
-      double increment = fullAngle / static_cast<double>(NUM_SEGMENTS - 1);
-
-      PolylineSceneLayer::Chain chain;
-      chain.reserve(NUM_SEGMENTS);
-
-      double theta = startAngle;
-      for (unsigned int i = 0; i < NUM_SEGMENTS; i++)
-      {
-        chain.push_back(ScenePoint2D(middle_.GetX() + radius * cos(theta),
-                                     middle_.GetY() + radius * sin(theta)));
-        theta += increment;
-      }
-        
-      if (IsHover())
-      {
-        polyline.AddChain(chain, false /* closed */, GetHoverColor());
-      }
-      else
-      {
-        polyline.AddChain(chain, false /* closed */, GetColor());
-      }
+      polyline.AddArc(middle_, radius, radius, startAngle, endAngle, GetActiveColor(), NUM_SEGMENTS);
     }
       
     virtual void RenderOtherLayers(MacroSceneLayer& macro,
@@ -626,12 +732,14 @@
     {
     }
 
-    virtual void MovePreview(const ScenePoint2D& delta) ORTHANC_OVERRIDE
+    virtual void MovePreview(const ScenePoint2D& delta,
+                             const Scene2D& scene) ORTHANC_OVERRIDE
     {
       throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);  // No hit is possible
     }
 
-    virtual void MoveDone(const ScenePoint2D& delta) ORTHANC_OVERRIDE
+    virtual void MoveDone(const ScenePoint2D& delta,
+                          const Scene2D& scene) ORTHANC_OVERRIDE
     {
       throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);  // No hit is possible
     }
@@ -668,7 +776,39 @@
     {
       SetModified(true);
       content_.reset(dynamic_cast<TextSceneLayer*>(content.Clone()));
-    }        
+    }
+
+    void SetText(const std::string& text)
+    {
+      if (content_.get() == NULL)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+      }
+      else
+      {
+        SetModified(true);
+        content_->SetText(text);
+      }
+    }
+
+    std::string GetText() const
+    {
+      return content_->GetText();
+    }
+
+    void SetPosition(double x,
+                     double y)
+    {
+      if (content_.get() == NULL)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+      }
+      else
+      {
+        SetModified(true);
+        content_->SetPosition(x, y);
+      }
+    }
 
     virtual bool IsHit(const ScenePoint2D& p,
                        const Scene2D& scene) const ORTHANC_OVERRIDE
@@ -688,7 +828,7 @@
       {
         std::unique_ptr<TextSceneLayer> layer(reinterpret_cast<TextSceneLayer*>(content_->Clone()));
 
-        layer->SetColor(IsHover() ? GetHoverColor() : GetColor());
+        layer->SetColor(GetActiveColor());
           
         if (first_)
         {
@@ -702,18 +842,168 @@
       }
     }
 
-    virtual void MovePreview(const ScenePoint2D& delta) ORTHANC_OVERRIDE
+    virtual void MovePreview(const ScenePoint2D& delta,
+                             const Scene2D& scene) ORTHANC_OVERRIDE
     {
       throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);  // No hit is possible
     }
 
-    virtual void MoveDone(const ScenePoint2D& delta) ORTHANC_OVERRIDE
+    virtual void MoveDone(const ScenePoint2D& delta,
+                          const Scene2D& scene) ORTHANC_OVERRIDE
     {
       throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);  // No hit is possible
     }
   };
 
 
+  class AnnotationsSceneLayer::Ellipse : public GeometricPrimitive
+  {
+  private:
+    ScenePoint2D  p1_;
+    ScenePoint2D  p2_;
+    ScenePoint2D  delta_;
+
+    double GetCenterX() const
+    {
+      return (p1_.GetX() + p2_.GetX()) / 2.0 + delta_.GetX();
+    }
+
+    double GetCenterY() const
+    {
+      return (p1_.GetY() + p2_.GetY()) / 2.0 + delta_.GetY();
+    }
+
+    double GetRadiusX() const
+    {
+      return std::abs(p1_.GetX() - p2_.GetX()) / 2.0;
+    }
+
+    double GetRadiusY() const
+    {
+      return std::abs(p1_.GetY() - p2_.GetY()) / 2.0;
+    }
+    
+  public:
+    Ellipse(Annotation& parentAnnotation,
+            const ScenePoint2D& p1,
+            const ScenePoint2D& p2) :
+      GeometricPrimitive(parentAnnotation, 2),
+      p1_(p1),
+      p2_(p2),
+      delta_(0, 0)
+    {
+    }
+
+    void SetPosition(const ScenePoint2D& p1,
+                     const ScenePoint2D& p2)
+    {
+      SetModified(true);
+      p1_ = p1;
+      p2_ = p2;
+      delta_ = ScenePoint2D(0, 0);
+    }
+
+    ScenePoint2D GetPosition1() const
+    {
+      return p1_ + delta_;
+    }
+
+    ScenePoint2D GetPosition2() const
+    {
+      return p2_ + delta_;
+    }
+
+    double GetArea() const
+    {
+      return PI * GetRadiusX() * GetRadiusY();
+    }
+
+    bool IsPointInside(const ScenePoint2D& p) const
+    {
+      const double radiusX = GetRadiusX();
+      const double radiusY = GetRadiusY();
+
+      double a, b, x, y;
+      
+      if (radiusX > radiusY)
+      {
+        // The ellipse is horizontal => we are in the case described
+        // on Wikipedia:
+        // https://en.wikipedia.org/wiki/Ellipse#Standard_equation
+
+        a = radiusX;
+        b = radiusY;
+        x = p.GetX() - GetCenterX();
+        y = p.GetY() - GetCenterY();
+      }
+      else
+      {
+        a = radiusY;
+        b = radiusX;
+        x = p.GetY() - GetCenterY();
+        y = p.GetX() - GetCenterX();
+      }
+      
+      const double c = sqrt(a * a - b * b);
+
+      return (sqrt((x - c) * (x - c) + y * y) +
+              sqrt((x + c) * (x + c) + y * y)) <= 2.0 * a;
+    }
+    
+    virtual bool IsHit(const ScenePoint2D& p,
+                       const Scene2D& scene) const ORTHANC_OVERRIDE
+    {
+      const double zoom = scene.GetSceneToCanvasTransform().ComputeZoom();
+
+      const double radiusX = GetRadiusX();
+      const double radiusY = GetRadiusY();
+
+      // Warning: This is only an approximation of the
+      // point-to-ellipse distance, as explained here:
+      // https://blog.chatfield.io/simple-method-for-distance-to-ellipse/
+      
+      const double x = (p.GetX() - GetCenterX()) / radiusX;
+      const double y = (p.GetY() - GetCenterY()) / radiusY;
+      const double t = atan2(y, x);
+      const double xx = cos(t) - x;
+      const double yy = sin(t) - y;
+
+      const double approximateDistance = sqrt(xx * xx + yy * yy) * (radiusX + radiusY) / 2.0;
+      return std::abs(approximateDistance) * zoom <= HANDLE_SIZE / 2.0;
+    }
+
+    virtual void RenderPolylineLayer(PolylineSceneLayer& polyline,
+                                     const Scene2D& scene) ORTHANC_OVERRIDE
+    {
+      static unsigned int NUM_SEGMENTS = 128;
+      polyline.AddArc(GetCenterX(), GetCenterY(), GetRadiusX(), GetRadiusY(), 0, 2.0 * PI, GetActiveColor(), NUM_SEGMENTS);
+    }
+      
+    virtual void RenderOtherLayers(MacroSceneLayer& macro,
+                                   const Scene2D& scene) ORTHANC_OVERRIDE
+    {
+    }
+
+    virtual void MovePreview(const ScenePoint2D& delta,
+                             const Scene2D& scene) ORTHANC_OVERRIDE
+    {
+      SetModified(true);
+      delta_ = delta;
+      GetParentAnnotation().SignalMove(*this, scene);
+    }
+
+    virtual void MoveDone(const ScenePoint2D& delta,
+                          const Scene2D& scene) ORTHANC_OVERRIDE
+    {
+      SetModified(true);
+      p1_ = p1_ + delta;
+      p2_ = p2_ + delta;
+      delta_ = ScenePoint2D(0, 0);
+      GetParentAnnotation().SignalMove(*this, scene);
+    }
+  };
+
+    
   class AnnotationsSceneLayer::EditPrimitiveTracker : public IFlexiblePointerTracker
   {
   private:
@@ -736,20 +1026,23 @@
     {
     }
 
-    virtual void PointerMove(const PointerEvent& event) ORTHANC_OVERRIDE
+    virtual void PointerMove(const PointerEvent& event,
+                             const Scene2D& scene) ORTHANC_OVERRIDE
     {
-      primitive_.MovePreview(event.GetMainPosition().Apply(canvasToScene_) - sceneClick_);
+      primitive_.MovePreview(event.GetMainPosition().Apply(canvasToScene_) - sceneClick_, scene);
       that_.BroadcastMessage(AnnotationChangedMessage(that_));
     }
       
-    virtual void PointerUp(const PointerEvent& event) ORTHANC_OVERRIDE
+    virtual void PointerUp(const PointerEvent& event,
+                           const Scene2D& scene) ORTHANC_OVERRIDE
     {
-      primitive_.MoveDone(event.GetMainPosition().Apply(canvasToScene_) - sceneClick_);
+      primitive_.MoveDone(event.GetMainPosition().Apply(canvasToScene_) - sceneClick_, scene);
       alive_ = false;
       that_.BroadcastMessage(AnnotationChangedMessage(that_));
     }
 
-    virtual void PointerDown(const PointerEvent& event) ORTHANC_OVERRIDE
+    virtual void PointerDown(const PointerEvent& event,
+                             const Scene2D& scene) ORTHANC_OVERRIDE
     {
     }
 
@@ -758,9 +1051,10 @@
       return alive_;
     }
 
-    virtual void Cancel() ORTHANC_OVERRIDE
+    virtual void Cancel(const Scene2D& scene) ORTHANC_OVERRIDE
     {
-      primitive_.MoveDone(ScenePoint2D(0, 0));
+      //primitive_.MoveDone(ScenePoint2D(0, 0), scene);
+      primitive_.MoveDone(sceneClick_, scene);   // TODO Check this
     }
   };
 
@@ -768,22 +1062,118 @@
   class AnnotationsSceneLayer::SegmentAnnotation : public Annotation
   {
   private:
-    bool      showLabel_;
     Handle&   handle1_;
     Handle&   handle2_;
     Segment&  segment_;
     Text&     label_;
 
+  protected:
+    void SetLabelContent(const TextSceneLayer& content)
+    {
+      label_.SetContent(content);
+    }
+
+    std::string GetCurrentLabel() const
+    {
+      return label_.GetText();
+    } 
+
+    const Handle& GetHandle1() const
+    {
+      return handle1_;
+    }
+
+    const Handle& GetHandle2() const
+    {
+      return handle2_;
+    }
+
+    void SetStartArrow(bool enabled)
+    {
+      segment_.SetStartArrow(enabled);
+    }
+
+    void SetEndArrow(bool enabled)
+    {
+      segment_.SetEndArrow(enabled);
+    }
+
+  public:
+    SegmentAnnotation(AnnotationsSceneLayer& that,
+                      Handle::Shape shape1,
+                      const ScenePoint2D& p1,
+                      Handle::Shape shape2,
+                      const ScenePoint2D& p2) :
+      Annotation(that),
+      handle1_(AddTypedPrimitive<Handle>(new Handle(*this, shape1, p1))),
+      handle2_(AddTypedPrimitive<Handle>(new Handle(*this, shape2, p2))),
+      segment_(AddTypedPrimitive<Segment>(new Segment(*this, p1, p2))),
+      label_(AddTypedPrimitive<Text>(new Text(that, *this)))
+    {
+      label_.SetColor(COLOR_TEXT);
+    }
+
+    virtual unsigned int GetHandlesCount() const ORTHANC_OVERRIDE
+    {
+      return 2;
+    }
+
+    virtual Handle& GetHandle(unsigned int index) const ORTHANC_OVERRIDE
+    {
+      switch (index)
+      {
+        case 0:
+          return handle1_;
+
+        case 1:
+          return handle2_;
+
+        default:
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+      }
+    }
+
+    virtual void SignalMove(GeometricPrimitive& primitive,
+                            const Scene2D& scene) ORTHANC_OVERRIDE
+    {
+      if (&primitive == &handle1_ ||
+          &primitive == &handle2_)
+      {
+        segment_.SetPosition(handle1_.GetCenter(), handle2_.GetCenter());
+      }
+      else if (&primitive == &segment_)
+      {
+        handle1_.SetCenter(segment_.GetPosition1());
+        handle2_.SetCenter(segment_.GetPosition2());
+      }
+      else
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+      }
+    }
+
+    virtual void UpdateProbe(const Scene2D& scene) ORTHANC_OVERRIDE
+    {
+    }
+  };
+
+
+  class AnnotationsSceneLayer::LengthAnnotation : public SegmentAnnotation
+  {
+  private:
+    Units  units_;
+    bool   showLabel_;
+
     void UpdateLabel()
     {
       if (showLabel_)
       {
         TextSceneLayer content;
 
-        double x1 = handle1_.GetCenter().GetX();
-        double y1 = handle1_.GetCenter().GetY();
-        double x2 = handle2_.GetCenter().GetX();
-        double y2 = handle2_.GetCenter().GetY();
+        double x1 = GetHandle1().GetCenter().GetX();
+        double y1 = GetHandle1().GetCenter().GetY();
+        double x2 = GetHandle2().GetCenter().GetX();
+        double y2 = GetHandle2().GetCenter().GetY();
         
         // Put the label to the right of the right-most handle
         if (x1 < x2)
@@ -802,7 +1192,7 @@
         double dy = y1 - y2;
         char buf[32];
 
-        switch (GetUnits())
+        switch (units_)
         {
           case Units_Millimeters:
             sprintf(buf, "%0.2f cm", sqrt(dx * dx + dy * dy) / 10.0);
@@ -818,61 +1208,38 @@
             
         content.SetText(buf);
 
-        label_.SetContent(content);
+        SetLabelContent(content);
       }
     }
 
   public:
-    SegmentAnnotation(AnnotationsSceneLayer& that,
-                      Units units,
-                      bool showLabel,
-                      const ScenePoint2D& p1,
-                      const ScenePoint2D& p2) :
-      Annotation(that, units),
-      showLabel_(showLabel),
-      handle1_(AddTypedPrimitive<Handle>(new Handle(*this, p1))),
-      handle2_(AddTypedPrimitive<Handle>(new Handle(*this, p2))),
-      segment_(AddTypedPrimitive<Segment>(new Segment(*this, p1, p2))),
-      label_(AddTypedPrimitive<Text>(new Text(that, *this)))
+    LengthAnnotation(AnnotationsSceneLayer& that,
+                     Units units,
+                     bool showLabel,
+                     const ScenePoint2D& p1,
+                     const ScenePoint2D& p2) :
+      SegmentAnnotation(that, Handle::Shape_Square, p1, Handle::Shape_Square, p2),
+      units_(units),
+      showLabel_(showLabel)
     {
-      label_.SetColor(COLOR_TEXT);
       UpdateLabel();
     }
 
-    Handle& GetHandle1() const
-    {
-      return handle1_;
-    }
-
-    Handle& GetHandle2() const
-    {
-      return handle2_;
-    }
-
-    virtual void SignalMove(GeometricPrimitive& primitive) ORTHANC_OVERRIDE
+    virtual void SignalMove(GeometricPrimitive& primitive,
+                            const Scene2D& scene) ORTHANC_OVERRIDE
     {
-      if (&primitive == &handle1_ ||
-          &primitive == &handle2_)
-      {
-        segment_.SetPosition(handle1_.GetCenter(), handle2_.GetCenter());
-      }
-      else if (&primitive == &segment_)
-      {
-        handle1_.SetCenter(segment_.GetPosition1());
-        handle2_.SetCenter(segment_.GetPosition2());
-      }
-        
+      SegmentAnnotation::SignalMove(primitive, scene);
       UpdateLabel();
     }
 
     virtual void Serialize(Json::Value& target) ORTHANC_OVERRIDE
     {
       target = Json::objectValue;
-      target[KEY_TYPE] = VALUE_SEGMENT;
-      target[KEY_X1] = handle1_.GetCenter().GetX();
-      target[KEY_Y1] = handle1_.GetCenter().GetY();
-      target[KEY_X2] = handle2_.GetCenter().GetX();
-      target[KEY_Y2] = handle2_.GetCenter().GetY();
+      target[KEY_TYPE] = VALUE_LENGTH;
+      target[KEY_X1] = GetHandle1().GetCenter().GetX();
+      target[KEY_Y1] = GetHandle1().GetCenter().GetY();
+      target[KEY_X2] = GetHandle2().GetCenter().GetX();
+      target[KEY_Y2] = GetHandle2().GetCenter().GetY();
     }
 
     static void Unserialize(AnnotationsSceneLayer& target,
@@ -888,13 +1255,280 @@
           source[KEY_X2].isNumeric() &&
           source[KEY_Y2].isNumeric())
       {
-        new SegmentAnnotation(target, units, true,
-                              ScenePoint2D(source[KEY_X1].asDouble(), source[KEY_Y1].asDouble()),
-                              ScenePoint2D(source[KEY_X2].asDouble(), source[KEY_Y2].asDouble()));
+        new LengthAnnotation(target, units, true,
+                             ScenePoint2D(source[KEY_X1].asDouble(), source[KEY_Y1].asDouble()),
+                             ScenePoint2D(source[KEY_X2].asDouble(), source[KEY_Y2].asDouble()));
+      }
+      else
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, "Cannot unserialize a length annotation");
+      }
+    }
+  };
+
+
+  class AnnotationsSceneLayer::TextAnnotation : public SegmentAnnotation
+  {
+  public:
+    TextAnnotation(AnnotationsSceneLayer& that,
+                   const std::string& label,
+                   const ScenePoint2D& pointedPosition,
+                   const ScenePoint2D& labelPosition) :
+      SegmentAnnotation(that, Handle::Shape_Invisible, pointedPosition /* p1 */,
+                        Handle::Shape_Square, labelPosition /* p2 */)
+    {
+      SetStartArrow(true);
+      UpdateLabel(label);
+    }
+
+    ScenePoint2D GetPointedPosition() const
+    {
+      return GetHandle1().GetCenter();
+    }
+
+    ScenePoint2D GetLabelPosition() const
+    {
+      return GetHandle2().GetCenter();
+    }
+
+    void UpdateLabel(const std::string& label)
+    {
+      TextSceneLayer content;
+
+      double x1 = GetHandle1().GetCenter().GetX();
+      double x2 = GetHandle2().GetCenter().GetX();
+      double y2 = GetHandle2().GetCenter().GetY();
+        
+      if (x1 < x2)
+      {
+        content.SetAnchor(BitmapAnchor_CenterLeft);
+      }
+      else
+      {
+        content.SetAnchor(BitmapAnchor_CenterRight);
+      }
+
+      content.SetPosition(x2, y2);
+      content.SetBorder(10);
+      content.SetText(label);
+
+      SetLabelContent(content);
+    }
+
+    void UpdateLabel()
+    {
+      UpdateLabel(GetCurrentLabel());
+    }
+    
+    virtual void SignalMove(GeometricPrimitive& primitive,
+                            const Scene2D& scene) ORTHANC_OVERRIDE
+    {
+      SegmentAnnotation::SignalMove(primitive, scene);
+      UpdateLabel();
+    }
+
+    virtual void Serialize(Json::Value& target) ORTHANC_OVERRIDE
+    {
+      target = Json::objectValue;
+      target[KEY_TYPE] = VALUE_TEXT_ANNOTATION;
+      target[KEY_X1] = GetHandle1().GetCenter().GetX();
+      target[KEY_Y1] = GetHandle1().GetCenter().GetY();
+      target[KEY_X2] = GetHandle2().GetCenter().GetX();
+      target[KEY_Y2] = GetHandle2().GetCenter().GetY();
+      target[KEY_LABEL] = GetCurrentLabel();
+    }
+
+    static void Unserialize(AnnotationsSceneLayer& target,
+                            const Json::Value& source)
+    {
+      if (source.isMember(KEY_X1) &&
+          source.isMember(KEY_Y1) &&
+          source.isMember(KEY_X2) &&
+          source.isMember(KEY_Y2) &&
+          source.isMember(KEY_LABEL) &&
+          source[KEY_X1].isNumeric() &&
+          source[KEY_Y1].isNumeric() &&
+          source[KEY_X2].isNumeric() &&
+          source[KEY_Y2].isNumeric() &&
+          source[KEY_LABEL].isString())
+      {
+        new TextAnnotation(target, source[KEY_LABEL].asString(),
+                           ScenePoint2D(source[KEY_X1].asDouble(), source[KEY_Y1].asDouble()),
+                           ScenePoint2D(source[KEY_X2].asDouble(), source[KEY_Y2].asDouble()));
       }
       else
       {
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, "Cannot unserialize an segment annotation");
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, "Cannot unserialize a text annotation");
+      }
+    }
+  };
+
+
+  // Use this class to avoid unnecessary probing if neither the scene,
+  // nor the probe, has changed
+  class AnnotationsSceneLayer::ProbingAnnotation : public Annotation
+  {
+  private:
+    int       probedLayer_;
+    bool      probeChanged_;
+    uint64_t  lastLayerRevision_;
+
+  protected:
+    virtual void UpdateProbeForLayer(const ISceneLayer& layer) = 0;
+
+    void TagProbeAsChanged()
+    {
+      probeChanged_ = true;
+    }
+
+  public:
+    explicit ProbingAnnotation(AnnotationsSceneLayer& that) :
+      Annotation(that),
+      probedLayer_(that.GetProbedLayer()),
+      probeChanged_(true),
+      lastLayerRevision_(0)
+    {
+    }
+
+    virtual void UpdateProbe(const Scene2D& scene) ORTHANC_OVERRIDE
+    {
+      if (scene.HasLayer(probedLayer_))
+      {
+        const ISceneLayer& layer = scene.GetLayer(probedLayer_);
+        if (probeChanged_ ||
+            layer.GetRevision() != lastLayerRevision_)
+        {
+          UpdateProbeForLayer(layer);
+          probeChanged_ = false;
+          lastLayerRevision_ = layer.GetRevision();
+        }
+      }
+    }
+  };
+
+    
+  class AnnotationsSceneLayer::PixelProbeAnnotation : public ProbingAnnotation
+  {
+  private:
+    Handle&   handle_;
+    Text&     label_;
+
+  protected:
+    virtual void UpdateProbeForLayer(const ISceneLayer& layer) ORTHANC_OVERRIDE
+    {
+      if (layer.GetType() == ISceneLayer::Type_FloatTexture ||
+          layer.GetType() == ISceneLayer::Type_ColorTexture)
+      {
+        const TextureBaseSceneLayer& texture = dynamic_cast<const TextureBaseSceneLayer&>(layer);
+        const AffineTransform2D sceneToTexture = AffineTransform2D::Invert(texture.GetTransform());
+
+        double sceneX = handle_.GetCenter().GetX();
+        double sceneY = handle_.GetCenter().GetY();
+        sceneToTexture.Apply(sceneX, sceneY);
+          
+        int x = static_cast<int>(std::floor(sceneX));
+        int y = static_cast<int>(std::floor(sceneY));
+
+        const Orthanc::ImageAccessor& image = texture.GetTexture();
+        
+        if (x >= 0 &&
+            y >= 0 &&
+            x < static_cast<int>(image.GetWidth()) &&
+            y < static_cast<int>(image.GetHeight()))
+        {
+          char buf[64];
+
+          switch (image.GetFormat())
+          {
+            case Orthanc::PixelFormat_Float32:
+              sprintf(buf, "(%d,%d): %.01f", x, y, Orthanc::ImageTraits<Orthanc::PixelFormat_Float32>::GetFloatPixel(
+                        image, static_cast<unsigned int>(x), static_cast<unsigned int>(y)));
+              break;
+
+            case Orthanc::PixelFormat_RGB24:
+            {
+              Orthanc::PixelTraits<Orthanc::PixelFormat_RGB24>::PixelType pixel;
+              Orthanc::ImageTraits<Orthanc::PixelFormat_RGB24>::GetPixel(
+                pixel, image, static_cast<unsigned int>(x), static_cast<unsigned int>(y));
+              sprintf(buf, "(%d,%d): (%d,%d,%d)", x, y, pixel.red_, pixel.green_, pixel.blue_);
+              break;
+            }
+
+            default:
+              break;
+          }
+          
+          label_.SetText(buf);
+        }
+        else
+        {
+          label_.SetText("?");
+        }
+      }
+    }
+
+  public:
+    PixelProbeAnnotation(AnnotationsSceneLayer& that,
+                         const ScenePoint2D& p) :
+      ProbingAnnotation(that),
+      handle_(AddTypedPrimitive<Handle>(new Handle(*this, Handle::Shape_CrossedSquare, p))),
+      label_(AddTypedPrimitive<Text>(new Text(that, *this)))
+    {
+      TextSceneLayer content;
+      content.SetPosition(handle_.GetCenter().GetX(), handle_.GetCenter().GetY());
+      content.SetAnchor(BitmapAnchor_CenterLeft);
+      content.SetBorder(10);
+      content.SetText("?");
+
+      label_.SetContent(content);      
+      label_.SetColor(COLOR_TEXT);
+    }
+
+    virtual unsigned int GetHandlesCount() const ORTHANC_OVERRIDE
+    {
+      return 1;
+    }
+
+    virtual Handle& GetHandle(unsigned int index) const ORTHANC_OVERRIDE
+    {
+      if (index == 0)
+      {
+        return handle_;
+      }
+      else
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+      }
+    }
+
+    virtual void SignalMove(GeometricPrimitive& primitive,
+                            const Scene2D& scene) ORTHANC_OVERRIDE
+    {
+      label_.SetPosition(handle_.GetCenter().GetX(), handle_.GetCenter().GetY());
+      TagProbeAsChanged();
+    }
+
+    virtual void Serialize(Json::Value& target) ORTHANC_OVERRIDE
+    {
+      target = Json::objectValue;
+      target[KEY_TYPE] = VALUE_PIXEL_PROBE;
+      target[KEY_X] = handle_.GetCenter().GetX();
+      target[KEY_Y] = handle_.GetCenter().GetY();
+    }
+
+    static void Unserialize(AnnotationsSceneLayer& target,
+                            const Json::Value& source)
+    {
+      if (source.isMember(KEY_X) &&
+          source.isMember(KEY_Y) &&
+          source[KEY_X].isNumeric() &&
+          source[KEY_Y].isNumeric())
+      {
+        new PixelProbeAnnotation(target, ScenePoint2D(source[KEY_X].asDouble(), source[KEY_Y].asDouble()));
+      }
+      else
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, "Cannot unserialize a pixel probe");
       }
     }
   };
@@ -943,14 +1577,13 @@
 
   public:
     AngleAnnotation(AnnotationsSceneLayer& that,
-                    Units units,
                     const ScenePoint2D& start,
                     const ScenePoint2D& middle,
                     const ScenePoint2D& end) :
-      Annotation(that, units),
-      startHandle_(AddTypedPrimitive<Handle>(new Handle(*this, start))),
-      middleHandle_(AddTypedPrimitive<Handle>(new Handle(*this, middle))),
-      endHandle_(AddTypedPrimitive<Handle>(new Handle(*this, end))),
+      Annotation(that),
+      startHandle_(AddTypedPrimitive<Handle>(new Handle(*this, Handle::Shape_Square, start))),
+      middleHandle_(AddTypedPrimitive<Handle>(new Handle(*this, Handle::Shape_Square, middle))),
+      endHandle_(AddTypedPrimitive<Handle>(new Handle(*this, Handle::Shape_Square, end))),
       segment1_(AddTypedPrimitive<Segment>(new Segment(*this, start, middle))),
       segment2_(AddTypedPrimitive<Segment>(new Segment(*this, middle, end))),
       arc_(AddTypedPrimitive<Arc>(new Arc(*this, start, middle, end))),
@@ -960,12 +1593,31 @@
       UpdateLabel();
     }
 
-    Handle& GetEndHandle() const
+    virtual unsigned int GetHandlesCount() const ORTHANC_OVERRIDE
+    {
+      return 3;
+    }
+
+    virtual Handle& GetHandle(unsigned int index) const ORTHANC_OVERRIDE
     {
-      return endHandle_;
+      switch (index)
+      {
+        case 0:
+          return startHandle_;
+
+        case 1:
+          return middleHandle_;
+
+        case 2:
+          return endHandle_;
+
+        default:
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+      }
     }
 
-    virtual void SignalMove(GeometricPrimitive& primitive) ORTHANC_OVERRIDE
+    virtual void SignalMove(GeometricPrimitive& primitive,
+                            const Scene2D& scene) ORTHANC_OVERRIDE
     {
       if (&primitive == &startHandle_)
       {
@@ -999,10 +1651,18 @@
         arc_.SetMiddle(segment2_.GetPosition1());
         arc_.SetEnd(segment2_.GetPosition2());
       }
+      else
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+      }
 
       UpdateLabel();
     }
 
+    virtual void UpdateProbe(const Scene2D& scene) ORTHANC_OVERRIDE
+    {
+    }
+    
     virtual void Serialize(Json::Value& target) ORTHANC_OVERRIDE
     {
       target = Json::objectValue;
@@ -1016,7 +1676,6 @@
     }
 
     static void Unserialize(AnnotationsSceneLayer& target,
-                            Units units,
                             const Json::Value& source)
     {
       if (source.isMember(KEY_X1) &&
@@ -1032,7 +1691,7 @@
           source[KEY_X3].isNumeric() &&
           source[KEY_Y3].isNumeric())
       {
-        new AngleAnnotation(target, units,
+        new AngleAnnotation(target,
                             ScenePoint2D(source[KEY_X1].asDouble(), source[KEY_Y1].asDouble()),
                             ScenePoint2D(source[KEY_X2].asDouble(), source[KEY_Y2].asDouble()),
                             ScenePoint2D(source[KEY_X3].asDouble(), source[KEY_Y3].asDouble()));
@@ -1048,6 +1707,7 @@
   class AnnotationsSceneLayer::CircleAnnotation : public Annotation
   {
   private:
+    Units     units_;
     Handle&   handle1_;
     Handle&   handle2_;
     Segment&  segment_;
@@ -1084,7 +1744,7 @@
         
       char buf[32];
 
-      switch (GetUnits())
+      switch (units_)
       {
         case Units_Millimeters:
           sprintf(buf, "%0.2f cm\n%0.2f cm%c%c",
@@ -1112,9 +1772,10 @@
                      Units units,
                      const ScenePoint2D& p1,
                      const ScenePoint2D& p2) :
-      Annotation(that, units),
-      handle1_(AddTypedPrimitive<Handle>(new Handle(*this, p1))),
-      handle2_(AddTypedPrimitive<Handle>(new Handle(*this, p2))),
+      Annotation(that),
+      units_(units),
+      handle1_(AddTypedPrimitive<Handle>(new Handle(*this, Handle::Shape_Square, p1))),
+      handle2_(AddTypedPrimitive<Handle>(new Handle(*this, Handle::Shape_Square, p2))),
       segment_(AddTypedPrimitive<Segment>(new Segment(*this, p1, p2))),
       circle_(AddTypedPrimitive<Circle>(new Circle(*this, p1, p2))),
       label_(AddTypedPrimitive<Text>(new Text(that, *this)))
@@ -1123,12 +1784,28 @@
       UpdateLabel();
     }
 
-    Handle& GetHandle2() const
+    virtual unsigned int GetHandlesCount() const ORTHANC_OVERRIDE
+    {
+      return 2;
+    }
+
+    virtual Handle& GetHandle(unsigned int index) const ORTHANC_OVERRIDE
     {
-      return handle2_;
+      switch (index)
+      {
+        case 0:
+          return handle1_;
+
+        case 1:
+          return handle2_;
+
+        default:
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+      }
     }
 
-    virtual void SignalMove(GeometricPrimitive& primitive) ORTHANC_OVERRIDE
+    virtual void SignalMove(GeometricPrimitive& primitive,
+                            const Scene2D& scene) ORTHANC_OVERRIDE
     {
       if (&primitive == &handle1_ ||
           &primitive == &handle2_)
@@ -1148,10 +1825,18 @@
         handle2_.SetCenter(circle_.GetPosition2());
         segment_.SetPosition(circle_.GetPosition1(), circle_.GetPosition2());
       }
+      else
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+      }
         
       UpdateLabel();
     }
 
+    virtual void UpdateProbe(const Scene2D& scene) ORTHANC_OVERRIDE
+    {
+    }
+
     virtual void Serialize(Json::Value& target) ORTHANC_OVERRIDE
     {
       target = Json::objectValue;
@@ -1187,60 +1872,523 @@
   };
 
     
-  class AnnotationsSceneLayer::CreateSegmentOrCircleTracker : public IFlexiblePointerTracker
+  class AnnotationsSceneLayer::RectangleProbeAnnotation : public ProbingAnnotation
   {
   private:
-    AnnotationsSceneLayer&  that_;
-    Annotation*             annotation_;
-    AffineTransform2D       canvasToScene_;
-    Handle*                 handle2_;
+    Units     units_;
+    Handle&   handle1_;
+    Handle&   handle2_;
+    Segment&  segment1_;
+    Segment&  segment2_;
+    Segment&  segment3_;
+    Segment&  segment4_;
+    Text&     label_;
+
+  protected:
+    virtual void UpdateProbeForLayer(const ISceneLayer& layer) ORTHANC_OVERRIDE
+    {
+      double x1 = handle1_.GetCenter().GetX();
+      double y1 = handle1_.GetCenter().GetY();
+      double x2 = handle2_.GetCenter().GetX();
+      double y2 = handle2_.GetCenter().GetY();
+
+      {
+        // Put the label to the right of the right-most handle
+        //const double y = std::min(y1, y2);
+        const double y = (y1 + y2) / 2.0;
+        if (x1 < x2)
+        {
+          label_.SetPosition(x2, y);
+        }
+        else
+        {
+          label_.SetPosition(x1, y);
+        }
+      }
+
+      std::string text;
       
+      char buf[32];
+
+      if (units_ == Units_Millimeters)
+      {
+        const double area = std::abs(x1 - x2) * std::abs(y1 - y2);
+
+        sprintf(buf, "Area: %0.2f cm%c%c",
+                area / 100.0,
+                0xc2, 0xb2 /* two bytes corresponding to two power in UTF-8 */);
+        text = buf;
+      }
+
+      if (layer.GetType() == ISceneLayer::Type_FloatTexture)
+      {
+        const TextureBaseSceneLayer& texture = dynamic_cast<const TextureBaseSceneLayer&>(layer);
+        const AffineTransform2D sceneToTexture = AffineTransform2D::Invert(texture.GetTransform());
+
+        const Orthanc::ImageAccessor& image = texture.GetTexture();
+        assert(image.GetFormat() == Orthanc::PixelFormat_Float32);
+
+        sceneToTexture.Apply(x1, y1);
+        sceneToTexture.Apply(x2, y2);
+        int ix1 = static_cast<int>(std::floor(x1));
+        int iy1 = static_cast<int>(std::floor(y1));
+        int ix2 = static_cast<int>(std::floor(x2));
+        int iy2 = static_cast<int>(std::floor(y2));
+
+        if (ix1 > ix2)
+        {
+          std::swap(ix1, ix2);
+        }
+
+        if (iy1 > iy2)
+        {
+          std::swap(iy1, iy2);
+        }
+
+        LinearAlgebra::OnlineVarianceEstimator estimator;
+
+        for (int y = std::max(0, iy1); y <= std::min(static_cast<int>(image.GetHeight()) - 1, iy2); y++)
+        {
+          int x = std::max(0, ix1);
+          
+          const float* p = reinterpret_cast<const float*>(image.GetConstRow(y)) + x;
+
+          for (; x <= std::min(static_cast<int>(image.GetWidth()) - 1, ix2); x++, p++)
+          {
+            estimator.AddSample(*p);
+          }
+        }
+
+        if (estimator.GetCount() > 0)
+        {
+          if (!text.empty())
+          {
+            text += "\n";
+          }
+          sprintf(buf, "Mean: %0.1f\nStdDev: %0.1f", estimator.GetMean(), estimator.GetStandardDeviation());
+          text += buf;
+        }
+      }
+      
+      label_.SetText(text);
+    }
+    
   public:
-    CreateSegmentOrCircleTracker(AnnotationsSceneLayer& that,
-                                 Units units,
-                                 bool isCircle,
-                                 const ScenePoint2D& sceneClick,
-                                 const AffineTransform2D& canvasToScene) :
-      that_(that),
-      annotation_(NULL),
-      canvasToScene_(canvasToScene),
-      handle2_(NULL)
+    RectangleProbeAnnotation(AnnotationsSceneLayer& that,
+                             Units units,
+                             const ScenePoint2D& p1,
+                             const ScenePoint2D& p2) :
+      ProbingAnnotation(that),
+      units_(units),
+      handle1_(AddTypedPrimitive<Handle>(new Handle(*this, Handle::Shape_Square, p1))),
+      handle2_(AddTypedPrimitive<Handle>(new Handle(*this, Handle::Shape_Square, p2))),
+      segment1_(AddTypedPrimitive<Segment>(new Segment(*this, p1.GetX(), p1.GetY(), p2.GetX(), p1.GetY()))),
+      segment2_(AddTypedPrimitive<Segment>(new Segment(*this, p2.GetX(), p1.GetY(), p2.GetX(), p2.GetY()))),
+      segment3_(AddTypedPrimitive<Segment>(new Segment(*this, p1.GetX(), p2.GetY(), p2.GetX(), p2.GetY()))),
+      segment4_(AddTypedPrimitive<Segment>(new Segment(*this, p1.GetX(), p1.GetY(), p1.GetX(), p2.GetY()))),
+      label_(AddTypedPrimitive<Text>(new Text(that, *this)))
+    {
+      TextSceneLayer content;
+      content.SetAnchor(BitmapAnchor_CenterLeft);
+      content.SetBorder(10);
+      content.SetText("?");
+
+      label_.SetContent(content);
+      label_.SetColor(COLOR_TEXT);
+    }
+
+    virtual unsigned int GetHandlesCount() const ORTHANC_OVERRIDE
+    {
+      return 2;
+    }
+
+    virtual Handle& GetHandle(unsigned int index) const ORTHANC_OVERRIDE
+    {
+      switch (index)
+      {
+        case 0:
+          return handle1_;
+
+        case 1:
+          return handle2_;
+
+        default:
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+      }
+    }
+
+    virtual void SignalMove(GeometricPrimitive& primitive,
+                            const Scene2D& scene) ORTHANC_OVERRIDE
     {
-      if (isCircle)
+      if (&primitive == &handle1_ ||
+          &primitive == &handle2_)
+      {
+        double x1 = handle1_.GetCenter().GetX();
+        double y1 = handle1_.GetCenter().GetY();
+        double x2 = handle2_.GetCenter().GetX();
+        double y2 = handle2_.GetCenter().GetY();
+        
+        segment1_.SetPosition(x1, y1, x2, y1);
+        segment2_.SetPosition(x2, y1, x2, y2);
+        segment3_.SetPosition(x1, y2, x2, y2);
+        segment4_.SetPosition(x1, y1, x1, y2);
+      }
+      else if (&primitive == &segment1_ ||
+               &primitive == &segment2_ ||
+               &primitive == &segment3_ ||
+               &primitive == &segment4_)
       {
-        annotation_ = new CircleAnnotation(that, units, sceneClick, sceneClick);
-        handle2_ = &dynamic_cast<CircleAnnotation*>(annotation_)->GetHandle2();
+        const Segment& segment = dynamic_cast<const Segment&>(primitive);
+        double x1 = segment.GetPosition1().GetX();
+        double y1 = segment.GetPosition1().GetY();
+        double x2 = segment.GetPosition2().GetX();
+        double y2 = segment.GetPosition2().GetY();
+
+        if (&primitive == &segment1_)
+        {        
+          y2 = y1 + handle2_.GetCenter().GetY() - handle1_.GetCenter().GetY();
+        }
+        else if (&primitive == &segment2_)
+        {
+          x1 = x2 + handle1_.GetCenter().GetX() - handle2_.GetCenter().GetX();
+        }
+        else if (&primitive == &segment3_)
+        {
+          y1 = y2 + handle1_.GetCenter().GetY() - handle2_.GetCenter().GetY();
+        }
+        else if (&primitive == &segment4_)
+        {
+          x2 = x1 + handle2_.GetCenter().GetX() - handle1_.GetCenter().GetX();
+        }
+        else
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+        }
+
+        handle1_.SetCenter(x1, y1);
+        handle2_.SetCenter(x2, y2);
+
+        if (&primitive != &segment1_)
+        {
+          segment1_.SetPosition(x1, y1, x2, y1);
+        }
+        
+        if (&primitive != &segment2_)
+        {
+          segment2_.SetPosition(x2, y1, x2, y2);
+        }
+
+        if (&primitive != &segment3_)
+        {
+          segment3_.SetPosition(x1, y2, x2, y2);
+        }
+
+        if (&primitive != &segment4_)
+        {
+          segment4_.SetPosition(x1, y1, x1, y2);
+        }
+      }
+      else
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+      }
+      
+      TagProbeAsChanged();
+    }
+
+    virtual void Serialize(Json::Value& target) ORTHANC_OVERRIDE
+    {
+      target = Json::objectValue;
+      target[KEY_TYPE] = VALUE_RECTANGLE_PROBE;
+      target[KEY_X1] = handle1_.GetCenter().GetX();
+      target[KEY_Y1] = handle1_.GetCenter().GetY();
+      target[KEY_X2] = handle2_.GetCenter().GetX();
+      target[KEY_Y2] = handle2_.GetCenter().GetY();
+    }
+
+    static void Unserialize(AnnotationsSceneLayer& target,
+                            Units units,
+                            const Json::Value& source)
+    {
+      if (source.isMember(KEY_X1) &&
+          source.isMember(KEY_Y1) &&
+          source.isMember(KEY_X2) &&
+          source.isMember(KEY_Y2) &&
+          source[KEY_X1].isNumeric() &&
+          source[KEY_Y1].isNumeric() &&
+          source[KEY_X2].isNumeric() &&
+          source[KEY_Y2].isNumeric())
+      {
+        new RectangleProbeAnnotation(target, units,
+                                     ScenePoint2D(source[KEY_X1].asDouble(), source[KEY_Y1].asDouble()),
+                                     ScenePoint2D(source[KEY_X2].asDouble(), source[KEY_Y2].asDouble()));
       }
       else
       {
-        annotation_ = new SegmentAnnotation(that, units, true /* show label */, sceneClick, sceneClick);
-        handle2_ = &dynamic_cast<SegmentAnnotation*>(annotation_)->GetHandle2();
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, "Cannot unserialize a rectangle probe annotation");
+      }
+    }
+  };
+
+  
+  class AnnotationsSceneLayer::EllipseProbeAnnotation : public ProbingAnnotation
+  {
+  private:
+    Units     units_;
+    Handle&   handle1_;
+    Handle&   handle2_;
+    Ellipse&  ellipse_;
+    Text&     label_;
+
+  protected:
+    virtual void UpdateProbeForLayer(const ISceneLayer& layer) ORTHANC_OVERRIDE
+    {
+      double x1 = handle1_.GetCenter().GetX();
+      double y1 = handle1_.GetCenter().GetY();
+      double x2 = handle2_.GetCenter().GetX();
+      double y2 = handle2_.GetCenter().GetY();
+
+      {
+        // Put the label to the right of the right-most handle
+        //const double y = std::min(y1, y2);
+        const double y = (y1 + y2) / 2.0;
+        if (x1 < x2)
+        {
+          label_.SetPosition(x2, y);
+        }
+        else
+        {
+          label_.SetPosition(x1, y);
+        }
+      }
+
+      std::string text;
+      
+      char buf[32];
+
+      if (units_ == Units_Millimeters)
+      {
+        sprintf(buf, "Area: %0.2f cm%c%c",
+                ellipse_.GetArea() / 100.0,
+                0xc2, 0xb2 /* two bytes corresponding to two power in UTF-8 */);
+        text = buf;
+      }
+
+      if (layer.GetType() == ISceneLayer::Type_FloatTexture)
+      {
+        const TextureBaseSceneLayer& texture = dynamic_cast<const TextureBaseSceneLayer&>(layer);
+        const AffineTransform2D& textureToScene = texture.GetTransform();
+        const AffineTransform2D sceneToTexture = AffineTransform2D::Invert(textureToScene);
+
+        const Orthanc::ImageAccessor& image = texture.GetTexture();
+        assert(image.GetFormat() == Orthanc::PixelFormat_Float32);
+
+        sceneToTexture.Apply(x1, y1);
+        sceneToTexture.Apply(x2, y2);
+        int ix1 = static_cast<int>(std::floor(x1));
+        int iy1 = static_cast<int>(std::floor(y1));
+        int ix2 = static_cast<int>(std::floor(x2));
+        int iy2 = static_cast<int>(std::floor(y2));
+
+        if (ix1 > ix2)
+        {
+          std::swap(ix1, ix2);
+        }
+
+        if (iy1 > iy2)
+        {
+          std::swap(iy1, iy2);
+        }
+
+        LinearAlgebra::OnlineVarianceEstimator estimator;
+
+        for (int y = std::max(0, iy1); y <= std::min(static_cast<int>(image.GetHeight()) - 1, iy2); y++)
+        {
+          int x = std::max(0, ix1);
+          const float* p = reinterpret_cast<const float*>(image.GetConstRow(y)) + x;
+
+          for (; x <= std::min(static_cast<int>(image.GetWidth()) - 1, ix2); x++, p++)
+          {
+            double yy = static_cast<double>(y) + 0.5;
+            double xx = static_cast<double>(x) + 0.5;
+            textureToScene.Apply(xx, yy);
+            if (ellipse_.IsPointInside(ScenePoint2D(xx, yy)))
+            {
+              estimator.AddSample(*p);
+            }
+          }
+        }
+
+        if (estimator.GetCount() > 0)
+        {
+          if (!text.empty())
+          {
+            text += "\n";
+          }
+          sprintf(buf, "Mean: %0.1f\nStdDev: %0.1f", estimator.GetMean(), estimator.GetStandardDeviation());
+          text += buf;
+        }
       }
-        
+      
+      label_.SetText(text);
+    }
+    
+  public:
+    EllipseProbeAnnotation(AnnotationsSceneLayer& that,
+                           Units units,
+                           const ScenePoint2D& p1,
+                           const ScenePoint2D& p2) :
+      ProbingAnnotation(that),
+      units_(units),
+      handle1_(AddTypedPrimitive<Handle>(new Handle(*this, Handle::Shape_Square, p1))),
+      handle2_(AddTypedPrimitive<Handle>(new Handle(*this, Handle::Shape_Square, p2))),
+      ellipse_(AddTypedPrimitive<Ellipse>(new Ellipse(*this, p1, p2))),
+      label_(AddTypedPrimitive<Text>(new Text(that, *this)))
+    {
+      TextSceneLayer content;
+      content.SetAnchor(BitmapAnchor_CenterLeft);
+      content.SetBorder(10);
+      content.SetText("?");
+
+      label_.SetContent(content);
+      label_.SetColor(COLOR_TEXT);
+    }
+
+    virtual unsigned int GetHandlesCount() const ORTHANC_OVERRIDE
+    {
+      return 2;
+    }
+
+    virtual Handle& GetHandle(unsigned int index) const ORTHANC_OVERRIDE
+    {
+      switch (index)
+      {
+        case 0:
+          return handle1_;
+
+        case 1:
+          return handle2_;
+
+        default:
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+      }
+    }
+
+    virtual void SignalMove(GeometricPrimitive& primitive,
+                            const Scene2D& scene) ORTHANC_OVERRIDE
+    {
+      if (&primitive == &handle1_ ||
+          &primitive == &handle2_)
+      {
+        ellipse_.SetPosition(handle1_.GetCenter(), handle2_.GetCenter());
+      }
+      else if (&primitive == &ellipse_)
+      {
+        handle1_.SetCenter(ellipse_.GetPosition1());
+        handle2_.SetCenter(ellipse_.GetPosition2());
+      }
+      else
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+      }
+      
+      TagProbeAsChanged();
+    }
+
+    virtual void Serialize(Json::Value& target) ORTHANC_OVERRIDE
+    {
+      target = Json::objectValue;
+      target[KEY_TYPE] = VALUE_ELLIPSE_PROBE;
+      target[KEY_X1] = handle1_.GetCenter().GetX();
+      target[KEY_Y1] = handle1_.GetCenter().GetY();
+      target[KEY_X2] = handle2_.GetCenter().GetX();
+      target[KEY_Y2] = handle2_.GetCenter().GetY();
+    }
+
+    static void Unserialize(AnnotationsSceneLayer& target,
+                            Units units,
+                            const Json::Value& source)
+    {
+      if (source.isMember(KEY_X1) &&
+          source.isMember(KEY_Y1) &&
+          source.isMember(KEY_X2) &&
+          source.isMember(KEY_Y2) &&
+          source[KEY_X1].isNumeric() &&
+          source[KEY_Y1].isNumeric() &&
+          source[KEY_X2].isNumeric() &&
+          source[KEY_Y2].isNumeric())
+      {
+        new EllipseProbeAnnotation(target, units,
+                                     ScenePoint2D(source[KEY_X1].asDouble(), source[KEY_Y1].asDouble()),
+                                     ScenePoint2D(source[KEY_X2].asDouble(), source[KEY_Y2].asDouble()));
+      }
+      else
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, "Cannot unserialize an ellipse probe annotation");
+      }
+    }
+  };
+
+  
+  class AnnotationsSceneLayer::CreateTwoHandlesTracker : public IFlexiblePointerTracker
+  {
+  private:
+    AnnotationsSceneLayer&  layer_;
+    Annotation*             annotation_;
+    AffineTransform2D       canvasToScene_;
+
+  protected:
+    AnnotationsSceneLayer& GetLayer() const
+    {
+      return layer_;
+    }
+    
+    const Annotation& GetAnnotation() const
+    {
+      if (IsAlive())
+      {
+        assert(annotation_ != NULL);
+        return *annotation_;
+      }
+      else
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+      }
+    }
+      
+  public:
+    CreateTwoHandlesTracker(Annotation& annotation,
+                            const AffineTransform2D& canvasToScene) :
+      layer_(annotation.GetParentLayer()),
+      annotation_(&annotation),
+      canvasToScene_(canvasToScene)
+    {
       assert(annotation_ != NULL &&
-             handle2_ != NULL);
+             annotation_->GetHandlesCount() >= 2);
     }
 
-    virtual void PointerMove(const PointerEvent& event) ORTHANC_OVERRIDE
+    virtual void PointerMove(const PointerEvent& event,
+                             const Scene2D& scene) ORTHANC_OVERRIDE
     {
       if (annotation_ != NULL)
       {
-        assert(handle2_ != NULL);
-        handle2_->SetCenter(event.GetMainPosition().Apply(canvasToScene_));
-        annotation_->SignalMove(*handle2_);
-
-        that_.BroadcastMessage(AnnotationChangedMessage(that_));
+        annotation_->GetHandle(1).SetCenter(event.GetMainPosition().Apply(canvasToScene_));
+        annotation_->SignalMove(annotation_->GetHandle(1), scene);
+
+        layer_.BroadcastMessage(AnnotationChangedMessage(layer_));
       }
     }
       
-    virtual void PointerUp(const PointerEvent& event) ORTHANC_OVERRIDE
+    virtual void PointerUp(const PointerEvent& event,
+                           const Scene2D& scene) ORTHANC_OVERRIDE
     {
       annotation_ = NULL;  // IsAlive() becomes false
 
-      that_.BroadcastMessage(AnnotationAddedMessage(that_));
+      layer_.BroadcastMessage(AnnotationAddedMessage(layer_));
     }
 
-    virtual void PointerDown(const PointerEvent& event) ORTHANC_OVERRIDE
+    virtual void PointerDown(const PointerEvent& event,
+                             const Scene2D& scene) ORTHANC_OVERRIDE
     {
     }
 
@@ -1249,11 +2397,11 @@
       return (annotation_ != NULL);
     }
 
-    virtual void Cancel() ORTHANC_OVERRIDE
+    virtual void Cancel(const Scene2D& scene) ORTHANC_OVERRIDE
     {
       if (annotation_ != NULL)
       {
-        that_.DeleteAnnotation(annotation_);
+        layer_.DeleteAnnotation(annotation_);
         annotation_ = NULL;
       }
     }
@@ -1264,52 +2412,53 @@
   {
   private:
     AnnotationsSceneLayer&  that_;
-    SegmentAnnotation*      segment_;
+    LengthAnnotation*       length_;
     AngleAnnotation*        angle_;
     AffineTransform2D       canvasToScene_;
       
   public:
     CreateAngleTracker(AnnotationsSceneLayer& that,
-                       Units units,
                        const ScenePoint2D& sceneClick,
                        const AffineTransform2D& canvasToScene) :
       that_(that),
-      segment_(NULL),
+      length_(NULL),
       angle_(NULL),
       canvasToScene_(canvasToScene)
     {
-      segment_ = new SegmentAnnotation(that, units, false /* no length label */, sceneClick, sceneClick);
+      length_ = new LengthAnnotation(that, that.GetUnits(), false /* no length label */, sceneClick, sceneClick);
     }
 
-    virtual void PointerMove(const PointerEvent& event) ORTHANC_OVERRIDE
+    virtual void PointerMove(const PointerEvent& event,
+                             const Scene2D& scene) ORTHANC_OVERRIDE
     {
-      if (segment_ != NULL)
+      if (length_ != NULL)
       {
-        segment_->GetHandle2().SetCenter(event.GetMainPosition().Apply(canvasToScene_));
-        segment_->SignalMove(segment_->GetHandle2());
+        length_->GetHandle(1).SetCenter(event.GetMainPosition().Apply(canvasToScene_));
+        length_->SignalMove(length_->GetHandle(1), scene);
         that_.BroadcastMessage(AnnotationChangedMessage(that_));
       }
 
       if (angle_ != NULL)
       {
-        angle_->GetEndHandle().SetCenter(event.GetMainPosition().Apply(canvasToScene_));
-        angle_->SignalMove(angle_->GetEndHandle());
+        angle_->GetHandle(2).SetCenter(event.GetMainPosition().Apply(canvasToScene_));
+        angle_->SignalMove(angle_->GetHandle(2), scene);
         that_.BroadcastMessage(AnnotationChangedMessage(that_));
       }
     }
       
-    virtual void PointerUp(const PointerEvent& event) ORTHANC_OVERRIDE
+    virtual void PointerUp(const PointerEvent& event,
+                           const Scene2D& scene) ORTHANC_OVERRIDE
     {
-      if (segment_ != NULL)
+      if (length_ != NULL)
       {
         // End of first step: The first segment is available, now create the angle
 
-        angle_ = new AngleAnnotation(that_, segment_->GetUnits(), segment_->GetHandle1().GetCenter(),
-                                     segment_->GetHandle2().GetCenter(),
-                                     segment_->GetHandle2().GetCenter());
+        angle_ = new AngleAnnotation(that_, length_->GetHandle(0).GetCenter(),
+                                     length_->GetHandle(1).GetCenter(),
+                                     length_->GetHandle(1).GetCenter());
           
-        that_.DeleteAnnotation(segment_);
-        segment_ = NULL;
+        that_.DeleteAnnotation(length_);
+        length_ = NULL;
 
         that_.BroadcastMessage(AnnotationChangedMessage(that_));
       }
@@ -1321,22 +2470,23 @@
       }
     }
 
-    virtual void PointerDown(const PointerEvent& event) ORTHANC_OVERRIDE
+    virtual void PointerDown(const PointerEvent& event,
+                             const Scene2D& scene) ORTHANC_OVERRIDE
     {
     }
 
     virtual bool IsAlive() const ORTHANC_OVERRIDE
     {
-      return (segment_ != NULL ||
+      return (length_ != NULL ||
               angle_ != NULL);
     }
 
-    virtual void Cancel() ORTHANC_OVERRIDE
+    virtual void Cancel(const Scene2D& scene) ORTHANC_OVERRIDE
     {
-      if (segment_ != NULL)
+      if (length_ != NULL)
       {
-        that_.DeleteAnnotation(segment_);
-        segment_ = NULL;
+        that_.DeleteAnnotation(length_);
+        length_ = NULL;
       }
 
       if (angle_ != NULL)
@@ -1348,6 +2498,73 @@
   };
 
 
+  class AnnotationsSceneLayer::CreatePixelProbeTracker : public IFlexiblePointerTracker
+  {
+  public:
+    CreatePixelProbeTracker(AnnotationsSceneLayer& that,
+                            const ScenePoint2D& sceneClick,
+                            const Scene2D& scene)
+    {
+      PixelProbeAnnotation* annotation = new PixelProbeAnnotation(that, sceneClick);
+      annotation->UpdateProbe(scene);
+      that.BroadcastMessage(AnnotationAddedMessage(that));
+    }
+
+    virtual void PointerMove(const PointerEvent& event,
+                             const Scene2D& scene) ORTHANC_OVERRIDE
+    {
+    }
+      
+    virtual void PointerUp(const PointerEvent& event,
+                           const Scene2D& scene) ORTHANC_OVERRIDE
+    {
+    }
+
+    virtual void PointerDown(const PointerEvent& event,
+                             const Scene2D& scene) ORTHANC_OVERRIDE
+    {
+    }
+
+    virtual bool IsAlive() const ORTHANC_OVERRIDE
+    {
+      return false;
+    }
+
+    virtual void Cancel(const Scene2D& scene) ORTHANC_OVERRIDE
+    {
+    }
+  };
+
+
+  class AnnotationsSceneLayer::CreateTextAnnotationTracker : public CreateTwoHandlesTracker
+  {
+  public:
+    CreateTextAnnotationTracker(AnnotationsSceneLayer& that,
+                                const std::string& label,
+                                const ScenePoint2D& position,
+                                const AffineTransform2D& canvasToScene) :
+      CreateTwoHandlesTracker(*new TextAnnotation(that, label, position, position), canvasToScene)
+    {
+    }
+
+    virtual void PointerUp(const PointerEvent& event,
+                           const Scene2D& scene) ORTHANC_OVERRIDE
+    {
+      std::unique_ptr<TextAnnotationRequiredMessage> request;
+      
+      {
+        const TextAnnotation& annotation = dynamic_cast<const TextAnnotation&>(GetAnnotation());
+        request.reset(new TextAnnotationRequiredMessage(GetLayer(), annotation.GetPointedPosition(), annotation.GetLabelPosition()));
+      }
+      
+      Cancel(scene);  // Warning: "annotation_" is now invalid!
+      
+      GetLayer().BroadcastMessage(AnnotationChangedMessage(GetLayer()));
+      GetLayer().BroadcastMessage(*request);
+    }
+  };
+
+
   // Dummy tracker that is only used for deletion, in order to warn
   // the caller that the mouse action was taken into consideration
   class AnnotationsSceneLayer::RemoveTracker : public IFlexiblePointerTracker
@@ -1357,15 +2574,18 @@
     {
     }
 
-    virtual void PointerMove(const PointerEvent& event) ORTHANC_OVERRIDE
+    virtual void PointerMove(const PointerEvent& event,
+                             const Scene2D& scene) ORTHANC_OVERRIDE
     {
     }
       
-    virtual void PointerUp(const PointerEvent& event) ORTHANC_OVERRIDE
+    virtual void PointerUp(const PointerEvent& event,
+                           const Scene2D& scene) ORTHANC_OVERRIDE
     {
     }
 
-    virtual void PointerDown(const PointerEvent& event) ORTHANC_OVERRIDE
+    virtual void PointerDown(const PointerEvent& event,
+                             const Scene2D& scene) ORTHANC_OVERRIDE
     {
     }
 
@@ -1374,7 +2594,7 @@
       return false;
     }
 
-    virtual void Cancel() ORTHANC_OVERRIDE
+    virtual void Cancel(const Scene2D& scene) ORTHANC_OVERRIDE
     {
     }
   };
@@ -1421,7 +2641,8 @@
     activeTool_(Tool_Edit),
     macroLayerIndex_(macroLayerIndex),
     polylineSubLayer_(0),  // dummy initialization
-    units_(Units_Pixels)
+    units_(Units_Pixels),
+    probedLayer_(0)
   {
   }
     
@@ -1450,10 +2671,11 @@
   }
 
 
-  void AnnotationsSceneLayer::AddSegmentAnnotation(const ScenePoint2D& p1,
-                                                   const ScenePoint2D& p2)
+  void AnnotationsSceneLayer::AddLengthAnnotation(const ScenePoint2D& p1,
+                                                  const ScenePoint2D& p2)
   {
-    annotations_.insert(new SegmentAnnotation(*this, units_, true /* show label */, p1, p2));
+    annotations_.insert(new LengthAnnotation(*this, units_, true /* show label */, p1, p2));
+    BroadcastMessage(AnnotationChangedMessage(*this));
   }
   
 
@@ -1461,6 +2683,7 @@
                                                   const ScenePoint2D& p2)
   {
     annotations_.insert(new CircleAnnotation(*this, units_, p1, p2));
+    BroadcastMessage(AnnotationChangedMessage(*this));
   }
   
 
@@ -1468,12 +2691,20 @@
                                                  const ScenePoint2D& p2,
                                                  const ScenePoint2D& p3)
   {
-    annotations_.insert(new AngleAnnotation(*this, units_, p1, p2, p3));
+    annotations_.insert(new AngleAnnotation(*this, p1, p2, p3));
+    BroadcastMessage(AnnotationChangedMessage(*this));
   }
   
 
   void AnnotationsSceneLayer::Render(Scene2D& scene)
   {
+    // First, update the probes
+    for (Annotations::const_iterator it = annotations_.begin(); it != annotations_.end(); ++it)
+    {
+      assert(*it != NULL);
+      (*it)->UpdateProbe(scene);
+    }
+
     MacroSceneLayer* macro = NULL;
 
     if (scene.HasLayer(macroLayerIndex_))
@@ -1566,6 +2797,10 @@
   IFlexiblePointerTracker* AnnotationsSceneLayer::CreateTracker(const ScenePoint2D& p,
                                                                 const Scene2D& scene)
   {
+    /**
+     * WARNING: The created trackers must NOT keep a reference to "scene"!
+     **/
+
     if (activeTool_ == Tool_None)
     {
       return NULL;
@@ -1606,14 +2841,38 @@
       {
         switch (activeTool_)
         {
-          case Tool_Segment:
-            return new CreateSegmentOrCircleTracker(*this, units_, false /* segment */, s, scene.GetCanvasToSceneTransform());
+          case Tool_Length:
+          {
+            Annotation* annotation = new LengthAnnotation(*this, units_, true /* show label */, s, s);
+            return new CreateTwoHandlesTracker(*annotation, scene.GetCanvasToSceneTransform());
+          }
 
           case Tool_Circle:
-            return new CreateSegmentOrCircleTracker(*this, units_, true /* circle */, s, scene.GetCanvasToSceneTransform());
+          {
+            Annotation* annotation = new CircleAnnotation(*this, units_, s, s);
+            return new CreateTwoHandlesTracker(*annotation, scene.GetCanvasToSceneTransform());
+          }
 
           case Tool_Angle:
-            return new CreateAngleTracker(*this, units_, s, scene.GetCanvasToSceneTransform());
+            return new CreateAngleTracker(*this, s, scene.GetCanvasToSceneTransform());
+
+          case Tool_PixelProbe:
+            return new CreatePixelProbeTracker(*this, s, scene);
+
+          case Tool_RectangleProbe:
+          {
+            Annotation* annotation = new RectangleProbeAnnotation(*this, units_, s, s);
+            return new CreateTwoHandlesTracker(*annotation, scene.GetCanvasToSceneTransform());
+          }
+
+          case Tool_EllipseProbe:
+          {
+            Annotation* annotation = new EllipseProbeAnnotation(*this, units_, s, s);
+            return new CreateTwoHandlesTracker(*annotation, scene.GetCanvasToSceneTransform());
+          }
+
+          case Tool_TextAnnotation:
+            return new CreateTextAnnotationTracker(*this, "" /* empty label */, s, scene.GetCanvasToSceneTransform());
 
           default:
             return NULL;
@@ -1698,15 +2957,31 @@
 
       if (type == VALUE_ANGLE)
       {
-        AngleAnnotation::Unserialize(*this, units_, annotations[i]);
+        AngleAnnotation::Unserialize(*this, annotations[i]);
       }
       else if (type == VALUE_CIRCLE)
       {
         CircleAnnotation::Unserialize(*this, units_, annotations[i]);
       }
-      else if (type == VALUE_SEGMENT)
+      else if (type == VALUE_LENGTH)
+      {
+        LengthAnnotation::Unserialize(*this, units_, annotations[i]);
+      }
+      else if (type == VALUE_PIXEL_PROBE)
+      {
+        PixelProbeAnnotation::Unserialize(*this, annotations[i]);
+      }
+      else if (type == VALUE_RECTANGLE_PROBE)
       {
-        SegmentAnnotation::Unserialize(*this, units_, annotations[i]);
+        RectangleProbeAnnotation::Unserialize(*this, units_, annotations[i]);
+      }
+      else if (type == VALUE_ELLIPSE_PROBE)
+      {
+        EllipseProbeAnnotation::Unserialize(*this, units_, annotations[i]);
+      }
+      else if (type == VALUE_TEXT_ANNOTATION)
+      {
+        TextAnnotation::Unserialize(*this, annotations[i]);
       }
       else
       {
@@ -1714,4 +2989,13 @@
       }
     }
   }
+
+
+  void AnnotationsSceneLayer::AddTextAnnotation(const std::string& label,
+                                                const ScenePoint2D& pointedPosition,
+                                                const ScenePoint2D& labelPosition)
+  {
+    annotations_.insert(new TextAnnotation(*this, label, pointedPosition, labelPosition));
+    BroadcastMessage(AnnotationChangedMessage(*this));
+  }
 }
--- a/OrthancStone/Sources/Scene2D/AnnotationsSceneLayer.h	Fri Oct 28 07:47:55 2022 +0200
+++ b/OrthancStone/Sources/Scene2D/AnnotationsSceneLayer.h	Wed Nov 02 15:14:56 2022 +0100
@@ -34,14 +34,47 @@
     ORTHANC_STONE_DEFINE_ORIGIN_MESSAGE(__FILE__, __LINE__, AnnotationRemovedMessage, AnnotationsSceneLayer);
     ORTHANC_STONE_DEFINE_ORIGIN_MESSAGE(__FILE__, __LINE__, AnnotationChangedMessage, AnnotationsSceneLayer);
 
+    class TextAnnotationRequiredMessage : public OriginMessage<AnnotationsSceneLayer>
+    {
+      ORTHANC_STONE_MESSAGE(__FILE__, __LINE__);
+
+    private:
+      ScenePoint2D pointedPosition_;
+      ScenePoint2D labelPosition_;
+
+    public:
+      TextAnnotationRequiredMessage(const AnnotationsSceneLayer& origin,
+                                    ScenePoint2D pointedPosition,
+                                    ScenePoint2D labelPosition) :
+        OriginMessage(origin),
+        pointedPosition_(pointedPosition),
+        labelPosition_(labelPosition)
+      {
+      }
+      
+      const ScenePoint2D& GetPointedPosition() const
+      {
+        return pointedPosition_;
+      }
+      
+      const ScenePoint2D& GetLabelPosition() const
+      {
+        return labelPosition_;
+      }
+    };
+    
     enum Tool
     {
       Tool_Edit,
       Tool_None,
-      Tool_Segment,
+      Tool_Length,
       Tool_Angle,
       Tool_Circle,
-      Tool_Remove
+      Tool_Remove,
+      Tool_PixelProbe,
+      Tool_RectangleProbe,
+      Tool_EllipseProbe,
+      Tool_TextAnnotation
     };
 
   private:
@@ -51,16 +84,25 @@
     class Circle;    
     class Arc;
     class Text;
+    class Ellipse;
 
     class Annotation;
+    class ProbingAnnotation;
+    class PixelProbeAnnotation;
     class SegmentAnnotation;
+    class LengthAnnotation;
+    class TextAnnotation;
     class AngleAnnotation;
     class CircleAnnotation;
+    class RectangleProbeAnnotation;
+    class EllipseProbeAnnotation;
     
     class EditPrimitiveTracker;
-    class CreateSegmentOrCircleTracker;
+    class CreateTwoHandlesTracker;
     class CreateAngleTracker;
+    class CreatePixelProbeTracker;
     class RemoveTracker;
+    class CreateTextAnnotationTracker;
 
     typedef std::set<GeometricPrimitive*>  GeometricPrimitives;
     typedef std::set<Annotation*>          Annotations;
@@ -73,6 +115,7 @@
     Annotations          annotations_;
     SubLayers            subLayersToRemove_;
     Units                units_;
+    int                  probedLayer_;
 
     void AddAnnotation(Annotation* annotation);
     
@@ -109,8 +152,8 @@
       return units_;
     }
 
-    void AddSegmentAnnotation(const ScenePoint2D& p1,
-                              const ScenePoint2D& p2);
+    void AddLengthAnnotation(const ScenePoint2D& p1,
+                             const ScenePoint2D& p2);
 
     void AddCircleAnnotation(const ScenePoint2D& p1,
                              const ScenePoint2D& p2);
@@ -132,5 +175,19 @@
     void Serialize(Json::Value& target) const;
     
     void Unserialize(const Json::Value& serialized);
+
+    void SetProbedLayer(int layer)
+    {
+      probedLayer_ = layer;
+    }
+
+    int GetProbedLayer() const
+    {
+      return probedLayer_;
+    }
+
+    void AddTextAnnotation(const std::string& label,
+                           const ScenePoint2D& pointedPosition,
+                           const ScenePoint2D& labelPosition);
   };
 }
--- a/OrthancStone/Sources/Scene2D/GrayscaleWindowingSceneTracker.cpp	Fri Oct 28 07:47:55 2022 +0200
+++ b/OrthancStone/Sources/Scene2D/GrayscaleWindowingSceneTracker.cpp	Wed Nov 02 15:14:56 2022 +0100
@@ -118,7 +118,7 @@
                                                                  const PointerEvent& event,
                                                                  unsigned int canvasWidth,
                                                                  unsigned int canvasHeight) :
-    OneGesturePointerTracker(viewport),
+    viewport_(viewport),
     layerIndex_(layerIndex),
     clickX_(event.GetMainPosition().GetX()),
     clickY_(event.GetMainPosition().GetY())
@@ -154,7 +154,8 @@
     }
   }
   
-  void GrayscaleWindowingSceneTracker::PointerMove(const PointerEvent& event)
+  void GrayscaleWindowingSceneTracker::PointerMove(const PointerEvent& event,
+                                                   const Scene2D& scene)
   {
     if (active_)
     {
@@ -173,7 +174,7 @@
     }
   }
 
-  void GrayscaleWindowingSceneTracker::Cancel()
+  void GrayscaleWindowingSceneTracker::Cancel(const Scene2D& scene)
   {
     SetWindowing(originalCenter_, originalWidth_);
   }
--- a/OrthancStone/Sources/Scene2D/GrayscaleWindowingSceneTracker.h	Fri Oct 28 07:47:55 2022 +0200
+++ b/OrthancStone/Sources/Scene2D/GrayscaleWindowingSceneTracker.h	Wed Nov 02 15:14:56 2022 +0100
@@ -23,10 +23,8 @@
 
 #pragma once
 
-
 #include "../Scene2DViewport/OneGesturePointerTracker.h"
 #include "../Viewport/IViewport.h"
-#include "Internals/FixedPointAligner.h"
 
 #include <boost/weak_ptr.hpp>
 
@@ -35,6 +33,8 @@
   class GrayscaleWindowingSceneTracker : public OneGesturePointerTracker
   {
   private:
+    boost::weak_ptr<IViewport> viewport_;
+    
     bool    active_;
     int     layerIndex_;
     double  normalization_;
@@ -53,8 +53,9 @@
                                    unsigned int canvasWidth,
                                    unsigned int canvasHeight);
 
-    virtual void PointerMove(const PointerEvent& event) ORTHANC_OVERRIDE;
+    virtual void PointerMove(const PointerEvent& event,
+                             const Scene2D& scene) ORTHANC_OVERRIDE;
     
-    virtual void Cancel() ORTHANC_OVERRIDE;
+    virtual void Cancel(const Scene2D& scene) ORTHANC_OVERRIDE;
   };
 }
--- a/OrthancStone/Sources/Scene2D/Internals/FixedPointAligner.cpp	Fri Oct 28 07:47:55 2022 +0200
+++ b/OrthancStone/Sources/Scene2D/Internals/FixedPointAligner.cpp	Wed Nov 02 15:14:56 2022 +0100
@@ -20,43 +20,31 @@
  * <http://www.gnu.org/licenses/>.
  **/
 
-#include "../../Scene2DViewport/ViewportController.h"
+
 #include "FixedPointAligner.h"
 
+#include <OrthancException.h>
+
 namespace OrthancStone
 {
   namespace Internals
   {
-    FixedPointAligner::FixedPointAligner(boost::weak_ptr<IViewport> viewport,
-                                         const ScenePoint2D& p) 
-      : viewport_(viewport)
-      , canvas_(p)
+    FixedPointAligner::FixedPointAligner(const ViewportController& controller,
+                                         const ScenePoint2D& p) :
+      canvas_(p)
     {
-      std::unique_ptr<IViewport::ILock> lock(GetViewportLock());
-      pivot_ = canvas_.Apply(lock->GetController().GetCanvasToSceneTransform());
+      pivot_ = canvas_.Apply(controller.GetCanvasToSceneTransform());
     }
 
-    IViewport::ILock* FixedPointAligner::GetViewportLock()
+    void FixedPointAligner::Apply(ViewportController& controller)
     {
-      boost::shared_ptr<IViewport> viewport = viewport_.lock();
-      if (viewport)
-        return viewport->Lock();
-      else
-        return NULL;
-    }
-    
-    void FixedPointAligner::Apply()
-    {
-      std::unique_ptr<IViewport::ILock> lock(GetViewportLock());
-      ScenePoint2D p = canvas_.Apply(
-        lock->GetController().GetCanvasToSceneTransform());
+      ScenePoint2D p = canvas_.Apply(controller.GetCanvasToSceneTransform());
 
-      lock->GetController().SetSceneToCanvasTransform(
+      controller.SetSceneToCanvasTransform(
         AffineTransform2D::Combine(
-          lock->GetController().GetSceneToCanvasTransform(),
+          controller.GetSceneToCanvasTransform(),
           AffineTransform2D::CreateOffset(p.GetX() - pivot_.GetX(),
                                           p.GetY() - pivot_.GetY())));
-      lock->Invalidate();
     }
   }
 }
--- a/OrthancStone/Sources/Scene2D/Internals/FixedPointAligner.h	Fri Oct 28 07:47:55 2022 +0200
+++ b/OrthancStone/Sources/Scene2D/Internals/FixedPointAligner.h	Wed Nov 02 15:14:56 2022 +0100
@@ -22,11 +22,7 @@
 
 #pragma once
 
-#include "../../Scene2DViewport/PredeclaredTypes.h"
-#include "../../Scene2D/ScenePoint2D.h"
-#include "../../Viewport/IViewport.h"
-
-#include <boost/weak_ptr.hpp>
+#include "../../Scene2DViewport/ViewportController.h"
 
 namespace OrthancStone
 {
@@ -37,21 +33,14 @@
     class FixedPointAligner : public boost::noncopyable
     {
     private:
-      boost::weak_ptr<IViewport> viewport_;
-      ScenePoint2D               pivot_;
-      ScenePoint2D               canvas_;
-
-      /**
-      This will return a scoped lock to the viewport.
-      If the viewport does not exist anymore, then nullptr is returned.
-      */
-      IViewport::ILock* GetViewportLock();
+      ScenePoint2D  pivot_;
+      ScenePoint2D  canvas_;
 
     public:
-      FixedPointAligner(boost::weak_ptr<IViewport> viewport,
+      FixedPointAligner(const ViewportController& controller,
                         const ScenePoint2D& p);
 
-      void Apply();
+      void Apply(ViewportController& controller);
     };
   }
 }
--- a/OrthancStone/Sources/Scene2D/Internals/OpenGLFloatTextureProgram.cpp	Fri Oct 28 07:47:55 2022 +0200
+++ b/OrthancStone/Sources/Scene2D/Internals/OpenGLFloatTextureProgram.cpp	Wed Nov 02 15:14:56 2022 +0100
@@ -41,7 +41,7 @@
   "void main()                                       \n"
   "{                                                 \n"
   "  vec4 t = texture2D(u_texture, v_texcoord);      \n"
-  "  float v = (t.r * 256.0 + t.g) * 256.0;          \n"
+  "  float v = (t.r * 256.0 + t.g) * 255.0;          \n"
   "  v = v * u_slope + u_offset;                     \n"  // (*)
   "  float a = u_windowCenter - u_windowWidth / 2.0; \n"
   "  float dy = 1.0 / u_windowWidth;                 \n"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancStone/Sources/Scene2D/MagnifyingGlassTracker.cpp	Wed Nov 02 15:14:56 2022 +0100
@@ -0,0 +1,94 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2022 Osimis S.A., Belgium
+ * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "MagnifyingGlassTracker.h"
+
+#include "../Scene2DViewport/ViewportController.h"
+#include "../Viewport/ViewportLocker.h"
+
+namespace OrthancStone
+{
+  void MagnifyingGlassTracker::Update(const ViewportLocker& locker,
+                                      const PointerEvent& event)
+  {
+    ScenePoint2D p = event.GetMainPosition().Apply(originalCanvasToScene_);
+
+    locker.GetController().SetSceneToCanvasTransform(
+      AffineTransform2D::Combine(
+        originalSceneToCanvas_,
+        AffineTransform2D::CreateOffset(p.GetX(), p.GetY()),
+        AffineTransform2D::CreateScaling(5, 5),
+        AffineTransform2D::CreateOffset(-pivot_.GetX(), -pivot_.GetY())));
+
+    locker.Invalidate();
+  }
+    
+
+  MagnifyingGlassTracker::MagnifyingGlassTracker(boost::weak_ptr<IViewport> viewport,
+                                                 const PointerEvent& event) :
+    viewport_(viewport)
+  {
+    ViewportLocker locker(viewport_);
+    
+    if (locker.IsValid())
+    {
+      originalSceneToCanvas_ = locker.GetController().GetSceneToCanvasTransform();
+      originalCanvasToScene_ = locker.GetController().GetCanvasToSceneTransform();
+      pivot_ = event.GetMainPosition().Apply(locker.GetController().GetCanvasToSceneTransform());
+
+      Update(locker, event);
+    }
+  }
+      
+
+  void MagnifyingGlassTracker::PointerUp(const PointerEvent& event,
+                                         const Scene2D& scene)
+  {
+    Cancel(scene);
+    OneGesturePointerTracker::PointerUp(event, scene);
+  }
+
+  
+  void MagnifyingGlassTracker::PointerMove(const PointerEvent& event,
+                                           const Scene2D& scene)
+  {
+    ViewportLocker locker(viewport_);
+    
+    if (locker.IsValid())
+    {
+      Update(locker, event);
+    }
+  }
+    
+
+  void MagnifyingGlassTracker::Cancel(const Scene2D& scene)
+  {
+    ViewportLocker locker(viewport_);
+    
+    if (locker.IsValid())
+    {
+      locker.GetController().SetSceneToCanvasTransform(originalSceneToCanvas_);
+      locker.Invalidate();
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancStone/Sources/Scene2D/MagnifyingGlassTracker.h	Wed Nov 02 15:14:56 2022 +0100
@@ -0,0 +1,56 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2022 Osimis S.A., Belgium
+ * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../Scene2DViewport/OneGesturePointerTracker.h"
+#include "../Viewport/ViewportLocker.h"
+
+#include <boost/weak_ptr.hpp>
+
+namespace OrthancStone
+{
+  class MagnifyingGlassTracker : public OneGesturePointerTracker
+  {
+  private:
+    boost::weak_ptr<IViewport>  viewport_;
+    ScenePoint2D                pivot_;
+    AffineTransform2D           originalSceneToCanvas_;
+    AffineTransform2D           originalCanvasToScene_;
+
+    void Update(const ViewportLocker& locker,
+                const PointerEvent& event);
+    
+  public:
+    MagnifyingGlassTracker(boost::weak_ptr<IViewport> viewport,
+                           const PointerEvent& event);
+    
+    virtual void PointerUp(const PointerEvent& event,
+                           const Scene2D& scene) ORTHANC_OVERRIDE;
+    
+    virtual void PointerMove(const PointerEvent& event,
+                             const Scene2D& scene) ORTHANC_OVERRIDE;
+    
+    virtual void Cancel(const Scene2D& scene) ORTHANC_OVERRIDE;
+  };
+}
--- a/OrthancStone/Sources/Scene2D/PanSceneTracker.cpp	Fri Oct 28 07:47:55 2022 +0200
+++ b/OrthancStone/Sources/Scene2D/PanSceneTracker.cpp	Wed Nov 02 15:14:56 2022 +0100
@@ -22,44 +22,52 @@
 
 
 #include "PanSceneTracker.h"
-#include "../Viewport/IViewport.h"
+
 #include "../Scene2DViewport/ViewportController.h"
-
-#include <memory>
+#include "../Viewport/ViewportLocker.h"
 
 namespace OrthancStone
 {
   PanSceneTracker::PanSceneTracker(boost::weak_ptr<IViewport> viewport,
-                                   const PointerEvent& event)
-    : OneGesturePointerTracker(viewport)
+                                   const PointerEvent& event) :
+    viewport_(viewport)
   {
+    ViewportLocker locker(viewport_);
     
-    std::unique_ptr<IViewport::ILock> lock(GetViewportLock());
-
-    originalSceneToCanvas_ = lock->GetController().GetSceneToCanvasTransform();
-    originalCanvasToScene_ = lock->GetController().GetCanvasToSceneTransform();
-
-    pivot_ = event.GetMainPosition().Apply(originalCanvasToScene_);
+    if (locker.IsValid())
+    {
+      originalSceneToCanvas_ = locker.GetController().GetSceneToCanvasTransform();
+      originalCanvasToScene_ = locker.GetController().GetCanvasToSceneTransform();
+      pivot_ = event.GetMainPosition().Apply(originalCanvasToScene_);
+    }
   }
 
 
-  void PanSceneTracker::PointerMove(const PointerEvent& event)
+  void PanSceneTracker::PointerMove(const PointerEvent& event,
+                                    const Scene2D& scene)
   {
     ScenePoint2D p = event.GetMainPosition().Apply(originalCanvasToScene_);
 
-    std::unique_ptr<IViewport::ILock> lock(GetViewportLock());
-
-    lock->GetController().SetSceneToCanvasTransform(
-      AffineTransform2D::Combine(
-        originalSceneToCanvas_,
-        AffineTransform2D::CreateOffset(p.GetX() - pivot_.GetX(),
-                                        p.GetY() - pivot_.GetY())));
-    lock->Invalidate();
+    ViewportLocker locker(viewport_);
+    
+    if (locker.IsValid())
+    {
+      locker.GetController().SetSceneToCanvasTransform(
+        AffineTransform2D::Combine(
+          originalSceneToCanvas_,
+          AffineTransform2D::CreateOffset(p.GetX() - pivot_.GetX(),
+                                          p.GetY() - pivot_.GetY())));
+      locker.Invalidate();
+    }
   }
 
-  void PanSceneTracker::Cancel()
+  void PanSceneTracker::Cancel(const Scene2D& scene)
   {
-    std::unique_ptr<IViewport::ILock> lock(GetViewportLock());
-    lock->GetController().SetSceneToCanvasTransform(originalSceneToCanvas_);
+    ViewportLocker locker(viewport_);
+    
+    if (locker.IsValid())
+    {
+      locker.GetController().SetSceneToCanvasTransform(originalSceneToCanvas_);
+    }
   }
 }
--- a/OrthancStone/Sources/Scene2D/PanSceneTracker.h	Fri Oct 28 07:47:55 2022 +0200
+++ b/OrthancStone/Sources/Scene2D/PanSceneTracker.h	Wed Nov 02 15:14:56 2022 +0100
@@ -29,17 +29,19 @@
 {
   class PanSceneTracker : public OneGesturePointerTracker
   {
+  private:
+    boost::weak_ptr<IViewport> viewport_;
+    ScenePoint2D               pivot_;
+    AffineTransform2D          originalSceneToCanvas_;
+    AffineTransform2D          originalCanvasToScene_;
+
   public:
     PanSceneTracker(boost::weak_ptr<IViewport> viewport,
                     const PointerEvent& event);
 
-    virtual void PointerMove(const PointerEvent& event) ORTHANC_OVERRIDE;
+    virtual void PointerMove(const PointerEvent& event,
+                             const Scene2D& scene) ORTHANC_OVERRIDE;
     
-    virtual void Cancel() ORTHANC_OVERRIDE;
-
-  private:
-    ScenePoint2D           pivot_;
-    AffineTransform2D      originalSceneToCanvas_;
-    AffineTransform2D      originalCanvasToScene_;
+    virtual void Cancel(const Scene2D& scene) ORTHANC_OVERRIDE;
   };
 }
--- a/OrthancStone/Sources/Scene2D/PolylineSceneLayer.cpp	Fri Oct 28 07:47:55 2022 +0200
+++ b/OrthancStone/Sources/Scene2D/PolylineSceneLayer.cpp	Wed Nov 02 15:14:56 2022 +0100
@@ -25,6 +25,10 @@
 
 #include <OrthancException.h>
 
+#include <boost/math/constants/constants.hpp>
+
+static const double PI = boost::math::constants::pi<double>();
+
 namespace OrthancStone
 {
   void PolylineSceneLayer::Copy(const PolylineSceneLayer& other)
@@ -107,4 +111,75 @@
       }
     }
   }
+
+
+  void PolylineSceneLayer::AddArc(double centerX,
+                                  double centerY,
+                                  double radiusX,
+                                  double radiusY,
+                                  double startAngle,
+                                  double endAngle,
+                                  Color color,
+                                  unsigned int countSegments)
+  {
+    assert(countSegments != 0);
+
+    if (endAngle >= startAngle)
+    {
+      double increment = (endAngle - startAngle) / static_cast<double>(countSegments - 1);
+
+      Chain chain;
+      chain.resize(countSegments);
+        
+      double theta = startAngle;
+      for (unsigned int i = 0; i < countSegments; i++)
+      {
+        chain[i] = ScenePoint2D(centerX + radiusX * cos(theta),
+                                centerY + radiusY * sin(theta));
+        theta += increment;
+      }
+        
+      AddChain(chain, false, color);
+    }
+  }
+
+
+  void PolylineSceneLayer::AddCircle(double centerX,
+                                     double centerY,
+                                     double radius,
+                                     Color color,
+                                     unsigned int countSegments)
+  {
+    AddArc(centerX, centerY, radius, radius, 0, 2.0 * PI, color, countSegments);
+  }
+  
+
+  void PolylineSceneLayer::AddRectangle(double x1,
+                                        double y1,
+                                        double x2,
+                                        double y2,
+                                        Color color)
+  {
+    Chain chain;
+    chain.resize(4);
+    chain[0] = ScenePoint2D(x1, y1);
+    chain[1] = ScenePoint2D(x2, y1);
+    chain[2] = ScenePoint2D(x2, y2);
+    chain[3] = ScenePoint2D(x1, y2);
+    AddChain(chain, true, color);
+  }
+
+
+  void PolylineSceneLayer::AddSegment(double x1,
+                                      double y1,
+                                      double x2,
+                                      double y2,
+                                      Color color)
+  {
+    Chain chain;
+    chain.resize(2);
+    chain[0] = ScenePoint2D(x1, y1);
+    chain[1] = ScenePoint2D(x2, y2);
+    AddChain(chain, false, color);
+  }
 }
--- a/OrthancStone/Sources/Scene2D/PolylineSceneLayer.h	Fri Oct 28 07:47:55 2022 +0200
+++ b/OrthancStone/Sources/Scene2D/PolylineSceneLayer.h	Wed Nov 02 15:14:56 2022 +0100
@@ -121,5 +121,58 @@
     }
 
     virtual void GetBoundingBox(Extent2D& target) const ORTHANC_OVERRIDE;
+
+    void AddArc(double centerX,
+                double centerY,
+                double radiusX,
+                double radiusY,
+                double startAngle,
+                double endAngle,
+                Color color,
+                unsigned int countSegments);
+    
+    void AddCircle(double centerX,
+                   double centerY,
+                   double radius,
+                   Color color,
+                   unsigned int countSegments);
+
+    void AddRectangle(double x1,
+                      double y1,
+                      double x2,
+                      double y2,
+                      Color color);
+
+    void AddSegment(double x1,
+                    double y1,
+                    double x2,
+                    double y2,
+                    Color color);
+
+    void AddArc(const ScenePoint2D& center,
+                double radiusX,
+                double radiusY,
+                double startAngle,
+                double endAngle,
+                Color color,
+                unsigned int countSegments)
+    {
+      AddArc(center.GetX(), center.GetY(), radiusX, radiusY, startAngle, endAngle, color, countSegments);
+    }
+    
+    void AddCircle(const ScenePoint2D& center,
+                   double radius,
+                   Color color,
+                   unsigned int countSegments)
+    {
+      AddCircle(center.GetX(), center.GetY(), radius, color, countSegments);
+    }
+
+    void AddSegment(const ScenePoint2D& p1,
+                    const ScenePoint2D& p2,
+                    Color color)
+    {
+      AddSegment(p1.GetX(), p1.GetY(), p2.GetX(), p2.GetY(), color);
+    }
   };
 }
--- a/OrthancStone/Sources/Scene2D/RotateSceneTracker.cpp	Fri Oct 28 07:47:55 2022 +0200
+++ b/OrthancStone/Sources/Scene2D/RotateSceneTracker.cpp	Wed Nov 02 15:14:56 2022 +0100
@@ -21,57 +21,73 @@
  **/
 
 #include "RotateSceneTracker.h"
+
 #include "../Scene2DViewport/ViewportController.h"
+#include "../Viewport/ViewportLocker.h"
 
 namespace OrthancStone
 {
   RotateSceneTracker::RotateSceneTracker(boost::weak_ptr<IViewport> viewport,
                                          const PointerEvent& event) :
-    OneGesturePointerTracker(viewport),
+    viewport_(viewport),
     click_(event.GetMainPosition()),
-    aligner_(viewport, click_),
     referenceAngle_(0),
     isFirst_(true)
   {
-    std::unique_ptr<IViewport::ILock> lock(GetViewportLock());
-    originalSceneToCanvas_ = lock->GetController().GetSceneToCanvasTransform();
-  }
-
-  
-  void RotateSceneTracker::PointerMove(const PointerEvent& event)
-  {
-    ScenePoint2D p = event.GetMainPosition();
-    double dx = p.GetX() - click_.GetX();
-    double dy = p.GetY() - click_.GetY();
-
-    if (std::abs(dx) > 5.0 ||
-        std::abs(dy) > 5.0)
+    ViewportLocker locker(viewport_);
+    
+    if (locker.IsValid())
     {
-      double a = atan2(dy, dx);
-
-      if (isFirst_)
-      {
-        referenceAngle_ = a;
-        isFirst_ = false;
-      }
-
-      std::unique_ptr<IViewport::ILock> lock(GetViewportLock());
-
-      lock->GetController().SetSceneToCanvasTransform(
-        AffineTransform2D::Combine(
-          AffineTransform2D::CreateRotation(a - referenceAngle_),
-          originalSceneToCanvas_));
-      aligner_.Apply();
-      lock->Invalidate();
+      aligner_.reset(new Internals::FixedPointAligner(locker.GetController(), click_));
+      originalSceneToCanvas_ = locker.GetController().GetSceneToCanvasTransform();
     }
   }
 
   
-  void RotateSceneTracker::Cancel()
+  void RotateSceneTracker::PointerMove(const PointerEvent& event,
+                                       const Scene2D& scene)
   {
-    // See remark above
-    std::unique_ptr<IViewport::ILock> lock(GetViewportLock());
-    lock->GetController().SetSceneToCanvasTransform(originalSceneToCanvas_);
-    lock->Invalidate();
+    if (aligner_.get() != NULL)
+    {
+      ScenePoint2D p = event.GetMainPosition();
+      double dx = p.GetX() - click_.GetX();
+      double dy = p.GetY() - click_.GetY();
+
+      if (std::abs(dx) > 5.0 ||
+          std::abs(dy) > 5.0)
+      {
+        double a = atan2(dy, dx);
+
+        if (isFirst_)
+        {
+          referenceAngle_ = a;
+          isFirst_ = false;
+        }
+
+        ViewportLocker locker(viewport_);
+    
+        if (locker.IsValid())
+        {
+          locker.GetController().SetSceneToCanvasTransform(
+            AffineTransform2D::Combine(
+              AffineTransform2D::CreateRotation(a - referenceAngle_),
+              originalSceneToCanvas_));
+          aligner_->Apply(locker.GetController());
+          locker.Invalidate();
+        }
+      }
+    }
+  }
+
+  
+  void RotateSceneTracker::Cancel(const Scene2D& scene)
+  {
+    ViewportLocker locker(viewport_);
+    
+    if (locker.IsValid())
+    {
+      locker.GetController().SetSceneToCanvasTransform(originalSceneToCanvas_);
+      locker.Invalidate();
+    }
   }
 }
--- a/OrthancStone/Sources/Scene2D/RotateSceneTracker.h	Fri Oct 28 07:47:55 2022 +0200
+++ b/OrthancStone/Sources/Scene2D/RotateSceneTracker.h	Wed Nov 02 15:14:56 2022 +0100
@@ -32,18 +32,21 @@
   class RotateSceneTracker : public OneGesturePointerTracker
   {
   private:
+    boost::weak_ptr<IViewport>   viewport_;
     ScenePoint2D                 click_;
-    Internals::FixedPointAligner aligner_;
     double                       referenceAngle_;
     bool                         isFirst_;
     AffineTransform2D            originalSceneToCanvas_;
 
+    std::unique_ptr<Internals::FixedPointAligner>  aligner_;
+    
   public:
     RotateSceneTracker(boost::weak_ptr<IViewport> viewport,
                        const PointerEvent& event);
 
-    virtual void PointerMove(const PointerEvent& event) ORTHANC_OVERRIDE;
+    virtual void PointerMove(const PointerEvent& event,
+                             const Scene2D& scene) ORTHANC_OVERRIDE;
     
-    virtual void Cancel() ORTHANC_OVERRIDE;
+    virtual void Cancel(const Scene2D& scene) ORTHANC_OVERRIDE;
   };
 }
--- a/OrthancStone/Sources/Scene2D/Scene2D.cpp	Fri Oct 28 07:47:55 2022 +0200
+++ b/OrthancStone/Sources/Scene2D/Scene2D.cpp	Wed Nov 02 15:14:56 2022 +0100
@@ -241,17 +241,34 @@
     }
   }
 
-  void Scene2D::FitContent(unsigned int canvasWidth,
+  
+  static void AddTransformedPoint(Extent2D& extent,
+                                  const AffineTransform2D& forcedTransform,
+                                  double x,
+                                  double y)
+  {
+    forcedTransform.Apply(x, y);
+    extent.AddPoint(x, y);
+  }
+
+  
+  void Scene2D::FitContent(const AffineTransform2D& forcedTransform,
+                           unsigned int canvasWidth,
                            unsigned int canvasHeight)
   {
     Extent2D extent;
-
     GetBoundingBox(extent);
 
     if (!extent.IsEmpty())
     {
-      double zoomX = static_cast<double>(canvasWidth) / extent.GetWidth();
-      double zoomY = static_cast<double>(canvasHeight) / extent.GetHeight();
+      Extent2D extent2;
+      AddTransformedPoint(extent2, forcedTransform, extent.GetX1(), extent.GetY1());
+      AddTransformedPoint(extent2, forcedTransform, extent.GetX1(), extent.GetY2());
+      AddTransformedPoint(extent2, forcedTransform, extent.GetX2(), extent.GetY2());
+      AddTransformedPoint(extent2, forcedTransform, extent.GetX2(), extent.GetY1());
+
+      double zoomX = static_cast<double>(canvasWidth) / extent2.GetWidth();
+      double zoomY = static_cast<double>(canvasHeight) / extent2.GetHeight();
 
       double zoom = std::min(zoomX, zoomY);
       if (LinearAlgebra::IsCloseToZero(zoom))
@@ -259,8 +276,8 @@
         zoom = 1;
       }
 
-      double panX = extent.GetCenterX();
-      double panY = extent.GetCenterY();
+      double panX = extent2.GetCenterX();
+      double panY = extent2.GetCenterY();
 
       // Bring the center of the scene to (0,0)
       AffineTransform2D t1 = AffineTransform2D::CreateOffset(-panX, -panY);
@@ -268,7 +285,45 @@
       // Scale the scene
       AffineTransform2D t2 = AffineTransform2D::CreateScaling(zoom, zoom);
 
-      SetSceneToCanvasTransform(AffineTransform2D::Combine(t2, t1));
+      SetSceneToCanvasTransform(AffineTransform2D::Combine(t2, t1, forcedTransform));
     }
   }
+
+
+  void Scene2D::FitContent(unsigned int canvasWidth,
+                           unsigned int canvasHeight)
+  {
+    FitContent(AffineTransform2D() /* identity transform */, canvasWidth, canvasHeight);
+  }
+
+
+  void Scene2D::RotateViewport(double angle,
+                               unsigned int canvasWidth,
+                               unsigned int canvasHeight)
+  {
+    AffineTransform2D transform = AffineTransform2D::Combine(
+      AffineTransform2D::CreateRotation(angle),
+      GetSceneToCanvasTransform());
+    FitContent(transform, canvasWidth, canvasHeight);
+  }
+
+
+  void Scene2D::FlipViewportX(unsigned int canvasWidth,
+                              unsigned int canvasHeight)
+  {
+    AffineTransform2D transform = AffineTransform2D::Combine(
+      AffineTransform2D::CreateFlipX(),
+      GetSceneToCanvasTransform());
+    FitContent(transform, canvasWidth, canvasHeight);
+  }
+
+
+  void Scene2D::FlipViewportY(unsigned int canvasWidth,
+                              unsigned int canvasHeight)
+  {
+    AffineTransform2D transform = AffineTransform2D::Combine(
+      AffineTransform2D::CreateFlipY(),
+      GetSceneToCanvasTransform());
+    FitContent(transform, canvasWidth, canvasHeight);
+  }
 }
--- a/OrthancStone/Sources/Scene2D/Scene2D.h	Fri Oct 28 07:47:55 2022 +0200
+++ b/OrthancStone/Sources/Scene2D/Scene2D.h	Wed Nov 02 15:14:56 2022 +0100
@@ -60,6 +60,10 @@
 
     Scene2D(const Scene2D& other);
     
+    void FitContent(const AffineTransform2D& forcedTransform,
+                    unsigned int canvasWidth,
+                    unsigned int canvasHeight);
+
   public:
     Scene2D() : layerCounter_(0)
     {
@@ -118,5 +122,15 @@
                     unsigned int canvasHeight);
 
     void GetBoundingBox(Extent2D& target) const;
+
+    void RotateViewport(double angle,
+                        unsigned int canvasWidth,
+                        unsigned int canvasHeight);
+
+    void FlipViewportX(unsigned int canvasWidth,
+                       unsigned int canvasHeight);
+
+    void FlipViewportY(unsigned int canvasWidth,
+                       unsigned int canvasHeight);
   };
 }
--- a/OrthancStone/Sources/Scene2D/ZoomSceneTracker.cpp	Fri Oct 28 07:47:55 2022 +0200
+++ b/OrthancStone/Sources/Scene2D/ZoomSceneTracker.cpp	Wed Nov 02 15:14:56 2022 +0100
@@ -22,38 +22,39 @@
 
 
 #include "ZoomSceneTracker.h"
+
 #include "../Scene2DViewport/ViewportController.h"
+#include "../Viewport/ViewportLocker.h"
 
 namespace OrthancStone
 {
   ZoomSceneTracker::ZoomSceneTracker(boost::weak_ptr<IViewport> viewport,
                                      const PointerEvent& event,
-                                     unsigned int canvasHeight)
-    : OneGesturePointerTracker(viewport)
-    , clickY_(event.GetMainPosition().GetY())
-    , aligner_(viewport, event.GetMainPosition())
-  {
+                                     unsigned int canvasHeight) :
+    viewport_(viewport),
+    clickY_(event.GetMainPosition().GetY())
+  {    
+    ViewportLocker locker(viewport_);
     
-    std::unique_ptr<IViewport::ILock> lock(GetViewportLock());
-    originalSceneToCanvas_ = lock->GetController().GetSceneToCanvasTransform();
+    if (locker.IsValid())
+    {
+      originalSceneToCanvas_ = locker.GetController().GetSceneToCanvasTransform();
 
-    if (canvasHeight <= 3)
-    {
-      active_ = false;
-    }
-    else
-    {
-      normalization_ = 1.0 / static_cast<double>(canvasHeight - 1);
-      active_ = true;
+      if (canvasHeight > 3)
+      {
+        normalization_ = 1.0 / static_cast<double>(canvasHeight - 1);
+        aligner_.reset(new Internals::FixedPointAligner(locker.GetController(), event.GetMainPosition()));
+      }
     }
   }
   
-  void ZoomSceneTracker::PointerMove(const PointerEvent& event)
+  void ZoomSceneTracker::PointerMove(const PointerEvent& event,
+                                     const Scene2D& scene)
   {
     static const double MIN_ZOOM = -4;
     static const double MAX_ZOOM = 4;
       
-    if (active_)
+    if (aligner_.get() != NULL)
     {
       double y = event.GetMainPosition().GetY();
       
@@ -78,20 +79,28 @@
 
       double zoom = pow(2.0, z);
 
-      std::unique_ptr<IViewport::ILock> lock(GetViewportLock());
-      lock->GetController().SetSceneToCanvasTransform(
-        AffineTransform2D::Combine(
-          AffineTransform2D::CreateScaling(zoom, zoom),
-          originalSceneToCanvas_));
-      aligner_.Apply();
-      lock->Invalidate();
+      ViewportLocker locker(viewport_);
+    
+      if (locker.IsValid())
+      {
+        locker.GetController().SetSceneToCanvasTransform(
+          AffineTransform2D::Combine(
+            AffineTransform2D::CreateScaling(zoom, zoom),
+            originalSceneToCanvas_));
+        aligner_->Apply(locker.GetController());
+        locker.Invalidate();
+      }
     }
   }
 
-  void ZoomSceneTracker::Cancel()
+  void ZoomSceneTracker::Cancel(const Scene2D& scene)
   {
-    std::unique_ptr<IViewport::ILock> lock(GetViewportLock());
-    lock->GetController().SetSceneToCanvasTransform(originalSceneToCanvas_);
-    lock->Invalidate();
+    ViewportLocker locker(viewport_);
+    
+    if (locker.IsValid())
+    {
+      locker.GetController().SetSceneToCanvasTransform(originalSceneToCanvas_);
+      locker.Invalidate();
+    }
   }
 }
--- a/OrthancStone/Sources/Scene2D/ZoomSceneTracker.h	Fri Oct 28 07:47:55 2022 +0200
+++ b/OrthancStone/Sources/Scene2D/ZoomSceneTracker.h	Wed Nov 02 15:14:56 2022 +0100
@@ -34,20 +34,22 @@
 {
   class ZoomSceneTracker : public OneGesturePointerTracker
   {
+  private:
+    boost::weak_ptr<IViewport>    viewport_;
+    double                        clickY_;
+    double                        normalization_;
+    AffineTransform2D             originalSceneToCanvas_;
+
+    std::unique_ptr<Internals::FixedPointAligner>  aligner_;
+    
   public:
     ZoomSceneTracker(boost::weak_ptr<IViewport> viewport,
                      const PointerEvent& event,
                      unsigned int canvasHeight);
 
-    virtual void PointerMove(const PointerEvent& event) ORTHANC_OVERRIDE;
-    virtual void Cancel() ORTHANC_OVERRIDE;
-  
-  private:
-    double                        clickY_;
-    bool                          active_;
-    double                        normalization_;
-    Internals::FixedPointAligner  aligner_;
-    AffineTransform2D             originalSceneToCanvas_;
-
+    virtual void PointerMove(const PointerEvent& event,
+                             const Scene2D& scene) ORTHANC_OVERRIDE;
+    
+    virtual void Cancel(const Scene2D& scene) ORTHANC_OVERRIDE;
   };
 }
--- a/OrthancStone/Sources/Scene2DViewport/CreateAngleMeasureTracker.cpp	Fri Oct 28 07:47:55 2022 +0200
+++ b/OrthancStone/Sources/Scene2DViewport/CreateAngleMeasureTracker.cpp	Wed Nov 02 15:14:56 2022 +0100
@@ -47,7 +47,8 @@
   {
   }
 
-  void CreateAngleMeasureTracker::PointerMove(const PointerEvent& event)
+  void CreateAngleMeasureTracker::PointerMove(const PointerEvent& event,
+                                              const Scene2D& scene)
   {
     if (!alive_)
     {
@@ -83,7 +84,8 @@
     }
   }
 
-  void CreateAngleMeasureTracker::PointerUp(const PointerEvent& e)
+  void CreateAngleMeasureTracker::PointerUp(const PointerEvent& e,
+                                            const Scene2D& scene)
   {
     // TODO: the current app does not prevent multiple PointerDown AND
     // PointerUp to be sent to the tracker.
@@ -108,7 +110,8 @@
     }
   }
 
-  void CreateAngleMeasureTracker::PointerDown(const PointerEvent& e)
+  void CreateAngleMeasureTracker::PointerDown(const PointerEvent& e,
+                                              const Scene2D& scene)
   {
     switch (state_)
     {
--- a/OrthancStone/Sources/Scene2DViewport/CreateAngleMeasureTracker.h	Fri Oct 28 07:47:55 2022 +0200
+++ b/OrthancStone/Sources/Scene2DViewport/CreateAngleMeasureTracker.h	Wed Nov 02 15:14:56 2022 +0100
@@ -45,9 +45,14 @@
 
     ~CreateAngleMeasureTracker();
 
-    virtual void PointerMove(const PointerEvent& e) ORTHANC_OVERRIDE;
-    virtual void PointerUp(const PointerEvent& e) ORTHANC_OVERRIDE;
-    virtual void PointerDown(const PointerEvent& e) ORTHANC_OVERRIDE;
+    virtual void PointerMove(const PointerEvent& e,
+                             const Scene2D& scene) ORTHANC_OVERRIDE;
+    
+    virtual void PointerUp(const PointerEvent& e,
+                           const Scene2D& scene) ORTHANC_OVERRIDE;
+    
+    virtual void PointerDown(const PointerEvent& e,
+                             const Scene2D& scene) ORTHANC_OVERRIDE;
 
   private:
     boost::shared_ptr<CreateAngleMeasureCommand> GetCommand();
--- a/OrthancStone/Sources/Scene2DViewport/CreateLineMeasureTracker.cpp	Fri Oct 28 07:47:55 2022 +0200
+++ b/OrthancStone/Sources/Scene2DViewport/CreateLineMeasureTracker.cpp	Wed Nov 02 15:14:56 2022 +0100
@@ -48,7 +48,8 @@
 
   }
 
-  void CreateLineMeasureTracker::PointerMove(const PointerEvent& event)
+  void CreateLineMeasureTracker::PointerMove(const PointerEvent& event,
+                                             const Scene2D& scene)
   {
     if (!alive_)
     {
@@ -73,7 +74,8 @@
     GetCommand()->SetEnd(scenePos);
   }
 
-  void CreateLineMeasureTracker::PointerUp(const PointerEvent& e)
+  void CreateLineMeasureTracker::PointerUp(const PointerEvent& e,
+                                           const Scene2D& scene)
   {
     // TODO: the current app does not prevent multiple PointerDown AND
     // PointerUp to be sent to the tracker.
@@ -83,7 +85,8 @@
     alive_ = false;
   }
 
-  void CreateLineMeasureTracker::PointerDown(const PointerEvent& e)
+  void CreateLineMeasureTracker::PointerDown(const PointerEvent& e,
+                                             const Scene2D& scene)
   {
     LOG(WARNING) << "Additional touches (fingers, pen, mouse buttons...) "
       "are ignored when the line measure creation tracker is active";
--- a/OrthancStone/Sources/Scene2DViewport/CreateLineMeasureTracker.h	Fri Oct 28 07:47:55 2022 +0200
+++ b/OrthancStone/Sources/Scene2DViewport/CreateLineMeasureTracker.h	Wed Nov 02 15:14:56 2022 +0100
@@ -45,9 +45,12 @@
 
     ~CreateLineMeasureTracker();
 
-    virtual void PointerMove(const PointerEvent& e) ORTHANC_OVERRIDE;
-    virtual void PointerUp(const PointerEvent& e) ORTHANC_OVERRIDE;
-    virtual void PointerDown(const PointerEvent& e) ORTHANC_OVERRIDE;
+    virtual void PointerMove(const PointerEvent& e,
+                             const Scene2D& scene) ORTHANC_OVERRIDE;
+    virtual void PointerUp(const PointerEvent& e,
+                             const Scene2D& scene) ORTHANC_OVERRIDE;
+    virtual void PointerDown(const PointerEvent& e,
+                             const Scene2D& scene) ORTHANC_OVERRIDE;
 
   private:
     boost::shared_ptr<CreateLineMeasureCommand> GetCommand();
--- a/OrthancStone/Sources/Scene2DViewport/EditAngleMeasureTracker.cpp	Fri Oct 28 07:47:55 2022 +0200
+++ b/OrthancStone/Sources/Scene2DViewport/EditAngleMeasureTracker.cpp	Wed Nov 02 15:14:56 2022 +0100
@@ -50,15 +50,16 @@
 
   }
 
-  void EditAngleMeasureTracker::PointerMove(const PointerEvent& e)
+  void EditAngleMeasureTracker::PointerMove(const PointerEvent& e,
+                                            const Scene2D& scene)
   {
     std::unique_ptr<IViewport::ILock> lock(GetViewportLock());
     
     ViewportController& controller = lock->GetController();
-    const Scene2D& scene = controller.GetScene();
+    const Scene2D& scene2 = controller.GetScene();
 
     ScenePoint2D scenePos = e.GetMainPosition().Apply(
-      scene.GetCanvasToSceneTransform());
+      scene2.GetCanvasToSceneTransform());
 
     ScenePoint2D delta = scenePos - GetOriginalClickPosition();
 
@@ -106,12 +107,14 @@
     }
   }
 
-  void EditAngleMeasureTracker::PointerUp(const PointerEvent& e)
+  void EditAngleMeasureTracker::PointerUp(const PointerEvent& e,
+                                          const Scene2D& scene)
   {
     alive_ = false;
   }
 
-  void EditAngleMeasureTracker::PointerDown(const PointerEvent& e)
+  void EditAngleMeasureTracker::PointerDown(const PointerEvent& e,
+                                            const Scene2D& scene)
   {
     LOG(WARNING) << "Additional touches (fingers, pen, mouse buttons...) "
       "are ignored when the edit angle tracker is active";
--- a/OrthancStone/Sources/Scene2DViewport/EditAngleMeasureTracker.h	Fri Oct 28 07:47:55 2022 +0200
+++ b/OrthancStone/Sources/Scene2DViewport/EditAngleMeasureTracker.h	Wed Nov 02 15:14:56 2022 +0100
@@ -45,9 +45,14 @@
 
     ~EditAngleMeasureTracker();
 
-    virtual void PointerMove(const PointerEvent& e) ORTHANC_OVERRIDE;
-    virtual void PointerUp(const PointerEvent& e) ORTHANC_OVERRIDE;
-    virtual void PointerDown(const PointerEvent& e) ORTHANC_OVERRIDE;
+    virtual void PointerMove(const PointerEvent& e,
+                             const Scene2D& scene) ORTHANC_OVERRIDE;
+    
+    virtual void PointerUp(const PointerEvent& e,
+                           const Scene2D& scene) ORTHANC_OVERRIDE;
+    
+    virtual void PointerDown(const PointerEvent& e,
+                             const Scene2D& scene) ORTHANC_OVERRIDE;
 
   private:
     AngleMeasureTool::AngleHighlightArea modifiedZone_;
--- a/OrthancStone/Sources/Scene2DViewport/EditLineMeasureTracker.cpp	Fri Oct 28 07:47:55 2022 +0200
+++ b/OrthancStone/Sources/Scene2DViewport/EditLineMeasureTracker.cpp	Wed Nov 02 15:14:56 2022 +0100
@@ -46,14 +46,15 @@
     command_.reset(new EditLineMeasureCommand(measureTool, viewport));
   }
 
-  void EditLineMeasureTracker::PointerMove(const PointerEvent& e)
+  void EditLineMeasureTracker::PointerMove(const PointerEvent& e,
+                                           const Scene2D& scene)
   {
     std::unique_ptr<IViewport::ILock> lock(GetViewportLock());
     ViewportController& controller = lock->GetController();
-    const Scene2D& scene = controller.GetScene();
+    const Scene2D& scene2 = controller.GetScene();
 
     ScenePoint2D scenePos = e.GetMainPosition().Apply(
-      scene.GetCanvasToSceneTransform());
+      scene2.GetCanvasToSceneTransform());
 
     ScenePoint2D delta = scenePos - GetOriginalClickPosition();
 
@@ -91,12 +92,14 @@
     }
   }
 
-  void EditLineMeasureTracker::PointerUp(const PointerEvent& e)
+  void EditLineMeasureTracker::PointerUp(const PointerEvent& e,
+                                         const Scene2D& scene)
   {
     alive_ = false;
   }
 
-  void EditLineMeasureTracker::PointerDown(const PointerEvent& e)
+  void EditLineMeasureTracker::PointerDown(const PointerEvent& e,
+                                           const Scene2D& scene)
   {
     LOG(WARNING) << "Additional touches (fingers, pen, mouse buttons...) "
       "are ignored when the edit line tracker is active";
--- a/OrthancStone/Sources/Scene2DViewport/EditLineMeasureTracker.h	Fri Oct 28 07:47:55 2022 +0200
+++ b/OrthancStone/Sources/Scene2DViewport/EditLineMeasureTracker.h	Wed Nov 02 15:14:56 2022 +0100
@@ -48,10 +48,13 @@
       boost::weak_ptr<IViewport>      viewport,
       const PointerEvent&             e);
 
-    virtual void PointerMove(const PointerEvent& e) ORTHANC_OVERRIDE;
+    virtual void PointerMove(const PointerEvent& e,
+                             const Scene2D& scene) ORTHANC_OVERRIDE;
     
-    virtual void PointerUp(const PointerEvent& e) ORTHANC_OVERRIDE;
+    virtual void PointerUp(const PointerEvent& e,
+                           const Scene2D& scene) ORTHANC_OVERRIDE;
     
-    virtual void PointerDown(const PointerEvent& e) ORTHANC_OVERRIDE;
+    virtual void PointerDown(const PointerEvent& e,
+                             const Scene2D& scene) ORTHANC_OVERRIDE;
   };
 }
--- a/OrthancStone/Sources/Scene2DViewport/IFlexiblePointerTracker.h	Fri Oct 28 07:47:55 2022 +0200
+++ b/OrthancStone/Sources/Scene2DViewport/IFlexiblePointerTracker.h	Wed Nov 02 15:14:56 2022 +0100
@@ -45,13 +45,15 @@
     /**
     This method will be repeatedly called during user interaction
     */
-    virtual void PointerMove(const PointerEvent& event) = 0;
+    virtual void PointerMove(const PointerEvent& event,
+                             const Scene2D& scene) = 0;
 
     /**
     This method will be called when a touch/pointer is removed (mouse up, 
     pen lift, finger removed...)
     */
-    virtual void PointerUp(const PointerEvent& event) = 0;
+    virtual void PointerUp(const PointerEvent& event,
+                           const Scene2D& scene) = 0;
 
     /**
     This method will be called when a touch/pointer is added (mouse down, 
@@ -63,7 +65,8 @@
     Thus, if you count the PointerDown vs PointerUp, there will be an extra
     PointerUp.
     */
-    virtual void PointerDown(const PointerEvent& event) = 0;
+    virtual void PointerDown(const PointerEvent& event,
+                             const Scene2D& scene) = 0;
 
     /**
     This method will be repeatedly called by the tracker owner (for instance,
@@ -77,6 +80,6 @@
     its changes to the underlying model. If the model has been modified during
     tracker lifetime, it must be restored to its initial value
     */
-    virtual void Cancel() = 0;
+    virtual void Cancel(const Scene2D& scene) = 0;
   };
 }
--- a/OrthancStone/Sources/Scene2DViewport/MeasureTrackers.cpp	Fri Oct 28 07:47:55 2022 +0200
+++ b/OrthancStone/Sources/Scene2DViewport/MeasureTrackers.cpp	Wed Nov 02 15:14:56 2022 +0100
@@ -42,7 +42,7 @@
       return NULL;
   }
 
-  void CreateMeasureTracker::Cancel()
+  void CreateMeasureTracker::Cancel(const Scene2D& scene)
   {
     commitResult_ = false;
     alive_ = false;
@@ -93,7 +93,7 @@
       return NULL;
   }
 
-  void EditMeasureTracker::Cancel()
+  void EditMeasureTracker::Cancel(const Scene2D& scene)
   {
     commitResult_ = false;
     alive_ = false;
--- a/OrthancStone/Sources/Scene2DViewport/MeasureTrackers.h	Fri Oct 28 07:47:55 2022 +0200
+++ b/OrthancStone/Sources/Scene2DViewport/MeasureTrackers.h	Wed Nov 02 15:14:56 2022 +0100
@@ -57,7 +57,7 @@
     virtual ~CreateMeasureTracker();
     
   public:
-    virtual void Cancel() ORTHANC_OVERRIDE;
+    virtual void Cancel(const Scene2D& scene) ORTHANC_OVERRIDE;
     
     virtual bool IsAlive() const ORTHANC_OVERRIDE;
   };
@@ -91,7 +91,7 @@
     }
 
   public:
-    virtual void Cancel() ORTHANC_OVERRIDE;
+    virtual void Cancel(const Scene2D& scene) ORTHANC_OVERRIDE;
     
     virtual bool IsAlive() const ORTHANC_OVERRIDE;
   };
--- a/OrthancStone/Sources/Scene2DViewport/OneGesturePointerTracker.cpp	Fri Oct 28 07:47:55 2022 +0200
+++ b/OrthancStone/Sources/Scene2DViewport/OneGesturePointerTracker.cpp	Wed Nov 02 15:14:56 2022 +0100
@@ -29,23 +29,14 @@
 
 namespace OrthancStone
 {
-  OneGesturePointerTracker::OneGesturePointerTracker(boost::weak_ptr<IViewport> viewport) :
+  OneGesturePointerTracker::OneGesturePointerTracker() :
     alive_(true),
-    currentTouchCount_(1),
-    viewport_(viewport)
+    currentTouchCount_(1)
   {
   }
 
-  IViewport::ILock* OneGesturePointerTracker::GetViewportLock()
-  {
-    boost::shared_ptr<IViewport> viewport = viewport_.lock();
-    if (viewport)
-      return viewport->Lock();
-    else
-      return NULL;
-  }
-
-  void OneGesturePointerTracker::PointerUp(const PointerEvent& event)
+  void OneGesturePointerTracker::PointerUp(const PointerEvent& event,
+                                           const Scene2D& scene)
   {
     // pointer up is only called for the LAST up event in case of a multi-touch
     // gesture
@@ -59,7 +50,8 @@
     }
   }
 
-  void OneGesturePointerTracker::PointerDown(const PointerEvent& event)
+  void OneGesturePointerTracker::PointerDown(const PointerEvent& event,
+                                             const Scene2D& scene)
   {
     // additional touches are not taken into account but we need to count 
     // the number of active touches
--- a/OrthancStone/Sources/Scene2DViewport/OneGesturePointerTracker.h	Fri Oct 28 07:47:55 2022 +0200
+++ b/OrthancStone/Sources/Scene2DViewport/OneGesturePointerTracker.h	Wed Nov 02 15:14:56 2022 +0100
@@ -24,8 +24,6 @@
 
 #include "IFlexiblePointerTracker.h"
 
-#include "../Viewport/IViewport.h"
-
 #include <Compatibility.h>  // For ORTHANC_OVERRIDE
 
 #include <boost/shared_ptr.hpp>
@@ -51,21 +49,14 @@
     bool  alive_;
     int   currentTouchCount_;
 
-  protected:
-    boost::weak_ptr<IViewport> viewport_;
-
-    /**
-    This will return a scoped lock to the viewport.
-    If the viewport does not exist anymore, then nullptr is returned.
-    */
-    IViewport::ILock* GetViewportLock();
-
   public:
-    explicit OneGesturePointerTracker(boost::weak_ptr<IViewport> viewport);
+    explicit OneGesturePointerTracker();
     
-    virtual void PointerUp(const PointerEvent& event) ORTHANC_OVERRIDE;
+    virtual void PointerUp(const PointerEvent& event,
+                           const Scene2D& scene) ORTHANC_OVERRIDE;
     
-    virtual void PointerDown(const PointerEvent& event) ORTHANC_OVERRIDE;
+    virtual void PointerDown(const PointerEvent& event,
+                             const Scene2D& scene) ORTHANC_OVERRIDE;
     
     virtual bool IsAlive() const ORTHANC_OVERRIDE;
   };
--- a/OrthancStone/Sources/Scene2DViewport/ViewportController.cpp	Fri Oct 28 07:47:55 2022 +0200
+++ b/OrthancStone/Sources/Scene2DViewport/ViewportController.cpp	Wed Nov 02 15:14:56 2022 +0100
@@ -222,7 +222,7 @@
     {
       // We are dealing with a multi-stage tracker (that is made of several 
       // interactions)
-      activeTracker_->PointerDown(event);
+      activeTracker_->PointerDown(event, GetScene());
 
       if (!activeTracker_->IsAlive())
       {
@@ -253,7 +253,7 @@
   {
     if (activeTracker_)
     {
-      activeTracker_->PointerMove(event);
+      activeTracker_->PointerMove(event, GetScene());
       return true;
     }
     else
@@ -266,7 +266,7 @@
   {
     if (activeTracker_)
     {
-      activeTracker_->PointerUp(event);
+      activeTracker_->PointerUp(event, GetScene());
 
       if (!activeTracker_->IsAlive())
       {
--- a/OrthancStone/Sources/StoneEnumerations.h	Fri Oct 28 07:47:55 2022 +0200
+++ b/OrthancStone/Sources/StoneEnumerations.h	Wed Nov 02 15:14:56 2022 +0100
@@ -156,6 +156,7 @@
     MouseAction_Zoom,
     MouseAction_Rotate,
     MouseAction_GrayscaleWindowing,
+    MouseAction_MagnifyingGlass,
     MouseAction_None
   };
 
--- a/OrthancStone/Sources/Toolbox/AffineTransform2D.cpp	Fri Oct 28 07:47:55 2022 +0200
+++ b/OrthancStone/Sources/Toolbox/AffineTransform2D.cpp	Wed Nov 02 15:14:56 2022 +0100
@@ -293,4 +293,22 @@
       return t;
     }
   }
+
+  
+  AffineTransform2D AffineTransform2D::CreateFlipX()
+  {
+    AffineTransform2D t;
+    t.matrix_(0, 0) = -1;
+    t.matrix_(1, 1) = 1;
+    return t;
+  }
+
+  
+  AffineTransform2D AffineTransform2D::CreateFlipY()
+  {
+    AffineTransform2D t;
+    t.matrix_(0, 0) = 1;
+    t.matrix_(1, 1) = -1;
+    return t;
+  }
 }
--- a/OrthancStone/Sources/Toolbox/AffineTransform2D.h	Fri Oct 28 07:47:55 2022 +0200
+++ b/OrthancStone/Sources/Toolbox/AffineTransform2D.h	Wed Nov 02 15:14:56 2022 +0100
@@ -36,7 +36,7 @@
     Matrix  matrix_;
 
   public:
-    AffineTransform2D();
+    AffineTransform2D();  // Create the identity transform
 
     // The matrix must be 3x3, without perspective effects
     explicit AffineTransform2D(const Matrix& m);
@@ -106,5 +106,9 @@
                                         bool flipY,
                                         unsigned int width,
                                         unsigned int height);
+
+    static AffineTransform2D CreateFlipX();
+
+    static AffineTransform2D CreateFlipY();
   };
 }
--- a/OrthancStone/Sources/Toolbox/CoordinateSystem3D.cpp	Fri Oct 28 07:47:55 2022 +0200
+++ b/OrthancStone/Sources/Toolbox/CoordinateSystem3D.cpp	Wed Nov 02 15:14:56 2022 +0100
@@ -398,9 +398,9 @@
     const char orientationY = v[1] < 0 ? 'A' : 'P';
     const char orientationZ = v[2] < 0 ? 'F' : 'H';
 
-    double absX = abs(v[0]);
-    double absY = abs(v[1]);
-    double absZ = abs(v[2]);
+    double absX = std::abs(v[0]);
+    double absY = std::abs(v[1]);
+    double absZ = std::abs(v[2]);
 
     std::string result;
 
--- a/OrthancStone/Sources/Toolbox/LinearAlgebra.cpp	Fri Oct 28 07:47:55 2022 +0200
+++ b/OrthancStone/Sources/Toolbox/LinearAlgebra.cpp	Wed Nov 02 15:14:56 2022 +0100
@@ -41,6 +41,59 @@
 {
   namespace LinearAlgebra
   {
+    void OnlineVarianceEstimator::AddSample(double value)
+    {
+      count_++;
+      sum_ += value;
+      sumOfSquares_ += value * value;
+    }
+
+
+    void OnlineVarianceEstimator::Clear()
+    {
+      count_ = 0;
+      sum_ = 0;
+      sumOfSquares_ = 0;
+    }
+      
+
+    double OnlineVarianceEstimator::GetMean() const
+    {
+      if (count_ == 0)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+      }
+      else
+      {
+        return sum_ / static_cast<double>(count_);
+      }
+    }
+      
+
+    double OnlineVarianceEstimator::GetVariance() const
+    {
+      if (count_ == 0)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+      }        
+      else if (count_ == 1)
+      {
+        return 0;
+      }
+      else
+      {
+        const double n = static_cast<double>(count_);
+        return (sumOfSquares_ * n - sum_ * sum_) / (n * (n - 1.0));
+      }
+    }
+
+      
+    double OnlineVarianceEstimator::GetStandardDeviation() const
+    {
+      return sqrt(GetVariance());
+    }
+    
+
     void Print(const Vector& v)
     {
       for (size_t i = 0; i < v.size(); i++)
--- a/OrthancStone/Sources/Toolbox/LinearAlgebra.h	Fri Oct 28 07:47:55 2022 +0200
+++ b/OrthancStone/Sources/Toolbox/LinearAlgebra.h	Wed Nov 02 15:14:56 2022 +0100
@@ -46,6 +46,35 @@
 
   namespace LinearAlgebra
   {
+    class OnlineVarianceEstimator
+    {
+    private:
+      unsigned int  count_;
+      double        sum_;
+      double        sumOfSquares_;
+
+    public:
+      OnlineVarianceEstimator()
+      {
+        Clear();
+      }
+
+      unsigned int GetCount() const
+      {
+        return count_;
+      }
+
+      void AddSample(double value);
+
+      void Clear();
+
+      double GetMean() const;  // Same as "mean()" in Matlab/Octave
+
+      double GetVariance() const;  // Same as "var()" in Matlab/Octave
+
+      double GetStandardDeviation() const;  // Same as "std()" in Matlab/Octave
+    };
+    
     void Print(const Vector& v);
 
     void Print(const Matrix& m);
@@ -301,5 +330,5 @@
     double ComputeMedian(std::vector<double>& v);
 
     float ComputeMedian(std::vector<float>& v);
-  };
+  }
 }
--- a/OrthancStone/Sources/Viewport/DefaultViewportInteractor.cpp	Fri Oct 28 07:47:55 2022 +0200
+++ b/OrthancStone/Sources/Viewport/DefaultViewportInteractor.cpp	Wed Nov 02 15:14:56 2022 +0100
@@ -23,6 +23,7 @@
 #include "DefaultViewportInteractor.h"
 
 #include "../Scene2D/GrayscaleWindowingSceneTracker.h"
+#include "../Scene2D/MagnifyingGlassTracker.h"
 #include "../Scene2D/PanSceneTracker.h"
 #include "../Scene2D/RotateSceneTracker.h"
 #include "../Scene2D/ZoomSceneTracker.h"
@@ -77,6 +78,9 @@
       case MouseAction_Zoom:
         return new ZoomSceneTracker(viewport, event, viewportHeight);
       
+      case MouseAction_MagnifyingGlass:
+        return new MagnifyingGlassTracker(viewport, event);
+
       default:
         throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
     }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancStone/Sources/Viewport/ViewportLocker.cpp	Wed Nov 02 15:14:56 2022 +0100
@@ -0,0 +1,51 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2022 Osimis S.A., Belgium
+ * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "ViewportLocker.h"
+
+#include <OrthancException.h>
+
+namespace OrthancStone
+{
+  IViewport::ILock& ViewportLocker::GetLock() const
+  {
+    if (lock2_.get() == NULL)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      return *lock2_;
+    }
+  }
+  
+
+  ViewportLocker::ViewportLocker(boost::weak_ptr<IViewport> viewport) :
+    lock1_(viewport.lock())
+  {
+    if (lock1_)
+    {
+      lock2_.reset(lock1_->Lock());
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancStone/Sources/Viewport/ViewportLocker.h	Wed Nov 02 15:14:56 2022 +0100
@@ -0,0 +1,61 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2022 Osimis S.A., Belgium
+ * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "IViewport.h"
+
+namespace OrthancStone
+{
+  class ViewportLocker : public boost::noncopyable
+  {
+  private:
+    boost::shared_ptr<IViewport>       lock1_;
+    std::unique_ptr<IViewport::ILock>  lock2_;
+
+    IViewport::ILock& GetLock() const;
+
+  public:
+    explicit ViewportLocker(IViewport& viewport) :
+      lock2_(viewport.Lock())
+    {
+    }
+
+    explicit ViewportLocker(boost::weak_ptr<IViewport> viewport);
+
+    bool IsValid() const
+    {
+      return (lock2_.get() != NULL);
+    }
+
+    ViewportController& GetController() const
+    {
+      return GetLock().GetController();
+    }
+
+    void Invalidate() const
+    {
+      return GetLock().Invalidate();
+    }
+  };
+}
--- a/OrthancStone/UnitTestsSources/GenericToolboxTests.cpp	Fri Oct 28 07:47:55 2022 +0200
+++ b/OrthancStone/UnitTestsSources/GenericToolboxTests.cpp	Wed Nov 02 15:14:56 2022 +0100
@@ -4499,3 +4499,28 @@
   ASSERT_DOUBLE_EQ(9093     , v[10]);
   ASSERT_DOUBLE_EQ(0        , v[11]);
 }
+
+
+TEST(LinearAlgebra, OnlineVarianceEstimator)
+{
+  OrthancStone::LinearAlgebra::OnlineVarianceEstimator e;
+  ASSERT_EQ(0u, e.GetCount());
+  ASSERT_THROW(e.GetMean(), Orthanc::OrthancException);
+  ASSERT_THROW(e.GetVariance(), Orthanc::OrthancException);
+  ASSERT_THROW(e.GetStandardDeviation(), Orthanc::OrthancException);
+
+  e.AddSample(42);
+  ASSERT_EQ(1u, e.GetCount());
+  ASSERT_DOUBLE_EQ(42.0, e.GetMean());
+  ASSERT_DOUBLE_EQ(0.0, e.GetVariance());
+  ASSERT_DOUBLE_EQ(0.0, e.GetStandardDeviation());
+
+  e.Clear();
+  e.AddSample(87.9);
+  e.AddSample(-82.4);
+  e.AddSample(17.3);
+  ASSERT_EQ(3u, e.GetCount());
+  ASSERT_DOUBLE_EQ(7.6, e.GetMean());
+  ASSERT_DOUBLE_EQ(7321.09, e.GetVariance());
+  ASSERT_DOUBLE_EQ(85.5633683301447, e.GetStandardDeviation());  
+}
--- a/TODO	Fri Oct 28 07:47:55 2022 +0200
+++ b/TODO	Wed Nov 02 15:14:56 2022 +0100
@@ -17,10 +17,6 @@
 * Configurable keyboard shortcuts. See Osimis Web viewer:
   https://bitbucket.org/osimis/osimis-webviewer-plugin/src/master/doc/default-configuration.json
 
-* Flip X/Y : Should change the affine transformation of the 2D scene,
-  not the texture. Flipping the texture makes the reference lines and
-  crosshair invalid.
-
 * Handle mobile gestures.
 
 * Display GSPS layers (Grayscale Softcopy Presentation State).
@@ -36,9 +32,10 @@
 
   https://groups.google.com/g/orthanc-users/c/_kDp_ieYTgI/m/KHBxpSSOCQAJ
   
-* add a button (and/or a keyboard shortcut) to select the next series in the main viewport
-  https://groups.google.com/g/orthanc-users/c/u_lH9aqKsdw/m/KQ7U9CkiAAAJ.  In Osimis viewer,
-  this was implemented by up/down arrow keys (prev/next series)
+* Add a button (and/or a keyboard shortcut) to select the next series
+  in the main viewport. In Osimis viewer, this was implemented by
+  up/down arrow keys (prev/next series).
+  https://groups.google.com/g/orthanc-users/c/u_lH9aqKsdw/m/KQ7U9CkiAAAJ.
 
 ------------
 Known issues
@@ -52,12 +49,6 @@
   https://groups.google.com/g/orthanc-users/c/7SgedbIiA2k/
   https://groups.google.com/g/orthanc-users/c/RfQJFgkOUYY/m/za7rkcLNBQAJ
 
-* the authorization header ('token' query arg) is not included in the HTTP headers.
-  Tested with https://bitbucket.org/osimis/orthanc-setup-samples/src/master/docker/authorization-plugin-viewer-query-args/.
-
-* the plugin does not work with DicomWeb plugin version 1.10.1 -> only 2 numbers are allowed:
-  https://groups.google.com/g/orthanc-users/c/xMcicKAldpM/m/b5Gz3wnyAQAJ
-
 -----------------
 Code refactorings
 -----------------
@@ -73,12 +64,6 @@
 Wishlist
 --------
 
-* Vertical "timeline" to see the position of the current frame in the
-  series, and to change the current frame by clicking on the timeline.
-
-* Display a pixel probe with the Hounsfield Unit.
-  https://groups.google.com/g/orthanc-users/c/m7S0wbYYW5s/m/MBaxIQ_IAAAJ
-
 * Display video files even if the Orthanc REST API is not available
   (using pure DICOMweb). This could possible be done using the
   DICOMweb Bulk Data URI, and/or a dedicated JavaScript video player.
@@ -98,9 +83,10 @@
   type of modality to be displayed):
   https://groups.google.com/g/orthanc-users/c/bGtK3q9ZUmg/m/gr8kCcVWCAAJ
 
-* When opening the viewer, directly load the first series in the viewport 
-  (like the OsimisViewer).  Whatchout the known issues wrt dropping a series
-  before the studies/../metadata have been loaded.
+* When opening the viewer, directly load the first series in the
+  viewport (like the OsimisViewer). Whatch out the known issue
+  wrt. dropping a series before the studies/../metadata have been
+  loaded.
 
 * Create secondary capture DICOM images with the annotations burned in:
   https://groups.google.com/g/orthanc-users/c/T3h-6dIjvww/m/sVtHxwT_AQAJ
@@ -109,7 +95,6 @@
   default view of the left toolbox:
   https://groups.google.com/g/orthanc-users/c/y2B_RIzegNc/m/H9MhY9y2AgAJ
 
-
 * Display the PatientName & ID in the page title such that it appears 
   in the browser tab name
   https://groups.google.com/g/orthanc-users/c/F7cK8axehfA/m/ku-CPsvbAQAJ
@@ -131,14 +116,12 @@
 == Stone of Orthanc C++ library ==
 ==================================
 
-
 -------
 General
 -------
 
 * Documentation
 
-
 -------------
 Optimizations
 -------------
@@ -147,7 +130,6 @@
 * Speedup RT-STRUCT loading
 * "ParseDicomFromOrthanc": new command
 
-
 -----------------
 Platform-specific
 -----------------
@@ -158,4 +140,4 @@
   long as the Oracle singleton is never destroyed. A cleaner solution
   would be similar to "WebAssemblyViewport::CreateObjectCookie()".
 
-* Add precompiled headers for Microsoft Visual Studio
+* Add precompiled headers for Microsoft Visual Studio.