view StoneWebViewer/WebAssembly/StoneWebViewer.cpp @ 1526:61023b0d39c8

Reverted the Stone Web Viewer plugin to rev. 307a805d0587 (mistakenly changed to serve the RT Viewer and make it available in the Orthanc Explorer while it should have been done in a separate plugin)
author Benjamin Golinvaux <bgo@osimis.io>
date Sun, 02 Aug 2020 13:53:48 +0200
parents 2b7d34cb764f
children d3cafeef07bb
line wrap: on
line source

/**
 * Stone of Orthanc
 * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 * Department, University Hospital of Liege, Belgium
 * Copyright (C) 2017-2020 Osimis S.A., Belgium
 *
 * This program is free software: you can redistribute it and/or
 * modify it under the terms of the GNU Affero General Public License
 * as published by the Free Software Foundation, either version 3 of
 * the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Affero General Public License for more details.
 * 
 * You should have received a copy of the GNU Affero General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 **/


#include <emscripten.h>


#define DISPATCH_JAVASCRIPT_EVENT(name)                         \
  EM_ASM(                                                       \
    const customEvent = document.createEvent("CustomEvent");    \
    customEvent.initCustomEvent(name, false, false, undefined); \
    window.dispatchEvent(customEvent);                          \
    );


#define EXTERN_CATCH_EXCEPTIONS                         \
  catch (Orthanc::OrthancException& e)                  \
  {                                                     \
    LOG(ERROR) << "OrthancException: " << e.What();     \
    DISPATCH_JAVASCRIPT_EVENT("StoneException");        \
  }                                                     \
  catch (OrthancStone::StoneException& e)               \
  {                                                     \
    LOG(ERROR) << "StoneException: " << e.What();       \
    DISPATCH_JAVASCRIPT_EVENT("StoneException");        \
  }                                                     \
  catch (std::exception& e)                             \
  {                                                     \
    LOG(ERROR) << "Runtime error: " << e.what();        \
    DISPATCH_JAVASCRIPT_EVENT("StoneException");        \
  }                                                     \
  catch (...)                                           \
  {                                                     \
    LOG(ERROR) << "Native exception";                   \
    DISPATCH_JAVASCRIPT_EVENT("StoneException");        \
  }


#include <Cache/MemoryObjectCache.h>
#include <DicomFormat/DicomArray.h>
#include <DicomParsing/Internals/DicomImageDecoder.h>
#include <Images/Image.h>
#include <Images/ImageProcessing.h>
#include <Images/JpegReader.h>
#include <Logging.h>

#include "../../OrthancStone/Sources/Loaders/DicomResourcesLoader.h"
#include "../../OrthancStone/Sources/Loaders/SeriesMetadataLoader.h"
#include "../../OrthancStone/Sources/Loaders/SeriesThumbnailsLoader.h"
#include "../../OrthancStone/Sources/Loaders/WebAssemblyLoadersContext.h"
#include "../../OrthancStone/Sources/Messages/ObserverBase.h"
#include "../../OrthancStone/Sources/Oracle/ParseDicomFromWadoCommand.h"
#include "../../OrthancStone/Sources/Scene2D/ColorTextureSceneLayer.h"
#include "../../OrthancStone/Sources/Scene2D/FloatTextureSceneLayer.h"
#include "../../OrthancStone/Sources/Scene2D/PolylineSceneLayer.h"
#include "../../OrthancStone/Sources/StoneException.h"
#include "../../OrthancStone/Sources/Toolbox/DicomInstanceParameters.h"
#include "../../OrthancStone/Sources/Toolbox/GeometryToolbox.h"
#include "../../OrthancStone/Sources/Toolbox/SortedFrames.h"
#include "../../OrthancStone/Sources/Viewport/WebGLViewport.h"

#include <boost/make_shared.hpp>
#include <stdio.h>


enum EMSCRIPTEN_KEEPALIVE ThumbnailType
{
  ThumbnailType_Image,
    ThumbnailType_NoPreview,
    ThumbnailType_Pdf,
    ThumbnailType_Video,
    ThumbnailType_Loading,
    ThumbnailType_Unknown
};


enum EMSCRIPTEN_KEEPALIVE DisplayedFrameQuality
{
DisplayedFrameQuality_None,
  DisplayedFrameQuality_Low,
  DisplayedFrameQuality_High
  };
  


static const int PRIORITY_HIGH = -100;
static const int PRIORITY_LOW = 100;
static const int PRIORITY_NORMAL = 0;

static const unsigned int QUALITY_JPEG = 0;
static const unsigned int QUALITY_FULL = 1;

class ResourcesLoader : public OrthancStone::ObserverBase<ResourcesLoader>
{
public:
  class IObserver : public boost::noncopyable
  {
  public:
    virtual ~IObserver()
    {
    }

    virtual void SignalResourcesLoaded() = 0;

    virtual void SignalSeriesThumbnailLoaded(const std::string& studyInstanceUid,
                                             const std::string& seriesInstanceUid) = 0;

    virtual void SignalSeriesMetadataLoaded(const std::string& studyInstanceUid,
                                            const std::string& seriesInstanceUid) = 0;
  };
  
private:
  std::unique_ptr<IObserver>                               observer_;
  OrthancStone::DicomSource                                source_;
  size_t                                                   pending_;
  boost::shared_ptr<OrthancStone::LoadedDicomResources>    studies_;
  boost::shared_ptr<OrthancStone::LoadedDicomResources>    series_;
  boost::shared_ptr<OrthancStone::DicomResourcesLoader>    resourcesLoader_;
  boost::shared_ptr<OrthancStone::SeriesThumbnailsLoader>  thumbnailsLoader_;
  boost::shared_ptr<OrthancStone::SeriesMetadataLoader>    metadataLoader_;

  ResourcesLoader(const OrthancStone::DicomSource& source) :
    source_(source),
    pending_(0),
    studies_(new OrthancStone::LoadedDicomResources(Orthanc::DICOM_TAG_STUDY_INSTANCE_UID)),
    series_(new OrthancStone::LoadedDicomResources(Orthanc::DICOM_TAG_SERIES_INSTANCE_UID))
  {
  }

  void Handle(const OrthancStone::DicomResourcesLoader::SuccessMessage& message)
  {
    const Orthanc::SingleValueObject<Orthanc::ResourceType>& payload =
      dynamic_cast<const Orthanc::SingleValueObject<Orthanc::ResourceType>&>(message.GetUserPayload());
    
    OrthancStone::LoadedDicomResources& dicom = *message.GetResources();
    
    LOG(INFO) << "resources loaded: " << dicom.GetSize()
              << ", " << Orthanc::EnumerationToString(payload.GetValue());

    if (payload.GetValue() == Orthanc::ResourceType_Series)
    {
      for (size_t i = 0; i < dicom.GetSize(); i++)
      {
        std::string studyInstanceUid, seriesInstanceUid;
        if (dicom.GetResource(i).LookupStringValue(
              studyInstanceUid, Orthanc::DICOM_TAG_STUDY_INSTANCE_UID, false) &&
            dicom.GetResource(i).LookupStringValue(
              seriesInstanceUid, Orthanc::DICOM_TAG_SERIES_INSTANCE_UID, false))
        {
          thumbnailsLoader_->ScheduleLoadThumbnail(source_, "", studyInstanceUid, seriesInstanceUid);
          metadataLoader_->ScheduleLoadSeries(PRIORITY_LOW + 1, source_, studyInstanceUid, seriesInstanceUid);
        }
      }
    }

    if (pending_ == 0)
    {
      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
    }
    else
    {
      pending_ --;
      if (pending_ == 0 &&
          observer_.get() != NULL)
      {
        observer_->SignalResourcesLoaded();
      }
    }
  }

  void Handle(const OrthancStone::SeriesThumbnailsLoader::SuccessMessage& message)
  {
    if (observer_.get() != NULL)
    {
      observer_->SignalSeriesThumbnailLoaded(
        message.GetStudyInstanceUid(), message.GetSeriesInstanceUid());
    }
  }

  void Handle(const OrthancStone::SeriesMetadataLoader::SuccessMessage& message)
  {
    if (observer_.get() != NULL)
    {
      observer_->SignalSeriesMetadataLoaded(
        message.GetStudyInstanceUid(), message.GetSeriesInstanceUid());
    }
  }

  void FetchInternal(const std::string& studyInstanceUid,
                     const std::string& seriesInstanceUid)
  {
    // Firstly, load the study
    Orthanc::DicomMap filter;
    filter.SetValue(Orthanc::DICOM_TAG_STUDY_INSTANCE_UID, studyInstanceUid, false);

    std::set<Orthanc::DicomTag> tags;
    tags.insert(Orthanc::DICOM_TAG_STUDY_DESCRIPTION);  // Necessary for Orthanc DICOMweb plugin

    resourcesLoader_->ScheduleQido(
      studies_, PRIORITY_HIGH, source_, Orthanc::ResourceType_Study, filter, tags,
      new Orthanc::SingleValueObject<Orthanc::ResourceType>(Orthanc::ResourceType_Study));

    // Secondly, load the series
    if (!seriesInstanceUid.empty())
    {
      filter.SetValue(Orthanc::DICOM_TAG_SERIES_INSTANCE_UID, seriesInstanceUid, false);
    }
    
    tags.insert(Orthanc::DICOM_TAG_SERIES_NUMBER);  // Necessary for Google Cloud Platform
    
    resourcesLoader_->ScheduleQido(
      series_, PRIORITY_HIGH, source_, Orthanc::ResourceType_Series, filter, tags,
      new Orthanc::SingleValueObject<Orthanc::ResourceType>(Orthanc::ResourceType_Series));

    pending_ += 2;
  }

public:
  static boost::shared_ptr<ResourcesLoader> Create(OrthancStone::ILoadersContext::ILock& lock,
                                                   const OrthancStone::DicomSource& source)
  {
    boost::shared_ptr<ResourcesLoader> loader(new ResourcesLoader(source));

    loader->resourcesLoader_ = OrthancStone::DicomResourcesLoader::Create(lock);
    loader->thumbnailsLoader_ = OrthancStone::SeriesThumbnailsLoader::Create(lock, PRIORITY_LOW);
    loader->metadataLoader_ = OrthancStone::SeriesMetadataLoader::Create(lock);
    
    loader->Register<OrthancStone::DicomResourcesLoader::SuccessMessage>(
      *loader->resourcesLoader_, &ResourcesLoader::Handle);

    loader->Register<OrthancStone::SeriesThumbnailsLoader::SuccessMessage>(
      *loader->thumbnailsLoader_, &ResourcesLoader::Handle);

    loader->Register<OrthancStone::SeriesMetadataLoader::SuccessMessage>(
      *loader->metadataLoader_, &ResourcesLoader::Handle);
    
    return loader;
  }
  
  void FetchAllStudies()
  {
    FetchInternal("", "");
  }
  
  void FetchStudy(const std::string& studyInstanceUid)
  {
    FetchInternal(studyInstanceUid, "");
  }
  
  void FetchSeries(const std::string& studyInstanceUid,
                   const std::string& seriesInstanceUid)
  {
    FetchInternal(studyInstanceUid, seriesInstanceUid);
  }

  size_t GetStudiesCount() const
  {
    return studies_->GetSize();
  }

  size_t GetSeriesCount() const
  {
    return series_->GetSize();
  }

  void GetStudy(Orthanc::DicomMap& target,
                size_t i)
  {
    target.Assign(studies_->GetResource(i));
  }

  void GetSeries(Orthanc::DicomMap& target,
                 size_t i)
  {
    target.Assign(series_->GetResource(i));

    // Complement with the study-level tags
    std::string studyInstanceUid;
    if (target.LookupStringValue(studyInstanceUid, Orthanc::DICOM_TAG_STUDY_INSTANCE_UID, false) &&
        studies_->HasResource(studyInstanceUid))
    {
      studies_->MergeResource(target, studyInstanceUid);
    }
  }

  OrthancStone::SeriesThumbnailType GetSeriesThumbnail(std::string& image,
                                                       std::string& mime,
                                                       const std::string& seriesInstanceUid)
  {
    return thumbnailsLoader_->GetSeriesThumbnail(image, mime, seriesInstanceUid);
  }

  void FetchSeriesMetadata(int priority,
                           const std::string& studyInstanceUid,
                           const std::string& seriesInstanceUid)
  {
    metadataLoader_->ScheduleLoadSeries(priority, source_, studyInstanceUid, seriesInstanceUid);
  }

  bool IsSeriesComplete(const std::string& seriesInstanceUid)
  {
    OrthancStone::SeriesMetadataLoader::Accessor accessor(*metadataLoader_, seriesInstanceUid);
    return accessor.IsComplete();
  }

  bool SortSeriesFrames(OrthancStone::SortedFrames& target,
                        const std::string& seriesInstanceUid)
  {
    OrthancStone::SeriesMetadataLoader::Accessor accessor(*metadataLoader_, seriesInstanceUid);
    
    if (accessor.IsComplete())
    {
      target.Clear();

      for (size_t i = 0; i < accessor.GetInstancesCount(); i++)
      {
        target.AddInstance(accessor.GetInstance(i));
      }

      target.Sort();
      
      return true;
    }
    else
    {
      return false;
    }
  }

  void AcquireObserver(IObserver* observer)
  {  
    observer_.reset(observer);
  }
};



class FramesCache : public boost::noncopyable
{
private:
  class CachedImage : public Orthanc::ICacheable
  {
  private:
    std::unique_ptr<Orthanc::ImageAccessor>  image_;
    unsigned int                             quality_;

  public:
    CachedImage(Orthanc::ImageAccessor* image,
                unsigned int quality) :
      image_(image),
      quality_(quality)
    {
      assert(image != NULL);
    }

    virtual size_t GetMemoryUsage() const
    {    
      assert(image_.get() != NULL);
      return (image_->GetBytesPerPixel() * image_->GetPitch() * image_->GetHeight());
    }

    const Orthanc::ImageAccessor& GetImage() const
    {
      assert(image_.get() != NULL);
      return *image_;
    }

    unsigned int GetQuality() const
    {
      return quality_;
    }
  };


  static std::string GetKey(const std::string& sopInstanceUid,
                            size_t frameIndex)
  {
    return sopInstanceUid + "|" + boost::lexical_cast<std::string>(frameIndex);
  }
  

  Orthanc::MemoryObjectCache  cache_;
  
public:
  FramesCache()
  {
    SetMaximumSize(100 * 1024 * 1024);  // 100 MB
  }
  
  size_t GetMaximumSize()
  {
    return cache_.GetMaximumSize();
  }
    
  void SetMaximumSize(size_t size)
  {
    cache_.SetMaximumSize(size);
  }

  /**
   * Returns "true" iff the provided image has better quality than the
   * previously cached one, or if no cache was previously available.
   **/
  bool Acquire(const std::string& sopInstanceUid,
               size_t frameIndex,
               Orthanc::ImageAccessor* image /* transfer ownership */,
               unsigned int quality)
  {
    std::unique_ptr<Orthanc::ImageAccessor> protection(image);
    
    if (image == NULL)
    {
      throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
    }
    else if (image->GetFormat() != Orthanc::PixelFormat_Float32 &&
             image->GetFormat() != Orthanc::PixelFormat_RGB24)
    {
      throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageFormat);
    }

    const std::string& key = GetKey(sopInstanceUid, frameIndex);

    bool invalidate = false;
    
    {
      /**
       * Access the previous cached entry, with side effect of tagging
       * it as the most recently accessed frame (update of LRU recycling)
       **/
      Orthanc::MemoryObjectCache::Accessor accessor(cache_, key, false /* unique lock */);

      if (accessor.IsValid())
      {
        const CachedImage& previous = dynamic_cast<const CachedImage&>(accessor.GetValue());
        
        // There is already a cached image for this frame
        if (previous.GetQuality() < quality)
        {
          // The previously stored image has poorer quality
          invalidate = true;
        }
        else
        {
          // No update in the quality, don't change the cache
          return false;   
        }
      }
      else
      {
        invalidate = false;
      }
    }

    if (invalidate)
    {
      cache_.Invalidate(key);
    }
        
    cache_.Acquire(key, new CachedImage(protection.release(), quality));
    return true;
  }

  class Accessor : public boost::noncopyable
  {
  private:
    Orthanc::MemoryObjectCache::Accessor accessor_;

    const CachedImage& GetCachedImage() const
    {
      if (IsValid())
      {
        return dynamic_cast<CachedImage&>(accessor_.GetValue());
      }
      else
      {
        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
      }
    }
    
  public:
    Accessor(FramesCache& that,
             const std::string& sopInstanceUid,
             size_t frameIndex) :
      accessor_(that.cache_, GetKey(sopInstanceUid, frameIndex), false /* shared lock */)
    {
    }

    bool IsValid() const
    {
      return accessor_.IsValid();
    }

    const Orthanc::ImageAccessor& GetImage() const
    {
      return GetCachedImage().GetImage();
    }

    unsigned int GetQuality() const
    {
      return GetCachedImage().GetQuality();
    }
  };
};



class SeriesCursor : public boost::noncopyable
{
public:
  enum Action
  {
    Action_FastPlus,
    Action_Plus,
    Action_None,
    Action_Minus,
    Action_FastMinus
  };
  
private:
  std::vector<size_t>  prefetch_;
  int                  framesCount_;
  int                  currentFrame_;
  bool                 isCircular_;
  int                  fastDelta_;
  Action               lastAction_;

  int ComputeNextFrame(int currentFrame,
                       Action action) const
  {
    if (framesCount_ == 0)
    {
      assert(currentFrame == 0);
      return 0;
    }

    int nextFrame = currentFrame;
    
    switch (action)
    {
      case Action_FastPlus:
        nextFrame += fastDelta_;
        break;

      case Action_Plus:
        nextFrame += 1;
        break;

      case Action_None:
        break;

      case Action_Minus:
        nextFrame -= 1;
        break;

      case Action_FastMinus:
        nextFrame -= fastDelta_;
        break;

      default:
        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
    }

    if (isCircular_)
    {
      while (nextFrame < 0)
      {
        nextFrame += framesCount_;
      }

      while (nextFrame >= framesCount_)
      {
        nextFrame -= framesCount_;
      }
    }
    else
    {
      if (nextFrame < 0)
      {
        nextFrame = 0;
      }
      else if (nextFrame >= framesCount_)
      {
        nextFrame = framesCount_ - 1;
      }
    }

    return nextFrame;
  }
  
  void UpdatePrefetch()
  {
    /**
     * This method will order the frames of the series according to
     * the number of "actions" (i.e. mouse wheels) that are necessary
     * to reach them, starting from the current frame. It is assumed
     * that once one action is done, it is more likely that the user
     * will do the same action just afterwards.
     **/
    
    prefetch_.clear();

    if (framesCount_ == 0)
    {
      return;
    }

    prefetch_.reserve(framesCount_);
    
    // Breadth-first search using a FIFO. The queue associates a frame
    // and the action that is the most likely in this frame
    typedef std::list< std::pair<int, Action> >  Queue;

    Queue queue;
    std::set<int>  visited;  // Frames that have already been visited

    queue.push_back(std::make_pair(currentFrame_, lastAction_));

    while (!queue.empty())
    {
      int frame = queue.front().first;
      Action previousAction = queue.front().second;
      queue.pop_front();

      if (visited.find(frame) == visited.end())
      {
        visited.insert(frame);
        prefetch_.push_back(frame);

        switch (previousAction)
        {
          case Action_None:
          case Action_Plus:
            queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_Plus), Action_Plus));
            queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_Minus), Action_Minus));
            queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_FastPlus), Action_FastPlus));
            queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_FastMinus), Action_FastMinus));
            break;
          
          case Action_Minus:
            queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_Minus), Action_Minus));
            queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_Plus), Action_Plus));
            queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_FastMinus), Action_FastMinus));
            queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_FastPlus), Action_FastPlus));
            break;

          case Action_FastPlus:
            queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_FastPlus), Action_FastPlus));
            queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_FastMinus), Action_FastMinus));
            queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_Plus), Action_Plus));
            queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_Minus), Action_Minus));
            break;
              
          case Action_FastMinus:
            queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_FastMinus), Action_FastMinus));
            queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_FastPlus), Action_FastPlus));
            queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_Minus), Action_Minus));
            queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_Plus), Action_Plus));
            break;

          default:
            throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
        }
      }
    }

    assert(prefetch_.size() == framesCount_);
  }

  bool CheckFrameIndex(int frame) const
  {
    return ((framesCount_ == 0 && frame == 0) ||
            (framesCount_ > 0 && frame >= 0 && frame < framesCount_));
  }
  
public:
  SeriesCursor(size_t framesCount) :
    framesCount_(framesCount),
    currentFrame_(framesCount / 2),  // Start at the middle frame    
    isCircular_(false),
    lastAction_(Action_None)
  {
    SetFastDelta(framesCount / 20);
    UpdatePrefetch();
  }

  void SetCircular(bool isCircular)
  {
    isCircular_ = isCircular;
    UpdatePrefetch();
  }

  void SetFastDelta(int delta)
  {
    fastDelta_ = (delta < 0 ? -delta : delta);

    if (fastDelta_ <= 0)
    {
      fastDelta_ = 1;
    }
  }

  void SetCurrentIndex(size_t frame)
  {
    if (frame >= framesCount_)
    {
      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
    }
    else
    {
      currentFrame_ = frame;
      lastAction_ = Action_None;
      UpdatePrefetch();
    }
  }

  size_t GetCurrentIndex() const
  {
    assert(CheckFrameIndex(currentFrame_));
    return static_cast<size_t>(currentFrame_);
  }

  void Apply(Action action)
  {
    currentFrame_ = ComputeNextFrame(currentFrame_, action);
    lastAction_ = action;
    UpdatePrefetch();
  }

  size_t GetPrefetchSize() const
  {
    assert(prefetch_.size() == framesCount_);
    return prefetch_.size();
  }

  size_t GetPrefetchFrameIndex(size_t i) const
  {
    if (i >= prefetch_.size())
    {
      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
    }
    else
    {
      assert(CheckFrameIndex(prefetch_[i]));
      return static_cast<size_t>(prefetch_[i]);
    }
  }
};




class FrameGeometry
{
private:
  bool                              isValid_;
  std::string                       frameOfReferenceUid_;
  OrthancStone::CoordinateSystem3D  coordinates_;
  double                            pixelSpacingX_;
  double                            pixelSpacingY_;
  OrthancStone::Extent2D            extent_;

public:
  FrameGeometry() :
    isValid_(false)
  {
  }
    
  FrameGeometry(const Orthanc::DicomMap& tags) :
    isValid_(false),
    coordinates_(tags)
  {
    if (!tags.LookupStringValue(
          frameOfReferenceUid_, Orthanc::DICOM_TAG_FRAME_OF_REFERENCE_UID, false))
    {
      frameOfReferenceUid_.clear();
    }

    OrthancStone::GeometryToolbox::GetPixelSpacing(pixelSpacingX_, pixelSpacingY_, tags);

    unsigned int rows, columns;
    if (tags.HasTag(Orthanc::DICOM_TAG_IMAGE_POSITION_PATIENT) &&
        tags.HasTag(Orthanc::DICOM_TAG_IMAGE_ORIENTATION_PATIENT) &&
        tags.ParseUnsignedInteger32(rows, Orthanc::DICOM_TAG_ROWS) &&
        tags.ParseUnsignedInteger32(columns, Orthanc::DICOM_TAG_COLUMNS))
    {
      double ox = -pixelSpacingX_ / 2.0;
      double oy = -pixelSpacingY_ / 2.0;
      extent_.AddPoint(ox, oy);
      extent_.AddPoint(ox + pixelSpacingX_ * static_cast<double>(columns),
                       oy + pixelSpacingY_ * static_cast<double>(rows));

      isValid_ = true;
    }
  }

  bool IsValid() const
  {
    return isValid_;
  }

  const std::string& GetFrameOfReferenceUid() const
  {
    if (isValid_)
    {
      return frameOfReferenceUid_;
    }
    else
    {
      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
    }
  }

  const OrthancStone::CoordinateSystem3D& GetCoordinates() const
  {
    if (isValid_)
    {
      return coordinates_;
    }
    else
    {
      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
    }
  }

  double GetPixelSpacingX() const
  {
    if (isValid_)
    {
      return pixelSpacingX_;
    }
    else
    {
      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
    }
  }

  double GetPixelSpacingY() const
  {
    if (isValid_)
    {
      return pixelSpacingY_;
    }
    else
    {
      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
    }
  }

  bool Intersect(double& x1,  // Coordinates of the clipped line (out)
                 double& y1,
                 double& x2,
                 double& y2,
                 const FrameGeometry& other) const
  {
    if (this == &other)
    {
      return false;
    }
    
    OrthancStone::Vector direction, origin;
        
    if (IsValid() &&
        other.IsValid() &&
        !extent_.IsEmpty() &&
        frameOfReferenceUid_ == other.frameOfReferenceUid_ &&
        OrthancStone::GeometryToolbox::IntersectTwoPlanes(
          origin, direction,
          coordinates_.GetOrigin(), coordinates_.GetNormal(),
          other.coordinates_.GetOrigin(), other.coordinates_.GetNormal()))
    {
      double ax, ay, bx, by;
      coordinates_.ProjectPoint(ax, ay, origin);
      coordinates_.ProjectPoint(bx, by, origin + 100.0 * direction);
      
      return OrthancStone::GeometryToolbox::ClipLineToRectangle(
        x1, y1, x2, y2,
        ax, ay, bx, by,
        extent_.GetX1(), extent_.GetY1(), extent_.GetX2(), extent_.GetY2());
    }
    else
    {
      return false;
    }
  }
};
  


class ViewerViewport : public OrthancStone::ObserverBase<ViewerViewport>
{
public:
  class IObserver : public boost::noncopyable
  {
  public:
    virtual ~IObserver()
    {
    }

    virtual void SignalFrameUpdated(const ViewerViewport& viewport,
                                    size_t currentFrame,
                                    size_t countFrames,
                                    DisplayedFrameQuality quality) = 0;
  };

private:
  static const int LAYER_TEXTURE = 0;
  static const int LAYER_REFERENCE_LINES = 1;
  
  
  class ICommand : public Orthanc::IDynamicObject
  {
  private:
    boost::shared_ptr<ViewerViewport>  viewport_;
    
  public:
    ICommand(boost::shared_ptr<ViewerViewport> viewport) :
      viewport_(viewport)
    {
      if (viewport == NULL)
      {
        throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
      }
    }
    
    virtual ~ICommand()
    {
    }

    ViewerViewport& GetViewport() const
    {
      assert(viewport_ != NULL);
      return *viewport_;
    }
    
    virtual void Handle(const OrthancStone::DicomResourcesLoader::SuccessMessage& message) const
    {
      throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
    }
    
    virtual void Handle(const OrthancStone::HttpCommand::SuccessMessage& message) const
    {
      throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
    }

    virtual void Handle(const OrthancStone::ParseDicomSuccessMessage& message) const
    {
      throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
    }
  };

  class SetDefaultWindowingCommand : public ICommand
  {
  public:
    SetDefaultWindowingCommand(boost::shared_ptr<ViewerViewport> viewport) :
      ICommand(viewport)
    {
    }
    
    virtual void Handle(const OrthancStone::DicomResourcesLoader::SuccessMessage& message) const ORTHANC_OVERRIDE
    {
      if (message.GetResources()->GetSize() != 1)
      {
        throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol);
      }

      const Orthanc::DicomMap& dicom = message.GetResources()->GetResource(0);
      
      {
        OrthancStone::DicomInstanceParameters params(dicom);

        if (params.HasDefaultWindowing())
        {
          GetViewport().defaultWindowingCenter_ = params.GetDefaultWindowingCenter();
          GetViewport().defaultWindowingWidth_ = params.GetDefaultWindowingWidth();
          LOG(INFO) << "Default windowing: " << params.GetDefaultWindowingCenter()
                    << "," << params.GetDefaultWindowingWidth();

          GetViewport().windowingCenter_ = params.GetDefaultWindowingCenter();
          GetViewport().windowingWidth_ = params.GetDefaultWindowingWidth();
        }
        else
        {
          LOG(INFO) << "No default windowing";
          GetViewport().ResetDefaultWindowing();
        }
      }

      GetViewport().DisplayCurrentFrame();
    }
  };

  class SetLowQualityFrame : public ICommand
  {
  private:
    std::string   sopInstanceUid_;
    unsigned int  frameIndex_;
    float         windowCenter_;
    float         windowWidth_;
    bool          isMonochrome1_;
    bool          isPrefetch_;
    
  public:
    SetLowQualityFrame(boost::shared_ptr<ViewerViewport> viewport,
                       const std::string& sopInstanceUid,
                       unsigned int frameIndex,
                       float windowCenter,
                       float windowWidth,
                       bool isMonochrome1,
                       bool isPrefetch) :
      ICommand(viewport),
      sopInstanceUid_(sopInstanceUid),
      frameIndex_(frameIndex),
      windowCenter_(windowCenter),
      windowWidth_(windowWidth),
      isMonochrome1_(isMonochrome1),
      isPrefetch_(isPrefetch)
    {
    }
    
    virtual void Handle(const OrthancStone::HttpCommand::SuccessMessage& message) const ORTHANC_OVERRIDE
    {
      std::unique_ptr<Orthanc::JpegReader> jpeg(new Orthanc::JpegReader);
      jpeg->ReadFromMemory(message.GetAnswer());

      bool updatedCache;
      
      switch (jpeg->GetFormat())
      {
        case Orthanc::PixelFormat_RGB24:
          updatedCache = GetViewport().cache_->Acquire(
            sopInstanceUid_, frameIndex_, jpeg.release(), QUALITY_JPEG);
          break;

        case Orthanc::PixelFormat_Grayscale8:
        {
          if (isMonochrome1_)
          {
            Orthanc::ImageProcessing::Invert(*jpeg);
          }

          std::unique_ptr<Orthanc::Image> converted(
            new Orthanc::Image(Orthanc::PixelFormat_Float32, jpeg->GetWidth(),
                               jpeg->GetHeight(), false));

          Orthanc::ImageProcessing::Convert(*converted, *jpeg);

          /**

             Orthanc::ImageProcessing::ShiftScale() computes "(x + offset) * scaling".
             The system to solve is thus:           

             (0 + offset) * scaling = windowingCenter - windowingWidth / 2     [a]
             (255 + offset) * scaling = windowingCenter + windowingWidth / 2   [b]

             Resolution:

             [b - a] => 255 * scaling = windowingWidth
             [a] => offset = (windowingCenter - windowingWidth / 2) / scaling

          **/

          const float scaling = windowWidth_ / 255.0f;
          const float offset = (OrthancStone::LinearAlgebra::IsCloseToZero(scaling) ? 0 :
                                (windowCenter_ - windowWidth_ / 2.0f) / scaling);

          Orthanc::ImageProcessing::ShiftScale(*converted, offset, scaling, false);
          updatedCache = GetViewport().cache_->Acquire(
            sopInstanceUid_, frameIndex_, converted.release(), QUALITY_JPEG);
          break;
        }

        default:
          throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
      }

      if (updatedCache)
      {
        GetViewport().SignalUpdatedFrame(sopInstanceUid_, frameIndex_);
      }

      if (isPrefetch_)
      {
        GetViewport().ScheduleNextPrefetch();
      }
    }
  };


  class SetFullDicomFrame : public ICommand
  {
  private:
    std::string   sopInstanceUid_;
    unsigned int  frameIndex_;
    bool          isPrefetch_;
    
  public:
    SetFullDicomFrame(boost::shared_ptr<ViewerViewport> viewport,
                      const std::string& sopInstanceUid,
                      unsigned int frameIndex,
                      bool isPrefetch) :
      ICommand(viewport),
      sopInstanceUid_(sopInstanceUid),
      frameIndex_(frameIndex),
      isPrefetch_(isPrefetch)
    {
    }
    
    virtual void Handle(const OrthancStone::ParseDicomSuccessMessage& message) const ORTHANC_OVERRIDE
    {
      Orthanc::DicomMap tags;
      message.GetDicom().ExtractDicomSummary(tags);

      std::string s;
      if (!tags.LookupStringValue(s, Orthanc::DICOM_TAG_SOP_INSTANCE_UID, false))
      {
        // Safety check
        throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
      }      
      
      std::unique_ptr<Orthanc::ImageAccessor> frame(
        Orthanc::DicomImageDecoder::Decode(message.GetDicom(), frameIndex_));

      if (frame.get() == NULL)
      {
        throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
      }

      bool updatedCache;

      if (frame->GetFormat() == Orthanc::PixelFormat_RGB24)
      {
        updatedCache = GetViewport().cache_->Acquire(
          sopInstanceUid_, frameIndex_, frame.release(), QUALITY_FULL);
      }
      else
      {
        double a = 1;
        double b = 0;

        double doseScaling;
        if (tags.ParseDouble(doseScaling, Orthanc::DICOM_TAG_DOSE_GRID_SCALING))
        {
          a = doseScaling;
        }
      
        double rescaleIntercept, rescaleSlope;
        if (tags.ParseDouble(rescaleIntercept, Orthanc::DICOM_TAG_RESCALE_INTERCEPT) &&
            tags.ParseDouble(rescaleSlope, Orthanc::DICOM_TAG_RESCALE_SLOPE))
        {
          a *= rescaleSlope;
          b = rescaleIntercept;
        }

        std::unique_ptr<Orthanc::ImageAccessor> converted(
          new Orthanc::Image(Orthanc::PixelFormat_Float32, frame->GetWidth(), frame->GetHeight(), false));
        Orthanc::ImageProcessing::Convert(*converted, *frame);
        Orthanc::ImageProcessing::ShiftScale2(*converted, b, a, false);

        updatedCache = GetViewport().cache_->Acquire(
          sopInstanceUid_, frameIndex_, converted.release(), QUALITY_FULL);
      }
      
      if (updatedCache)
      {
        GetViewport().SignalUpdatedFrame(sopInstanceUid_, frameIndex_);
      }

      if (isPrefetch_)
      {
        GetViewport().ScheduleNextPrefetch();
      }
    }
  };


  class PrefetchItem
  {
  private:
    size_t   frameIndex_;
    bool     isFull_;

  public:
    PrefetchItem(size_t frameIndex,
                 bool isFull) :
      frameIndex_(frameIndex),
      isFull_(isFull)
    {
    }

    size_t GetFrameIndex() const
    {
      return frameIndex_;
    }

    bool IsFull() const
    {
      return isFull_;
    }
  };
  

  std::unique_ptr<IObserver>                    observer_;
  OrthancStone::ILoadersContext&               context_;
  boost::shared_ptr<OrthancStone::WebGLViewport>   viewport_;
  boost::shared_ptr<OrthancStone::DicomResourcesLoader> loader_;
  OrthancStone::DicomSource                    source_;
  boost::shared_ptr<FramesCache>               cache_;  
  std::unique_ptr<OrthancStone::SortedFrames>  frames_;
  std::unique_ptr<SeriesCursor>                cursor_;
  float                                        windowingCenter_;
  float                                        windowingWidth_;
  float                                        defaultWindowingCenter_;
  float                                        defaultWindowingWidth_;
  bool                                         inverted_;
  bool                                         fitNextContent_;
  bool                                         isCtrlDown_;
  FrameGeometry                                currentFrameGeometry_;
  std::list<PrefetchItem>                      prefetchQueue_;

  void ScheduleNextPrefetch()
  {
    while (!prefetchQueue_.empty())
    {
      size_t index = prefetchQueue_.front().GetFrameIndex();
      bool isFull = prefetchQueue_.front().IsFull();
      prefetchQueue_.pop_front();
      
      const std::string sopInstanceUid = frames_->GetFrameSopInstanceUid(index);
      unsigned int frame = frames_->GetFrameIndex(index);

      {
        FramesCache::Accessor accessor(*cache_, sopInstanceUid, frame);
        if (!accessor.IsValid() ||
            (isFull && accessor.GetQuality() == 0))
        {
          if (isFull)
          {
            ScheduleLoadFullDicomFrame(index, PRIORITY_NORMAL, true);
          }
          else
          {
            ScheduleLoadRenderedFrame(index, PRIORITY_NORMAL, true);
          }
          return;
        }
      }
    }
  }
  
  
  void ResetDefaultWindowing()
  {
    defaultWindowingCenter_ = 128;
    defaultWindowingWidth_ = 256;

    windowingCenter_ = defaultWindowingCenter_;
    windowingWidth_ = defaultWindowingWidth_;

    inverted_ = false;
  }

  void SignalUpdatedFrame(const std::string& sopInstanceUid,
                          unsigned int frameIndex)
  {
    if (cursor_.get() != NULL &&
        frames_.get() != NULL)
    {
      size_t index = cursor_->GetCurrentIndex();

      if (frames_->GetFrameSopInstanceUid(index) == sopInstanceUid &&
          frames_->GetFrameIndex(index) == frameIndex)
      {
        DisplayCurrentFrame();
      }
    }
  }

  void DisplayCurrentFrame()
  {
    DisplayedFrameQuality quality = DisplayedFrameQuality_None;
    
    if (cursor_.get() != NULL &&
        frames_.get() != NULL)
    {
      const size_t index = cursor_->GetCurrentIndex();
      
      unsigned int cachedQuality;
      if (!DisplayFrame(cachedQuality, index))
      {
        // This frame is not cached yet: Load it
        if (source_.HasDicomWebRendered())
        {
          ScheduleLoadRenderedFrame(index, PRIORITY_HIGH, false /* not a prefetch */);
        }
        else
        {
          ScheduleLoadFullDicomFrame(index, PRIORITY_HIGH, false /* not a prefetch */);
        }
      }
      else if (cachedQuality < QUALITY_FULL)
      {
        // This frame is only available in low-res: Download the full DICOM
        ScheduleLoadFullDicomFrame(index, PRIORITY_HIGH, false /* not a prefetch */);
        quality = DisplayedFrameQuality_Low;
      }
      else
      {
        quality = DisplayedFrameQuality_High;
      }

      currentFrameGeometry_ = FrameGeometry(frames_->GetFrameTags(index));

      {
        // Prepare prefetching
        prefetchQueue_.clear();
        for (size_t i = 0; i < cursor_->GetPrefetchSize() && i < 16; i++)
        {
          size_t a = cursor_->GetPrefetchFrameIndex(i);
          if (a != index)
          {
            prefetchQueue_.push_back(PrefetchItem(a, i < 2));
          }
        }

        ScheduleNextPrefetch();
      }      

      if (observer_.get() != NULL)
      {
        observer_->SignalFrameUpdated(*this, cursor_->GetCurrentIndex(),
                                      frames_->GetFramesCount(), quality);
      }
    }
    else
    {
      currentFrameGeometry_ = FrameGeometry();
    }
  }

  void ClearViewport()
  {
    {
      std::unique_ptr<OrthancStone::IViewport::ILock> lock(viewport_->Lock());
      lock->GetController().GetScene().DeleteLayer(LAYER_TEXTURE);
      //lock->GetCompositor().Refresh(lock->GetController().GetScene());
      lock->Invalidate();
    }
  }

  bool DisplayFrame(unsigned int& quality,
                    size_t index)
  {
    if (frames_.get() == NULL)
    {
      return false;
    }

    const std::string sopInstanceUid = frames_->GetFrameSopInstanceUid(index);
    unsigned int frame = frames_->GetFrameIndex(index);

    FramesCache::Accessor accessor(*cache_, sopInstanceUid, frame);
    if (accessor.IsValid())
    {
      quality = accessor.GetQuality();

      std::unique_ptr<OrthancStone::TextureBaseSceneLayer> layer;

      switch (accessor.GetImage().GetFormat())
      {
        case Orthanc::PixelFormat_RGB24:
          layer.reset(new OrthancStone::ColorTextureSceneLayer(accessor.GetImage()));
          break;

        case Orthanc::PixelFormat_Float32:
        {
          std::unique_ptr<OrthancStone::FloatTextureSceneLayer> tmp(
            new OrthancStone::FloatTextureSceneLayer(accessor.GetImage()));
          tmp->SetCustomWindowing(windowingCenter_, windowingWidth_);
          tmp->SetInverted(inverted_ ^ frames_->IsFrameMonochrome1(index));
          layer.reset(tmp.release());
          break;
        }

        default:
          throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageFormat);
      }

      layer->SetLinearInterpolation(true);

      double pixelSpacingX, pixelSpacingY;
      OrthancStone::GeometryToolbox::GetPixelSpacing(
        pixelSpacingX, pixelSpacingY, frames_->GetFrameTags(index));
      layer->SetPixelSpacing(pixelSpacingX, pixelSpacingY);

      if (layer.get() == NULL)
      {
        return false;
      }
      else
      {
        std::unique_ptr<OrthancStone::IViewport::ILock> lock(viewport_->Lock());
        lock->GetController().GetScene().SetLayer(LAYER_TEXTURE, layer.release());

        if (fitNextContent_)
        {
          lock->GetCompositor().RefreshCanvasSize();
          lock->GetCompositor().FitContent(lock->GetController().GetScene());
          fitNextContent_ = false;
        }
        
        //lock->GetCompositor().Refresh(lock->GetController().GetScene());
        lock->Invalidate();
        return true;
      }
    }
    else
    {
      return false;
    }
  }

  void ScheduleLoadFullDicomFrame(size_t index,
                                  int priority,
                                  bool isPrefetch)
  {
    if (frames_.get() != NULL)
    {
      std::string sopInstanceUid = frames_->GetFrameSopInstanceUid(index);
      unsigned int frame = frames_->GetFrameIndex(index);
      
      {
        std::unique_ptr<OrthancStone::ILoadersContext::ILock> lock(context_.Lock());
        lock->Schedule(
          GetSharedObserver(), priority, OrthancStone::ParseDicomFromWadoCommand::Create(
            source_, frames_->GetStudyInstanceUid(), frames_->GetSeriesInstanceUid(),
            sopInstanceUid, false /* transcoding (TODO) */,
            Orthanc::DicomTransferSyntax_LittleEndianExplicit /* TODO */,
            new SetFullDicomFrame(GetSharedObserver(), sopInstanceUid, frame, isPrefetch)));
      }
    }
  }

  void ScheduleLoadRenderedFrame(size_t index,
                                 int priority,
                                 bool isPrefetch)
  {
    if (!source_.HasDicomWebRendered())
    {
      ScheduleLoadFullDicomFrame(index, priority, isPrefetch);
    }
    else if (frames_.get() != NULL)
    {
      std::string sopInstanceUid = frames_->GetFrameSopInstanceUid(index);
      unsigned int frame = frames_->GetFrameIndex(index);
      bool isMonochrome1 = frames_->IsFrameMonochrome1(index);

      const std::string uri = ("studies/" + frames_->GetStudyInstanceUid() +
                               "/series/" + frames_->GetSeriesInstanceUid() +
                               "/instances/" + sopInstanceUid +
                               "/frames/" + boost::lexical_cast<std::string>(frame + 1) + "/rendered");

      std::map<std::string, std::string> headers, arguments;
      arguments["window"] = (
        boost::lexical_cast<std::string>(defaultWindowingCenter_) + ","  +
        boost::lexical_cast<std::string>(defaultWindowingWidth_) + ",linear");

      std::unique_ptr<OrthancStone::IOracleCommand> command(
        source_.CreateDicomWebCommand(
          uri, arguments, headers, new SetLowQualityFrame(
            GetSharedObserver(), sopInstanceUid, frame,
            defaultWindowingCenter_, defaultWindowingWidth_, isMonochrome1, isPrefetch)));

      {
        std::unique_ptr<OrthancStone::ILoadersContext::ILock> lock(context_.Lock());
        lock->Schedule(GetSharedObserver(), priority, command.release());
      }
    }
  }

  ViewerViewport(OrthancStone::ILoadersContext& context,
                 const OrthancStone::DicomSource& source,
                 const std::string& canvas,
                 boost::shared_ptr<FramesCache> cache) :
    context_(context),
    source_(source),
    viewport_(OrthancStone::WebGLViewport::Create(canvas)),
    cache_(cache),
    fitNextContent_(true),
    isCtrlDown_(false)
  {
    if (!cache_)
    {
      throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
    }
    
    emscripten_set_wheel_callback(viewport_->GetCanvasCssSelector().c_str(), this, true, OnWheel);
    emscripten_set_keydown_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, this, false, OnKey);
    emscripten_set_keyup_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, this, false, OnKey);

    ResetDefaultWindowing();
  }

  static EM_BOOL OnKey(int eventType,
                       const EmscriptenKeyboardEvent *event,
                       void *userData)
  {
    /**
     * WARNING: There is a problem with Firefox 71 that seems to mess
     * the "ctrlKey" value.
     **/
    
    ViewerViewport& that = *reinterpret_cast<ViewerViewport*>(userData);
    that.isCtrlDown_ = event->ctrlKey;
    return false;
  }

  
  static EM_BOOL OnWheel(int eventType,
                         const EmscriptenWheelEvent *wheelEvent,
                         void *userData)
  {
    ViewerViewport& that = *reinterpret_cast<ViewerViewport*>(userData);

    if (that.cursor_.get() != NULL)
    {
      if (wheelEvent->deltaY < 0)
      {
        that.ChangeFrame(that.isCtrlDown_ ? SeriesCursor::Action_FastMinus : SeriesCursor::Action_Minus);
      }
      else if (wheelEvent->deltaY > 0)
      {
        that.ChangeFrame(that.isCtrlDown_ ? SeriesCursor::Action_FastPlus : SeriesCursor::Action_Plus);
      }
    }
    
    return true;
  }

  void Handle(const OrthancStone::DicomResourcesLoader::SuccessMessage& message)
  {
    dynamic_cast<const ICommand&>(message.GetUserPayload()).Handle(message);
  }

  void Handle(const OrthancStone::HttpCommand::SuccessMessage& message)
  {
    dynamic_cast<const ICommand&>(message.GetOrigin().GetPayload()).Handle(message);
  }

  void Handle(const OrthancStone::ParseDicomSuccessMessage& message)
  {
    dynamic_cast<const ICommand&>(message.GetOrigin().GetPayload()).Handle(message);
  }
  
public:
  static boost::shared_ptr<ViewerViewport> Create(OrthancStone::ILoadersContext::ILock& lock,
                                                  const OrthancStone::DicomSource& source,
                                                  const std::string& canvas,
                                                  boost::shared_ptr<FramesCache> cache)
  {
    boost::shared_ptr<ViewerViewport> viewport(
      new ViewerViewport(lock.GetContext(), source, canvas, cache));

    viewport->loader_ = OrthancStone::DicomResourcesLoader::Create(lock);
    viewport->Register<OrthancStone::DicomResourcesLoader::SuccessMessage>(
      *viewport->loader_, &ViewerViewport::Handle);

    viewport->Register<OrthancStone::HttpCommand::SuccessMessage>(
      lock.GetOracleObservable(), &ViewerViewport::Handle);

    viewport->Register<OrthancStone::ParseDicomSuccessMessage>(
      lock.GetOracleObservable(), &ViewerViewport::Handle);

    return viewport;    
  }

  void SetFrames(OrthancStone::SortedFrames* frames)
  {
    if (frames == NULL)
    {
      throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
    }

    fitNextContent_ = true;

    frames_.reset(frames);
    cursor_.reset(new SeriesCursor(frames_->GetFramesCount()));

    LOG(INFO) << "Number of frames in series: " << frames_->GetFramesCount();

    ResetDefaultWindowing();
    ClearViewport();
    prefetchQueue_.clear();
    currentFrameGeometry_ = FrameGeometry();

    if (observer_.get() != NULL)
    {
      observer_->SignalFrameUpdated(*this, cursor_->GetCurrentIndex(),
                                    frames_->GetFramesCount(), DisplayedFrameQuality_None);
    }
    
    if (frames_->GetFramesCount() != 0)
    {
      const std::string& sopInstanceUid = frames_->GetFrameSopInstanceUid(cursor_->GetCurrentIndex());

      {
        // Fetch the default windowing for the central instance
        const std::string uri = ("studies/" + frames_->GetStudyInstanceUid() +
                                 "/series/" + frames_->GetSeriesInstanceUid() +
                                 "/instances/" + sopInstanceUid + "/metadata");
        
        loader_->ScheduleGetDicomWeb(
          boost::make_shared<OrthancStone::LoadedDicomResources>(Orthanc::DICOM_TAG_SOP_INSTANCE_UID),
          0, source_, uri, new SetDefaultWindowingCommand(GetSharedObserver()));
      }
    }
  }

  // This method is used when the layout of the HTML page changes,
  // which does not trigger the "emscripten_set_resize_callback()"
  void UpdateSize(bool fitContent)
  {
    std::unique_ptr<OrthancStone::IViewport::ILock> lock(viewport_->Lock());
    lock->GetCompositor().RefreshCanvasSize();

    if (fitContent)
    {
      lock->GetCompositor().FitContent(lock->GetController().GetScene());
    }

    lock->Invalidate();
  }

  void AcquireObserver(IObserver* observer)
  {  
    observer_.reset(observer);
  }

  const std::string& GetCanvasId() const
  {
    assert(viewport_);
    return viewport_->GetCanvasId();
  }

  void ChangeFrame(SeriesCursor::Action action)
  {
    if (cursor_.get() != NULL)
    {
      size_t previous = cursor_->GetCurrentIndex();
      
      cursor_->Apply(action);
      
      size_t current = cursor_->GetCurrentIndex();
      if (previous != current)
      {
        DisplayCurrentFrame();
      }
    }
  }

  const FrameGeometry& GetCurrentFrameGeometry() const
  {
    return currentFrameGeometry_;
  }

  void UpdateReferenceLines(const std::list<const FrameGeometry*>& planes)
  {
    std::unique_ptr<OrthancStone::PolylineSceneLayer> layer(new OrthancStone::PolylineSceneLayer);
    
    if (GetCurrentFrameGeometry().IsValid())
    {
      for (std::list<const FrameGeometry*>::const_iterator
             it = planes.begin(); it != planes.end(); ++it)
      {
        assert(*it != NULL);
        
        double x1, y1, x2, y2;
        if (GetCurrentFrameGeometry().Intersect(x1, y1, x2, y2, **it))
        {
          OrthancStone::PolylineSceneLayer::Chain chain;
          chain.push_back(OrthancStone::ScenePoint2D(x1, y1));
          chain.push_back(OrthancStone::ScenePoint2D(x2, y2));
          layer->AddChain(chain, false, 0, 255, 0);
        }
      }
    }
    
    {
      std::unique_ptr<OrthancStone::IViewport::ILock> lock(viewport_->Lock());

      if (layer->GetChainsCount() == 0)
      {
        lock->GetController().GetScene().DeleteLayer(LAYER_REFERENCE_LINES);
      }
      else
      {
        lock->GetController().GetScene().SetLayer(LAYER_REFERENCE_LINES, layer.release());
      }
      
      //lock->GetCompositor().Refresh(lock->GetController().GetScene());
      lock->Invalidate();
    }
  }


  void ClearReferenceLines()
  {
    {
      std::unique_ptr<OrthancStone::IViewport::ILock> lock(viewport_->Lock());
      lock->GetController().GetScene().DeleteLayer(LAYER_REFERENCE_LINES);
      lock->Invalidate();
    }
  }


  void SetDefaultWindowing()
  {
    SetWindowing(defaultWindowingCenter_, defaultWindowingWidth_);
  }

  void SetWindowing(float windowingCenter,
                    float windowingWidth)
  {
    windowingCenter_ = windowingCenter;
    windowingWidth_ = windowingWidth;

    {
      std::unique_ptr<OrthancStone::IViewport::ILock> lock(viewport_->Lock());

      if (lock->GetController().GetScene().HasLayer(LAYER_TEXTURE) &&
          lock->GetController().GetScene().GetLayer(LAYER_TEXTURE).GetType() ==
          OrthancStone::ISceneLayer::Type_FloatTexture)
      {
        dynamic_cast<OrthancStone::FloatTextureSceneLayer&>(
          lock->GetController().GetScene().GetLayer(LAYER_TEXTURE)).
          SetCustomWindowing(windowingCenter_, windowingWidth_);
        lock->Invalidate();
      }
    }
  }

  void Invert()
  {
    inverted_ = !inverted_;
    
    {
      std::unique_ptr<OrthancStone::IViewport::ILock> lock(viewport_->Lock());

      if (lock->GetController().GetScene().HasLayer(LAYER_TEXTURE) &&
          lock->GetController().GetScene().GetLayer(LAYER_TEXTURE).GetType() ==
          OrthancStone::ISceneLayer::Type_FloatTexture)
      {
        OrthancStone::FloatTextureSceneLayer& layer = 
          dynamic_cast<OrthancStone::FloatTextureSceneLayer&>(
            lock->GetController().GetScene().GetLayer(LAYER_TEXTURE));

        // NB: Using "IsInverted()" instead of "inverted_" is for
        // compatibility with MONOCHROME1 images
        layer.SetInverted(!layer.IsInverted());
        lock->Invalidate();
      }
    }
  }
};





typedef std::map<std::string, boost::shared_ptr<ViewerViewport> >  Viewports;
static Viewports allViewports_;
static bool showReferenceLines_ = true;


static void UpdateReferenceLines()
{
  if (showReferenceLines_)
  {
    std::list<const FrameGeometry*> planes;
    
    for (Viewports::const_iterator it = allViewports_.begin(); it != allViewports_.end(); ++it)
    {
      assert(it->second != NULL);
      planes.push_back(&it->second->GetCurrentFrameGeometry());
    }

    for (Viewports::iterator it = allViewports_.begin(); it != allViewports_.end(); ++it)
    {
      assert(it->second != NULL);
      it->second->UpdateReferenceLines(planes);
    }
  }
  else
  {
    for (Viewports::iterator it = allViewports_.begin(); it != allViewports_.end(); ++it)
    {
      assert(it->second != NULL);
      it->second->ClearReferenceLines();
    }
  }
}


class WebAssemblyObserver : public ResourcesLoader::IObserver,
                            public ViewerViewport::IObserver
{
public:
  virtual void SignalResourcesLoaded() ORTHANC_OVERRIDE
  {
    DISPATCH_JAVASCRIPT_EVENT("ResourcesLoaded");
  }

  virtual void SignalSeriesThumbnailLoaded(const std::string& studyInstanceUid,
                                           const std::string& seriesInstanceUid) ORTHANC_OVERRIDE
  {
    EM_ASM({
        const customEvent = document.createEvent("CustomEvent");
        customEvent.initCustomEvent("ThumbnailLoaded", false, false,
                                    { "studyInstanceUid" : UTF8ToString($0),
                                        "seriesInstanceUid" : UTF8ToString($1) });
        window.dispatchEvent(customEvent);
      },
      studyInstanceUid.c_str(),
      seriesInstanceUid.c_str());
  }

  virtual void SignalSeriesMetadataLoaded(const std::string& studyInstanceUid,
                                          const std::string& seriesInstanceUid) ORTHANC_OVERRIDE
  {
    EM_ASM({
        const customEvent = document.createEvent("CustomEvent");
        customEvent.initCustomEvent("MetadataLoaded", false, false,
                                    { "studyInstanceUid" : UTF8ToString($0),
                                        "seriesInstanceUid" : UTF8ToString($1) });
        window.dispatchEvent(customEvent);
      },
      studyInstanceUid.c_str(),
      seriesInstanceUid.c_str());
  }

  virtual void SignalFrameUpdated(const ViewerViewport& viewport,
                                  size_t currentFrame,
                                  size_t countFrames,
                                  DisplayedFrameQuality quality) ORTHANC_OVERRIDE
  {
    EM_ASM({
        const customEvent = document.createEvent("CustomEvent");
        customEvent.initCustomEvent("FrameUpdated", false, false,
                                    { "canvasId" : UTF8ToString($0),
                                        "currentFrame" : $1,
                                        "framesCount" : $2,
                                        "quality" : $3 });
        window.dispatchEvent(customEvent);
      },
      viewport.GetCanvasId().c_str(),
      static_cast<int>(currentFrame),
      static_cast<int>(countFrames),
      quality);


    UpdateReferenceLines();
  };
};



static OrthancStone::DicomSource source_;
static boost::shared_ptr<FramesCache> cache_;
static boost::shared_ptr<OrthancStone::WebAssemblyLoadersContext> context_;
static std::string stringBuffer_;



static void FormatTags(std::string& target,
                       const Orthanc::DicomMap& tags)
{
  Orthanc::DicomArray arr(tags);
  Json::Value v = Json::objectValue;

  for (size_t i = 0; i < arr.GetSize(); i++)
  {
    const Orthanc::DicomElement& element = arr.GetElement(i);
    if (!element.GetValue().IsBinary() &&
        !element.GetValue().IsNull())
    {
      v[element.GetTag().Format()] = element.GetValue().GetContent();
    }
  }

  target = v.toStyledString();
}


static ResourcesLoader& GetResourcesLoader()
{
  static boost::shared_ptr<ResourcesLoader>  resourcesLoader_;

  if (!resourcesLoader_)
  {
    std::unique_ptr<OrthancStone::ILoadersContext::ILock> lock(context_->Lock());
    resourcesLoader_ = ResourcesLoader::Create(*lock, source_);
    resourcesLoader_->AcquireObserver(new WebAssemblyObserver);
  }

  return *resourcesLoader_;
}


static boost::shared_ptr<ViewerViewport> GetViewport(const std::string& canvas)
{
  Viewports::iterator found = allViewports_.find(canvas);
  if (found == allViewports_.end())
  {
    std::unique_ptr<OrthancStone::ILoadersContext::ILock> lock(context_->Lock());
    boost::shared_ptr<ViewerViewport> viewport(ViewerViewport::Create(*lock, source_, canvas, cache_));
    viewport->AcquireObserver(new WebAssemblyObserver);
    allViewports_[canvas] = viewport;
    return viewport;
  }
  else
  {
    return found->second;
  }
}


extern "C"
{
  int main(int argc, char const *argv[]) 
  {
    printf("OK\n");
    Orthanc::InitializeFramework("", true);
    Orthanc::Logging::EnableInfoLevel(true);
    //Orthanc::Logging::EnableTraceLevel(true);

    context_.reset(new OrthancStone::WebAssemblyLoadersContext(1, 4, 1));
    cache_.reset(new FramesCache);
    
    DISPATCH_JAVASCRIPT_EVENT("StoneInitialized");
  }


  EMSCRIPTEN_KEEPALIVE
  void SetOrthancRoot(const char* uri,
                      int useRendered)
  {
    try
    {
      context_->SetLocalOrthanc(uri);  // For "source_.SetDicomWebThroughOrthancSource()"
      source_.SetDicomWebSource(std::string(uri) + "/dicom-web");
      source_.SetDicomWebRendered(useRendered != 0);
    }
    EXTERN_CATCH_EXCEPTIONS;
  }
  

  EMSCRIPTEN_KEEPALIVE
  void SetDicomWebServer(const char* serverName,
                         int hasRendered)
  {
    try
    {
      source_.SetDicomWebThroughOrthancSource(serverName);
      source_.SetDicomWebRendered(hasRendered != 0);
    }
    EXTERN_CATCH_EXCEPTIONS;
  }
  

  EMSCRIPTEN_KEEPALIVE
  void FetchAllStudies()
  {
    try
    {
      GetResourcesLoader().FetchAllStudies();
    }
    EXTERN_CATCH_EXCEPTIONS;
  }

  EMSCRIPTEN_KEEPALIVE
  void FetchStudy(const char* studyInstanceUid)
  {
    try
    {
      GetResourcesLoader().FetchStudy(studyInstanceUid);
    }
    EXTERN_CATCH_EXCEPTIONS;
  }

  EMSCRIPTEN_KEEPALIVE
  void FetchSeries(const char* studyInstanceUid,
                   const char* seriesInstanceUid)
  {
    try
    {
      GetResourcesLoader().FetchSeries(studyInstanceUid, seriesInstanceUid);
    }
    EXTERN_CATCH_EXCEPTIONS;
  }
  
  EMSCRIPTEN_KEEPALIVE
  int GetStudiesCount()
  {
    try
    {
      return GetResourcesLoader().GetStudiesCount();
    }
    EXTERN_CATCH_EXCEPTIONS;
    return 0;  // on exception
  }
  
  EMSCRIPTEN_KEEPALIVE
  int GetSeriesCount()
  {
    try
    {
      return GetResourcesLoader().GetSeriesCount();
    }
    EXTERN_CATCH_EXCEPTIONS;
    return 0;  // on exception
  }


  EMSCRIPTEN_KEEPALIVE
  const char* GetStringBuffer()
  {
    return stringBuffer_.c_str();
  }
  

  EMSCRIPTEN_KEEPALIVE
  void LoadStudyTags(int i)
  {
    try
    {
      if (i < 0)
      {
        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
      }
      
      Orthanc::DicomMap dicom;
      GetResourcesLoader().GetStudy(dicom, i);
      FormatTags(stringBuffer_, dicom);
    }
    EXTERN_CATCH_EXCEPTIONS;
  }
  

  EMSCRIPTEN_KEEPALIVE
  void LoadSeriesTags(int i)
  {
    try
    {
      if (i < 0)
      {
        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
      }
      
      Orthanc::DicomMap dicom;
      GetResourcesLoader().GetSeries(dicom, i);
      FormatTags(stringBuffer_, dicom);
    }
    EXTERN_CATCH_EXCEPTIONS;
  }
  

  EMSCRIPTEN_KEEPALIVE
  int LoadSeriesThumbnail(const char* seriesInstanceUid)
  {
    try
    {
      std::string image, mime;
      switch (GetResourcesLoader().GetSeriesThumbnail(image, mime, seriesInstanceUid))
      {
        case OrthancStone::SeriesThumbnailType_Image:
          Orthanc::Toolbox::EncodeDataUriScheme(stringBuffer_, mime, image);
          return ThumbnailType_Image;
          
        case OrthancStone::SeriesThumbnailType_Pdf:
          return ThumbnailType_Pdf;
          
        case OrthancStone::SeriesThumbnailType_Video:
          return ThumbnailType_Video;
          
        case OrthancStone::SeriesThumbnailType_NotLoaded:
          return ThumbnailType_Loading;
          
        case OrthancStone::SeriesThumbnailType_Unsupported:
          return ThumbnailType_NoPreview;

        default:
          return ThumbnailType_Unknown;
      }
    }
    EXTERN_CATCH_EXCEPTIONS;
    return ThumbnailType_Unknown;
  }


  EMSCRIPTEN_KEEPALIVE
  void SpeedUpFetchSeriesMetadata(const char* studyInstanceUid,
                                  const char* seriesInstanceUid)
  {
    try
    {
      GetResourcesLoader().FetchSeriesMetadata(PRIORITY_HIGH, studyInstanceUid, seriesInstanceUid);
    }
    EXTERN_CATCH_EXCEPTIONS;
  }


  EMSCRIPTEN_KEEPALIVE
  int IsSeriesComplete(const char* seriesInstanceUid)
  {
    try
    {
      return GetResourcesLoader().IsSeriesComplete(seriesInstanceUid) ? 1 : 0;
    }
    EXTERN_CATCH_EXCEPTIONS;
    return 0;
  }

  EMSCRIPTEN_KEEPALIVE
  int LoadSeriesInViewport(const char* canvas,
                           const char* seriesInstanceUid)
  {
    try
    {
      std::unique_ptr<OrthancStone::SortedFrames> frames(new OrthancStone::SortedFrames);
      
      if (GetResourcesLoader().SortSeriesFrames(*frames, seriesInstanceUid))
      {
        GetViewport(canvas)->SetFrames(frames.release());
        return 1;
      }
      else
      {
        return 0;
      }
    }
    EXTERN_CATCH_EXCEPTIONS;
    return 0;
  }


  EMSCRIPTEN_KEEPALIVE
  void AllViewportsUpdateSize(int fitContent)
  {
    try
    {
      for (Viewports::iterator it = allViewports_.begin(); it != allViewports_.end(); ++it)
      {
        assert(it->second != NULL);
        it->second->UpdateSize(fitContent != 0);
      }
    }
    EXTERN_CATCH_EXCEPTIONS;
  }


  EMSCRIPTEN_KEEPALIVE
  void DecrementFrame(const char* canvas,
                      int fitContent)
  {
    try
    {
      GetViewport(canvas)->ChangeFrame(SeriesCursor::Action_Minus);
    }
    EXTERN_CATCH_EXCEPTIONS;
  }


  EMSCRIPTEN_KEEPALIVE
  void IncrementFrame(const char* canvas,
                      int fitContent)
  {
    try
    {
      GetViewport(canvas)->ChangeFrame(SeriesCursor::Action_Plus);
    }
    EXTERN_CATCH_EXCEPTIONS;
  }  


  EMSCRIPTEN_KEEPALIVE
  void ShowReferenceLines(int show)
  {
    try
    {
      showReferenceLines_ = (show != 0);
      UpdateReferenceLines();
    }
    EXTERN_CATCH_EXCEPTIONS;
  }  


  EMSCRIPTEN_KEEPALIVE
  void SetDefaultWindowing(const char* canvas)
  {
    try
    {
      GetViewport(canvas)->SetDefaultWindowing();
    }
    EXTERN_CATCH_EXCEPTIONS;
  }  


  EMSCRIPTEN_KEEPALIVE
  void SetWindowing(const char* canvas,
                    int center,
                    int width)
  {
    try
    {
      GetViewport(canvas)->SetWindowing(center, width);
    }
    EXTERN_CATCH_EXCEPTIONS;
  }  


  EMSCRIPTEN_KEEPALIVE
  void InvertContrast(const char* canvas)
  {
    try
    {
      GetViewport(canvas)->Invert();
    }
    EXTERN_CATCH_EXCEPTIONS;
  }  
}