Mercurial > hg > orthanc-stone
changeset 2026:04148de691a7 deep-learning
integration mainline->deep-learning
author | Sebastien Jodogne <s.jodogne@gmail.com> |
---|---|
date | Mon, 05 Dec 2022 08:29:49 +0100 |
parents | 37d6805b80ee (current diff) 8ff083f67628 (diff) |
children | 84ad648b86ac |
files | Applications/StoneWebViewer/WebApplication/app.js Applications/StoneWebViewer/WebApplication/index.html Applications/StoneWebViewer/WebAssembly/StoneWebViewer.cpp |
diffstat | 9 files changed, 491 insertions(+), 134 deletions(-) [+] |
line wrap: on
line diff
--- a/Applications/StoneWebViewer/NEWS Fri Nov 18 00:37:00 2022 +0100 +++ b/Applications/StoneWebViewer/NEWS Mon Dec 05 08:29:49 2022 +0100 @@ -2,6 +2,24 @@ =============================== +Version 2.5 (2022-12-05) +======================== + +* Click-drag is available on the vertical slider +* Added key bindings: + - Left/right arrows to change the active frame + - Up/down arrows to change the active series + - Page up/down to change the active study + - Space bar to play/pause videos +* New URL argument "menu" to change the layout of the list of studies/series +* The first series to be loaded is now automatically opened +* Annotations are grouped into one submenu for narrow screens +* Support generation of ZIP archives in the presence of authorization tokens +* Fix measurement of arcs +* Width of the vertical slider has doubled to ease user interactions +* Patient sex is displayed in the top-left information panel + + Version 2.4 (2022-11-02) ========================
--- a/Applications/StoneWebViewer/NOTES.txt Fri Nov 18 00:37:00 2022 +0100 +++ b/Applications/StoneWebViewer/NOTES.txt Mon Dec 05 08:29:49 2022 +0100 @@ -12,14 +12,8 @@ in the background. -- Contrarily to the Osimis Web viewer, the Stone Web viewer does not - currently support annotations, and will not support Live Share. - - -- The Stone Web viewer has no "timeline" bar to see the position of - the current frame in the series. However, pressing the "Ctrl" key - together with mouse wheel enables fast move, i.e. this changes the - current frame by skipping 1/20th of the frames in the series. +- The Stone Web viewer does not support Live Share that was available + in old versions of the Osimis Web viewer. - The Stone Web viewer displays a color block at the bottom-right of @@ -130,6 +124,16 @@ "Authorization: Bearer Hello" +Additional options +================== + +- If present in the URL, the "menu" argument can be used to set the + default layout of the left-hand list of studies/series. Its allowed + values are "hidden", "small", "grid" (default value at the study + level), or "full" (default value at the series level). (new in Stone + Web viewer 2.5) + + Dynamic actions using messages ==============================
--- a/Applications/StoneWebViewer/WebApplication/app-fixes.css Fri Nov 18 00:37:00 2022 +0100 +++ b/Applications/StoneWebViewer/WebApplication/app-fixes.css Mon Dec 05 08:29:49 2022 +0100 @@ -50,12 +50,12 @@ top: 0; bottom: 0; right: 0; - width: 10px; + width: 20px; background-color: #1b663e; } .wvInfoRightMargin { - right: 10px !important; /* must match the "width" of "wvVerticalScrollbar" */ + right: 20px !important; /* must match the "width" of "wvVerticalScrollbar" */ } .wvVerticalScrollbarHighlight {
--- a/Applications/StoneWebViewer/WebApplication/app.js Fri Nov 18 00:37:00 2022 +0100 +++ b/Applications/StoneWebViewer/WebApplication/app.js Mon Dec 05 08:29:49 2022 +0100 @@ -34,6 +34,7 @@ var SERIES_DESCRIPTION = '0008,103e'; var MODALITY = '0008,0060'; var PATIENT_BIRTH_DATE = '0010,0030'; +var PATIENT_SEX = '0010,0040'; // Registry of the PDF series for which the instance metadata is still waiting var pendingSeriesPdf_ = {}; @@ -52,6 +53,9 @@ var MOUSE_TOOL_CREATE_TEXT_ANNOTATION = 12; // New in 2.4 var MOUSE_TOOL_MAGNIFYING_GLASS = 13; // New in 2.4 +var hasAuthorizationToken = false; +var axiosHeaders = {}; + function getParameterFromUrl(key) { var url = window.location.search.substring(1); @@ -124,6 +128,17 @@ } +function LookupIndexOfResource(array, tag, value) { + for (var i = 0; i < array.length; i++) { + if (array[i].tags[tag] == value) { + return i; + } + } + + return -1; +} + + /** * Enable support for tooltips in Bootstrap. This function must be * called after each modification to the DOM that introduces new @@ -139,6 +154,63 @@ } +function TriggerDownloadFromUri(uri, filename, mime) +{ + if (hasAuthorizationToken) { + axios.get(uri, { + headers: axiosHeaders, + responseType: 'arraybuffer' + }) + .then(function(response) { + const blob = new Blob([ response.data ], { type: mime }); + const url = URL.createObjectURL(blob); + + //window.open(url, '_blank'); + + // https://stackoverflow.com/a/19328891 + var a = document.createElement("a"); + document.body.appendChild(a); + a.style = "display: none"; + a.href = url; + a.download = filename; + a.click(); + window.URL.revokeObjectURL(url); + }); + + } else { + // This version was used in Stone Web viewer <= 2.4, but doesn't + // work with authorization headers + + /** + * The use of "window.open()" below might be blocked (depending on + * the browser criteria to block popup). As a consequence, we + * prefer to set "window.location". + * https://www.nngroup.com/articles/the-top-ten-web-design-mistakes-of-1999/ + **/ + // window.open(uri, '_blank'); + window.location.href = uri; + } +} + + +/** + * The "mousemove" and "mouseup" events were added in Stone Web viewer + * 2.5 to allow click/drag on the vertical scrollbar. + **/ +var activeVerticalScrollbarViewport = null; +var activeVerticalScrollbarTarget = null; + +window.addEventListener('mousemove', function(event) { + if (activeVerticalScrollbarViewport !== null) { + activeVerticalScrollbarViewport.ClickVerticalScrollbar(event, activeVerticalScrollbarTarget); + event.preventDefault(); + } +}); + +window.addEventListener('mouseup', function(event) { + activeVerticalScrollbarViewport = null; +}); + Vue.component('viewport', { props: [ 'left', 'top', 'width', 'height', 'canvasId', 'active', 'content', 'viewportIndex', @@ -250,19 +322,33 @@ this.videoUri = ''; if (this.globalConfiguration.OrthancApiRoot) { var that = this; - axios.post(that.globalConfiguration.OrthancApiRoot + '/tools/find', - { - Level : 'Instance', - Query : { - StudyInstanceUID: studyInstanceUid - } - }) + axios.post(that.globalConfiguration.OrthancApiRoot + '/tools/find', { + Level : 'Instance', + Query : { + StudyInstanceUID: studyInstanceUid + } + }, { + headers: axiosHeaders + }) .then(function(response) { if (response.data.length != 1) { throw(''); } else { - that.videoUri = that.globalConfiguration.OrthancApiRoot + '/instances/' + response.data[0] + '/frames/0/raw'; + var uri = that.globalConfiguration.OrthancApiRoot + '/instances/' + response.data[0] + '/frames/0/raw'; + + if (hasAuthorizationToken) { + axios.get(uri, { + headers: axiosHeaders, + responseType: 'arraybuffer' + }) + .then(function(response) { + const blob = new Blob([ response.data ]); + that.videoUri = URL.createObjectURL(blob); + }); + } else { + that.videoUri = uri; + } } }) .catch(function(error) { @@ -331,6 +417,12 @@ that.windowingWidth = args.detail.windowingWidth; } }); + + window.addEventListener('KeyCineSwitch', function(args) { + if (that.active) { + that.KeyCineSwitch(); + } + }); }, methods: { DragDrop: function(event) { @@ -353,7 +445,7 @@ }, CinePlay: function() { this.cineControls = true; - this.cineIncrement = 1; + this.cineIncrement = -1; this.UpdateCine(); }, CinePause: function() { @@ -367,9 +459,16 @@ }, CineBackward: function() { this.cineControls = true; - this.cineIncrement = -1; + this.cineIncrement = 1; this.UpdateCine(); }, + KeyCineSwitch: function() { + if (this.cineIncrement != 0) { + this.CinePause(); + } else { + this.CinePlay(); + } + }, UpdateCine: function() { // Cancel the previous cine loop, if any if (this.cineTimeoutId !== null) { @@ -411,6 +510,23 @@ if (reschedule) { this.cineTimeoutId = setTimeout(this.CineCallback, 1000.0 / this.cineFramesPerSecond); } + }, + ClickVerticalScrollbar: function(event, target) { + if (target == undefined) { + target = event.currentTarget; + activeVerticalScrollbarViewport = this; + activeVerticalScrollbarTarget = target; + } + + var offset = target.getClientRects()[0]; + var y = event.clientY - offset.top; + var height = target.offsetHeight; + var frame = Math.min(this.numberOfFrames - 1, Math.floor(y * this.numberOfFrames / (height - 1))); + + if (frame >= 0 && + frame < this.numberOfFrames) { + this.currentFrame = frame; + } } } }); @@ -426,6 +542,7 @@ leftVisible: true, viewportLayoutButtonsVisible: false, mouseActionsVisible: false, + annotationActionsVisible: false, activeViewport: 0, showInfo: true, showReferenceLines: true, @@ -437,6 +554,7 @@ orthancSystem: {}, // Only available if "OrthancApiRoot" configuration option is set stoneWebViewerVersion: '...', emscriptenVersion: '...', + isFirstSeries: true, modalWarning: false, modalNotDiagnostic: false, @@ -541,8 +659,26 @@ }); } }, + + GetActiveViewportSeriesTags: function() { + if (this.activeViewport == 1) { + return this.viewport1Content.series.tags; + } + else if (this.activeViewport == 2) { + return this.viewport2Content.series.tags; + } + else if (this.activeViewport == 3) { + return this.viewport3Content.series.tags; + } + else if (this.activeViewport == 4) { + return this.viewport4Content.series.tags; + } + else { + return null; + } + }, - GetActiveSeries: function() { + GetActiveSeriesInstanceUid: function() { var s = []; if ('tags' in this.viewport1Content.series) @@ -689,6 +825,15 @@ } }, + SetViewportVirtualSeries: function(viewportIndex, seriesInstanceUid, virtualSeriesId) { + if (seriesInstanceUid in this.seriesIndex) { + this.SetViewportSeries(viewportIndex, { + seriesIndex: this.seriesIndex[seriesInstanceUid], + virtualSeriesId: virtualSeriesId + }); + } + }, + SetViewportSeries: function(viewportIndex, info) { var series = this.series[info.seriesIndex]; @@ -716,6 +861,9 @@ virtualSeriesId: info.virtualSeriesId }; } + + // Give the focus to this viewport (new in Stone Web viewer 2.5) + this.activeViewport = viewportIndex; }, ClickSeries: function(seriesIndex) { @@ -1071,22 +1219,16 @@ this.archiveJob.length > 0) { var that = this; - axios.get(that.globalConfiguration.OrthancApiRoot + '/jobs/' + that.archiveJob) + axios.get(that.globalConfiguration.OrthancApiRoot + '/jobs/' + that.archiveJob, { + headers: axiosHeaders + }) .then(function(response) { console.log('Progress of archive job ' + that.archiveJob + ': ' + response.data['Progress'] + '%'); var state = response.data['State']; if (state == 'Success') { that.creatingArchive = false; var uri = that.globalConfiguration.OrthancApiRoot + '/jobs/' + that.archiveJob + '/archive'; - - /** - * The use of "window.open()" below might be blocked - * (depending on the browser criteria to block popup). - * As a consequence, we prefer to set "window.location". - * https://www.nngroup.com/articles/the-top-ten-web-design-mistakes-of-1999/ - **/ - // window.open(uri, '_blank'); - window.location = uri; + TriggerDownloadFromUri(uri, that.archiveJob + '.zip', 'application/zip'); } else if (state == 'Running') { setTimeout(that.CheckIsDownloadComplete, 1000); @@ -1108,7 +1250,9 @@ console.log('Creating archive for study: ' + studyInstanceUid); var that = this; - axios.post(this.globalConfiguration.OrthancApiRoot + '/tools/lookup', studyInstanceUid) + axios.post(this.globalConfiguration.OrthancApiRoot + '/tools/lookup', studyInstanceUid, { + headers: axiosHeaders + }) .then(function(response) { if (response.data.length != 1) { throw(''); @@ -1127,12 +1271,13 @@ // ZIP streaming is available (this is Orthanc >= // 1.9.4): Simply give the hand to Orthanc event.preventDefault(); - window.location.href = uri; - + TriggerDownloadFromUri(uri, orthancId + '.zip', 'application/zip'); } else { // ZIP streaming is not available: Create a job to create the archive axios.post(uri, { 'Asynchronous' : true + }, { + headers: axiosHeaders }) .then(function(response) { that.creatingArchive = true; @@ -1145,12 +1290,74 @@ .catch(function (error) { alert('Cannot find the study in Orthanc'); }); - + }, + + ApplyDeepLearning: function() { + stone.ApplyDeepLearningModel(this.GetActiveCanvas()); }, - ApplyDeepLearning: function() - { - stone.ApplyDeepLearningModel(this.GetActiveCanvas()); + ChangeActiveSeries: function(offset) { + var seriesTags = this.GetActiveViewportSeriesTags(); + if (seriesTags !== null) { + var studyIndex = LookupIndexOfResource(this.studies, STUDY_INSTANCE_UID, seriesTags[STUDY_INSTANCE_UID]); + if (studyIndex != -1) { + var virtualSeriesId = this.GetActiveVirtualSeries(); + if (virtualSeriesId.length > 0) { + virtualSeriesId = virtualSeriesId[0]; + } else { + virtualSeriesId = ''; + } + + var seriesInStudyIndices = this.studies[studyIndex].series; + for (var i = 0; i < seriesInStudyIndices.length; i++) { + var series = this.series[seriesInStudyIndices[i]]; + if (this.series[seriesInStudyIndices[i]].tags[SERIES_INSTANCE_UID] == seriesTags[SERIES_INSTANCE_UID]) { + if (series.virtualSeries !== null) { + for (var j = 0; j < series.virtualSeries.length; j++) { + if (series.virtualSeries[j].ID == virtualSeriesId) { + var next = j + offset; + if (next >= 0 && + next < series.virtualSeries.length) { + this.SetViewportVirtualSeries(this.activeViewport, seriesTags[SERIES_INSTANCE_UID], series.virtualSeries[next].ID); + } + return; + } + } + } + else { + var next = i + offset; + if (next >= 0 && + next < seriesInStudyIndices.length) { + this.SetViewportSeriesInstanceUid(this.activeViewport, this.series[seriesInStudyIndices[next]].tags[SERIES_INSTANCE_UID]); + } + return; + } + } + } + } + } + }, + + ChangeActiveStudy: function(offset) { + var seriesTags = this.GetActiveViewportSeriesTags(); + if (seriesTags !== null) { + var studyIndex = LookupIndexOfResource(this.studies, STUDY_INSTANCE_UID, seriesTags[STUDY_INSTANCE_UID]); + if (studyIndex != -1) { + var next = studyIndex + offset; + if (next >= 0 && + next < this.studies.length) { + var nextStudy = this.studies[next]; + if (nextStudy.series.length > 0) { + var seriesIndex = nextStudy.series[0]; + if (this.series[seriesIndex].virtualSeries !== null) { + this.ClickVirtualSeries(seriesIndex, this.series[seriesIndex].virtualSeries[0].ID); + } else { + this.ClickSeries(seriesIndex); + } + } + } + } + } } }, @@ -1187,6 +1394,12 @@ var studyInstanceUid = args.detail.studyInstanceUid; var seriesInstanceUid = args.detail.seriesInstanceUid; that.UpdateIsSeriesComplete(studyInstanceUid, seriesInstanceUid); + + // Automatically open the first series to be loaded (new in Stone Web viewer 2.5) + if (that.isFirstSeries) { + that.SetViewportSeriesInstanceUid(1, seriesInstanceUid); + that.isFirstSeries = false; + } }); window.addEventListener('StoneAnnotationAdded', function() { @@ -1205,6 +1418,49 @@ args.detail.labelX, args.detail.labelY); } }); + + window.addEventListener('keydown', function(event) { + var canvas = that.GetActiveCanvas(); + if (canvas != '') { + switch (event.key) { + case 'Left': + case 'ArrowLeft': + stone.DecrementFrame(canvas, false); + break; + + case 'Right': + case 'ArrowRight': + stone.IncrementFrame(canvas, false); + break; + + case 'Up': + case 'ArrowUp': + that.ChangeActiveSeries(-1); + break + + case 'Down': + case 'ArrowDown': + that.ChangeActiveSeries(1); + break; + + case 'PageUp': + that.ChangeActiveStudy(-1); + break; + + case 'PageDown': + that.ChangeActiveStudy(1); + break; + + case ' ': + case 'Space': + dispatchEvent(new CustomEvent('KeyCineSwitch', { })); + break; + + default: + break; + } + } + }); } }); @@ -1235,9 +1491,20 @@ // Bearer token is new in Stone Web viewer 2.0 var token = getParameterFromUrl('token'); if (token !== undefined) { + hasAuthorizationToken = true; stone.AddHttpHeader('Authorization', 'Bearer ' + token); + axiosHeaders['Authorization'] = 'Bearer ' + token; } + if (app.globalConfiguration.OrthancApiRoot) { + axios.get(app.globalConfiguration.OrthancApiRoot + '/system', { + headers: axiosHeaders + }) + .then(function (response) { + app.orthancSystem = response.data; + }); + } + /** * Calls to "stone.XXX()" can be reordered after this point. @@ -1310,6 +1577,21 @@ alert('No study, nor patient was provided in the URL!'); } } + + // New in Stone Web viewer 2.5 + var menu = getParameterFromUrl('menu'); + if (menu !== undefined) { + if (menu == 'hidden') { + app.leftVisible = false; + } else if (menu == 'small' || + menu == 'grid' || + menu == 'full') { + app.leftVisible = true; + app.leftMode = menu; + } else { + alert('Bad value for the "menu" option in the URL (can be "hidden", "small", "grid", or "full"): ' + menu); + } + } }); @@ -1393,13 +1675,6 @@ .catch(function (error) { alert('Cannot load the WebAssembly framework'); }); - - if (app.globalConfiguration.OrthancApiRoot) { - axios.get(app.globalConfiguration.OrthancApiRoot + '/system') - .then(function (response) { - app.orthancSystem = response.data; - }); - } }) .catch(function (error) { alert('Cannot load the configuration file');
--- a/Applications/StoneWebViewer/WebApplication/index.html Fri Nov 18 00:37:00 2022 +0100 +++ b/Applications/StoneWebViewer/WebApplication/index.html Mon Dec 05 08:29:49 2022 +0100 @@ -36,6 +36,7 @@ <h3>Versions</h3> <p> Stone Web viewer: {{ stoneWebViewerVersion }} <br/> + <span v-if="orthancSystem.Version">Orthanc: {{ orthancSystem.Version }} <br/></span> Emscripten compiler: {{ emscriptenVersion }} </p> </div> @@ -74,6 +75,7 @@ <h3>Versions</h3> <p> Stone Web viewer: {{ stoneWebViewerVersion }} <br/> + <span v-if="orthancSystem.Version">Orthanc: {{ orthancSystem.Version }} <br/></span> Emscripten compiler: {{ emscriptenVersion }} </p> <h3>User preferences</h3> @@ -217,7 +219,7 @@ <!-- Series without multiple multiframe instances --> <span v-for="seriesIndex in study.series"> <li class="wvSerieslist__seriesItem" - v-bind:class="{ highlighted : GetActiveSeries().includes(series[seriesIndex].tags[SERIES_INSTANCE_UID]), 'wvSerieslist__seriesItem--list' : leftMode != 'grid', 'wvSerieslist__seriesItem--grid' : leftMode == 'grid' }" + v-bind:class="{ highlighted : GetActiveSeriesInstanceUid().includes(series[seriesIndex].tags[SERIES_INSTANCE_UID]), 'wvSerieslist__seriesItem--list' : leftMode != 'grid', 'wvSerieslist__seriesItem--grid' : leftMode == 'grid' }" v-on:dragstart="SeriesDragStart($event, seriesIndex)" v-on:click="ClickSeries(seriesIndex)" v-if="series[seriesIndex].virtualSeries === null"> @@ -348,9 +350,9 @@ <div class="tbGroup"> <div class="tbGroup__toggl"> <button class="wvButton" - v-bind:class="{ 'wvButton--underline' : !viewportLayoutButtonsVisible }" + v-bind:class="{ 'wvButton--underline' : !viewportLayoutButtonsVisible, 'wvButton--border' : viewportLayoutButtonsVisible }" data-toggle="tooltip" data-title="Change layout" - @click="viewportLayoutButtonsVisible = !viewportLayoutButtonsVisible;HideAllTooltips()"> + @click="viewportLayoutButtonsVisible = !viewportLayoutButtonsVisible;mouseActionsVisible=false;annotationActionsVisible=false;HideAllTooltips()"> <i class="fa fa-th"></i> </button> </div> @@ -385,9 +387,9 @@ <div class="tbGroup"> <div class="tbGroup__toggl"> <button class="wvButton" - v-bind:class="{ 'wvButton--underline' : !mouseActionsVisible }" + v-bind:class="{ 'wvButton--underline' : !mouseActionsVisible, 'wvButton--border' : mouseActionsVisible }" data-toggle="tooltip" data-title="Mouse actions" - @click="mouseActionsVisible = !mouseActionsVisible;HideAllTooltips()"> + @click="viewportLayoutButtonsVisible=false;mouseActionsVisible = !mouseActionsVisible;annotationActionsVisible=false;HideAllTooltips()"> <i class="fa fa-mouse-pointer"></i> </button> </div> @@ -540,77 +542,94 @@ </button> </div> - <div class="ng-scope inline-object"> - <button class="wvButton--underline text-center" - 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-ruler"></i> - </button> - </div> <div class="ng-scope inline-object"> - <button class="wvButton--underline text-center" - 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-drafting-compass"></i> - </button> - </div> + <div class="tbGroup"> + <div class="tbGroup__toggl"> + <button class="wvButton" + v-bind:class="{ 'wvButton--underline' : !annotationActionsVisible, 'wvButton--border' : annotationActionsVisible }" + data-toggle="tooltip" data-title="Annotations" + @click="viewportLayoutButtonsVisible=false;mouseActionsVisible=false;annotationActionsVisible = !annotationActionsVisible;HideAllTooltips()"> + <i class="fas fa-pencil-ruler"></i> + </button> + </div> + + <div class="tbGroup__buttons--bottom" v-show="annotationActionsVisible"> + <div class="ng-scope inline-object"> + <button class="wvButton--underline text-center" + 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-ruler"></i> + </button> + </div> + + <div class="ng-scope inline-object"> + <button class="wvButton--underline text-center" + 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-drafting-compass"></i> + </button> + </div> + + <div class="ng-scope inline-object"> + <button class="wvButton--underline text-center" + v-bind:class="{ 'active' : mouseTool == MOUSE_TOOL_CREATE_CIRCLE }" + v-on:click="SetLeftMouseButtonAction(MOUSE_TOOL_CREATE_CIRCLE, stone.WebViewerAction.CREATE_CIRCLE)" + data-toggle="tooltip" data-title="Measure circle"> + <i class="far fa-circle"></i> + </button> + </div> - <div class="ng-scope inline-object"> - <button class="wvButton--underline text-center" - v-bind:class="{ 'active' : mouseTool == MOUSE_TOOL_CREATE_CIRCLE }" - v-on:click="SetLeftMouseButtonAction(MOUSE_TOOL_CREATE_CIRCLE, stone.WebViewerAction.CREATE_CIRCLE)" - data-toggle="tooltip" data-title="Measure circle"> - <i class="far fa-circle"></i> - </button> - </div> + <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_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 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 annotation"> + <i class="fas fa-trash"></i> + </button> + </div> + </div> + </div> </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 annotation"> - <i class="fas fa-trash"></i> - </button> - </div> <div class="ng-scope inline-object"> <button class="wvButton--underline text-center" @@ -754,7 +773,7 @@ <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)));"> + v-on:mousedown="ClickVerticalScrollbar($event)"> <div class="wvVerticalScrollbarHighlight" v-bind:style="{ top: (currentFrame / (numberOfFrames - 1) * 95.0) + '%' }"> </div> @@ -764,7 +783,8 @@ <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]) }} + {{ app.FormatDate(content.series.tags[PATIENT_BIRTH_DATE]) }} - + {{ content.series.tags[PATIENT_SEX] }} </div> <div v-if="'tags' in content.series" class="wv-overlay-topright" v-bind:class="{ 'wvInfoRightMargin' : numberOfFrames > 1 }"> @@ -810,13 +830,13 @@ </button> </div> <div class="btn-group btn-group-sm" role="group"> - <button type="button" class="btn btn-primary" @click="CinePlay()"> + <button type="button" class="btn btn-primary" @click="CineBackward()"> <i class="fas fa-play fa-flip-horizontal"></i> </button> <button type="button" class="btn btn-primary" @click="CinePause()"> <i class="fas fa-pause"></i> </button> - <button type="button" class="btn btn-primary" @click="CineBackward()"> + <button type="button" class="btn btn-primary" @click="CinePlay()"> <i class="fas fa-play"></i> </button> </div>
--- a/Applications/StoneWebViewer/WebAssembly/StoneWebViewer.cpp Fri Nov 18 00:37:00 2022 +0100 +++ b/Applications/StoneWebViewer/WebAssembly/StoneWebViewer.cpp Mon Dec 05 08:29:49 2022 +0100 @@ -3530,6 +3530,23 @@ Redraw(); } } + + + void SignalSynchronizedBrowsing() + { + if (synchronizationEnabled_ && + frames_.get() != NULL && + cursor_.get() != NULL) + { + const size_t currentCursorIndex = cursor_->GetCurrentIndex(); + + const OrthancStone::CoordinateSystem3D current = + frames_->GetFrameGeometry(currentCursorIndex); + + observer_->SignalSynchronizedBrowsing( + *this, current.GetOrigin() + synchronizationOffset_, current.GetNormal()); + } + } }; @@ -4492,7 +4509,9 @@ { try { - return GetViewport(canvas)->ChangeFrame(SeriesCursor::Action_Minus, isCircular) ? 1 : 0; + bool changed = GetViewport(canvas)->ChangeFrame(SeriesCursor::Action_Minus, isCircular); + GetViewport(canvas)->SignalSynchronizedBrowsing(); + return changed ? 1 : 0; } EXTERN_CATCH_EXCEPTIONS; return 0; @@ -4505,7 +4524,9 @@ { try { - return GetViewport(canvas)->ChangeFrame(SeriesCursor::Action_Plus, isCircular) ? 1 : 0; + bool changed = GetViewport(canvas)->ChangeFrame(SeriesCursor::Action_Plus, isCircular); + GetViewport(canvas)->SignalSynchronizedBrowsing(); + return changed ? 1 : 0; } EXTERN_CATCH_EXCEPTIONS; return 0; @@ -4521,6 +4542,7 @@ if (frameNumber >= 0) { GetViewport(canvas)->SetFrame(static_cast<unsigned int>(frameNumber)); + GetViewport(canvas)->SignalSynchronizedBrowsing(); } } EXTERN_CATCH_EXCEPTIONS; @@ -4533,6 +4555,7 @@ try { GetViewport(canvas)->GoToFirstFrame(); + GetViewport(canvas)->SignalSynchronizedBrowsing(); } EXTERN_CATCH_EXCEPTIONS; } @@ -4544,6 +4567,7 @@ try { GetViewport(canvas)->GoToLastFrame(); + GetViewport(canvas)->SignalSynchronizedBrowsing(); } EXTERN_CATCH_EXCEPTIONS; }
--- a/CITATION.cff Fri Nov 18 00:37:00 2022 +0100 +++ b/CITATION.cff Mon Dec 05 08:29:49 2022 +0100 @@ -17,5 +17,5 @@ repository-code: 'https://hg.orthanc-server.com/orthanc-stone/' url: 'https://www.orthanc-server.com/' license: AGPL-3.0-or-later -version: 2.4 -date-released: 2022-11-02 +version: 2.5 +date-released: 2022-12-05
--- a/OrthancStone/Sources/Scene2D/AnnotationsSceneLayer.cpp Fri Nov 18 00:37:00 2022 +0100 +++ b/OrthancStone/Sources/Scene2D/AnnotationsSceneLayer.cpp Mon Dec 05 08:29:49 2022 +0100 @@ -653,21 +653,34 @@ const double yc = middle_.GetY(); const double x2 = end_.GetX(); const double y2 = end_.GetY(); - - startAngle = atan2(y1 - yc, x1 - xc); - endAngle = atan2(y2 - yc, x2 - xc); - - fullAngle = endAngle - startAngle; - - while (fullAngle < -PI) + + double referenceAngle = atan2(y1 - yc, x1 - xc); + double secondAngle = atan2(y2 - yc, x2 - xc); + + secondAngle -= referenceAngle; + + while (secondAngle >= PI) + { + secondAngle -= 2.0 * PI; + } + + while (secondAngle <= -PI) { - fullAngle += 2.0 * PI; + secondAngle += 2.0 * PI; + } + + if (secondAngle < 0) + { + startAngle = referenceAngle + secondAngle; + endAngle = referenceAngle; } - - while (fullAngle >= PI) + else { - fullAngle -= 2.0 * PI; + startAngle = referenceAngle; + endAngle = referenceAngle + secondAngle; } + + fullAngle = endAngle - startAngle; } public:
--- a/TODO Fri Nov 18 00:37:00 2022 +0100 +++ b/TODO Mon Dec 05 08:29:49 2022 +0100 @@ -37,6 +37,9 @@ up/down arrow keys (prev/next series). https://groups.google.com/g/orthanc-users/c/u_lH9aqKsdw/m/KQ7U9CkiAAAJ. +* Minor: Rotate the anchors of the text after rotation of the display. + + ------------ Known issues ------------