comparison StoneWebViewer/WebAssembly/Test.cpp @ 1495:fb74ed5d8c22

initial commit of the Stone Web viewer
author Sebastien Jodogne <s.jodogne@gmail.com>
date Thu, 25 Jun 2020 16:51:10 +0200
parents
children 244ad1e4e76a
comparison
equal deleted inserted replaced
1494:5a3ef478deb6 1495:fb74ed5d8c22
1 /**
2 * Stone of Orthanc
3 * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
4 * Department, University Hospital of Liege, Belgium
5 * Copyright (C) 2017-2020 Osimis S.A., Belgium
6 *
7 * This program is free software: you can redistribute it and/or
8 * modify it under the terms of the GNU Affero General Public License
9 * as published by the Free Software Foundation, either version 3 of
10 * the License, or (at your option) any later version.
11 *
12 * This program is distributed in the hope that it will be useful, but
13 * WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15 * Affero General Public License for more details.
16 *
17 * You should have received a copy of the GNU Affero General Public License
18 * along with this program. If not, see <http://www.gnu.org/licenses/>.
19 **/
20
21
22 #include <emscripten.h>
23
24
25 #define DISPATCH_JAVASCRIPT_EVENT(name) \
26 EM_ASM( \
27 const customEvent = document.createEvent("CustomEvent"); \
28 customEvent.initCustomEvent(name, false, false, undefined); \
29 window.dispatchEvent(customEvent); \
30 );
31
32
33 #define EXTERN_CATCH_EXCEPTIONS \
34 catch (Orthanc::OrthancException& e) \
35 { \
36 LOG(ERROR) << "OrthancException: " << e.What(); \
37 DISPATCH_JAVASCRIPT_EVENT("StoneException"); \
38 } \
39 catch (OrthancStone::StoneException& e) \
40 { \
41 LOG(ERROR) << "StoneException: " << e.What(); \
42 DISPATCH_JAVASCRIPT_EVENT("StoneException"); \
43 } \
44 catch (std::exception& e) \
45 { \
46 LOG(ERROR) << "Runtime error: " << e.what(); \
47 DISPATCH_JAVASCRIPT_EVENT("StoneException"); \
48 } \
49 catch (...) \
50 { \
51 LOG(ERROR) << "Native exception"; \
52 DISPATCH_JAVASCRIPT_EVENT("StoneException"); \
53 }
54
55
56 #include <Cache/MemoryObjectCache.h>
57 #include <DicomFormat/DicomArray.h>
58 #include <DicomParsing/Internals/DicomImageDecoder.h>
59 #include <Images/Image.h>
60 #include <Images/ImageProcessing.h>
61 #include <Images/JpegReader.h>
62 #include <Logging.h>
63
64 #include "../../Framework/Loaders/DicomResourcesLoader.h"
65 #include "../../Framework/Loaders/SeriesMetadataLoader.h"
66 #include "../../Framework/Loaders/SeriesThumbnailsLoader.h"
67 #include "../../Framework/Loaders/WebAssemblyLoadersContext.h"
68 #include "../../Framework/Messages/ObserverBase.h"
69 #include "../../Framework/Oracle/ParseDicomFromWadoCommand.h"
70 #include "../../Framework/Scene2D/ColorTextureSceneLayer.h"
71 #include "../../Framework/Scene2D/FloatTextureSceneLayer.h"
72 #include "../../Framework/Scene2D/PolylineSceneLayer.h"
73 #include "../../Framework/StoneException.h"
74 #include "../../Framework/Toolbox/DicomInstanceParameters.h"
75 #include "../../Framework/Toolbox/GeometryToolbox.h"
76 #include "../../Framework/Toolbox/SortedFrames.h"
77 #include "../../Framework/Viewport/WebGLViewport.h"
78
79 #include <boost/make_shared.hpp>
80 #include <stdio.h>
81
82
83 enum EMSCRIPTEN_KEEPALIVE ThumbnailType
84 {
85 ThumbnailType_Image,
86 ThumbnailType_NoPreview,
87 ThumbnailType_Pdf,
88 ThumbnailType_Video,
89 ThumbnailType_Loading,
90 ThumbnailType_Unknown
91 };
92
93
94 enum EMSCRIPTEN_KEEPALIVE DisplayedFrameQuality
95 {
96 DisplayedFrameQuality_None,
97 DisplayedFrameQuality_Low,
98 DisplayedFrameQuality_High
99 };
100
101
102
103 static const int PRIORITY_HIGH = -100;
104 static const int PRIORITY_LOW = 100;
105 static const int PRIORITY_NORMAL = 0;
106
107 static const unsigned int QUALITY_JPEG = 0;
108 static const unsigned int QUALITY_FULL = 1;
109
110 class ResourcesLoader : public OrthancStone::ObserverBase<ResourcesLoader>
111 {
112 public:
113 class IObserver : public boost::noncopyable
114 {
115 public:
116 virtual ~IObserver()
117 {
118 }
119
120 virtual void SignalResourcesLoaded() = 0;
121
122 virtual void SignalSeriesThumbnailLoaded(const std::string& studyInstanceUid,
123 const std::string& seriesInstanceUid) = 0;
124
125 virtual void SignalSeriesMetadataLoaded(const std::string& studyInstanceUid,
126 const std::string& seriesInstanceUid) = 0;
127 };
128
129 private:
130 std::unique_ptr<IObserver> observer_;
131 OrthancStone::DicomSource source_;
132 size_t pending_;
133 boost::shared_ptr<OrthancStone::LoadedDicomResources> studies_;
134 boost::shared_ptr<OrthancStone::LoadedDicomResources> series_;
135 boost::shared_ptr<OrthancStone::DicomResourcesLoader> resourcesLoader_;
136 boost::shared_ptr<OrthancStone::SeriesThumbnailsLoader> thumbnailsLoader_;
137 boost::shared_ptr<OrthancStone::SeriesMetadataLoader> metadataLoader_;
138
139 ResourcesLoader(const OrthancStone::DicomSource& source) :
140 source_(source),
141 pending_(0),
142 studies_(new OrthancStone::LoadedDicomResources(Orthanc::DICOM_TAG_STUDY_INSTANCE_UID)),
143 series_(new OrthancStone::LoadedDicomResources(Orthanc::DICOM_TAG_SERIES_INSTANCE_UID))
144 {
145 }
146
147 void Handle(const OrthancStone::DicomResourcesLoader::SuccessMessage& message)
148 {
149 const Orthanc::SingleValueObject<Orthanc::ResourceType>& payload =
150 dynamic_cast<const Orthanc::SingleValueObject<Orthanc::ResourceType>&>(message.GetUserPayload());
151
152 OrthancStone::LoadedDicomResources& dicom = *message.GetResources();
153
154 LOG(INFO) << "resources loaded: " << dicom.GetSize()
155 << ", " << Orthanc::EnumerationToString(payload.GetValue());
156
157 if (payload.GetValue() == Orthanc::ResourceType_Series)
158 {
159 for (size_t i = 0; i < dicom.GetSize(); i++)
160 {
161 std::string studyInstanceUid, seriesInstanceUid;
162 if (dicom.GetResource(i).LookupStringValue(
163 studyInstanceUid, Orthanc::DICOM_TAG_STUDY_INSTANCE_UID, false) &&
164 dicom.GetResource(i).LookupStringValue(
165 seriesInstanceUid, Orthanc::DICOM_TAG_SERIES_INSTANCE_UID, false))
166 {
167 thumbnailsLoader_->ScheduleLoadThumbnail(source_, "", studyInstanceUid, seriesInstanceUid);
168 metadataLoader_->ScheduleLoadSeries(PRIORITY_LOW + 1, source_, studyInstanceUid, seriesInstanceUid);
169 }
170 }
171 }
172
173 if (pending_ == 0)
174 {
175 throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
176 }
177 else
178 {
179 pending_ --;
180 if (pending_ == 0 &&
181 observer_.get() != NULL)
182 {
183 observer_->SignalResourcesLoaded();
184 }
185 }
186 }
187
188 void Handle(const OrthancStone::SeriesThumbnailsLoader::SuccessMessage& message)
189 {
190 if (observer_.get() != NULL)
191 {
192 observer_->SignalSeriesThumbnailLoaded(
193 message.GetStudyInstanceUid(), message.GetSeriesInstanceUid());
194 }
195 }
196
197 void Handle(const OrthancStone::SeriesMetadataLoader::SuccessMessage& message)
198 {
199 if (observer_.get() != NULL)
200 {
201 observer_->SignalSeriesMetadataLoaded(
202 message.GetStudyInstanceUid(), message.GetSeriesInstanceUid());
203 }
204 }
205
206 void FetchInternal(const std::string& studyInstanceUid,
207 const std::string& seriesInstanceUid)
208 {
209 // Firstly, load the study
210 Orthanc::DicomMap filter;
211 filter.SetValue(Orthanc::DICOM_TAG_STUDY_INSTANCE_UID, studyInstanceUid, false);
212
213 std::set<Orthanc::DicomTag> tags;
214 tags.insert(Orthanc::DICOM_TAG_STUDY_DESCRIPTION); // Necessary for Orthanc DICOMweb plugin
215
216 resourcesLoader_->ScheduleQido(
217 studies_, PRIORITY_HIGH, source_, Orthanc::ResourceType_Study, filter, tags,
218 new Orthanc::SingleValueObject<Orthanc::ResourceType>(Orthanc::ResourceType_Study));
219
220 // Secondly, load the series
221 if (!seriesInstanceUid.empty())
222 {
223 filter.SetValue(Orthanc::DICOM_TAG_SERIES_INSTANCE_UID, seriesInstanceUid, false);
224 }
225
226 tags.insert(Orthanc::DICOM_TAG_SERIES_NUMBER); // Necessary for Google Cloud Platform
227
228 resourcesLoader_->ScheduleQido(
229 series_, PRIORITY_HIGH, source_, Orthanc::ResourceType_Series, filter, tags,
230 new Orthanc::SingleValueObject<Orthanc::ResourceType>(Orthanc::ResourceType_Series));
231
232 pending_ += 2;
233 }
234
235 public:
236 static boost::shared_ptr<ResourcesLoader> Create(OrthancStone::ILoadersContext::ILock& lock,
237 const OrthancStone::DicomSource& source)
238 {
239 boost::shared_ptr<ResourcesLoader> loader(new ResourcesLoader(source));
240
241 loader->resourcesLoader_ = OrthancStone::DicomResourcesLoader::Create(lock);
242 loader->thumbnailsLoader_ = OrthancStone::SeriesThumbnailsLoader::Create(lock, PRIORITY_LOW);
243 loader->metadataLoader_ = OrthancStone::SeriesMetadataLoader::Create(lock);
244
245 loader->Register<OrthancStone::DicomResourcesLoader::SuccessMessage>(
246 *loader->resourcesLoader_, &ResourcesLoader::Handle);
247
248 loader->Register<OrthancStone::SeriesThumbnailsLoader::SuccessMessage>(
249 *loader->thumbnailsLoader_, &ResourcesLoader::Handle);
250
251 loader->Register<OrthancStone::SeriesMetadataLoader::SuccessMessage>(
252 *loader->metadataLoader_, &ResourcesLoader::Handle);
253
254 return loader;
255 }
256
257 void FetchAllStudies()
258 {
259 FetchInternal("", "");
260 }
261
262 void FetchStudy(const std::string& studyInstanceUid)
263 {
264 FetchInternal(studyInstanceUid, "");
265 }
266
267 void FetchSeries(const std::string& studyInstanceUid,
268 const std::string& seriesInstanceUid)
269 {
270 FetchInternal(studyInstanceUid, seriesInstanceUid);
271 }
272
273 size_t GetStudiesCount() const
274 {
275 return studies_->GetSize();
276 }
277
278 size_t GetSeriesCount() const
279 {
280 return series_->GetSize();
281 }
282
283 void GetStudy(Orthanc::DicomMap& target,
284 size_t i)
285 {
286 target.Assign(studies_->GetResource(i));
287 }
288
289 void GetSeries(Orthanc::DicomMap& target,
290 size_t i)
291 {
292 target.Assign(series_->GetResource(i));
293
294 // Complement with the study-level tags
295 std::string studyInstanceUid;
296 if (target.LookupStringValue(studyInstanceUid, Orthanc::DICOM_TAG_STUDY_INSTANCE_UID, false) &&
297 studies_->HasResource(studyInstanceUid))
298 {
299 studies_->MergeResource(target, studyInstanceUid);
300 }
301 }
302
303 OrthancStone::SeriesThumbnailType GetSeriesThumbnail(std::string& image,
304 std::string& mime,
305 const std::string& seriesInstanceUid)
306 {
307 return thumbnailsLoader_->GetSeriesThumbnail(image, mime, seriesInstanceUid);
308 }
309
310 void FetchSeriesMetadata(int priority,
311 const std::string& studyInstanceUid,
312 const std::string& seriesInstanceUid)
313 {
314 metadataLoader_->ScheduleLoadSeries(priority, source_, studyInstanceUid, seriesInstanceUid);
315 }
316
317 bool IsSeriesComplete(const std::string& seriesInstanceUid)
318 {
319 OrthancStone::SeriesMetadataLoader::Accessor accessor(*metadataLoader_, seriesInstanceUid);
320 return accessor.IsComplete();
321 }
322
323 bool SortSeriesFrames(OrthancStone::SortedFrames& target,
324 const std::string& seriesInstanceUid)
325 {
326 OrthancStone::SeriesMetadataLoader::Accessor accessor(*metadataLoader_, seriesInstanceUid);
327
328 if (accessor.IsComplete())
329 {
330 target.Clear();
331
332 for (size_t i = 0; i < accessor.GetInstancesCount(); i++)
333 {
334 target.AddInstance(accessor.GetInstance(i));
335 }
336
337 target.Sort();
338
339 return true;
340 }
341 else
342 {
343 return false;
344 }
345 }
346
347 void AcquireObserver(IObserver* observer)
348 {
349 observer_.reset(observer);
350 }
351 };
352
353
354
355 class FramesCache : public boost::noncopyable
356 {
357 private:
358 class CachedImage : public Orthanc::ICacheable
359 {
360 private:
361 std::unique_ptr<Orthanc::ImageAccessor> image_;
362 unsigned int quality_;
363
364 public:
365 CachedImage(Orthanc::ImageAccessor* image,
366 unsigned int quality) :
367 image_(image),
368 quality_(quality)
369 {
370 assert(image != NULL);
371 }
372
373 virtual size_t GetMemoryUsage() const
374 {
375 assert(image_.get() != NULL);
376 return (image_->GetBytesPerPixel() * image_->GetPitch() * image_->GetHeight());
377 }
378
379 const Orthanc::ImageAccessor& GetImage() const
380 {
381 assert(image_.get() != NULL);
382 return *image_;
383 }
384
385 unsigned int GetQuality() const
386 {
387 return quality_;
388 }
389 };
390
391
392 static std::string GetKey(const std::string& sopInstanceUid,
393 size_t frameIndex)
394 {
395 return sopInstanceUid + "|" + boost::lexical_cast<std::string>(frameIndex);
396 }
397
398
399 Orthanc::MemoryObjectCache cache_;
400
401 public:
402 FramesCache()
403 {
404 SetMaximumSize(100 * 1024 * 1024); // 100 MB
405 }
406
407 size_t GetMaximumSize()
408 {
409 return cache_.GetMaximumSize();
410 }
411
412 void SetMaximumSize(size_t size)
413 {
414 cache_.SetMaximumSize(size);
415 }
416
417 /**
418 * Returns "true" iff the provided image has better quality than the
419 * previously cached one, or if no cache was previously available.
420 **/
421 bool Acquire(const std::string& sopInstanceUid,
422 size_t frameIndex,
423 Orthanc::ImageAccessor* image /* transfer ownership */,
424 unsigned int quality)
425 {
426 std::unique_ptr<Orthanc::ImageAccessor> protection(image);
427
428 if (image == NULL)
429 {
430 throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
431 }
432 else if (image->GetFormat() != Orthanc::PixelFormat_Float32 &&
433 image->GetFormat() != Orthanc::PixelFormat_RGB24)
434 {
435 throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageFormat);
436 }
437
438 const std::string& key = GetKey(sopInstanceUid, frameIndex);
439
440 bool invalidate = false;
441
442 {
443 /**
444 * Access the previous cached entry, with side effect of tagging
445 * it as the most recently accessed frame (update of LRU recycling)
446 **/
447 Orthanc::MemoryObjectCache::Accessor accessor(cache_, key, false /* unique lock */);
448
449 if (accessor.IsValid())
450 {
451 const CachedImage& previous = dynamic_cast<const CachedImage&>(accessor.GetValue());
452
453 // There is already a cached image for this frame
454 if (previous.GetQuality() < quality)
455 {
456 // The previously stored image has poorer quality
457 invalidate = true;
458 }
459 else
460 {
461 // No update in the quality, don't change the cache
462 return false;
463 }
464 }
465 else
466 {
467 invalidate = false;
468 }
469 }
470
471 if (invalidate)
472 {
473 cache_.Invalidate(key);
474 }
475
476 cache_.Acquire(key, new CachedImage(protection.release(), quality));
477 return true;
478 }
479
480 class Accessor : public boost::noncopyable
481 {
482 private:
483 Orthanc::MemoryObjectCache::Accessor accessor_;
484
485 const CachedImage& GetCachedImage() const
486 {
487 if (IsValid())
488 {
489 return dynamic_cast<CachedImage&>(accessor_.GetValue());
490 }
491 else
492 {
493 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
494 }
495 }
496
497 public:
498 Accessor(FramesCache& that,
499 const std::string& sopInstanceUid,
500 size_t frameIndex) :
501 accessor_(that.cache_, GetKey(sopInstanceUid, frameIndex), false /* shared lock */)
502 {
503 }
504
505 bool IsValid() const
506 {
507 return accessor_.IsValid();
508 }
509
510 const Orthanc::ImageAccessor& GetImage() const
511 {
512 return GetCachedImage().GetImage();
513 }
514
515 unsigned int GetQuality() const
516 {
517 return GetCachedImage().GetQuality();
518 }
519 };
520 };
521
522
523
524 class SeriesCursor : public boost::noncopyable
525 {
526 public:
527 enum Action
528 {
529 Action_FastPlus,
530 Action_Plus,
531 Action_None,
532 Action_Minus,
533 Action_FastMinus
534 };
535
536 private:
537 std::vector<size_t> prefetch_;
538 int framesCount_;
539 int currentFrame_;
540 bool isCircular_;
541 int fastDelta_;
542 Action lastAction_;
543
544 int ComputeNextFrame(int currentFrame,
545 Action action) const
546 {
547 if (framesCount_ == 0)
548 {
549 assert(currentFrame == 0);
550 return 0;
551 }
552
553 int nextFrame = currentFrame;
554
555 switch (action)
556 {
557 case Action_FastPlus:
558 nextFrame += fastDelta_;
559 break;
560
561 case Action_Plus:
562 nextFrame += 1;
563 break;
564
565 case Action_None:
566 break;
567
568 case Action_Minus:
569 nextFrame -= 1;
570 break;
571
572 case Action_FastMinus:
573 nextFrame -= fastDelta_;
574 break;
575
576 default:
577 throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
578 }
579
580 if (isCircular_)
581 {
582 while (nextFrame < 0)
583 {
584 nextFrame += framesCount_;
585 }
586
587 while (nextFrame >= framesCount_)
588 {
589 nextFrame -= framesCount_;
590 }
591 }
592 else
593 {
594 if (nextFrame < 0)
595 {
596 nextFrame = 0;
597 }
598 else if (nextFrame >= framesCount_)
599 {
600 nextFrame = framesCount_ - 1;
601 }
602 }
603
604 return nextFrame;
605 }
606
607 void UpdatePrefetch()
608 {
609 /**
610 * This method will order the frames of the series according to
611 * the number of "actions" (i.e. mouse wheels) that are necessary
612 * to reach them, starting from the current frame. It is assumed
613 * that once one action is done, it is more likely that the user
614 * will do the same action just afterwards.
615 **/
616
617 prefetch_.clear();
618
619 if (framesCount_ == 0)
620 {
621 return;
622 }
623
624 prefetch_.reserve(framesCount_);
625
626 // Breadth-first search using a FIFO. The queue associates a frame
627 // and the action that is the most likely in this frame
628 typedef std::list< std::pair<int, Action> > Queue;
629
630 Queue queue;
631 std::set<int> visited; // Frames that have already been visited
632
633 queue.push_back(std::make_pair(currentFrame_, lastAction_));
634
635 while (!queue.empty())
636 {
637 int frame = queue.front().first;
638 Action previousAction = queue.front().second;
639 queue.pop_front();
640
641 if (visited.find(frame) == visited.end())
642 {
643 visited.insert(frame);
644 prefetch_.push_back(frame);
645
646 switch (previousAction)
647 {
648 case Action_None:
649 case Action_Plus:
650 queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_Plus), Action_Plus));
651 queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_Minus), Action_Minus));
652 queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_FastPlus), Action_FastPlus));
653 queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_FastMinus), Action_FastMinus));
654 break;
655
656 case Action_Minus:
657 queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_Minus), Action_Minus));
658 queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_Plus), Action_Plus));
659 queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_FastMinus), Action_FastMinus));
660 queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_FastPlus), Action_FastPlus));
661 break;
662
663 case Action_FastPlus:
664 queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_FastPlus), Action_FastPlus));
665 queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_FastMinus), Action_FastMinus));
666 queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_Plus), Action_Plus));
667 queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_Minus), Action_Minus));
668 break;
669
670 case Action_FastMinus:
671 queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_FastMinus), Action_FastMinus));
672 queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_FastPlus), Action_FastPlus));
673 queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_Minus), Action_Minus));
674 queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_Plus), Action_Plus));
675 break;
676
677 default:
678 throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
679 }
680 }
681 }
682
683 assert(prefetch_.size() == framesCount_);
684 }
685
686 bool CheckFrameIndex(int frame) const
687 {
688 return ((framesCount_ == 0 && frame == 0) ||
689 (framesCount_ > 0 && frame >= 0 && frame < framesCount_));
690 }
691
692 public:
693 SeriesCursor(size_t framesCount) :
694 framesCount_(framesCount),
695 currentFrame_(framesCount / 2), // Start at the middle frame
696 isCircular_(false),
697 lastAction_(Action_None)
698 {
699 SetFastDelta(framesCount / 20);
700 UpdatePrefetch();
701 }
702
703 void SetCircular(bool isCircular)
704 {
705 isCircular_ = isCircular;
706 UpdatePrefetch();
707 }
708
709 void SetFastDelta(int delta)
710 {
711 fastDelta_ = (delta < 0 ? -delta : delta);
712
713 if (fastDelta_ <= 0)
714 {
715 fastDelta_ = 1;
716 }
717 }
718
719 void SetCurrentIndex(size_t frame)
720 {
721 if (frame >= framesCount_)
722 {
723 throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
724 }
725 else
726 {
727 currentFrame_ = frame;
728 lastAction_ = Action_None;
729 UpdatePrefetch();
730 }
731 }
732
733 size_t GetCurrentIndex() const
734 {
735 assert(CheckFrameIndex(currentFrame_));
736 return static_cast<size_t>(currentFrame_);
737 }
738
739 void Apply(Action action)
740 {
741 currentFrame_ = ComputeNextFrame(currentFrame_, action);
742 lastAction_ = action;
743 UpdatePrefetch();
744 }
745
746 size_t GetPrefetchSize() const
747 {
748 assert(prefetch_.size() == framesCount_);
749 return prefetch_.size();
750 }
751
752 size_t GetPrefetchFrameIndex(size_t i) const
753 {
754 if (i >= prefetch_.size())
755 {
756 throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
757 }
758 else
759 {
760 assert(CheckFrameIndex(prefetch_[i]));
761 return static_cast<size_t>(prefetch_[i]);
762 }
763 }
764 };
765
766
767
768
769 class FrameGeometry
770 {
771 private:
772 bool isValid_;
773 std::string frameOfReferenceUid_;
774 OrthancStone::CoordinateSystem3D coordinates_;
775 double pixelSpacingX_;
776 double pixelSpacingY_;
777 OrthancStone::Extent2D extent_;
778
779 public:
780 FrameGeometry() :
781 isValid_(false)
782 {
783 }
784
785 FrameGeometry(const Orthanc::DicomMap& tags) :
786 isValid_(false),
787 coordinates_(tags)
788 {
789 if (!tags.LookupStringValue(
790 frameOfReferenceUid_, Orthanc::DICOM_TAG_FRAME_OF_REFERENCE_UID, false))
791 {
792 frameOfReferenceUid_.clear();
793 }
794
795 OrthancStone::GeometryToolbox::GetPixelSpacing(pixelSpacingX_, pixelSpacingY_, tags);
796
797 unsigned int rows, columns;
798 if (tags.HasTag(Orthanc::DICOM_TAG_IMAGE_POSITION_PATIENT) &&
799 tags.HasTag(Orthanc::DICOM_TAG_IMAGE_ORIENTATION_PATIENT) &&
800 tags.ParseUnsignedInteger32(rows, Orthanc::DICOM_TAG_ROWS) &&
801 tags.ParseUnsignedInteger32(columns, Orthanc::DICOM_TAG_COLUMNS))
802 {
803 double ox = -pixelSpacingX_ / 2.0;
804 double oy = -pixelSpacingY_ / 2.0;
805 extent_.AddPoint(ox, oy);
806 extent_.AddPoint(ox + pixelSpacingX_ * static_cast<double>(columns),
807 oy + pixelSpacingY_ * static_cast<double>(rows));
808
809 isValid_ = true;
810 }
811 }
812
813 bool IsValid() const
814 {
815 return isValid_;
816 }
817
818 const std::string& GetFrameOfReferenceUid() const
819 {
820 if (isValid_)
821 {
822 return frameOfReferenceUid_;
823 }
824 else
825 {
826 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
827 }
828 }
829
830 const OrthancStone::CoordinateSystem3D& GetCoordinates() const
831 {
832 if (isValid_)
833 {
834 return coordinates_;
835 }
836 else
837 {
838 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
839 }
840 }
841
842 double GetPixelSpacingX() const
843 {
844 if (isValid_)
845 {
846 return pixelSpacingX_;
847 }
848 else
849 {
850 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
851 }
852 }
853
854 double GetPixelSpacingY() const
855 {
856 if (isValid_)
857 {
858 return pixelSpacingY_;
859 }
860 else
861 {
862 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
863 }
864 }
865
866 bool Intersect(double& x1, // Coordinates of the clipped line (out)
867 double& y1,
868 double& x2,
869 double& y2,
870 const FrameGeometry& other) const
871 {
872 if (this == &other)
873 {
874 return false;
875 }
876
877 OrthancStone::Vector direction, origin;
878
879 if (IsValid() &&
880 other.IsValid() &&
881 !extent_.IsEmpty() &&
882 frameOfReferenceUid_ == other.frameOfReferenceUid_ &&
883 OrthancStone::GeometryToolbox::IntersectTwoPlanes(
884 origin, direction,
885 coordinates_.GetOrigin(), coordinates_.GetNormal(),
886 other.coordinates_.GetOrigin(), other.coordinates_.GetNormal()))
887 {
888 double ax, ay, bx, by;
889 coordinates_.ProjectPoint(ax, ay, origin);
890 coordinates_.ProjectPoint(bx, by, origin + 100.0 * direction);
891
892 return OrthancStone::GeometryToolbox::ClipLineToRectangle(
893 x1, y1, x2, y2,
894 ax, ay, bx, by,
895 extent_.GetX1(), extent_.GetY1(), extent_.GetX2(), extent_.GetY2());
896 }
897 else
898 {
899 return false;
900 }
901 }
902 };
903
904
905
906 class ViewerViewport : public OrthancStone::ObserverBase<ViewerViewport>
907 {
908 public:
909 class IObserver : public boost::noncopyable
910 {
911 public:
912 virtual ~IObserver()
913 {
914 }
915
916 virtual void SignalFrameUpdated(const ViewerViewport& viewport,
917 size_t currentFrame,
918 size_t countFrames,
919 DisplayedFrameQuality quality) = 0;
920 };
921
922 private:
923 static const int LAYER_TEXTURE = 0;
924 static const int LAYER_REFERENCE_LINES = 1;
925
926
927 class ICommand : public Orthanc::IDynamicObject
928 {
929 private:
930 boost::shared_ptr<ViewerViewport> viewport_;
931
932 public:
933 ICommand(boost::shared_ptr<ViewerViewport> viewport) :
934 viewport_(viewport)
935 {
936 if (viewport == NULL)
937 {
938 throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
939 }
940 }
941
942 virtual ~ICommand()
943 {
944 }
945
946 ViewerViewport& GetViewport() const
947 {
948 assert(viewport_ != NULL);
949 return *viewport_;
950 }
951
952 virtual void Handle(const OrthancStone::DicomResourcesLoader::SuccessMessage& message) const
953 {
954 throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
955 }
956
957 virtual void Handle(const OrthancStone::HttpCommand::SuccessMessage& message) const
958 {
959 throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
960 }
961
962 virtual void Handle(const OrthancStone::ParseDicomSuccessMessage& message) const
963 {
964 throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
965 }
966 };
967
968 class SetDefaultWindowingCommand : public ICommand
969 {
970 public:
971 SetDefaultWindowingCommand(boost::shared_ptr<ViewerViewport> viewport) :
972 ICommand(viewport)
973 {
974 }
975
976 virtual void Handle(const OrthancStone::DicomResourcesLoader::SuccessMessage& message) const ORTHANC_OVERRIDE
977 {
978 if (message.GetResources()->GetSize() != 1)
979 {
980 throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol);
981 }
982
983 const Orthanc::DicomMap& dicom = message.GetResources()->GetResource(0);
984
985 {
986 OrthancStone::DicomInstanceParameters params(dicom);
987
988 if (params.HasDefaultWindowing())
989 {
990 GetViewport().defaultWindowingCenter_ = params.GetDefaultWindowingCenter();
991 GetViewport().defaultWindowingWidth_ = params.GetDefaultWindowingWidth();
992 LOG(INFO) << "Default windowing: " << params.GetDefaultWindowingCenter()
993 << "," << params.GetDefaultWindowingWidth();
994
995 GetViewport().windowingCenter_ = params.GetDefaultWindowingCenter();
996 GetViewport().windowingWidth_ = params.GetDefaultWindowingWidth();
997 }
998 else
999 {
1000 LOG(INFO) << "No default windowing";
1001 GetViewport().ResetDefaultWindowing();
1002 }
1003 }
1004
1005 GetViewport().DisplayCurrentFrame();
1006 }
1007 };
1008
1009 class SetLowQualityFrame : public ICommand
1010 {
1011 private:
1012 std::string sopInstanceUid_;
1013 unsigned int frameIndex_;
1014 float windowCenter_;
1015 float windowWidth_;
1016 bool isMonochrome1_;
1017 bool isPrefetch_;
1018
1019 public:
1020 SetLowQualityFrame(boost::shared_ptr<ViewerViewport> viewport,
1021 const std::string& sopInstanceUid,
1022 unsigned int frameIndex,
1023 float windowCenter,
1024 float windowWidth,
1025 bool isMonochrome1,
1026 bool isPrefetch) :
1027 ICommand(viewport),
1028 sopInstanceUid_(sopInstanceUid),
1029 frameIndex_(frameIndex),
1030 windowCenter_(windowCenter),
1031 windowWidth_(windowWidth),
1032 isMonochrome1_(isMonochrome1),
1033 isPrefetch_(isPrefetch)
1034 {
1035 }
1036
1037 virtual void Handle(const OrthancStone::HttpCommand::SuccessMessage& message) const ORTHANC_OVERRIDE
1038 {
1039 std::unique_ptr<Orthanc::JpegReader> jpeg(new Orthanc::JpegReader);
1040 jpeg->ReadFromMemory(message.GetAnswer());
1041
1042 bool updatedCache;
1043
1044 switch (jpeg->GetFormat())
1045 {
1046 case Orthanc::PixelFormat_RGB24:
1047 updatedCache = GetViewport().cache_->Acquire(
1048 sopInstanceUid_, frameIndex_, jpeg.release(), QUALITY_JPEG);
1049 break;
1050
1051 case Orthanc::PixelFormat_Grayscale8:
1052 {
1053 if (isMonochrome1_)
1054 {
1055 Orthanc::ImageProcessing::Invert(*jpeg);
1056 }
1057
1058 std::unique_ptr<Orthanc::Image> converted(
1059 new Orthanc::Image(Orthanc::PixelFormat_Float32, jpeg->GetWidth(),
1060 jpeg->GetHeight(), false));
1061
1062 Orthanc::ImageProcessing::Convert(*converted, *jpeg);
1063
1064 /**
1065
1066 Orthanc::ImageProcessing::ShiftScale() computes "(x + offset) * scaling".
1067 The system to solve is thus:
1068
1069 (0 + offset) * scaling = windowingCenter - windowingWidth / 2 [a]
1070 (255 + offset) * scaling = windowingCenter + windowingWidth / 2 [b]
1071
1072 Resolution:
1073
1074 [b - a] => 255 * scaling = windowingWidth
1075 [a] => offset = (windowingCenter - windowingWidth / 2) / scaling
1076
1077 **/
1078
1079 const float scaling = windowWidth_ / 255.0f;
1080 const float offset = (OrthancStone::LinearAlgebra::IsCloseToZero(scaling) ? 0 :
1081 (windowCenter_ - windowWidth_ / 2.0f) / scaling);
1082
1083 Orthanc::ImageProcessing::ShiftScale(*converted, offset, scaling, false);
1084 updatedCache = GetViewport().cache_->Acquire(
1085 sopInstanceUid_, frameIndex_, converted.release(), QUALITY_JPEG);
1086 break;
1087 }
1088
1089 default:
1090 throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
1091 }
1092
1093 if (updatedCache)
1094 {
1095 GetViewport().SignalUpdatedFrame(sopInstanceUid_, frameIndex_);
1096 }
1097
1098 if (isPrefetch_)
1099 {
1100 GetViewport().ScheduleNextPrefetch();
1101 }
1102 }
1103 };
1104
1105
1106 class SetFullDicomFrame : public ICommand
1107 {
1108 private:
1109 std::string sopInstanceUid_;
1110 unsigned int frameIndex_;
1111 bool isPrefetch_;
1112
1113 public:
1114 SetFullDicomFrame(boost::shared_ptr<ViewerViewport> viewport,
1115 const std::string& sopInstanceUid,
1116 unsigned int frameIndex,
1117 bool isPrefetch) :
1118 ICommand(viewport),
1119 sopInstanceUid_(sopInstanceUid),
1120 frameIndex_(frameIndex),
1121 isPrefetch_(isPrefetch)
1122 {
1123 }
1124
1125 virtual void Handle(const OrthancStone::ParseDicomSuccessMessage& message) const ORTHANC_OVERRIDE
1126 {
1127 Orthanc::DicomMap tags;
1128 message.GetDicom().ExtractDicomSummary(tags);
1129
1130 std::string s;
1131 if (!tags.LookupStringValue(s, Orthanc::DICOM_TAG_SOP_INSTANCE_UID, false))
1132 {
1133 // Safety check
1134 throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
1135 }
1136
1137 std::unique_ptr<Orthanc::ImageAccessor> frame(
1138 Orthanc::DicomImageDecoder::Decode(message.GetDicom(), frameIndex_));
1139
1140 if (frame.get() == NULL)
1141 {
1142 throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
1143 }
1144
1145 bool updatedCache;
1146
1147 if (frame->GetFormat() == Orthanc::PixelFormat_RGB24)
1148 {
1149 updatedCache = GetViewport().cache_->Acquire(
1150 sopInstanceUid_, frameIndex_, frame.release(), QUALITY_FULL);
1151 }
1152 else
1153 {
1154 double a = 1;
1155 double b = 0;
1156
1157 double doseScaling;
1158 if (tags.ParseDouble(doseScaling, Orthanc::DICOM_TAG_DOSE_GRID_SCALING))
1159 {
1160 a = doseScaling;
1161 }
1162
1163 double rescaleIntercept, rescaleSlope;
1164 if (tags.ParseDouble(rescaleIntercept, Orthanc::DICOM_TAG_RESCALE_INTERCEPT) &&
1165 tags.ParseDouble(rescaleSlope, Orthanc::DICOM_TAG_RESCALE_SLOPE))
1166 {
1167 a *= rescaleSlope;
1168 b = rescaleIntercept;
1169 }
1170
1171 std::unique_ptr<Orthanc::ImageAccessor> converted(
1172 new Orthanc::Image(Orthanc::PixelFormat_Float32, frame->GetWidth(), frame->GetHeight(), false));
1173 Orthanc::ImageProcessing::Convert(*converted, *frame);
1174 Orthanc::ImageProcessing::ShiftScale2(*converted, b, a, false);
1175
1176 updatedCache = GetViewport().cache_->Acquire(
1177 sopInstanceUid_, frameIndex_, converted.release(), QUALITY_FULL);
1178 }
1179
1180 if (updatedCache)
1181 {
1182 GetViewport().SignalUpdatedFrame(sopInstanceUid_, frameIndex_);
1183 }
1184
1185 if (isPrefetch_)
1186 {
1187 GetViewport().ScheduleNextPrefetch();
1188 }
1189 }
1190 };
1191
1192
1193 class PrefetchItem
1194 {
1195 private:
1196 size_t frameIndex_;
1197 bool isFull_;
1198
1199 public:
1200 PrefetchItem(size_t frameIndex,
1201 bool isFull) :
1202 frameIndex_(frameIndex),
1203 isFull_(isFull)
1204 {
1205 }
1206
1207 size_t GetFrameIndex() const
1208 {
1209 return frameIndex_;
1210 }
1211
1212 bool IsFull() const
1213 {
1214 return isFull_;
1215 }
1216 };
1217
1218
1219 std::unique_ptr<IObserver> observer_;
1220 OrthancStone::ILoadersContext& context_;
1221 boost::shared_ptr<OrthancStone::WebGLViewport> viewport_;
1222 boost::shared_ptr<OrthancStone::DicomResourcesLoader> loader_;
1223 OrthancStone::DicomSource source_;
1224 boost::shared_ptr<FramesCache> cache_;
1225 std::unique_ptr<OrthancStone::SortedFrames> frames_;
1226 std::unique_ptr<SeriesCursor> cursor_;
1227 float windowingCenter_;
1228 float windowingWidth_;
1229 float defaultWindowingCenter_;
1230 float defaultWindowingWidth_;
1231 bool inverted_;
1232 bool fitNextContent_;
1233 bool isCtrlDown_;
1234 FrameGeometry currentFrameGeometry_;
1235 std::list<PrefetchItem> prefetchQueue_;
1236
1237 void ScheduleNextPrefetch()
1238 {
1239 while (!prefetchQueue_.empty())
1240 {
1241 size_t index = prefetchQueue_.front().GetFrameIndex();
1242 bool isFull = prefetchQueue_.front().IsFull();
1243 prefetchQueue_.pop_front();
1244
1245 const std::string sopInstanceUid = frames_->GetFrameSopInstanceUid(index);
1246 unsigned int frame = frames_->GetFrameIndex(index);
1247
1248 {
1249 FramesCache::Accessor accessor(*cache_, sopInstanceUid, frame);
1250 if (!accessor.IsValid() ||
1251 (isFull && accessor.GetQuality() == 0))
1252 {
1253 if (isFull)
1254 {
1255 ScheduleLoadFullDicomFrame(index, PRIORITY_NORMAL, true);
1256 }
1257 else
1258 {
1259 ScheduleLoadRenderedFrame(index, PRIORITY_NORMAL, true);
1260 }
1261 return;
1262 }
1263 }
1264 }
1265 }
1266
1267
1268 void ResetDefaultWindowing()
1269 {
1270 defaultWindowingCenter_ = 128;
1271 defaultWindowingWidth_ = 256;
1272
1273 windowingCenter_ = defaultWindowingCenter_;
1274 windowingWidth_ = defaultWindowingWidth_;
1275
1276 inverted_ = false;
1277 }
1278
1279 void SignalUpdatedFrame(const std::string& sopInstanceUid,
1280 unsigned int frameIndex)
1281 {
1282 if (cursor_.get() != NULL &&
1283 frames_.get() != NULL)
1284 {
1285 size_t index = cursor_->GetCurrentIndex();
1286
1287 if (frames_->GetFrameSopInstanceUid(index) == sopInstanceUid &&
1288 frames_->GetFrameIndex(index) == frameIndex)
1289 {
1290 DisplayCurrentFrame();
1291 }
1292 }
1293 }
1294
1295 void DisplayCurrentFrame()
1296 {
1297 DisplayedFrameQuality quality = DisplayedFrameQuality_None;
1298
1299 if (cursor_.get() != NULL &&
1300 frames_.get() != NULL)
1301 {
1302 const size_t index = cursor_->GetCurrentIndex();
1303
1304 unsigned int cachedQuality;
1305 if (!DisplayFrame(cachedQuality, index))
1306 {
1307 // This frame is not cached yet: Load it
1308 if (source_.HasDicomWebRendered())
1309 {
1310 ScheduleLoadRenderedFrame(index, PRIORITY_HIGH, false /* not a prefetch */);
1311 }
1312 else
1313 {
1314 ScheduleLoadFullDicomFrame(index, PRIORITY_HIGH, false /* not a prefetch */);
1315 }
1316 }
1317 else if (cachedQuality < QUALITY_FULL)
1318 {
1319 // This frame is only available in low-res: Download the full DICOM
1320 ScheduleLoadFullDicomFrame(index, PRIORITY_HIGH, false /* not a prefetch */);
1321 quality = DisplayedFrameQuality_Low;
1322 }
1323 else
1324 {
1325 quality = DisplayedFrameQuality_High;
1326 }
1327
1328 currentFrameGeometry_ = FrameGeometry(frames_->GetFrameTags(index));
1329
1330 {
1331 // Prepare prefetching
1332 prefetchQueue_.clear();
1333 for (size_t i = 0; i < cursor_->GetPrefetchSize() && i < 16; i++)
1334 {
1335 size_t a = cursor_->GetPrefetchFrameIndex(i);
1336 if (a != index)
1337 {
1338 prefetchQueue_.push_back(PrefetchItem(a, i < 2));
1339 }
1340 }
1341
1342 ScheduleNextPrefetch();
1343 }
1344
1345 if (observer_.get() != NULL)
1346 {
1347 observer_->SignalFrameUpdated(*this, cursor_->GetCurrentIndex(),
1348 frames_->GetFramesCount(), quality);
1349 }
1350 }
1351 else
1352 {
1353 currentFrameGeometry_ = FrameGeometry();
1354 }
1355 }
1356
1357 void ClearViewport()
1358 {
1359 {
1360 std::unique_ptr<OrthancStone::IViewport::ILock> lock(viewport_->Lock());
1361 lock->GetController().GetScene().DeleteLayer(LAYER_TEXTURE);
1362 //lock->GetCompositor().Refresh(lock->GetController().GetScene());
1363 lock->Invalidate();
1364 }
1365 }
1366
1367 bool DisplayFrame(unsigned int& quality,
1368 size_t index)
1369 {
1370 if (frames_.get() == NULL)
1371 {
1372 return false;
1373 }
1374
1375 const std::string sopInstanceUid = frames_->GetFrameSopInstanceUid(index);
1376 unsigned int frame = frames_->GetFrameIndex(index);
1377
1378 FramesCache::Accessor accessor(*cache_, sopInstanceUid, frame);
1379 if (accessor.IsValid())
1380 {
1381 quality = accessor.GetQuality();
1382
1383 std::unique_ptr<OrthancStone::TextureBaseSceneLayer> layer;
1384
1385 switch (accessor.GetImage().GetFormat())
1386 {
1387 case Orthanc::PixelFormat_RGB24:
1388 layer.reset(new OrthancStone::ColorTextureSceneLayer(accessor.GetImage()));
1389 break;
1390
1391 case Orthanc::PixelFormat_Float32:
1392 {
1393 std::unique_ptr<OrthancStone::FloatTextureSceneLayer> tmp(
1394 new OrthancStone::FloatTextureSceneLayer(accessor.GetImage()));
1395 tmp->SetCustomWindowing(windowingCenter_, windowingWidth_);
1396 tmp->SetInverted(inverted_ ^ frames_->IsFrameMonochrome1(index));
1397 layer.reset(tmp.release());
1398 break;
1399 }
1400
1401 default:
1402 throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageFormat);
1403 }
1404
1405 layer->SetLinearInterpolation(true);
1406
1407 double pixelSpacingX, pixelSpacingY;
1408 OrthancStone::GeometryToolbox::GetPixelSpacing(
1409 pixelSpacingX, pixelSpacingY, frames_->GetFrameTags(index));
1410 layer->SetPixelSpacing(pixelSpacingX, pixelSpacingY);
1411
1412 if (layer.get() == NULL)
1413 {
1414 return false;
1415 }
1416 else
1417 {
1418 std::unique_ptr<OrthancStone::IViewport::ILock> lock(viewport_->Lock());
1419 lock->GetController().GetScene().SetLayer(LAYER_TEXTURE, layer.release());
1420
1421 if (fitNextContent_)
1422 {
1423 lock->GetCompositor().RefreshCanvasSize();
1424 lock->GetCompositor().FitContent(lock->GetController().GetScene());
1425 fitNextContent_ = false;
1426 }
1427
1428 //lock->GetCompositor().Refresh(lock->GetController().GetScene());
1429 lock->Invalidate();
1430 return true;
1431 }
1432 }
1433 else
1434 {
1435 return false;
1436 }
1437 }
1438
1439 void ScheduleLoadFullDicomFrame(size_t index,
1440 int priority,
1441 bool isPrefetch)
1442 {
1443 if (frames_.get() != NULL)
1444 {
1445 std::string sopInstanceUid = frames_->GetFrameSopInstanceUid(index);
1446 unsigned int frame = frames_->GetFrameIndex(index);
1447
1448 {
1449 std::unique_ptr<OrthancStone::ILoadersContext::ILock> lock(context_.Lock());
1450 lock->Schedule(
1451 GetSharedObserver(), priority, OrthancStone::ParseDicomFromWadoCommand::Create(
1452 source_, frames_->GetStudyInstanceUid(), frames_->GetSeriesInstanceUid(),
1453 sopInstanceUid, false /* transcoding (TODO) */,
1454 Orthanc::DicomTransferSyntax_LittleEndianExplicit /* TODO */,
1455 new SetFullDicomFrame(GetSharedObserver(), sopInstanceUid, frame, isPrefetch)));
1456 }
1457 }
1458 }
1459
1460 void ScheduleLoadRenderedFrame(size_t index,
1461 int priority,
1462 bool isPrefetch)
1463 {
1464 if (!source_.HasDicomWebRendered())
1465 {
1466 ScheduleLoadFullDicomFrame(index, priority, isPrefetch);
1467 }
1468 else if (frames_.get() != NULL)
1469 {
1470 std::string sopInstanceUid = frames_->GetFrameSopInstanceUid(index);
1471 unsigned int frame = frames_->GetFrameIndex(index);
1472 bool isMonochrome1 = frames_->IsFrameMonochrome1(index);
1473
1474 const std::string uri = ("studies/" + frames_->GetStudyInstanceUid() +
1475 "/series/" + frames_->GetSeriesInstanceUid() +
1476 "/instances/" + sopInstanceUid +
1477 "/frames/" + boost::lexical_cast<std::string>(frame + 1) + "/rendered");
1478
1479 std::map<std::string, std::string> headers, arguments;
1480 arguments["window"] = (
1481 boost::lexical_cast<std::string>(defaultWindowingCenter_) + "," +
1482 boost::lexical_cast<std::string>(defaultWindowingWidth_) + ",linear");
1483
1484 std::unique_ptr<OrthancStone::IOracleCommand> command(
1485 source_.CreateDicomWebCommand(
1486 uri, arguments, headers, new SetLowQualityFrame(
1487 GetSharedObserver(), sopInstanceUid, frame,
1488 defaultWindowingCenter_, defaultWindowingWidth_, isMonochrome1, isPrefetch)));
1489
1490 {
1491 std::unique_ptr<OrthancStone::ILoadersContext::ILock> lock(context_.Lock());
1492 lock->Schedule(GetSharedObserver(), priority, command.release());
1493 }
1494 }
1495 }
1496
1497 ViewerViewport(OrthancStone::ILoadersContext& context,
1498 const OrthancStone::DicomSource& source,
1499 const std::string& canvas,
1500 boost::shared_ptr<FramesCache> cache) :
1501 context_(context),
1502 source_(source),
1503 viewport_(OrthancStone::WebGLViewport::Create(canvas)),
1504 cache_(cache),
1505 fitNextContent_(true),
1506 isCtrlDown_(false)
1507 {
1508 if (!cache_)
1509 {
1510 throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
1511 }
1512
1513 emscripten_set_wheel_callback(viewport_->GetCanvasCssSelector().c_str(), this, true, OnWheel);
1514 emscripten_set_keydown_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, this, false, OnKey);
1515 emscripten_set_keyup_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, this, false, OnKey);
1516
1517 ResetDefaultWindowing();
1518 }
1519
1520 static EM_BOOL OnKey(int eventType,
1521 const EmscriptenKeyboardEvent *event,
1522 void *userData)
1523 {
1524 /**
1525 * WARNING: There is a problem with Firefox 71 that seems to mess
1526 * the "ctrlKey" value.
1527 **/
1528
1529 ViewerViewport& that = *reinterpret_cast<ViewerViewport*>(userData);
1530 that.isCtrlDown_ = event->ctrlKey;
1531 return false;
1532 }
1533
1534
1535 static EM_BOOL OnWheel(int eventType,
1536 const EmscriptenWheelEvent *wheelEvent,
1537 void *userData)
1538 {
1539 ViewerViewport& that = *reinterpret_cast<ViewerViewport*>(userData);
1540
1541 if (that.cursor_.get() != NULL)
1542 {
1543 if (wheelEvent->deltaY < 0)
1544 {
1545 that.ChangeFrame(that.isCtrlDown_ ? SeriesCursor::Action_FastMinus : SeriesCursor::Action_Minus);
1546 }
1547 else if (wheelEvent->deltaY > 0)
1548 {
1549 that.ChangeFrame(that.isCtrlDown_ ? SeriesCursor::Action_FastPlus : SeriesCursor::Action_Plus);
1550 }
1551 }
1552
1553 return true;
1554 }
1555
1556 void Handle(const OrthancStone::DicomResourcesLoader::SuccessMessage& message)
1557 {
1558 dynamic_cast<const ICommand&>(message.GetUserPayload()).Handle(message);
1559 }
1560
1561 void Handle(const OrthancStone::HttpCommand::SuccessMessage& message)
1562 {
1563 dynamic_cast<const ICommand&>(message.GetOrigin().GetPayload()).Handle(message);
1564 }
1565
1566 void Handle(const OrthancStone::ParseDicomSuccessMessage& message)
1567 {
1568 dynamic_cast<const ICommand&>(message.GetOrigin().GetPayload()).Handle(message);
1569 }
1570
1571 public:
1572 static boost::shared_ptr<ViewerViewport> Create(OrthancStone::ILoadersContext::ILock& lock,
1573 const OrthancStone::DicomSource& source,
1574 const std::string& canvas,
1575 boost::shared_ptr<FramesCache> cache)
1576 {
1577 boost::shared_ptr<ViewerViewport> viewport(
1578 new ViewerViewport(lock.GetContext(), source, canvas, cache));
1579
1580 viewport->loader_ = OrthancStone::DicomResourcesLoader::Create(lock);
1581 viewport->Register<OrthancStone::DicomResourcesLoader::SuccessMessage>(
1582 *viewport->loader_, &ViewerViewport::Handle);
1583
1584 viewport->Register<OrthancStone::HttpCommand::SuccessMessage>(
1585 lock.GetOracleObservable(), &ViewerViewport::Handle);
1586
1587 viewport->Register<OrthancStone::ParseDicomSuccessMessage>(
1588 lock.GetOracleObservable(), &ViewerViewport::Handle);
1589
1590 return viewport;
1591 }
1592
1593 void SetFrames(OrthancStone::SortedFrames* frames)
1594 {
1595 if (frames == NULL)
1596 {
1597 throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
1598 }
1599
1600 fitNextContent_ = true;
1601
1602 frames_.reset(frames);
1603 cursor_.reset(new SeriesCursor(frames_->GetFramesCount()));
1604
1605 LOG(INFO) << "Number of frames in series: " << frames_->GetFramesCount();
1606
1607 ResetDefaultWindowing();
1608 ClearViewport();
1609 prefetchQueue_.clear();
1610 currentFrameGeometry_ = FrameGeometry();
1611
1612 if (observer_.get() != NULL)
1613 {
1614 observer_->SignalFrameUpdated(*this, cursor_->GetCurrentIndex(),
1615 frames_->GetFramesCount(), DisplayedFrameQuality_None);
1616 }
1617
1618 if (frames_->GetFramesCount() != 0)
1619 {
1620 const std::string& sopInstanceUid = frames_->GetFrameSopInstanceUid(cursor_->GetCurrentIndex());
1621
1622 {
1623 // Fetch the default windowing for the central instance
1624 const std::string uri = ("studies/" + frames_->GetStudyInstanceUid() +
1625 "/series/" + frames_->GetSeriesInstanceUid() +
1626 "/instances/" + sopInstanceUid + "/metadata");
1627
1628 loader_->ScheduleGetDicomWeb(
1629 boost::make_shared<OrthancStone::LoadedDicomResources>(Orthanc::DICOM_TAG_SOP_INSTANCE_UID),
1630 0, source_, uri, new SetDefaultWindowingCommand(GetSharedObserver()));
1631 }
1632 }
1633 }
1634
1635 // This method is used when the layout of the HTML page changes,
1636 // which does not trigger the "emscripten_set_resize_callback()"
1637 void UpdateSize(bool fitContent)
1638 {
1639 std::unique_ptr<OrthancStone::IViewport::ILock> lock(viewport_->Lock());
1640 lock->GetCompositor().RefreshCanvasSize();
1641
1642 if (fitContent)
1643 {
1644 lock->GetCompositor().FitContent(lock->GetController().GetScene());
1645 }
1646
1647 lock->Invalidate();
1648 }
1649
1650 void AcquireObserver(IObserver* observer)
1651 {
1652 observer_.reset(observer);
1653 }
1654
1655 const std::string& GetCanvasId() const
1656 {
1657 assert(viewport_);
1658 return viewport_->GetCanvasId();
1659 }
1660
1661 void ChangeFrame(SeriesCursor::Action action)
1662 {
1663 if (cursor_.get() != NULL)
1664 {
1665 size_t previous = cursor_->GetCurrentIndex();
1666
1667 cursor_->Apply(action);
1668
1669 size_t current = cursor_->GetCurrentIndex();
1670 if (previous != current)
1671 {
1672 DisplayCurrentFrame();
1673 }
1674 }
1675 }
1676
1677 const FrameGeometry& GetCurrentFrameGeometry() const
1678 {
1679 return currentFrameGeometry_;
1680 }
1681
1682 void UpdateReferenceLines(const std::list<const FrameGeometry*>& planes)
1683 {
1684 std::unique_ptr<OrthancStone::PolylineSceneLayer> layer(new OrthancStone::PolylineSceneLayer);
1685
1686 if (GetCurrentFrameGeometry().IsValid())
1687 {
1688 for (std::list<const FrameGeometry*>::const_iterator
1689 it = planes.begin(); it != planes.end(); ++it)
1690 {
1691 assert(*it != NULL);
1692
1693 double x1, y1, x2, y2;
1694 if (GetCurrentFrameGeometry().Intersect(x1, y1, x2, y2, **it))
1695 {
1696 OrthancStone::PolylineSceneLayer::Chain chain;
1697 chain.push_back(OrthancStone::ScenePoint2D(x1, y1));
1698 chain.push_back(OrthancStone::ScenePoint2D(x2, y2));
1699 layer->AddChain(chain, false, 0, 255, 0);
1700 }
1701 }
1702 }
1703
1704 {
1705 std::unique_ptr<OrthancStone::IViewport::ILock> lock(viewport_->Lock());
1706
1707 if (layer->GetChainsCount() == 0)
1708 {
1709 lock->GetController().GetScene().DeleteLayer(LAYER_REFERENCE_LINES);
1710 }
1711 else
1712 {
1713 lock->GetController().GetScene().SetLayer(LAYER_REFERENCE_LINES, layer.release());
1714 }
1715
1716 //lock->GetCompositor().Refresh(lock->GetController().GetScene());
1717 lock->Invalidate();
1718 }
1719 }
1720
1721
1722 void ClearReferenceLines()
1723 {
1724 {
1725 std::unique_ptr<OrthancStone::IViewport::ILock> lock(viewport_->Lock());
1726 lock->GetController().GetScene().DeleteLayer(LAYER_REFERENCE_LINES);
1727 lock->Invalidate();
1728 }
1729 }
1730
1731
1732 void SetDefaultWindowing()
1733 {
1734 SetWindowing(defaultWindowingCenter_, defaultWindowingWidth_);
1735 }
1736
1737 void SetWindowing(float windowingCenter,
1738 float windowingWidth)
1739 {
1740 windowingCenter_ = windowingCenter;
1741 windowingWidth_ = windowingWidth;
1742
1743 {
1744 std::unique_ptr<OrthancStone::IViewport::ILock> lock(viewport_->Lock());
1745
1746 if (lock->GetController().GetScene().HasLayer(LAYER_TEXTURE) &&
1747 lock->GetController().GetScene().GetLayer(LAYER_TEXTURE).GetType() ==
1748 OrthancStone::ISceneLayer::Type_FloatTexture)
1749 {
1750 dynamic_cast<OrthancStone::FloatTextureSceneLayer&>(
1751 lock->GetController().GetScene().GetLayer(LAYER_TEXTURE)).
1752 SetCustomWindowing(windowingCenter_, windowingWidth_);
1753 lock->Invalidate();
1754 }
1755 }
1756 }
1757
1758 void Invert()
1759 {
1760 inverted_ = !inverted_;
1761
1762 {
1763 std::unique_ptr<OrthancStone::IViewport::ILock> lock(viewport_->Lock());
1764
1765 if (lock->GetController().GetScene().HasLayer(LAYER_TEXTURE) &&
1766 lock->GetController().GetScene().GetLayer(LAYER_TEXTURE).GetType() ==
1767 OrthancStone::ISceneLayer::Type_FloatTexture)
1768 {
1769 OrthancStone::FloatTextureSceneLayer& layer =
1770 dynamic_cast<OrthancStone::FloatTextureSceneLayer&>(
1771 lock->GetController().GetScene().GetLayer(LAYER_TEXTURE));
1772
1773 // NB: Using "IsInverted()" instead of "inverted_" is for
1774 // compatibility with MONOCHROME1 images
1775 layer.SetInverted(!layer.IsInverted());
1776 lock->Invalidate();
1777 }
1778 }
1779 }
1780 };
1781
1782
1783
1784
1785
1786 typedef std::map<std::string, boost::shared_ptr<ViewerViewport> > Viewports;
1787 static Viewports allViewports_;
1788 static bool showReferenceLines_ = true;
1789
1790
1791 static void UpdateReferenceLines()
1792 {
1793 if (showReferenceLines_)
1794 {
1795 std::list<const FrameGeometry*> planes;
1796
1797 for (Viewports::const_iterator it = allViewports_.begin(); it != allViewports_.end(); ++it)
1798 {
1799 assert(it->second != NULL);
1800 planes.push_back(&it->second->GetCurrentFrameGeometry());
1801 }
1802
1803 for (Viewports::iterator it = allViewports_.begin(); it != allViewports_.end(); ++it)
1804 {
1805 assert(it->second != NULL);
1806 it->second->UpdateReferenceLines(planes);
1807 }
1808 }
1809 else
1810 {
1811 for (Viewports::iterator it = allViewports_.begin(); it != allViewports_.end(); ++it)
1812 {
1813 assert(it->second != NULL);
1814 it->second->ClearReferenceLines();
1815 }
1816 }
1817 }
1818
1819
1820 class WebAssemblyObserver : public ResourcesLoader::IObserver,
1821 public ViewerViewport::IObserver
1822 {
1823 public:
1824 virtual void SignalResourcesLoaded() ORTHANC_OVERRIDE
1825 {
1826 DISPATCH_JAVASCRIPT_EVENT("ResourcesLoaded");
1827 }
1828
1829 virtual void SignalSeriesThumbnailLoaded(const std::string& studyInstanceUid,
1830 const std::string& seriesInstanceUid) ORTHANC_OVERRIDE
1831 {
1832 EM_ASM({
1833 const customEvent = document.createEvent("CustomEvent");
1834 customEvent.initCustomEvent("ThumbnailLoaded", false, false,
1835 { "studyInstanceUid" : UTF8ToString($0),
1836 "seriesInstanceUid" : UTF8ToString($1) });
1837 window.dispatchEvent(customEvent);
1838 },
1839 studyInstanceUid.c_str(),
1840 seriesInstanceUid.c_str());
1841 }
1842
1843 virtual void SignalSeriesMetadataLoaded(const std::string& studyInstanceUid,
1844 const std::string& seriesInstanceUid) ORTHANC_OVERRIDE
1845 {
1846 EM_ASM({
1847 const customEvent = document.createEvent("CustomEvent");
1848 customEvent.initCustomEvent("MetadataLoaded", false, false,
1849 { "studyInstanceUid" : UTF8ToString($0),
1850 "seriesInstanceUid" : UTF8ToString($1) });
1851 window.dispatchEvent(customEvent);
1852 },
1853 studyInstanceUid.c_str(),
1854 seriesInstanceUid.c_str());
1855 }
1856
1857 virtual void SignalFrameUpdated(const ViewerViewport& viewport,
1858 size_t currentFrame,
1859 size_t countFrames,
1860 DisplayedFrameQuality quality) ORTHANC_OVERRIDE
1861 {
1862 EM_ASM({
1863 const customEvent = document.createEvent("CustomEvent");
1864 customEvent.initCustomEvent("FrameUpdated", false, false,
1865 { "canvasId" : UTF8ToString($0),
1866 "currentFrame" : $1,
1867 "framesCount" : $2,
1868 "quality" : $3 });
1869 window.dispatchEvent(customEvent);
1870 },
1871 viewport.GetCanvasId().c_str(),
1872 static_cast<int>(currentFrame),
1873 static_cast<int>(countFrames),
1874 quality);
1875
1876
1877 UpdateReferenceLines();
1878 };
1879 };
1880
1881
1882
1883 static OrthancStone::DicomSource source_;
1884 static boost::shared_ptr<FramesCache> cache_;
1885 static boost::shared_ptr<OrthancStone::WebAssemblyLoadersContext> context_;
1886 static std::string stringBuffer_;
1887
1888
1889
1890 static void FormatTags(std::string& target,
1891 const Orthanc::DicomMap& tags)
1892 {
1893 Orthanc::DicomArray arr(tags);
1894 Json::Value v = Json::objectValue;
1895
1896 for (size_t i = 0; i < arr.GetSize(); i++)
1897 {
1898 const Orthanc::DicomElement& element = arr.GetElement(i);
1899 if (!element.GetValue().IsBinary() &&
1900 !element.GetValue().IsNull())
1901 {
1902 v[element.GetTag().Format()] = element.GetValue().GetContent();
1903 }
1904 }
1905
1906 target = v.toStyledString();
1907 }
1908
1909
1910 static ResourcesLoader& GetResourcesLoader()
1911 {
1912 static boost::shared_ptr<ResourcesLoader> resourcesLoader_;
1913
1914 if (!resourcesLoader_)
1915 {
1916 std::unique_ptr<OrthancStone::ILoadersContext::ILock> lock(context_->Lock());
1917 resourcesLoader_ = ResourcesLoader::Create(*lock, source_);
1918 resourcesLoader_->AcquireObserver(new WebAssemblyObserver);
1919 }
1920
1921 return *resourcesLoader_;
1922 }
1923
1924
1925 static boost::shared_ptr<ViewerViewport> GetViewport(const std::string& canvas)
1926 {
1927 Viewports::iterator found = allViewports_.find(canvas);
1928 if (found == allViewports_.end())
1929 {
1930 std::unique_ptr<OrthancStone::ILoadersContext::ILock> lock(context_->Lock());
1931 boost::shared_ptr<ViewerViewport> viewport(ViewerViewport::Create(*lock, source_, canvas, cache_));
1932 viewport->AcquireObserver(new WebAssemblyObserver);
1933 allViewports_[canvas] = viewport;
1934 return viewport;
1935 }
1936 else
1937 {
1938 return found->second;
1939 }
1940 }
1941
1942
1943 extern "C"
1944 {
1945 int main(int argc, char const *argv[])
1946 {
1947 printf("OK\n");
1948 Orthanc::InitializeFramework("", true);
1949 Orthanc::Logging::EnableInfoLevel(true);
1950 //Orthanc::Logging::EnableTraceLevel(true);
1951
1952 context_.reset(new OrthancStone::WebAssemblyLoadersContext(1, 4, 1));
1953 cache_.reset(new FramesCache);
1954
1955 DISPATCH_JAVASCRIPT_EVENT("StoneInitialized");
1956 }
1957
1958
1959 EMSCRIPTEN_KEEPALIVE
1960 void SetOrthancRoot(const char* uri,
1961 int useRendered)
1962 {
1963 try
1964 {
1965 context_->SetLocalOrthanc(uri); // For "source_.SetDicomWebThroughOrthancSource()"
1966 source_.SetDicomWebSource(std::string(uri) + "/dicom-web");
1967 source_.SetDicomWebRendered(useRendered != 0);
1968 }
1969 EXTERN_CATCH_EXCEPTIONS;
1970 }
1971
1972
1973 EMSCRIPTEN_KEEPALIVE
1974 void SetDicomWebServer(const char* serverName,
1975 int hasRendered)
1976 {
1977 try
1978 {
1979 source_.SetDicomWebThroughOrthancSource(serverName);
1980 source_.SetDicomWebRendered(hasRendered != 0);
1981 }
1982 EXTERN_CATCH_EXCEPTIONS;
1983 }
1984
1985
1986 EMSCRIPTEN_KEEPALIVE
1987 void FetchAllStudies()
1988 {
1989 try
1990 {
1991 GetResourcesLoader().FetchAllStudies();
1992 }
1993 EXTERN_CATCH_EXCEPTIONS;
1994 }
1995
1996 EMSCRIPTEN_KEEPALIVE
1997 void FetchStudy(const char* studyInstanceUid)
1998 {
1999 try
2000 {
2001 GetResourcesLoader().FetchStudy(studyInstanceUid);
2002 }
2003 EXTERN_CATCH_EXCEPTIONS;
2004 }
2005
2006 EMSCRIPTEN_KEEPALIVE
2007 void FetchSeries(const char* studyInstanceUid,
2008 const char* seriesInstanceUid)
2009 {
2010 try
2011 {
2012 GetResourcesLoader().FetchSeries(studyInstanceUid, seriesInstanceUid);
2013 }
2014 EXTERN_CATCH_EXCEPTIONS;
2015 }
2016
2017 EMSCRIPTEN_KEEPALIVE
2018 int GetStudiesCount()
2019 {
2020 try
2021 {
2022 return GetResourcesLoader().GetStudiesCount();
2023 }
2024 EXTERN_CATCH_EXCEPTIONS;
2025 return 0; // on exception
2026 }
2027
2028 EMSCRIPTEN_KEEPALIVE
2029 int GetSeriesCount()
2030 {
2031 try
2032 {
2033 return GetResourcesLoader().GetSeriesCount();
2034 }
2035 EXTERN_CATCH_EXCEPTIONS;
2036 return 0; // on exception
2037 }
2038
2039
2040 EMSCRIPTEN_KEEPALIVE
2041 const char* GetStringBuffer()
2042 {
2043 return stringBuffer_.c_str();
2044 }
2045
2046
2047 EMSCRIPTEN_KEEPALIVE
2048 void LoadStudyTags(int i)
2049 {
2050 try
2051 {
2052 if (i < 0)
2053 {
2054 throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
2055 }
2056
2057 Orthanc::DicomMap dicom;
2058 GetResourcesLoader().GetStudy(dicom, i);
2059 FormatTags(stringBuffer_, dicom);
2060 }
2061 EXTERN_CATCH_EXCEPTIONS;
2062 }
2063
2064
2065 EMSCRIPTEN_KEEPALIVE
2066 void LoadSeriesTags(int i)
2067 {
2068 try
2069 {
2070 if (i < 0)
2071 {
2072 throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
2073 }
2074
2075 Orthanc::DicomMap dicom;
2076 GetResourcesLoader().GetSeries(dicom, i);
2077 FormatTags(stringBuffer_, dicom);
2078 }
2079 EXTERN_CATCH_EXCEPTIONS;
2080 }
2081
2082
2083 EMSCRIPTEN_KEEPALIVE
2084 int LoadSeriesThumbnail(const char* seriesInstanceUid)
2085 {
2086 try
2087 {
2088 std::string image, mime;
2089 switch (GetResourcesLoader().GetSeriesThumbnail(image, mime, seriesInstanceUid))
2090 {
2091 case OrthancStone::SeriesThumbnailType_Image:
2092 Orthanc::Toolbox::EncodeDataUriScheme(stringBuffer_, mime, image);
2093 return ThumbnailType_Image;
2094
2095 case OrthancStone::SeriesThumbnailType_Pdf:
2096 return ThumbnailType_Pdf;
2097
2098 case OrthancStone::SeriesThumbnailType_Video:
2099 return ThumbnailType_Video;
2100
2101 case OrthancStone::SeriesThumbnailType_NotLoaded:
2102 return ThumbnailType_Loading;
2103
2104 case OrthancStone::SeriesThumbnailType_Unsupported:
2105 return ThumbnailType_NoPreview;
2106
2107 default:
2108 return ThumbnailType_Unknown;
2109 }
2110 }
2111 EXTERN_CATCH_EXCEPTIONS;
2112 return ThumbnailType_Unknown;
2113 }
2114
2115
2116 EMSCRIPTEN_KEEPALIVE
2117 void SpeedUpFetchSeriesMetadata(const char* studyInstanceUid,
2118 const char* seriesInstanceUid)
2119 {
2120 try
2121 {
2122 GetResourcesLoader().FetchSeriesMetadata(PRIORITY_HIGH, studyInstanceUid, seriesInstanceUid);
2123 }
2124 EXTERN_CATCH_EXCEPTIONS;
2125 }
2126
2127
2128 EMSCRIPTEN_KEEPALIVE
2129 int IsSeriesComplete(const char* seriesInstanceUid)
2130 {
2131 try
2132 {
2133 return GetResourcesLoader().IsSeriesComplete(seriesInstanceUid) ? 1 : 0;
2134 }
2135 EXTERN_CATCH_EXCEPTIONS;
2136 return 0;
2137 }
2138
2139 EMSCRIPTEN_KEEPALIVE
2140 int LoadSeriesInViewport(const char* canvas,
2141 const char* seriesInstanceUid)
2142 {
2143 try
2144 {
2145 std::unique_ptr<OrthancStone::SortedFrames> frames(new OrthancStone::SortedFrames);
2146
2147 if (GetResourcesLoader().SortSeriesFrames(*frames, seriesInstanceUid))
2148 {
2149 GetViewport(canvas)->SetFrames(frames.release());
2150 return 1;
2151 }
2152 else
2153 {
2154 return 0;
2155 }
2156 }
2157 EXTERN_CATCH_EXCEPTIONS;
2158 return 0;
2159 }
2160
2161
2162 EMSCRIPTEN_KEEPALIVE
2163 void AllViewportsUpdateSize(int fitContent)
2164 {
2165 try
2166 {
2167 for (Viewports::iterator it = allViewports_.begin(); it != allViewports_.end(); ++it)
2168 {
2169 assert(it->second != NULL);
2170 it->second->UpdateSize(fitContent != 0);
2171 }
2172 }
2173 EXTERN_CATCH_EXCEPTIONS;
2174 }
2175
2176
2177 EMSCRIPTEN_KEEPALIVE
2178 void DecrementFrame(const char* canvas,
2179 int fitContent)
2180 {
2181 try
2182 {
2183 GetViewport(canvas)->ChangeFrame(SeriesCursor::Action_Minus);
2184 }
2185 EXTERN_CATCH_EXCEPTIONS;
2186 }
2187
2188
2189 EMSCRIPTEN_KEEPALIVE
2190 void IncrementFrame(const char* canvas,
2191 int fitContent)
2192 {
2193 try
2194 {
2195 GetViewport(canvas)->ChangeFrame(SeriesCursor::Action_Plus);
2196 }
2197 EXTERN_CATCH_EXCEPTIONS;
2198 }
2199
2200
2201 EMSCRIPTEN_KEEPALIVE
2202 void ShowReferenceLines(int show)
2203 {
2204 try
2205 {
2206 showReferenceLines_ = (show != 0);
2207 UpdateReferenceLines();
2208 }
2209 EXTERN_CATCH_EXCEPTIONS;
2210 }
2211
2212
2213 EMSCRIPTEN_KEEPALIVE
2214 void SetDefaultWindowing(const char* canvas)
2215 {
2216 try
2217 {
2218 GetViewport(canvas)->SetDefaultWindowing();
2219 }
2220 EXTERN_CATCH_EXCEPTIONS;
2221 }
2222
2223
2224 EMSCRIPTEN_KEEPALIVE
2225 void SetWindowing(const char* canvas,
2226 int center,
2227 int width)
2228 {
2229 try
2230 {
2231 GetViewport(canvas)->SetWindowing(center, width);
2232 }
2233 EXTERN_CATCH_EXCEPTIONS;
2234 }
2235
2236
2237 EMSCRIPTEN_KEEPALIVE
2238 void InvertContrast(const char* canvas)
2239 {
2240 try
2241 {
2242 GetViewport(canvas)->Invert();
2243 }
2244 EXTERN_CATCH_EXCEPTIONS;
2245 }
2246 }