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">&nbsp;<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">