changeset 1849:023cce3d7844

introduction of the concept of "virtual series"
author Sebastien Jodogne <s.jodogne@gmail.com>
date Tue, 29 Jun 2021 12:12:46 +0200
parents 3751485f1b2e
children 932dc2265baa
files Applications/StoneWebViewer/WebApplication/app.js Applications/StoneWebViewer/WebApplication/index.html Applications/StoneWebViewer/WebAssembly/StoneWebViewer.cpp
diffstat 3 files changed, 167 insertions(+), 75 deletions(-) [+]
line wrap: on
line diff
--- a/Applications/StoneWebViewer/WebApplication/app.js	Mon Jun 28 12:19:38 2021 +0200
+++ b/Applications/StoneWebViewer/WebApplication/app.js	Tue Jun 29 12:12:46 2021 +0200
@@ -188,10 +188,9 @@
 
         var that = this;
         Vue.nextTick(function() {
-          if (newVal.sopInstanceUid !== undefined &&
-              newVal.sopInstanceUid.length > 0) {
-            stone.LoadMultipartInstanceInViewport(
-              that.canvasId, seriesInstanceUid, newVal.sopInstanceUid);
+          if (newVal.virtualSeriesId !== undefined &&
+              newVal.virtualSeriesId.length > 0) {
+            stone.LoadVirtualSeriesInViewport(that.canvasId, newVal.virtualSeriesId);
           }
           else {
             stone.LoadSeriesInViewport(that.canvasId, seriesInstanceUid);
@@ -306,7 +305,7 @@
       // The "parseInt()" is because of Microsoft Edge Legacy (*)
       this.$emit('updated-series', {
         seriesIndex: parseInt(event.dataTransfer.getData('seriesIndex'), 10),
-        sopInstanceUid: event.dataTransfer.getData('sopInstanceUid')
+        virtualSeriesId: event.dataTransfer.getData('virtualSeriesId')
       });
     },
     MakeActive: function() {
@@ -451,7 +450,7 @@
       series: [],
       studies: [],
       seriesIndex: {},  // Maps "SeriesInstanceUID" to "index in this.series"
-      multiframeInstanceThumbnails: {}
+      virtualSeriesThumbnails: {}
     }
   },
   computed: {
@@ -520,20 +519,20 @@
       return s;
     },
 
-    GetActiveMultiframeInstances: function() {
+    GetActiveVirtualSeries: function() {
       var s = [];
 
-      if ('sopInstanceUid' in this.viewport1Content)
-        s.push(this.viewport1Content.sopInstanceUid);
+      if ('virtualSeriesId' in this.viewport1Content)
+        s.push(this.viewport1Content.virtualSeriesId);
 
-      if ('sopInstanceUid' in this.viewport2Content)
-        s.push(this.viewport2Content.sopInstanceUid);
+      if ('virtualSeriesId' in this.viewport2Content)
+        s.push(this.viewport2Content.virtualSeriesId);
 
-      if ('sopInstanceUid' in this.viewport3Content)
-        s.push(this.viewport3Content.sopInstanceUid);
+      if ('virtualSeriesId' in this.viewport3Content)
+        s.push(this.viewport3Content.virtualSeriesId);
 
-      if ('sopInstanceUid' in this.viewport4Content)
-        s.push(this.viewport4Content.sopInstanceUid);
+      if ('virtualSeriesId' in this.viewport4Content)
+        s.push(this.viewport4Content.virtualSeriesId);
 
       return s;
     },
@@ -604,7 +603,7 @@
               'type' : stone.ThumbnailType.LOADING,
               'color': study.color,
               'tags': sourceSeries[i],
-              'multiframeInstances': null
+              'virtualSeries': null
             });
           }
         }
@@ -625,9 +624,9 @@
       event.dataTransfer.setData('seriesIndex', seriesIndex.toString());
     },
 
-    MultiframeInstanceDragStart: function(event, seriesIndex, sopInstanceUid) {
+    VirtualSeriesDragStart: function(event, seriesIndex, virtualSeriesId) {
       event.dataTransfer.setData('seriesIndex', seriesIndex.toString());
-      event.dataTransfer.setData('sopInstanceUid', sopInstanceUid.toString());
+      event.dataTransfer.setData('virtualSeriesId', virtualSeriesId.toString());
     },
 
     SetViewportSeriesInstanceUid: function(viewportIndex, seriesInstanceUid) {
@@ -644,25 +643,25 @@
       if (viewportIndex == 1) {
         this.viewport1Content = {
           series: series,
-          sopInstanceUid: info.sopInstanceUid
+          virtualSeriesId: info.virtualSeriesId
         };
       }
       else if (viewportIndex == 2) {
         this.viewport2Content = {
           series: series,
-          sopInstanceUid: info.sopInstanceUid
+          virtualSeriesId: info.virtualSeriesId
         };
       }
       else if (viewportIndex == 3) {
         this.viewport3Content = {
           series: series,
-          sopInstanceUid: info.sopInstanceUid
+          virtualSeriesId: info.virtualSeriesId
         };
       }
       else if (viewportIndex == 4) {
         this.viewport4Content = {
           series: series,
-          sopInstanceUid: info.sopInstanceUid
+          virtualSeriesId: info.virtualSeriesId
         };
       }
     },
@@ -673,10 +672,10 @@
       });
     },
     
-    ClickMultiframeInstance: function(seriesIndex, sopInstanceUid) {
+    ClickVirtualSeries: function(seriesIndex, virtualSeriesId) {
       this.SetViewportSeries(this.activeViewport, {
         seriesIndex: seriesIndex,
-        sopInstanceUid: sopInstanceUid
+        virtualSeriesId: virtualSeriesId
       });
     },
     
@@ -800,8 +799,8 @@
             delete pendingSeriesPdf_[seriesInstanceUid];
           }
 
-          if (stone.LoadMultiframeInstancesFromSeries(seriesInstanceUid)) {
-            series.multiframeInstances = JSON.parse(stone.GetStringBuffer());
+          if (stone.LookupVirtualSeries(seriesInstanceUid)) {
+            series.virtualSeries = JSON.parse(stone.GetStringBuffer());
           }
         }
 
@@ -1060,8 +1059,8 @@
 
     var that = this;
     
-    window.addEventListener('MultiframeInstanceThumbnailLoaded', function(args) {
-      that.$set(that.multiframeInstanceThumbnails, args.detail.sopInstanceUid, args.detail.thumbnail);
+    window.addEventListener('VirtualSeriesThumbnailLoaded', function(args) {
+      that.$set(that.virtualSeriesThumbnails, args.detail.virtualSeriesId, args.detail.thumbnail);
     });
 
     window.addEventListener('ThumbnailLoaded', function(args) {
--- a/Applications/StoneWebViewer/WebApplication/index.html	Mon Jun 28 12:19:38 2021 +0200
+++ b/Applications/StoneWebViewer/WebApplication/index.html	Tue Jun 29 12:12:46 2021 +0200
@@ -209,7 +209,7 @@
                               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">
+                              v-if="series[seriesIndex].virtualSeries === null">
                             <div class="wvSerieslist__picture" style="z-index:0"
                                  draggable="true"
                                  v-if="series[seriesIndex].type != stone.ThumbnailType.UNKNOWN"
@@ -261,28 +261,28 @@
 
                           <!-- Series with multiple multiframe instances (CINE) -->
                           <li class="wvSerieslist__seriesItem"
-                              v-bind:class="{ highlighted : GetActiveMultiframeInstances().includes(sopInstanceUid), 'wvSerieslist__seriesItem--list' : leftMode != 'grid', 'wvSerieslist__seriesItem--grid' : leftMode == 'grid' }"
-                              v-for="(numberOfFrames, sopInstanceUid) in series[seriesIndex].multiframeInstances"
-                              v-on:dragstart="MultiframeInstanceDragStart($event, seriesIndex, sopInstanceUid)"
-                              v-on:click="ClickMultiframeInstance(seriesIndex, sopInstanceUid)">
+                              v-bind:class="{ highlighted : GetActiveVirtualSeries().includes(virtualSeries.ID), 'wvSerieslist__seriesItem--list' : leftMode != 'grid', 'wvSerieslist__seriesItem--grid' : leftMode == 'grid' }"
+                              v-for="virtualSeries in series[seriesIndex].virtualSeries"
+                              v-on:dragstart="VirtualSeriesDragStart($event, seriesIndex, virtualSeries.ID)"
+                              v-on:click="ClickVirtualSeries(seriesIndex, virtualSeries.ID)">
                             <div class="wvSerieslist__picture" style="z-index:0"
                                  draggable="true">
                               <img v-if="series[seriesIndex].type == stone.ThumbnailType.IMAGE"
-                                   v-bind:src="sopInstanceUid in multiframeInstanceThumbnails ? multiframeInstanceThumbnails[sopInstanceUid] : series[seriesIndex].thumbnail"
+                                   v-bind:src="virtualSeries.ID in virtualSeriesThumbnails ? virtualSeriesThumbnails[virtualSeries.ID] : series[seriesIndex].thumbnail"
                                    style="vertical-align:baseline"
                                    width="65px" height="65px"
                                    v-bind:title="leftMode == 'full' ? null : '[' + series[seriesIndex].tags[MODALITY] + '] ' + series[seriesIndex].tags[SERIES_DESCRIPTION]"
                                    />
                               
                               <div v-bind:class="'wvSerieslist__badge--' + study.color">
-                                {{ numberOfFrames }}
+                                {{ virtualSeries.NumberOfFrames }}
                               </div>
                             </div>
 
                             <div v-if="leftMode == 'full'" class="wvSerieslist__information"
                                  draggable="true"
-                                 v-on:dragstart="MultiframeInstanceDragStart($event, seriesIndex, sopInstanceUid)"
-                                 v-on:click="MultiframeInstanceDragStart($event, seriesIndex, sopInstanceUid)">
+                                 v-on:dragstart="VirtualSeriesDragStart($event, seriesIndex, virtualSeries.ID)"
+                                 v-on:click="ClickVirtualSeries(seriesIndex, virtualSeries.ID)">
                               <p class="wvSerieslist__label">
                                 [{{ series[seriesIndex].tags[MODALITY] }}]
                                 {{ series[seriesIndex].tags[SERIES_DESCRIPTION] }}
--- a/Applications/StoneWebViewer/WebAssembly/StoneWebViewer.cpp	Mon Jun 28 12:19:38 2021 +0200
+++ b/Applications/StoneWebViewer/WebAssembly/StoneWebViewer.cpp	Tue Jun 29 12:12:46 2021 +0200
@@ -187,6 +187,86 @@
 static const unsigned int DEFAULT_CINE_RATE = 30;
 
 
+
+class VirtualSeries : public boost::noncopyable
+{
+private:
+  class Item
+  {
+  private:
+    std::string   seriesInstanceUid_;
+    unsigned int  numberOfFrames_;
+
+  public:
+    Item(const std::string& seriesInstanceUid,
+         unsigned int numberOfFrames) :
+      seriesInstanceUid_(seriesInstanceUid),
+      numberOfFrames_(numberOfFrames)
+    {
+      if (numberOfFrames == 0)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+      }
+    }
+
+    const std::string& GetSeriesInstanceUid() const
+    {
+      return seriesInstanceUid_;
+    }
+
+    unsigned int GetNumberOfFrames() const
+    {
+      return numberOfFrames_;
+    }
+  };
+
+  typedef std::map<std::string, Item>  Content;
+
+  Content  content_;
+
+  const Item& GetItem(const std::string& id) const
+  {
+    Content::const_iterator found = content_.find(id);
+    
+    if (found == content_.end())
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+    else
+    {
+      return found->second;
+    }  
+  }
+  
+public:
+  std::string Add(const std::string& seriesInstanceUid,
+                  const std::string& sopInstanceUid,
+                  unsigned int numberOfFrames)
+  {
+    if (content_.find(sopInstanceUid) != content_.end())
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+    else
+    {
+      content_.insert(std::make_pair(sopInstanceUid, Item(seriesInstanceUid, numberOfFrames)));
+      return sopInstanceUid;
+    }
+  }
+
+  const std::string& GetSeriesInstanceUid(const std::string& id) const
+  {
+    return GetItem(id).GetSeriesInstanceUid();
+  }
+
+  unsigned int GetNumberOfFrames(const std::string& id) const
+  {
+    return GetItem(id).GetNumberOfFrames();
+  }
+};
+
+
+
 class ResourcesLoader : public OrthancStone::ObserverBase<ResourcesLoader>
 {
 public:
@@ -209,8 +289,8 @@
                                        const std::string& seriesInstanceUid,
                                        const std::string& pdf) = 0;
 
-    virtual void SignalMultiframeInstanceThumbnailLoaded(const std::string& sopInstanceUid,
-                                                         const std::string& jpeg) = 0;
+    virtual void SignalVirtualSeriesThumbnailLoaded(const std::string& virtualSeriesId,
+                                                    const std::string& jpeg) = 0;
   };
   
 private:
@@ -223,7 +303,7 @@
   boost::shared_ptr<OrthancStone::DicomResourcesLoader>    resourcesLoader_;
   boost::shared_ptr<OrthancStone::SeriesThumbnailsLoader>  thumbnailsLoader_;
   boost::shared_ptr<OrthancStone::SeriesMetadataLoader>    metadataLoader_;
-  std::set<std::string>                                    scheduledMultiframeInstances_;
+  std::set<std::string>                                    scheduledVirtualSeriesThumbnails_;
 
   explicit ResourcesLoader(OrthancStone::ILoadersContext& context,
                            const OrthancStone::DicomSource& source) :
@@ -378,13 +458,14 @@
     }
   }
 
-  void FetchInstanceThumbnail(const std::string& studyInstanceUid,
-                              const std::string& seriesInstanceUid,
-                              const std::string& sopInstanceUid)
+  void FetchVirtualSeriesThumbnail(const std::string& virtualSeriesId,
+                                   const std::string& studyInstanceUid,
+                                   const std::string& seriesInstanceUid,
+                                   const std::string& sopInstanceUid)
   {
-    if (scheduledMultiframeInstances_.find(sopInstanceUid) == scheduledMultiframeInstances_.end())
+    if (scheduledVirtualSeriesThumbnails_.find(virtualSeriesId) == scheduledVirtualSeriesThumbnails_.end())
     {
-      scheduledMultiframeInstances_.insert(sopInstanceUid);
+      scheduledVirtualSeriesThumbnails_.insert(virtualSeriesId);
       
       std::map<std::string, std::string> arguments;
       std::map<std::string, std::string> headers;
@@ -400,7 +481,7 @@
         std::unique_ptr<OrthancStone::ILoadersContext::ILock> lock(context_.Lock());
         lock->Schedule(
           GetSharedObserver(), PRIORITY_LOW + 2, source_.CreateDicomWebCommand(
-            uri, arguments, headers, new Orthanc::SingleValueObject<std::string>(sopInstanceUid)));
+            uri, arguments, headers, new Orthanc::SingleValueObject<std::string>(virtualSeriesId)));
       }
     }
   }
@@ -409,10 +490,10 @@
   {
     if (observer_.get() != NULL)
     {
-      const std::string& sopInstanceUid =
+      const std::string& virtualSeriesId =
         dynamic_cast<const Orthanc::SingleValueObject<std::string>&>(
           message.GetOrigin().GetPayload()).GetValue();
-      observer_->SignalMultiframeInstanceThumbnailLoaded(sopInstanceUid, message.GetAnswer());
+      observer_->SignalVirtualSeriesThumbnailLoaded(virtualSeriesId, message.GetAnswer());
     }
   }
 
@@ -525,38 +606,44 @@
     return accessor.IsComplete();
   }
 
-  bool LookupMultiframeSeries(std::map<std::string, unsigned int>& numberOfFramesPerInstance,
-                              const std::string& seriesInstanceUid)
+  bool LookupVirtualSeries(VirtualSeries& target /* out */,
+                           std::set<std::string>& virtualSeriesIds /* out */,
+                           const std::string& seriesInstanceUid)
   {
-    numberOfFramesPerInstance.clear();
-
     OrthancStone::SeriesMetadataLoader::Accessor accessor(*metadataLoader_, seriesInstanceUid);
     if (accessor.IsComplete() &&
         accessor.GetInstancesCount() >= 2)
     {
-      bool isMultiframe = false;
+      bool hasMultiframe = false;
       
       for (size_t i = 0; i < accessor.GetInstancesCount(); i++)
       {
         OrthancStone::DicomInstanceParameters p(accessor.GetInstance(i));
-        numberOfFramesPerInstance[p.GetSopInstanceUid()] = p.GetNumberOfFrames();
 
         if (p.GetNumberOfFrames() > 1)
         {
-          isMultiframe = true;
+          hasMultiframe = true;
         }
       }
 
-      if (isMultiframe)
+      if (hasMultiframe)
       {
         for (size_t i = 0; i < accessor.GetInstancesCount(); i++)
         {
           OrthancStone::DicomInstanceParameters p(accessor.GetInstance(i));
-          FetchInstanceThumbnail(p.GetStudyInstanceUid(), p.GetSeriesInstanceUid(), p.GetSopInstanceUid());
+
+          std::string virtualSeriesId = target.Add(seriesInstanceUid, p.GetSopInstanceUid(), p.GetNumberOfFrames());
+          virtualSeriesIds.insert(virtualSeriesId);
+          
+          FetchVirtualSeriesThumbnail(virtualSeriesId, p.GetStudyInstanceUid(), p.GetSeriesInstanceUid(), p.GetSopInstanceUid());
         }
+
+        return true;
       }
-
-      return isMultiframe;
+      else
+      {
+        return false;
+      }
     }
     else
     {
@@ -3187,20 +3274,20 @@
   }
 
 
-  virtual void SignalMultiframeInstanceThumbnailLoaded(const std::string& sopInstanceUid,
-                                                       const std::string& jpeg) ORTHANC_OVERRIDE
+  virtual void SignalVirtualSeriesThumbnailLoaded(const std::string& virtualSeriesId,
+                                                  const std::string& jpeg) ORTHANC_OVERRIDE
   {
     std::string dataUriScheme;
     Orthanc::Toolbox::EncodeDataUriScheme(dataUriScheme, "image/jpeg", jpeg);    
     
     EM_ASM({
         const customEvent = document.createEvent("CustomEvent");
-        customEvent.initCustomEvent("MultiframeInstanceThumbnailLoaded", false, false,
-                                    { "sopInstanceUid" : UTF8ToString($0),
+        customEvent.initCustomEvent("VirtualSeriesThumbnailLoaded", false, false,
+                                    { "virtualSeriesId" : UTF8ToString($0),
                                         "thumbnail" : UTF8ToString($1) });
         window.dispatchEvent(customEvent);
       },
-      sopInstanceUid.c_str(),
+      virtualSeriesId.c_str(),
       dataUriScheme.c_str());
   }
 
@@ -3271,6 +3358,7 @@
 static WebViewerAction leftButtonAction_ = WebViewerAction_Windowing;
 static WebViewerAction middleButtonAction_ = WebViewerAction_Pan;
 static WebViewerAction rightButtonAction_ = WebViewerAction_Zoom;
+static VirtualSeries virtualSeries_;
 
 
 static void FormatTags(std::string& target,
@@ -3617,15 +3705,17 @@
 
 
   EMSCRIPTEN_KEEPALIVE
-  int LoadMultipartInstanceInViewport(const char* canvas,
-                                      const char* seriesInstanceUid,
-                                      const char* sopInstanceUid)
+  int LoadVirtualSeriesInViewport(const char* canvas,
+                                  const char* virtualSeriesId)
   {
     try
     {
       std::unique_ptr<OrthancStone::SortedFrames> frames(new OrthancStone::SortedFrames);
+
+      const std::string sopInstanceUid = virtualSeriesId;  // TODO
       
-      if (GetResourcesLoader().SortMultipartInstanceFrames(*frames, seriesInstanceUid, sopInstanceUid))
+      if (GetResourcesLoader().SortMultipartInstanceFrames(
+            *frames, virtualSeries_.GetSeriesInstanceUid(virtualSeriesId), sopInstanceUid))
       {
         GetViewport(canvas)->SetFrames(frames.release());
         return 1;
@@ -3957,18 +4047,21 @@
 
 
   EMSCRIPTEN_KEEPALIVE
-  int LoadMultiframeInstancesFromSeries(const char* seriesInstanceUid)
+  int LookupVirtualSeries(const char* seriesInstanceUid)
   {
     try
     {
-      std::map<std::string, unsigned int> numberOfFramesPerInstance;
-      if (GetResourcesLoader().LookupMultiframeSeries(numberOfFramesPerInstance, seriesInstanceUid))
+      std::set<std::string> virtualSeriesIds;
+      if (GetResourcesLoader().LookupVirtualSeries(virtualSeries_, virtualSeriesIds, seriesInstanceUid))
       {
-        Json::Value json = Json::objectValue;
-        for (std::map<std::string, unsigned int>::const_iterator it =
-               numberOfFramesPerInstance.begin(); it != numberOfFramesPerInstance.end(); ++it)
+        Json::Value json = Json::arrayValue;
+        for (std::set<std::string>::const_iterator it = virtualSeriesIds.begin();
+             it != virtualSeriesIds.end(); ++it)
         {
-          json[it->first] = it->second;
+          Json::Value item = Json::objectValue;
+          item["ID"] = *it;
+          item["NumberOfFrames"] = virtualSeries_.GetNumberOfFrames(*it);
+          json.append(item);
         }
 
         stringBuffer_ = json.toStyledString();