Mercurial > hg > orthanc-stone
changeset 1709:2931f5e15320
download study from Stone Web viewer
author | Sebastien Jodogne <s.jodogne@gmail.com> |
---|---|
date | Mon, 30 Nov 2020 15:36:40 +0100 |
parents | eb59fbee071e |
children | 673c163e1b3e |
files | Applications/StoneWebViewer/NOTES.txt Applications/StoneWebViewer/Plugin/Plugin.cpp Applications/StoneWebViewer/WebApplication/app.js Applications/StoneWebViewer/WebApplication/configuration.json Applications/StoneWebViewer/WebApplication/index.html |
diffstat | 5 files changed, 144 insertions(+), 35 deletions(-) [+] |
line wrap: on
line diff
--- a/Applications/StoneWebViewer/NOTES.txt Sat Nov 28 16:16:24 2020 +0100 +++ b/Applications/StoneWebViewer/NOTES.txt Mon Nov 30 15:36:40 2020 +0100 @@ -12,6 +12,10 @@ background. +- Contrarily to the Osimis Web viewer, the Stone Web viewer doesn't + currently support annotations, and will not support Live Share. + + - The Stone Web viewer uses the DICOM identifiers. The Osimis Web viewer the Orthanc identifiers. https://book.orthanc-server.com/faq/orthanc-ids.html
--- a/Applications/StoneWebViewer/Plugin/Plugin.cpp Sat Nov 28 16:16:24 2020 +0100 +++ b/Applications/StoneWebViewer/Plugin/Plugin.cpp Mon Nov 30 15:36:40 2020 +0100 @@ -174,28 +174,38 @@ { static const char* CONFIG_SECTION = "StoneWebViewer"; - std::string config; + Json::Value config = Json::objectValue; OrthancPlugins::OrthancConfiguration orthanc; if (orthanc.IsSection(CONFIG_SECTION)) { OrthancPlugins::OrthancConfiguration section(false); orthanc.GetSection(section, CONFIG_SECTION); - - Json::Value wrapper = Json::objectValue; - wrapper[CONFIG_SECTION] = section.GetJson(); - config = wrapper.toStyledString(); + config[CONFIG_SECTION] = section.GetJson(); } else { LOG(WARNING) << "The Orthanc configuration file doesn't contain a section \"" << CONFIG_SECTION << "\" to configure the Stone Web viewer: " << "Will use default settings"; + + std::string s; Orthanc::EmbeddedResources::GetDirectoryResource( - config, Orthanc::EmbeddedResources::WEB_APPLICATION, "/configuration.json"); + s, Orthanc::EmbeddedResources::WEB_APPLICATION, "/configuration.json"); + + Json::Reader reader; + if (!reader.parse(s, config) || + config.type() != Json::objectValue) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, + "Cannot read the default configuration"); + } } - OrthancPluginAnswerBuffer(context, output, config.c_str(), config.size(), "application/json"); + config[CONFIG_SECTION]["OrthancApiRoot"] = ".."; + + const std::string s = config.toStyledString(); + OrthancPluginAnswerBuffer(context, output, s.c_str(), s.size(), "application/json"); } }
--- a/Applications/StoneWebViewer/WebApplication/app.js Sat Nov 28 16:16:24 2020 +0100 +++ b/Applications/StoneWebViewer/WebApplication/app.js Mon Nov 30 15:36:40 2020 +0100 @@ -27,6 +27,11 @@ var STUDY_INSTANCE_UID = '0020,000d'; var STUDY_DESCRIPTION = '0008,1030'; var STUDY_DATE = '0008,0020'; +var PATIENT_ID = '0010,0020'; +var PATIENT_NAME = '0010,0010'; +var SERIES_NUMBER = '0020,0011'; +var SERIES_DESCRIPTION = '0008,103e'; +var MODALITY = '0008,0060'; // Registry of the PDF series for which the instance metadata is still waiting var pendingSeriesPdf_ = {}; @@ -100,6 +105,21 @@ } +/** + * Enable support for tooltips in Bootstrap. This function must be + * called after each modification to the DOM that introduces new + * tooltips (e.g. after loading studies). + **/ +function RefreshTooltips() +{ + $('[data-toggle="tooltip"]').tooltip({ + placement: 'bottom', + container: 'body', + trigger: 'hover' + }); +} + + Vue.component('viewport', { props: [ 'left', 'top', 'width', 'height', 'canvasId', 'active', 'content', 'viewportIndex', @@ -329,6 +349,8 @@ showReferenceLines: true, synchronizedBrowsing: false, globalConfiguration: {}, + creatingArchive: false, + archiveJob: '', modalWarning: false, modalNotDiagnostic: false, @@ -539,6 +561,10 @@ this.series = series; this.seriesIndex = seriesIndex; this.ready = true; + + Vue.nextTick(function() { + RefreshTooltips(); + }); }, SeriesDragStart: function(event, seriesIndex) { @@ -872,6 +898,63 @@ } this.SetMouseButtonActions(left, middle, right); + }, + + CheckIsDownloadComplete: function() + { + if (this.creatingArchive && + this.archiveJob.length > 0) { + + var that = this; + axios.get(that.globalConfiguration.OrthancApiRoot + '/jobs/' + that.archiveJob) + .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; + window.open(that.globalConfiguration.OrthancApiRoot + '/jobs/' + that.archiveJob + '/archive'); + } + else if (state == 'Running') { + setTimeout(that.CheckIsDownloadComplete, 1000); + } + else { + alert('Error while creating the archive in Orthanc: ' + response.data['ErrorDescription']); + that.creatingArchive = false; + } + }) + .catch(function(error) { + alert('The archive job is not available anymore in Orthanc'); + that.creatingArchive = false; + }); + } + }, + + DownloadStudy: function(studyInstanceUid) + { + console.log('Creating archive for study: ' + studyInstanceUid); + + var that = this; + axios.post(this.globalConfiguration.OrthancApiRoot + '/tools/lookup', studyInstanceUid) + .then(function(response) { + if (response.data.length != 1) { + throw(''); + } + else { + var orthancId = response.data[0]['ID']; + axios.post(that.globalConfiguration.OrthancApiRoot + '/studies/' + orthancId + '/archive', + { + 'Asynchronous' : true + }) + .then(function(response) { + that.creatingArchive = true; + that.archiveJob = response.data.ID; + setTimeout(that.CheckIsDownloadComplete, 1000); + }); + } + }) + .catch(function (error) { + alert('Cannot find the study in Orthanc'); + }); } }, @@ -1013,12 +1096,7 @@ $(document).ready(function() { - // Enable support for tooltips in Bootstrap - $('[data-toggle="tooltip"]').tooltip({ - placement: 'bottom', - container: 'body', - trigger: 'hover' - }); + RefreshTooltips(); //app.modalWarning = true;
--- a/Applications/StoneWebViewer/WebApplication/configuration.json Sat Nov 28 16:16:24 2020 +0100 +++ b/Applications/StoneWebViewer/WebApplication/configuration.json Mon Nov 30 15:36:40 2020 +0100 @@ -47,7 +47,12 @@ * active viewport as a JPEG file. **/ "DownloadAsJpegEnabled" : true, - + + /** + * Enables/disables the button to download the display study. + **/ + "DownloadStudyEnabled" : true, + /** * The allowed origin for messages corresponding to dynamic actions * triggered by another Web page using "window.postMessage()". The @@ -56,6 +61,13 @@ * set, all the requests for dynamic actions will be rejected. * https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage **/ - "ExpectedMessageOrigin" : "http://localhost:8042" + "ExpectedMessageOrigin" : "http://localhost:8042", + + /** + * The following parameter can be set if running the Stone Web + * viewer from Orthanc, but without using the associated plugin. + * Using the plugin would overwrite this setting. + **/ + "OrthancApiRoot" : "schnol" } }
--- a/Applications/StoneWebViewer/WebApplication/index.html Sat Nov 28 16:16:24 2020 +0100 +++ b/Applications/StoneWebViewer/WebApplication/index.html Mon Nov 30 15:36:40 2020 +0100 @@ -142,9 +142,9 @@ v-bind:class="{ active: study.selected }" @click="study.selected = !study.selected"> <a> - {{ study.tags['0008,1030'] }} - <small v-if="study.tags['0008,0020'].length > 0"> - [{{ FormatDate(study.tags['0008,0020']) }}] + {{ study.tags[STUDY_DESCRIPTION] }} + <small v-if="study.tags[STUDY_DATE].length > 0"> + [{{ FormatDate(study.tags[STUDY_DATE]) }}] </small> <span v-if="study.selected"> <i class="fa fa-check"></i></span> </a> @@ -173,16 +173,21 @@ <!-- Actions --> <div class="wvStudyIsland__actions" v-bind:class="{ 'wvStudyIsland__actions--oneCol': leftMode == 'small' }"> - <a class="wvButton"> + <a class="wvButton" + v-show="globalConfiguration.DownloadStudyEnabled && 'OrthancApiRoot' in globalConfiguration"> <!-- download --> - <i class="fa fa-download"></i> + <i class="fa fa-download" v-show="!creatingArchive" + data-toggle="tooltip" data-title="Download the study" + @click="DownloadStudy(study.tags[STUDY_INSTANCE_UID])"></i> + <i class="fas fa-sync fa-spin" v-show="creatingArchive" + data-toggle="tooltip" data-title="A ZIP archive is being created by Orthanc..."></i> </a> </div> <!-- Title --> - {{ study.tags['0008,1030'] }} + {{ study.tags[STUDY_DESCRIPTION] }} <br/> - <small>{{ FormatDate(study.tags['0008,0020']) }}</small> + <small>{{ FormatDate(study.tags[STUDY_DATE]) }}</small> </div> <div class="wvStudyIsland__main"> @@ -191,7 +196,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['0020,000e']), 'wvSerieslist__seriesItem--list' : leftMode != 'grid', 'wvSerieslist__seriesItem--grid' : leftMode == 'grid' }" + v-bind:class="{ highlighted : GetActiveSeries().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].multiframeInstances === null"> @@ -214,7 +219,7 @@ <div v-if="[stone.ThumbnailType.IMAGE, stone.ThumbnailType.NO_PREVIEW].includes(series[seriesIndex].type)" class="wvSerieslist__placeholderIcon" - v-bind:title="leftMode == 'full' ? null : '[' + series[seriesIndex].tags['0008,0060'] + '] ' + series[seriesIndex].tags['0008,103e']"> + v-bind:title="leftMode == 'full' ? null : '[' + series[seriesIndex].tags[MODALITY] + '] ' + series[seriesIndex].tags[SERIES_DESCRIPTION]"> <i v-if="series[seriesIndex].type == stone.ThumbnailType.NO_PREVIEW" class="fa fa-eye-slash"></i> @@ -222,7 +227,7 @@ v-bind:src="series[seriesIndex].thumbnail" style="vertical-align:baseline" width="65px" height="65px" - v-bind:title="leftMode == 'full' ? null : '[' + series[seriesIndex].tags['0008,0060'] + '] ' + series[seriesIndex].tags['0008,103e']" + v-bind:title="leftMode == 'full' ? null : '[' + series[seriesIndex].tags[MODALITY] + '] ' + series[seriesIndex].tags[SERIES_DESCRIPTION]" /> <div v-bind:class="'wvSerieslist__badge--' + study.color" @@ -235,8 +240,8 @@ v-on:dragstart="SeriesDragStart($event, seriesIndex)" v-on:click="ClickSeries(seriesIndex)"> <p class="wvSerieslist__label"> - [{{ series[seriesIndex].tags['0008,0060'] }}] - {{ series[seriesIndex].tags['0008,103e'] }} + [{{ series[seriesIndex].tags[MODALITY] }}] + {{ series[seriesIndex].tags[SERIES_DESCRIPTION] }} </p> </div> </li> @@ -254,7 +259,7 @@ v-bind:src="sopInstanceUid in multiframeInstanceThumbnails ? multiframeInstanceThumbnails[sopInstanceUid] : series[seriesIndex].thumbnail" style="vertical-align:baseline" width="65px" height="65px" - v-bind:title="leftMode == 'full' ? null : '[' + series[seriesIndex].tags['0008,0060'] + '] ' + series[seriesIndex].tags['0008,103e']" + v-bind:title="leftMode == 'full' ? null : '[' + series[seriesIndex].tags[MODALITY] + '] ' + series[seriesIndex].tags[SERIES_DESCRIPTION]" /> <div v-bind:class="'wvSerieslist__badge--' + study.color"> @@ -267,8 +272,8 @@ v-on:dragstart="MultiframeInstanceDragStart($event, seriesIndex, sopInstanceUid)" v-on:click="MultiframeInstanceDragStart($event, seriesIndex, sopInstanceUid)"> <p class="wvSerieslist__label"> - [{{ series[seriesIndex].tags['0008,0060'] }}] - {{ series[seriesIndex].tags['0008,103e'] }} + [{{ series[seriesIndex].tags[MODALITY] }}] + {{ series[seriesIndex].tags[SERIES_DESCRIPTION] }} </p> </div> </li> @@ -601,13 +606,13 @@ <div v-show="showInfo"> <div class="wv-overlay"> <div v-if="'tags' in content.series" class="wv-overlay-topleft"> - {{ content.series.tags['0010,0010'] }}<br/> - {{ content.series.tags['0010,0020'] }} + {{ content.series.tags[PATIENT_NAME] }}<br/> + {{ content.series.tags[PATIENT_ID] }} </div> <div v-if="'tags' in content.series" class="wv-overlay-topright"> - {{ content.series.tags['0008,1030'] }}<br/> - {{ app.FormatDate(content.series.tags['0008,0020']) }}<br/> - {{ content.series.tags['0020,0011'] }} | {{ content.series.tags['0008,103e'] }} + {{ content.series.tags[STUDY_DESCRIPTION] }}<br/> + {{ app.FormatDate(content.series.tags[STUDY_DATE]) }}<br/> + {{ content.series.tags[SERIES_NUMBER] }} | {{ content.series.tags[SERIES_DESCRIPTION] }} </div> <div class="wv-overlay-timeline-wrapper wvPrintExclude"> <div style="text-align:left; padding:5px" v-show="numberOfFrames != 0">