comparison Applications/StoneWebViewer/WebAssembly/StoneWebViewer.cpp @ 1538:d1806b4e4839

moving OrthancStone/Samples/ as Applications/Samples/
author Sebastien Jodogne <s.jodogne@gmail.com>
date Tue, 11 Aug 2020 13:24:38 +0200
parents StoneWebViewer/WebAssembly/StoneWebViewer.cpp@7b326e5ee97b
children 8ddf77198ed7
comparison
equal deleted inserted replaced
1537:de8cf5859e84 1538:d1806b4e4839
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 "../../OrthancStone/Sources/Loaders/DicomResourcesLoader.h"
65 #include "../../OrthancStone/Sources/Loaders/SeriesMetadataLoader.h"
66 #include "../../OrthancStone/Sources/Loaders/SeriesThumbnailsLoader.h"
67 #include "../../OrthancStone/Sources/Loaders/WebAssemblyLoadersContext.h"
68 #include "../../OrthancStone/Sources/Messages/ObserverBase.h"
69 #include "../../OrthancStone/Sources/Oracle/ParseDicomFromWadoCommand.h"
70 #include "../../OrthancStone/Sources/Scene2D/ColorTextureSceneLayer.h"
71 #include "../../OrthancStone/Sources/Scene2D/FloatTextureSceneLayer.h"
72 #include "../../OrthancStone/Sources/Scene2D/PolylineSceneLayer.h"
73 #include "../../OrthancStone/Sources/StoneException.h"
74 #include "../../OrthancStone/Sources/Toolbox/DicomInstanceParameters.h"
75 #include "../../OrthancStone/Sources/Toolbox/GeometryToolbox.h"
76 #include "../../OrthancStone/Sources/Toolbox/SortedFrames.h"
77 #include "../../OrthancStone/Sources/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, ORTHANC_STONE_MAX_TAG_LENGTH);
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 {
1382 std::unique_ptr<OrthancStone::IViewport::ILock> lock(viewport_->Lock());
1383
1384 OrthancStone::Scene2D& scene = lock->GetController().GetScene();
1385
1386 // Save the current windowing (that could have been altered by
1387 // GrayscaleWindowingSceneTracker), so that it can be reused
1388 // by the next frames
1389 if (scene.HasLayer(LAYER_TEXTURE) &&
1390 scene.GetLayer(LAYER_TEXTURE).GetType() == OrthancStone::ISceneLayer::Type_FloatTexture)
1391 {
1392 OrthancStone::FloatTextureSceneLayer& layer =
1393 dynamic_cast<OrthancStone::FloatTextureSceneLayer&>(scene.GetLayer(LAYER_TEXTURE));
1394 layer.GetWindowing(windowingCenter_, windowingWidth_);
1395 }
1396 }
1397
1398 quality = accessor.GetQuality();
1399
1400 std::unique_ptr<OrthancStone::TextureBaseSceneLayer> layer;
1401
1402 switch (accessor.GetImage().GetFormat())
1403 {
1404 case Orthanc::PixelFormat_RGB24:
1405 layer.reset(new OrthancStone::ColorTextureSceneLayer(accessor.GetImage()));
1406 break;
1407
1408 case Orthanc::PixelFormat_Float32:
1409 {
1410 std::unique_ptr<OrthancStone::FloatTextureSceneLayer> tmp(
1411 new OrthancStone::FloatTextureSceneLayer(accessor.GetImage()));
1412 tmp->SetCustomWindowing(windowingCenter_, windowingWidth_);
1413 tmp->SetInverted(inverted_ ^ frames_->IsFrameMonochrome1(index));
1414 layer.reset(tmp.release());
1415 break;
1416 }
1417
1418 default:
1419 throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageFormat);
1420 }
1421
1422 layer->SetLinearInterpolation(true);
1423
1424 double pixelSpacingX, pixelSpacingY;
1425 OrthancStone::GeometryToolbox::GetPixelSpacing(
1426 pixelSpacingX, pixelSpacingY, frames_->GetFrameTags(index));
1427 layer->SetPixelSpacing(pixelSpacingX, pixelSpacingY);
1428
1429 if (layer.get() == NULL)
1430 {
1431 return false;
1432 }
1433 else
1434 {
1435 std::unique_ptr<OrthancStone::IViewport::ILock> lock(viewport_->Lock());
1436
1437 OrthancStone::Scene2D& scene = lock->GetController().GetScene();
1438
1439 scene.SetLayer(LAYER_TEXTURE, layer.release());
1440
1441 if (fitNextContent_)
1442 {
1443 lock->GetCompositor().RefreshCanvasSize();
1444 lock->GetCompositor().FitContent(scene);
1445 fitNextContent_ = false;
1446 }
1447
1448 //lock->GetCompositor().Refresh(scene);
1449 lock->Invalidate();
1450 return true;
1451 }
1452 }
1453 else
1454 {
1455 return false;
1456 }
1457 }
1458
1459 void ScheduleLoadFullDicomFrame(size_t index,
1460 int priority,
1461 bool isPrefetch)
1462 {
1463 if (frames_.get() != NULL)
1464 {
1465 std::string sopInstanceUid = frames_->GetFrameSopInstanceUid(index);
1466 unsigned int frame = frames_->GetFrameIndex(index);
1467
1468 {
1469 std::unique_ptr<OrthancStone::ILoadersContext::ILock> lock(context_.Lock());
1470 lock->Schedule(
1471 GetSharedObserver(), priority, OrthancStone::ParseDicomFromWadoCommand::Create(
1472 source_, frames_->GetStudyInstanceUid(), frames_->GetSeriesInstanceUid(),
1473 sopInstanceUid, false /* transcoding (TODO) */,
1474 Orthanc::DicomTransferSyntax_LittleEndianExplicit /* TODO */,
1475 new SetFullDicomFrame(GetSharedObserver(), sopInstanceUid, frame, isPrefetch)));
1476 }
1477 }
1478 }
1479
1480 void ScheduleLoadRenderedFrame(size_t index,
1481 int priority,
1482 bool isPrefetch)
1483 {
1484 if (!source_.HasDicomWebRendered())
1485 {
1486 ScheduleLoadFullDicomFrame(index, priority, isPrefetch);
1487 }
1488 else if (frames_.get() != NULL)
1489 {
1490 std::string sopInstanceUid = frames_->GetFrameSopInstanceUid(index);
1491 unsigned int frame = frames_->GetFrameIndex(index);
1492 bool isMonochrome1 = frames_->IsFrameMonochrome1(index);
1493
1494 const std::string uri = ("studies/" + frames_->GetStudyInstanceUid() +
1495 "/series/" + frames_->GetSeriesInstanceUid() +
1496 "/instances/" + sopInstanceUid +
1497 "/frames/" + boost::lexical_cast<std::string>(frame + 1) + "/rendered");
1498
1499 std::map<std::string, std::string> headers, arguments;
1500 arguments["window"] = (
1501 boost::lexical_cast<std::string>(windowingCenter_) + "," +
1502 boost::lexical_cast<std::string>(windowingWidth_) + ",linear");
1503
1504 std::unique_ptr<OrthancStone::IOracleCommand> command(
1505 source_.CreateDicomWebCommand(
1506 uri, arguments, headers, new SetLowQualityFrame(
1507 GetSharedObserver(), sopInstanceUid, frame,
1508 windowingCenter_, windowingWidth_, isMonochrome1, isPrefetch)));
1509
1510 {
1511 std::unique_ptr<OrthancStone::ILoadersContext::ILock> lock(context_.Lock());
1512 lock->Schedule(GetSharedObserver(), priority, command.release());
1513 }
1514 }
1515 }
1516
1517 ViewerViewport(OrthancStone::ILoadersContext& context,
1518 const OrthancStone::DicomSource& source,
1519 const std::string& canvas,
1520 boost::shared_ptr<FramesCache> cache) :
1521 context_(context),
1522 source_(source),
1523 viewport_(OrthancStone::WebGLViewport::Create(canvas)),
1524 cache_(cache),
1525 fitNextContent_(true),
1526 isCtrlDown_(false)
1527 {
1528 if (!cache_)
1529 {
1530 throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
1531 }
1532
1533 emscripten_set_wheel_callback(viewport_->GetCanvasCssSelector().c_str(), this, true, OnWheel);
1534 emscripten_set_keydown_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, this, false, OnKey);
1535 emscripten_set_keyup_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, this, false, OnKey);
1536
1537 ResetDefaultWindowing();
1538 }
1539
1540 static EM_BOOL OnKey(int eventType,
1541 const EmscriptenKeyboardEvent *event,
1542 void *userData)
1543 {
1544 /**
1545 * WARNING: There is a problem with Firefox 71 that seems to mess
1546 * the "ctrlKey" value.
1547 **/
1548
1549 ViewerViewport& that = *reinterpret_cast<ViewerViewport*>(userData);
1550 that.isCtrlDown_ = event->ctrlKey;
1551 return false;
1552 }
1553
1554
1555 static EM_BOOL OnWheel(int eventType,
1556 const EmscriptenWheelEvent *wheelEvent,
1557 void *userData)
1558 {
1559 ViewerViewport& that = *reinterpret_cast<ViewerViewport*>(userData);
1560
1561 if (that.cursor_.get() != NULL)
1562 {
1563 if (wheelEvent->deltaY < 0)
1564 {
1565 that.ChangeFrame(that.isCtrlDown_ ? SeriesCursor::Action_FastMinus : SeriesCursor::Action_Minus);
1566 }
1567 else if (wheelEvent->deltaY > 0)
1568 {
1569 that.ChangeFrame(that.isCtrlDown_ ? SeriesCursor::Action_FastPlus : SeriesCursor::Action_Plus);
1570 }
1571 }
1572
1573 return true;
1574 }
1575
1576 void Handle(const OrthancStone::DicomResourcesLoader::SuccessMessage& message)
1577 {
1578 dynamic_cast<const ICommand&>(message.GetUserPayload()).Handle(message);
1579 }
1580
1581 void Handle(const OrthancStone::HttpCommand::SuccessMessage& message)
1582 {
1583 dynamic_cast<const ICommand&>(message.GetOrigin().GetPayload()).Handle(message);
1584 }
1585
1586 void Handle(const OrthancStone::ParseDicomSuccessMessage& message)
1587 {
1588 dynamic_cast<const ICommand&>(message.GetOrigin().GetPayload()).Handle(message);
1589 }
1590
1591 public:
1592 static boost::shared_ptr<ViewerViewport> Create(OrthancStone::ILoadersContext::ILock& lock,
1593 const OrthancStone::DicomSource& source,
1594 const std::string& canvas,
1595 boost::shared_ptr<FramesCache> cache)
1596 {
1597 boost::shared_ptr<ViewerViewport> viewport(
1598 new ViewerViewport(lock.GetContext(), source, canvas, cache));
1599
1600 viewport->loader_ = OrthancStone::DicomResourcesLoader::Create(lock);
1601 viewport->Register<OrthancStone::DicomResourcesLoader::SuccessMessage>(
1602 *viewport->loader_, &ViewerViewport::Handle);
1603
1604 viewport->Register<OrthancStone::HttpCommand::SuccessMessage>(
1605 lock.GetOracleObservable(), &ViewerViewport::Handle);
1606
1607 viewport->Register<OrthancStone::ParseDicomSuccessMessage>(
1608 lock.GetOracleObservable(), &ViewerViewport::Handle);
1609
1610 return viewport;
1611 }
1612
1613 void SetFrames(OrthancStone::SortedFrames* frames)
1614 {
1615 if (frames == NULL)
1616 {
1617 throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
1618 }
1619
1620 fitNextContent_ = true;
1621
1622 frames_.reset(frames);
1623 cursor_.reset(new SeriesCursor(frames_->GetFramesCount()));
1624
1625 LOG(INFO) << "Number of frames in series: " << frames_->GetFramesCount();
1626
1627 ResetDefaultWindowing();
1628 ClearViewport();
1629 prefetchQueue_.clear();
1630 currentFrameGeometry_ = FrameGeometry();
1631
1632 if (observer_.get() != NULL)
1633 {
1634 observer_->SignalFrameUpdated(*this, cursor_->GetCurrentIndex(),
1635 frames_->GetFramesCount(), DisplayedFrameQuality_None);
1636 }
1637
1638 if (frames_->GetFramesCount() != 0)
1639 {
1640 const std::string& sopInstanceUid = frames_->GetFrameSopInstanceUid(cursor_->GetCurrentIndex());
1641
1642 {
1643 // Fetch the default windowing for the central instance
1644 const std::string uri = ("studies/" + frames_->GetStudyInstanceUid() +
1645 "/series/" + frames_->GetSeriesInstanceUid() +
1646 "/instances/" + sopInstanceUid + "/metadata");
1647
1648 loader_->ScheduleGetDicomWeb(
1649 boost::make_shared<OrthancStone::LoadedDicomResources>(Orthanc::DICOM_TAG_SOP_INSTANCE_UID),
1650 0, source_, uri, new SetDefaultWindowingCommand(GetSharedObserver()));
1651 }
1652 }
1653 }
1654
1655 // This method is used when the layout of the HTML page changes,
1656 // which does not trigger the "emscripten_set_resize_callback()"
1657 void UpdateSize(bool fitContent)
1658 {
1659 std::unique_ptr<OrthancStone::IViewport::ILock> lock(viewport_->Lock());
1660 lock->GetCompositor().RefreshCanvasSize();
1661
1662 if (fitContent)
1663 {
1664 lock->GetCompositor().FitContent(lock->GetController().GetScene());
1665 }
1666
1667 lock->Invalidate();
1668 }
1669
1670 void AcquireObserver(IObserver* observer)
1671 {
1672 observer_.reset(observer);
1673 }
1674
1675 const std::string& GetCanvasId() const
1676 {
1677 assert(viewport_);
1678 return viewport_->GetCanvasId();
1679 }
1680
1681 void ChangeFrame(SeriesCursor::Action action)
1682 {
1683 if (cursor_.get() != NULL)
1684 {
1685 size_t previous = cursor_->GetCurrentIndex();
1686
1687 cursor_->Apply(action);
1688
1689 size_t current = cursor_->GetCurrentIndex();
1690 if (previous != current)
1691 {
1692 DisplayCurrentFrame();
1693 }
1694 }
1695 }
1696
1697 const FrameGeometry& GetCurrentFrameGeometry() const
1698 {
1699 return currentFrameGeometry_;
1700 }
1701
1702 void UpdateReferenceLines(const std::list<const FrameGeometry*>& planes)
1703 {
1704 std::unique_ptr<OrthancStone::PolylineSceneLayer> layer(new OrthancStone::PolylineSceneLayer);
1705
1706 if (GetCurrentFrameGeometry().IsValid())
1707 {
1708 for (std::list<const FrameGeometry*>::const_iterator
1709 it = planes.begin(); it != planes.end(); ++it)
1710 {
1711 assert(*it != NULL);
1712
1713 double x1, y1, x2, y2;
1714 if (GetCurrentFrameGeometry().Intersect(x1, y1, x2, y2, **it))
1715 {
1716 OrthancStone::PolylineSceneLayer::Chain chain;
1717 chain.push_back(OrthancStone::ScenePoint2D(x1, y1));
1718 chain.push_back(OrthancStone::ScenePoint2D(x2, y2));
1719 layer->AddChain(chain, false, 0, 255, 0);
1720 }
1721 }
1722 }
1723
1724 {
1725 std::unique_ptr<OrthancStone::IViewport::ILock> lock(viewport_->Lock());
1726
1727 if (layer->GetChainsCount() == 0)
1728 {
1729 lock->GetController().GetScene().DeleteLayer(LAYER_REFERENCE_LINES);
1730 }
1731 else
1732 {
1733 lock->GetController().GetScene().SetLayer(LAYER_REFERENCE_LINES, layer.release());
1734 }
1735
1736 //lock->GetCompositor().Refresh(lock->GetController().GetScene());
1737 lock->Invalidate();
1738 }
1739 }
1740
1741
1742 void ClearReferenceLines()
1743 {
1744 {
1745 std::unique_ptr<OrthancStone::IViewport::ILock> lock(viewport_->Lock());
1746 lock->GetController().GetScene().DeleteLayer(LAYER_REFERENCE_LINES);
1747 lock->Invalidate();
1748 }
1749 }
1750
1751
1752 void SetDefaultWindowing()
1753 {
1754 SetWindowing(defaultWindowingCenter_, defaultWindowingWidth_);
1755 }
1756
1757 void SetWindowing(float windowingCenter,
1758 float windowingWidth)
1759 {
1760 windowingCenter_ = windowingCenter;
1761 windowingWidth_ = windowingWidth;
1762
1763 {
1764 std::unique_ptr<OrthancStone::IViewport::ILock> lock(viewport_->Lock());
1765
1766 if (lock->GetController().GetScene().HasLayer(LAYER_TEXTURE) &&
1767 lock->GetController().GetScene().GetLayer(LAYER_TEXTURE).GetType() ==
1768 OrthancStone::ISceneLayer::Type_FloatTexture)
1769 {
1770 dynamic_cast<OrthancStone::FloatTextureSceneLayer&>(
1771 lock->GetController().GetScene().GetLayer(LAYER_TEXTURE)).
1772 SetCustomWindowing(windowingCenter_, windowingWidth_);
1773 lock->Invalidate();
1774 }
1775 }
1776 }
1777
1778 void Invert()
1779 {
1780 inverted_ = !inverted_;
1781
1782 {
1783 std::unique_ptr<OrthancStone::IViewport::ILock> lock(viewport_->Lock());
1784
1785 if (lock->GetController().GetScene().HasLayer(LAYER_TEXTURE) &&
1786 lock->GetController().GetScene().GetLayer(LAYER_TEXTURE).GetType() ==
1787 OrthancStone::ISceneLayer::Type_FloatTexture)
1788 {
1789 OrthancStone::FloatTextureSceneLayer& layer =
1790 dynamic_cast<OrthancStone::FloatTextureSceneLayer&>(
1791 lock->GetController().GetScene().GetLayer(LAYER_TEXTURE));
1792
1793 // NB: Using "IsInverted()" instead of "inverted_" is for
1794 // compatibility with MONOCHROME1 images
1795 layer.SetInverted(!layer.IsInverted());
1796 lock->Invalidate();
1797 }
1798 }
1799 }
1800 };
1801
1802
1803
1804
1805
1806 typedef std::map<std::string, boost::shared_ptr<ViewerViewport> > Viewports;
1807 static Viewports allViewports_;
1808 static bool showReferenceLines_ = true;
1809
1810
1811 static void UpdateReferenceLines()
1812 {
1813 if (showReferenceLines_)
1814 {
1815 std::list<const FrameGeometry*> planes;
1816
1817 for (Viewports::const_iterator it = allViewports_.begin(); it != allViewports_.end(); ++it)
1818 {
1819 assert(it->second != NULL);
1820 planes.push_back(&it->second->GetCurrentFrameGeometry());
1821 }
1822
1823 for (Viewports::iterator it = allViewports_.begin(); it != allViewports_.end(); ++it)
1824 {
1825 assert(it->second != NULL);
1826 it->second->UpdateReferenceLines(planes);
1827 }
1828 }
1829 else
1830 {
1831 for (Viewports::iterator it = allViewports_.begin(); it != allViewports_.end(); ++it)
1832 {
1833 assert(it->second != NULL);
1834 it->second->ClearReferenceLines();
1835 }
1836 }
1837 }
1838
1839
1840 class WebAssemblyObserver : public ResourcesLoader::IObserver,
1841 public ViewerViewport::IObserver
1842 {
1843 public:
1844 virtual void SignalResourcesLoaded() ORTHANC_OVERRIDE
1845 {
1846 DISPATCH_JAVASCRIPT_EVENT("ResourcesLoaded");
1847 }
1848
1849 virtual void SignalSeriesThumbnailLoaded(const std::string& studyInstanceUid,
1850 const std::string& seriesInstanceUid) ORTHANC_OVERRIDE
1851 {
1852 EM_ASM({
1853 const customEvent = document.createEvent("CustomEvent");
1854 customEvent.initCustomEvent("ThumbnailLoaded", false, false,
1855 { "studyInstanceUid" : UTF8ToString($0),
1856 "seriesInstanceUid" : UTF8ToString($1) });
1857 window.dispatchEvent(customEvent);
1858 },
1859 studyInstanceUid.c_str(),
1860 seriesInstanceUid.c_str());
1861 }
1862
1863 virtual void SignalSeriesMetadataLoaded(const std::string& studyInstanceUid,
1864 const std::string& seriesInstanceUid) ORTHANC_OVERRIDE
1865 {
1866 EM_ASM({
1867 const customEvent = document.createEvent("CustomEvent");
1868 customEvent.initCustomEvent("MetadataLoaded", false, false,
1869 { "studyInstanceUid" : UTF8ToString($0),
1870 "seriesInstanceUid" : UTF8ToString($1) });
1871 window.dispatchEvent(customEvent);
1872 },
1873 studyInstanceUid.c_str(),
1874 seriesInstanceUid.c_str());
1875 }
1876
1877 virtual void SignalFrameUpdated(const ViewerViewport& viewport,
1878 size_t currentFrame,
1879 size_t countFrames,
1880 DisplayedFrameQuality quality) ORTHANC_OVERRIDE
1881 {
1882 EM_ASM({
1883 const customEvent = document.createEvent("CustomEvent");
1884 customEvent.initCustomEvent("FrameUpdated", false, false,
1885 { "canvasId" : UTF8ToString($0),
1886 "currentFrame" : $1,
1887 "framesCount" : $2,
1888 "quality" : $3 });
1889 window.dispatchEvent(customEvent);
1890 },
1891 viewport.GetCanvasId().c_str(),
1892 static_cast<int>(currentFrame),
1893 static_cast<int>(countFrames),
1894 quality);
1895
1896
1897 UpdateReferenceLines();
1898 };
1899 };
1900
1901
1902
1903 static OrthancStone::DicomSource source_;
1904 static boost::shared_ptr<FramesCache> cache_;
1905 static boost::shared_ptr<OrthancStone::WebAssemblyLoadersContext> context_;
1906 static std::string stringBuffer_;
1907
1908
1909
1910 static void FormatTags(std::string& target,
1911 const Orthanc::DicomMap& tags)
1912 {
1913 Orthanc::DicomArray arr(tags);
1914 Json::Value v = Json::objectValue;
1915
1916 for (size_t i = 0; i < arr.GetSize(); i++)
1917 {
1918 const Orthanc::DicomElement& element = arr.GetElement(i);
1919 if (!element.GetValue().IsBinary() &&
1920 !element.GetValue().IsNull())
1921 {
1922 v[element.GetTag().Format()] = element.GetValue().GetContent();
1923 }
1924 }
1925
1926 target = v.toStyledString();
1927 }
1928
1929
1930 static ResourcesLoader& GetResourcesLoader()
1931 {
1932 static boost::shared_ptr<ResourcesLoader> resourcesLoader_;
1933
1934 if (!resourcesLoader_)
1935 {
1936 std::unique_ptr<OrthancStone::ILoadersContext::ILock> lock(context_->Lock());
1937 resourcesLoader_ = ResourcesLoader::Create(*lock, source_);
1938 resourcesLoader_->AcquireObserver(new WebAssemblyObserver);
1939 }
1940
1941 return *resourcesLoader_;
1942 }
1943
1944
1945 static boost::shared_ptr<ViewerViewport> GetViewport(const std::string& canvas)
1946 {
1947 Viewports::iterator found = allViewports_.find(canvas);
1948 if (found == allViewports_.end())
1949 {
1950 std::unique_ptr<OrthancStone::ILoadersContext::ILock> lock(context_->Lock());
1951 boost::shared_ptr<ViewerViewport> viewport(ViewerViewport::Create(*lock, source_, canvas, cache_));
1952 viewport->AcquireObserver(new WebAssemblyObserver);
1953 allViewports_[canvas] = viewport;
1954 return viewport;
1955 }
1956 else
1957 {
1958 return found->second;
1959 }
1960 }
1961
1962
1963 extern "C"
1964 {
1965 int main(int argc, char const *argv[])
1966 {
1967 printf("OK\n");
1968 Orthanc::InitializeFramework("", true);
1969 Orthanc::Logging::EnableInfoLevel(true);
1970 //Orthanc::Logging::EnableTraceLevel(true);
1971
1972 context_.reset(new OrthancStone::WebAssemblyLoadersContext(1, 4, 1));
1973 cache_.reset(new FramesCache);
1974
1975 DISPATCH_JAVASCRIPT_EVENT("StoneInitialized");
1976 }
1977
1978
1979 EMSCRIPTEN_KEEPALIVE
1980 void SetOrthancRoot(const char* uri,
1981 int useRendered)
1982 {
1983 try
1984 {
1985 context_->SetLocalOrthanc(uri); // For "source_.SetDicomWebThroughOrthancSource()"
1986 source_.SetDicomWebSource(std::string(uri) + "/dicom-web");
1987 source_.SetDicomWebRendered(useRendered != 0);
1988 }
1989 EXTERN_CATCH_EXCEPTIONS;
1990 }
1991
1992
1993 EMSCRIPTEN_KEEPALIVE
1994 void SetDicomWebServer(const char* serverName,
1995 int hasRendered)
1996 {
1997 try
1998 {
1999 source_.SetDicomWebThroughOrthancSource(serverName);
2000 source_.SetDicomWebRendered(hasRendered != 0);
2001 }
2002 EXTERN_CATCH_EXCEPTIONS;
2003 }
2004
2005
2006 EMSCRIPTEN_KEEPALIVE
2007 void FetchAllStudies()
2008 {
2009 try
2010 {
2011 GetResourcesLoader().FetchAllStudies();
2012 }
2013 EXTERN_CATCH_EXCEPTIONS;
2014 }
2015
2016 EMSCRIPTEN_KEEPALIVE
2017 void FetchStudy(const char* studyInstanceUid)
2018 {
2019 try
2020 {
2021 GetResourcesLoader().FetchStudy(studyInstanceUid);
2022 }
2023 EXTERN_CATCH_EXCEPTIONS;
2024 }
2025
2026 EMSCRIPTEN_KEEPALIVE
2027 void FetchSeries(const char* studyInstanceUid,
2028 const char* seriesInstanceUid)
2029 {
2030 try
2031 {
2032 GetResourcesLoader().FetchSeries(studyInstanceUid, seriesInstanceUid);
2033 }
2034 EXTERN_CATCH_EXCEPTIONS;
2035 }
2036
2037 EMSCRIPTEN_KEEPALIVE
2038 int GetStudiesCount()
2039 {
2040 try
2041 {
2042 return GetResourcesLoader().GetStudiesCount();
2043 }
2044 EXTERN_CATCH_EXCEPTIONS;
2045 return 0; // on exception
2046 }
2047
2048 EMSCRIPTEN_KEEPALIVE
2049 int GetSeriesCount()
2050 {
2051 try
2052 {
2053 return GetResourcesLoader().GetSeriesCount();
2054 }
2055 EXTERN_CATCH_EXCEPTIONS;
2056 return 0; // on exception
2057 }
2058
2059
2060 EMSCRIPTEN_KEEPALIVE
2061 const char* GetStringBuffer()
2062 {
2063 return stringBuffer_.c_str();
2064 }
2065
2066
2067 EMSCRIPTEN_KEEPALIVE
2068 void LoadStudyTags(int i)
2069 {
2070 try
2071 {
2072 if (i < 0)
2073 {
2074 throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
2075 }
2076
2077 Orthanc::DicomMap dicom;
2078 GetResourcesLoader().GetStudy(dicom, i);
2079 FormatTags(stringBuffer_, dicom);
2080 }
2081 EXTERN_CATCH_EXCEPTIONS;
2082 }
2083
2084
2085 EMSCRIPTEN_KEEPALIVE
2086 void LoadSeriesTags(int i)
2087 {
2088 try
2089 {
2090 if (i < 0)
2091 {
2092 throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
2093 }
2094
2095 Orthanc::DicomMap dicom;
2096 GetResourcesLoader().GetSeries(dicom, i);
2097 FormatTags(stringBuffer_, dicom);
2098 }
2099 EXTERN_CATCH_EXCEPTIONS;
2100 }
2101
2102
2103 EMSCRIPTEN_KEEPALIVE
2104 int LoadSeriesThumbnail(const char* seriesInstanceUid)
2105 {
2106 try
2107 {
2108 std::string image, mime;
2109 switch (GetResourcesLoader().GetSeriesThumbnail(image, mime, seriesInstanceUid))
2110 {
2111 case OrthancStone::SeriesThumbnailType_Image:
2112 Orthanc::Toolbox::EncodeDataUriScheme(stringBuffer_, mime, image);
2113 return ThumbnailType_Image;
2114
2115 case OrthancStone::SeriesThumbnailType_Pdf:
2116 return ThumbnailType_Pdf;
2117
2118 case OrthancStone::SeriesThumbnailType_Video:
2119 return ThumbnailType_Video;
2120
2121 case OrthancStone::SeriesThumbnailType_NotLoaded:
2122 return ThumbnailType_Loading;
2123
2124 case OrthancStone::SeriesThumbnailType_Unsupported:
2125 return ThumbnailType_NoPreview;
2126
2127 default:
2128 return ThumbnailType_Unknown;
2129 }
2130 }
2131 EXTERN_CATCH_EXCEPTIONS;
2132 return ThumbnailType_Unknown;
2133 }
2134
2135
2136 EMSCRIPTEN_KEEPALIVE
2137 void SpeedUpFetchSeriesMetadata(const char* studyInstanceUid,
2138 const char* seriesInstanceUid)
2139 {
2140 try
2141 {
2142 GetResourcesLoader().FetchSeriesMetadata(PRIORITY_HIGH, studyInstanceUid, seriesInstanceUid);
2143 }
2144 EXTERN_CATCH_EXCEPTIONS;
2145 }
2146
2147
2148 EMSCRIPTEN_KEEPALIVE
2149 int IsSeriesComplete(const char* seriesInstanceUid)
2150 {
2151 try
2152 {
2153 return GetResourcesLoader().IsSeriesComplete(seriesInstanceUid) ? 1 : 0;
2154 }
2155 EXTERN_CATCH_EXCEPTIONS;
2156 return 0;
2157 }
2158
2159 EMSCRIPTEN_KEEPALIVE
2160 int LoadSeriesInViewport(const char* canvas,
2161 const char* seriesInstanceUid)
2162 {
2163 try
2164 {
2165 std::unique_ptr<OrthancStone::SortedFrames> frames(new OrthancStone::SortedFrames);
2166
2167 if (GetResourcesLoader().SortSeriesFrames(*frames, seriesInstanceUid))
2168 {
2169 GetViewport(canvas)->SetFrames(frames.release());
2170 return 1;
2171 }
2172 else
2173 {
2174 return 0;
2175 }
2176 }
2177 EXTERN_CATCH_EXCEPTIONS;
2178 return 0;
2179 }
2180
2181
2182 EMSCRIPTEN_KEEPALIVE
2183 void AllViewportsUpdateSize(int fitContent)
2184 {
2185 try
2186 {
2187 for (Viewports::iterator it = allViewports_.begin(); it != allViewports_.end(); ++it)
2188 {
2189 assert(it->second != NULL);
2190 it->second->UpdateSize(fitContent != 0);
2191 }
2192 }
2193 EXTERN_CATCH_EXCEPTIONS;
2194 }
2195
2196
2197 EMSCRIPTEN_KEEPALIVE
2198 void DecrementFrame(const char* canvas,
2199 int fitContent)
2200 {
2201 try
2202 {
2203 GetViewport(canvas)->ChangeFrame(SeriesCursor::Action_Minus);
2204 }
2205 EXTERN_CATCH_EXCEPTIONS;
2206 }
2207
2208
2209 EMSCRIPTEN_KEEPALIVE
2210 void IncrementFrame(const char* canvas,
2211 int fitContent)
2212 {
2213 try
2214 {
2215 GetViewport(canvas)->ChangeFrame(SeriesCursor::Action_Plus);
2216 }
2217 EXTERN_CATCH_EXCEPTIONS;
2218 }
2219
2220
2221 EMSCRIPTEN_KEEPALIVE
2222 void ShowReferenceLines(int show)
2223 {
2224 try
2225 {
2226 showReferenceLines_ = (show != 0);
2227 UpdateReferenceLines();
2228 }
2229 EXTERN_CATCH_EXCEPTIONS;
2230 }
2231
2232
2233 EMSCRIPTEN_KEEPALIVE
2234 void SetDefaultWindowing(const char* canvas)
2235 {
2236 try
2237 {
2238 GetViewport(canvas)->SetDefaultWindowing();
2239 }
2240 EXTERN_CATCH_EXCEPTIONS;
2241 }
2242
2243
2244 EMSCRIPTEN_KEEPALIVE
2245 void SetWindowing(const char* canvas,
2246 int center,
2247 int width)
2248 {
2249 try
2250 {
2251 GetViewport(canvas)->SetWindowing(center, width);
2252 }
2253 EXTERN_CATCH_EXCEPTIONS;
2254 }
2255
2256
2257 EMSCRIPTEN_KEEPALIVE
2258 void InvertContrast(const char* canvas)
2259 {
2260 try
2261 {
2262 GetViewport(canvas)->Invert();
2263 }
2264 EXTERN_CATCH_EXCEPTIONS;
2265 }
2266 }