Mercurial > hg > orthanc-stone
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"> - {{ currentFrame + 1 }} / {{ numberOfFrames }} + + <input type="text" v-model="currentFrameFromUser" class="wvInputInstanceNumber"> / {{ numberOfFrames }} + </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.