# HG changeset patch # User Sebastien Jodogne # Date 1606142367 -3600 # Node ID 570398585b5f2251f9ec2181e2b5cc2d0cf80ff6 # Parent 2c2512918a0fc3df2798b6fbc3f41dfa6cf0c5f8 start support of cine sequences diff -r 2c2512918a0f -r 570398585b5f Applications/StoneWebViewer/WebApplication/app.js --- a/Applications/StoneWebViewer/WebApplication/app.js Fri Nov 20 10:14:36 2020 +0100 +++ b/Applications/StoneWebViewer/WebApplication/app.js Mon Nov 23 15:39:27 2020 +0100 @@ -48,17 +48,39 @@ data: function () { return { stone: stone, // To access global object "stone" from "index.html" - status: 'waiting' + status: 'waiting', + cineControls: false, + cineIncrement: 0, + cineFramesPerSecond: 30, + cineTimeoutId: null, + cineLoadingFrame: false } }, - watch: { + watch: { + currentFrame: function(newVal, oldVal) { + /** + * The "FrameUpdated" event has been received, which indicates + * that the schedule frame has been displayed: The cine loop can + * proceed to the next frame (check out "CineCallback()"). + **/ + this.cineLoadingFrame = false; + }, series: function(newVal, oldVal) { this.status = 'loading'; + this.cineControls = false; + this.cineMode = ''; + this.cineLoadingFrame = false; + this.cineRate = 30; // Default value + + if (this.cineTimeoutId !== null) { + clearTimeout(this.cineTimeoutId); + this.cineTimeoutId = null; + } var studyInstanceUid = newVal.tags[STUDY_INSTANCE_UID]; var seriesInstanceUid = newVal.tags[SERIES_INSTANCE_UID]; stone.SpeedUpFetchSeriesMetadata(studyInstanceUid, seriesInstanceUid); - + if ((newVal.type == stone.ThumbnailType.IMAGE || newVal.type == stone.ThumbnailType.NO_PREVIEW) && newVal.complete) { @@ -96,7 +118,14 @@ }, mounted: function() { var that = this; - + + window.addEventListener('SeriesDetailsReady', function(args) { + var canvasId = args.detail.canvasId; + if (canvasId == that.canvasId) { + that.cineFramesPerSecond = stone.GetCineRate(canvasId); + } + }); + window.addEventListener('PdfLoaded', function(args) { var studyInstanceUid = args.detail.studyInstanceUid; var seriesInstanceUid = args.detail.seriesInstanceUid; @@ -137,11 +166,72 @@ MakeActive: function() { this.$emit('selected-viewport'); }, - DecrementFrame: function() { - stone.DecrementFrame(this.canvasId); + DecrementFrame: function(isCircular) { + return stone.DecrementFrame(this.canvasId, isCircular); + }, + IncrementFrame: function(isCircular) { + return stone.IncrementFrame(this.canvasId, isCircular); + }, + CinePlay: function() { + this.cineControls = true; + this.cineIncrement = 1; + this.UpdateCine(); + }, + CinePause: function() { + if (this.cineIncrement == 0) { + // Two clicks on the "pause" button will hide the playback control + this.cineControls = !this.cineControls; + } else { + this.cineIncrement = 0; + this.UpdateCine(); + } + }, + CineBackward: function() { + this.cineControls = true; + this.cineIncrement = -1; + this.UpdateCine(); }, - IncrementFrame: function() { - stone.IncrementFrame(this.canvasId); + UpdateCine: function() { + // Cancel the previous cine loop, if any + if (this.cineTimeoutId !== null) { + clearTimeout(this.cineTimeoutId); + this.cineTimeoutId = null; + } + + this.cineLoadingFrame = false; + + if (this.cineIncrement != 0) { + this.CineCallback(); + } + }, + CineCallback: function() { + var reschedule; + + if (this.cineLoadingFrame) { + /** + * Wait until the frame scheduled by the previous call to + * "CineCallback()" is actually displayed (i.e. we monitor the + * "FrameUpdated" event). Otherwise, the background loading + * process of the DICOM frames in C++ might be behind the + * advancement of the current frame, which freezes the + * display. + **/ + reschedule = true; + } else { + this.cineLoadingFrame = true; + + if (this.cineIncrement == 1) { + reschedule = this.DecrementFrame(true /* circular */); + } else if (this.cineIncrement == -1) { + reschedule = this.IncrementFrame(true /* circular */); + } else { + reschedule = false; // Increment is zero, this test is just for safety + } + } + + if (reschedule) { + this.cineTimeoutId = setTimeout(this.CineCallback, 1000.0 / this.cineFramesPerSecond); + } } } }); diff -r 2c2512918a0f -r 570398585b5f Applications/StoneWebViewer/WebApplication/index.html --- a/Applications/StoneWebViewer/WebApplication/index.html Fri Nov 20 10:14:36 2020 +0100 +++ b/Applications/StoneWebViewer/WebApplication/index.html Mon Nov 23 15:39:27 2020 +0100 @@ -556,28 +556,50 @@ style="position:absolute; left:0; top:0; width:100%; height:100%" oncontextmenu="return false"> -
+
-
+
{{ series.tags['0010,0010'] }}
{{ series.tags['0010,0020'] }}
-
+
{{ series.tags['0008,1030'] }}
{{ series.tags['0008,0020'] }}
{{ series.tags['0020,0011'] }} | {{ series.tags['0008,103e'] }}
-
- -   {{ currentFrame }} / {{ framesCount }}   - +
+
+
+ +
+ +   {{ currentFrame }} / {{ framesCount }}   + +
+ + + +
+
-
+
- [ this viewer cannot play videos ] + [ videos are not supported yet ] diff -r 2c2512918a0f -r 570398585b5f Applications/StoneWebViewer/WebAssembly/StoneWebViewer.cpp --- a/Applications/StoneWebViewer/WebAssembly/StoneWebViewer.cpp Fri Nov 20 10:14:36 2020 +0100 +++ b/Applications/StoneWebViewer/WebAssembly/StoneWebViewer.cpp Mon Nov 23 15:39:27 2020 +0100 @@ -167,6 +167,9 @@ static const unsigned int QUALITY_JPEG = 0; static const unsigned int QUALITY_FULL = 1; +static const unsigned int DEFAULT_CINE_RATE = 30; + + class ResourcesLoader : public OrthancStone::ObserverBase { public: @@ -687,12 +690,13 @@ std::vector prefetch_; int framesCount_; int currentFrame_; - bool isCircular_; + bool isCircularPrefetch_; int fastDelta_; Action lastAction_; int ComputeNextFrame(int currentFrame, - Action action) const + Action action, + bool isCircular) const { if (framesCount_ == 0) { @@ -727,7 +731,7 @@ throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); } - if (isCircular_) + if (isCircular) { while (nextFrame < 0) { @@ -797,31 +801,31 @@ { case Action_None: case Action_Plus: - queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_Plus), Action_Plus)); - queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_Minus), Action_Minus)); - queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_FastPlus), Action_FastPlus)); - queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_FastMinus), Action_FastMinus)); + queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_Plus, isCircularPrefetch_), Action_Plus)); + queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_Minus, isCircularPrefetch_), Action_Minus)); + queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_FastPlus, isCircularPrefetch_), Action_FastPlus)); + queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_FastMinus, isCircularPrefetch_), Action_FastMinus)); break; case Action_Minus: - queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_Minus), Action_Minus)); - queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_Plus), Action_Plus)); - queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_FastMinus), Action_FastMinus)); - queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_FastPlus), Action_FastPlus)); + queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_Minus, isCircularPrefetch_), Action_Minus)); + queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_Plus, isCircularPrefetch_), Action_Plus)); + queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_FastMinus, isCircularPrefetch_), Action_FastMinus)); + queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_FastPlus, isCircularPrefetch_), Action_FastPlus)); break; case Action_FastPlus: - queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_FastPlus), Action_FastPlus)); - queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_FastMinus), Action_FastMinus)); - queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_Plus), Action_Plus)); - queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_Minus), Action_Minus)); + queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_FastPlus, isCircularPrefetch_), Action_FastPlus)); + queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_FastMinus, isCircularPrefetch_), Action_FastMinus)); + queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_Plus, isCircularPrefetch_), Action_Plus)); + queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_Minus, isCircularPrefetch_), Action_Minus)); break; case Action_FastMinus: - queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_FastMinus), Action_FastMinus)); - queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_FastPlus), Action_FastPlus)); - queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_Minus), Action_Minus)); - queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_Plus), Action_Plus)); + queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_FastMinus, isCircularPrefetch_), Action_FastMinus)); + queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_FastPlus, isCircularPrefetch_), Action_FastPlus)); + queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_Minus, isCircularPrefetch_), Action_Minus)); + queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_Plus, isCircularPrefetch_), Action_Plus)); break; default: @@ -843,16 +847,16 @@ explicit SeriesCursor(size_t framesCount) : framesCount_(framesCount), currentFrame_(framesCount / 2), // Start at the middle frame - isCircular_(false), + isCircularPrefetch_(false), lastAction_(Action_None) { SetFastDelta(framesCount / 20); UpdatePrefetch(); } - void SetCircular(bool isCircular) + void SetCircularPrefetch(bool isCircularPrefetch) { - isCircular_ = isCircular; + isCircularPrefetch_ = isCircularPrefetch; UpdatePrefetch(); } @@ -886,9 +890,10 @@ return static_cast(currentFrame_); } - void Apply(Action action) + void Apply(Action action, + bool isCircular) { - currentFrame_ = ComputeNextFrame(currentFrame_, action); + currentFrame_ = ComputeNextFrame(currentFrame_, action, isCircular); lastAction_ = action; UpdatePrefetch(); } @@ -990,6 +995,8 @@ { } + virtual void SignalSeriesDetailsReady(const ViewerViewport& viewport) = 0; + virtual void SignalFrameUpdated(const ViewerViewport& viewport, size_t currentFrame, size_t countFrames, @@ -1045,10 +1052,10 @@ } }; - class SetDefaultWindowingCommand : public ICommand + class LoadSeriesDetailsFromInstance : public ICommand { public: - explicit SetDefaultWindowingCommand(boost::shared_ptr viewport) : + explicit LoadSeriesDetailsFromInstance(boost::shared_ptr viewport) : ICommand(viewport) { } @@ -1082,7 +1089,28 @@ } } + uint32_t cineRate; + if (dicom.ParseUnsignedInteger32(cineRate, Orthanc::DICOM_TAG_CINE_RATE) && + cineRate > 0) + { + /** + * If we detect a cine sequence, start on the first frame + * instead of on the middle frame. + **/ + GetViewport().cursor_->SetCurrentIndex(0); + GetViewport().cineRate_ = cineRate; + } + else + { + GetViewport().cineRate_ = DEFAULT_CINE_RATE; + } + GetViewport().Redraw(); + + if (GetViewport().observer_.get() != NULL) + { + GetViewport().observer_->SignalSeriesDetailsReady(GetViewport()); + } } }; @@ -1297,6 +1325,7 @@ float windowingWidth_; float defaultWindowingCenter_; float defaultWindowingWidth_; + unsigned int cineRate_; bool inverted_; bool flipX_; bool flipY_; @@ -1719,11 +1748,11 @@ { if (wheelEvent->deltaY < 0) { - that.ChangeFrame(that.isCtrlDown_ ? SeriesCursor::Action_FastMinus : SeriesCursor::Action_Minus); + that.ChangeFrame(that.isCtrlDown_ ? SeriesCursor::Action_FastMinus : SeriesCursor::Action_Minus, false /* not circular */); } else if (wheelEvent->deltaY > 0) { - that.ChangeFrame(that.isCtrlDown_ ? SeriesCursor::Action_FastPlus : SeriesCursor::Action_Plus); + that.ChangeFrame(that.isCtrlDown_ ? SeriesCursor::Action_FastPlus : SeriesCursor::Action_Plus, false /* not circular */); } } @@ -1788,7 +1817,8 @@ flipX_ = false; flipY_ = false; fitNextContent_ = true; - + cineRate_ = DEFAULT_CINE_RATE; + frames_.reset(frames); cursor_.reset(new SeriesCursor(frames_->GetFramesCount())); @@ -1821,14 +1851,14 @@ uid != OrthancStone::SopClassUid_RTStruct && GetSeriesThumbnailType(uid) != OrthancStone::SeriesThumbnailType_Video) { - // Fetch the default windowing for the central instance + // Fetch the details of the series from the central instance const std::string uri = ("studies/" + frames_->GetStudyInstanceUid() + "/series/" + frames_->GetSeriesInstanceUid() + "/instances/" + centralInstance.GetSopInstanceUid() + "/metadata"); loader_->ScheduleGetDicomWeb( boost::make_shared(Orthanc::DICOM_TAG_SOP_INSTANCE_UID), - 0, source_, uri, new SetDefaultWindowingCommand(GetSharedObserver())); + 0, source_, uri, new LoadSeriesDetailsFromInstance(GetSharedObserver())); } } @@ -1911,21 +1941,27 @@ } - void ChangeFrame(SeriesCursor::Action action) + // Returns "true" iff the frame has indeed changed + bool ChangeFrame(SeriesCursor::Action action, + bool isCircular) { if (cursor_.get() != NULL) { size_t previous = cursor_->GetCurrentIndex(); - cursor_->Apply(action); + cursor_->Apply(action, isCircular); size_t current = cursor_->GetCurrentIndex(); if (previous != current) { Redraw(); + return true; } } + + return false; } + bool GetCurrentFrameOfReferenceUid(std::string& frameOfReferenceUid) const { @@ -2213,6 +2249,11 @@ Redraw(); } } + + unsigned int GetCineRate() const + { + return cineRate_; + } }; @@ -2297,6 +2338,18 @@ } } + virtual void SignalSeriesDetailsReady(const ViewerViewport& viewport) ORTHANC_OVERRIDE + { + EM_ASM({ + const customEvent = document.createEvent("CustomEvent"); + customEvent.initCustomEvent("SeriesDetailsReady", false, false, + { "canvasId" : UTF8ToString($0) }); + window.dispatchEvent(customEvent); + }, + viewport.GetCanvasId().c_str() + ); + } + virtual void SignalFrameUpdated(const ViewerViewport& viewport, size_t currentFrame, size_t countFrames, @@ -2654,26 +2707,28 @@ EMSCRIPTEN_KEEPALIVE - void DecrementFrame(const char* canvas, - int fitContent) + int DecrementFrame(const char* canvas, + int isCircular) { try { - GetViewport(canvas)->ChangeFrame(SeriesCursor::Action_Minus); + return GetViewport(canvas)->ChangeFrame(SeriesCursor::Action_Minus, isCircular) ? 1 : 0; } EXTERN_CATCH_EXCEPTIONS; + return 0; } EMSCRIPTEN_KEEPALIVE - void IncrementFrame(const char* canvas, - int fitContent) + int IncrementFrame(const char* canvas, + int isCircular) { try { - GetViewport(canvas)->ChangeFrame(SeriesCursor::Action_Plus); + return GetViewport(canvas)->ChangeFrame(SeriesCursor::Action_Plus, isCircular) ? 1 : 0; } EXTERN_CATCH_EXCEPTIONS; + return 0; } @@ -2866,4 +2921,16 @@ } EXTERN_CATCH_EXCEPTIONS; } + + + EMSCRIPTEN_KEEPALIVE + unsigned int GetCineRate(const char* canvas) + { + try + { + return GetViewport(canvas)->GetCineRate(); + } + EXTERN_CATCH_EXCEPTIONS; + return 0; + } }