Mercurial > hg > orthanc-stone
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 } |