# HG changeset patch # User Sebastien Jodogne # Date 1624961566 -7200 # Node ID 023cce3d784497b0d6c90f5a0df6c7a194825238 # Parent 3751485f1b2e32c37af3e42d663c9b1653d6efd1 introduction of the concept of "virtual series" diff -r 3751485f1b2e -r 023cce3d7844 Applications/StoneWebViewer/WebApplication/app.js --- 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) { diff -r 3751485f1b2e -r 023cce3d7844 Applications/StoneWebViewer/WebApplication/index.html --- 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">
  • + 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)">
    - {{ numberOfFrames }} + {{ virtualSeries.NumberOfFrames }}
    + v-on:dragstart="VirtualSeriesDragStart($event, seriesIndex, virtualSeries.ID)" + v-on:click="ClickVirtualSeries(seriesIndex, virtualSeries.ID)">

    [{{ series[seriesIndex].tags[MODALITY] }}] {{ series[seriesIndex].tags[SERIES_DESCRIPTION] }} diff -r 3751485f1b2e -r 023cce3d7844 Applications/StoneWebViewer/WebAssembly/StoneWebViewer.cpp --- 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 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 { 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 resourcesLoader_; boost::shared_ptr thumbnailsLoader_; boost::shared_ptr metadataLoader_; - std::set scheduledMultiframeInstances_; + std::set 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 arguments; std::map headers; @@ -400,7 +481,7 @@ std::unique_ptr lock(context_.Lock()); lock->Schedule( GetSharedObserver(), PRIORITY_LOW + 2, source_.CreateDicomWebCommand( - uri, arguments, headers, new Orthanc::SingleValueObject(sopInstanceUid))); + uri, arguments, headers, new Orthanc::SingleValueObject(virtualSeriesId))); } } } @@ -409,10 +490,10 @@ { if (observer_.get() != NULL) { - const std::string& sopInstanceUid = + const std::string& virtualSeriesId = dynamic_cast&>( 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& numberOfFramesPerInstance, - const std::string& seriesInstanceUid) + bool LookupVirtualSeries(VirtualSeries& target /* out */, + std::set& 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 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 numberOfFramesPerInstance; - if (GetResourcesLoader().LookupMultiframeSeries(numberOfFramesPerInstance, seriesInstanceUid)) + std::set virtualSeriesIds; + if (GetResourcesLoader().LookupVirtualSeries(virtualSeries_, virtualSeriesIds, seriesInstanceUid)) { - Json::Value json = Json::objectValue; - for (std::map::const_iterator it = - numberOfFramesPerInstance.begin(); it != numberOfFramesPerInstance.end(); ++it) + Json::Value json = Json::arrayValue; + for (std::set::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();