diff Samples/WebAssembly/BasicMPR.cpp @ 820:270c31978df1

BasicMPR sample
author Sebastien Jodogne <s.jodogne@gmail.com>
date Wed, 29 May 2019 13:40:07 +0200
parents
children 76e8224bc300
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/WebAssembly/BasicMPR.cpp	Wed May 29 13:40:07 2019 +0200
@@ -0,0 +1,880 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 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>
+#include <emscripten/fetch.h>
+#include <emscripten/html5.h>
+
+#include "../../Framework/Loaders/OrthancSeriesVolumeProgressiveLoader.h"
+#include "../../Framework/OpenGL/WebAssemblyOpenGLContext.h"
+#include "../../Framework/Scene2D/GrayscaleStyleConfigurator.h"
+#include "../../Framework/Scene2D/OpenGLCompositor.h"
+#include "../../Framework/Scene2D/PanSceneTracker.h"
+#include "../../Framework/Scene2D/RotateSceneTracker.h"
+#include "../../Framework/Scene2D/ZoomSceneTracker.h"
+#include "../../Framework/Scene2DViewport/IFlexiblePointerTracker.h"
+#include "../../Framework/Scene2DViewport/ViewportController.h"
+#include "../../Framework/StoneInitialization.h"
+#include "../../Framework/Volumes/VolumeSceneLayerSource.h"
+
+#include <Core/OrthancException.h>
+#include <Core/Toolbox.h>
+
+
+static const unsigned int FONT_SIZE = 32;
+
+
+namespace OrthancStone
+{
+  class WebAssemblyViewport : public boost::noncopyable
+  {
+  private:
+    // the construction order is important because compositor_
+    // will hold a reference to the scene that belong to the 
+    // controller_ object
+    OpenGL::WebAssemblyOpenGLContext       context_;
+    boost::shared_ptr<ViewportController>  controller_;
+    OpenGLCompositor                       compositor_;
+
+    void SetupEvents(const std::string& canvas);
+
+  public:
+    WebAssemblyViewport(MessageBroker& broker,
+                        const std::string& canvas) :
+      context_(canvas),
+      controller_(new ViewportController(broker)),
+      compositor_(context_, *controller_->GetScene())
+    {
+      compositor_.SetFont(0, Orthanc::EmbeddedResources::UBUNTU_FONT, 
+                          FONT_SIZE, Orthanc::Encoding_Latin1);
+      SetupEvents(canvas);
+    }
+
+    Scene2D& GetScene()
+    {
+      return *controller_->GetScene();
+    }
+
+    const boost::shared_ptr<ViewportController>& GetController()
+    {
+      return controller_;
+    }
+
+    void UpdateSize()
+    {
+      context_.UpdateSize();
+      compositor_.UpdateSize();
+      Refresh();
+    }
+
+    void Refresh()
+    {
+      compositor_.Refresh();
+    }
+
+    const std::string& GetCanvasIdentifier() const
+    {
+      return context_.GetCanvasIdentifier();
+    }
+
+    ScenePoint2D GetPixelCenterCoordinates(int x, int y) const
+    {
+      return compositor_.GetPixelCenterCoordinates(x, y);
+    }
+
+    unsigned int GetCanvasWidth() const
+    {
+      return context_.GetCanvasWidth();
+    }
+
+    unsigned int GetCanvasHeight() const
+    {
+      return context_.GetCanvasHeight();
+    }
+  };
+
+  class ActiveTracker : public boost::noncopyable
+  {
+  private:
+    boost::shared_ptr<IFlexiblePointerTracker> tracker_;
+    std::string                             canvasIdentifier_;
+    bool                                    insideCanvas_;
+    
+  public:
+    ActiveTracker(const boost::shared_ptr<IFlexiblePointerTracker>& tracker,
+                  const WebAssemblyViewport& viewport) :
+      tracker_(tracker),
+      canvasIdentifier_(viewport.GetCanvasIdentifier()),
+      insideCanvas_(true)
+    {
+      if (tracker_.get() == NULL)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
+      }
+    }
+
+    bool IsAlive() const
+    {
+      return tracker_->IsAlive();
+    }
+
+    void PointerMove(const PointerEvent& event)
+    {
+      tracker_->PointerMove(event);
+    }
+
+    void PointerUp(const PointerEvent& event)
+    {
+      tracker_->PointerUp(event);
+    }
+  };
+}
+
+static OrthancStone::PointerEvent* ConvertMouseEvent(
+  const EmscriptenMouseEvent&        source,
+  OrthancStone::WebAssemblyViewport& viewport)
+{
+  std::auto_ptr<OrthancStone::PointerEvent> target(
+    new OrthancStone::PointerEvent);
+
+  target->AddPosition(viewport.GetPixelCenterCoordinates(
+    source.targetX, source.targetY));
+  target->SetAltModifier(source.altKey);
+  target->SetControlModifier(source.ctrlKey);
+  target->SetShiftModifier(source.shiftKey);
+
+  return target.release();
+}
+
+std::auto_ptr<OrthancStone::ActiveTracker> tracker_;
+
+EM_BOOL OnMouseEvent(int eventType, 
+                     const EmscriptenMouseEvent *mouseEvent, 
+                     void *userData)
+{
+  if (mouseEvent != NULL &&
+      userData != NULL)
+  {
+    OrthancStone::WebAssemblyViewport& viewport = 
+      *reinterpret_cast<OrthancStone::WebAssemblyViewport*>(userData);
+
+    switch (eventType)
+    {
+      case EMSCRIPTEN_EVENT_CLICK:
+      {
+        static unsigned int count = 0;
+        char buf[64];
+        sprintf(buf, "click %d", count++);
+
+        std::auto_ptr<OrthancStone::TextSceneLayer> layer(new OrthancStone::TextSceneLayer);
+        layer->SetText(buf);
+        viewport.GetScene().SetLayer(100, layer.release());
+        viewport.Refresh();
+        break;
+      }
+
+      case EMSCRIPTEN_EVENT_MOUSEDOWN:
+      {
+        boost::shared_ptr<OrthancStone::IFlexiblePointerTracker> t;
+
+        {
+          std::auto_ptr<OrthancStone::PointerEvent> event(
+            ConvertMouseEvent(*mouseEvent, viewport));
+
+          switch (mouseEvent->button)
+          {
+            case 0:  // Left button
+              emscripten_console_log("Creating RotateSceneTracker");
+              t.reset(new OrthancStone::RotateSceneTracker(
+                viewport.GetController(), *event));
+              break;
+
+            case 1:  // Middle button
+              emscripten_console_log("Creating PanSceneTracker");
+              LOG(INFO) << "Creating PanSceneTracker" ;
+              t.reset(new OrthancStone::PanSceneTracker(
+                viewport.GetController(), *event));
+              break;
+
+            case 2:  // Right button
+              emscripten_console_log("Creating ZoomSceneTracker");
+              t.reset(new OrthancStone::ZoomSceneTracker(
+                viewport.GetController(), *event, viewport.GetCanvasWidth()));
+              break;
+
+            default:
+              break;
+          }
+        }
+
+        if (t.get() != NULL)
+        {
+          tracker_.reset(
+            new OrthancStone::ActiveTracker(t, viewport));
+          viewport.Refresh();
+        }
+
+        break;
+      }
+
+      case EMSCRIPTEN_EVENT_MOUSEMOVE:
+        if (tracker_.get() != NULL)
+        {
+          std::auto_ptr<OrthancStone::PointerEvent> event(
+            ConvertMouseEvent(*mouseEvent, viewport));
+          tracker_->PointerMove(*event);
+          viewport.Refresh();
+        }
+        break;
+
+      case EMSCRIPTEN_EVENT_MOUSEUP:
+        if (tracker_.get() != NULL)
+        {
+          std::auto_ptr<OrthancStone::PointerEvent> event(
+            ConvertMouseEvent(*mouseEvent, viewport));
+          tracker_->PointerUp(*event);
+          viewport.Refresh();
+          if (!tracker_->IsAlive())
+            tracker_.reset();
+        }
+        break;
+
+      default:
+        break;
+    }
+  }
+
+  return true;
+}
+
+
+void OrthancStone::WebAssemblyViewport::SetupEvents(const std::string& canvas)
+{
+  emscripten_set_mousedown_callback(canvas.c_str(), this, false, OnMouseEvent);
+  emscripten_set_mousemove_callback(canvas.c_str(), this, false, OnMouseEvent);
+  emscripten_set_mouseup_callback(canvas.c_str(), this, false, OnMouseEvent);
+}
+
+
+
+
+namespace OrthancStone
+{
+  class VolumeSlicerViewport : public IObserver
+  {
+  private:
+    OrthancStone::WebAssemblyViewport      viewport_;
+    std::auto_ptr<VolumeSceneLayerSource>  source_;
+    VolumeProjection                       projection_;
+    std::vector<CoordinateSystem3D>        planes_;
+    size_t                                 currentPlane_;
+
+    void Handle(const DicomVolumeImage::GeometryReadyMessage& message)
+    {
+      LOG(INFO) << "Geometry is available";
+
+      const VolumeImageGeometry& geometry = message.GetOrigin().GetGeometry();
+
+      const unsigned int depth = geometry.GetProjectionDepth(projection_);
+      currentPlane_ = depth / 2;
+      
+      planes_.resize(depth);
+
+      for (unsigned int z = 0; z < depth; z++)
+      {
+        planes_[z] = geometry.GetProjectionSlice(projection_, z);
+      }
+    }
+    
+  public:
+    VolumeSlicerViewport(MessageBroker& broker,
+                         const std::string& canvas,
+                         VolumeProjection projection) :
+      IObserver(broker),
+      viewport_(broker, canvas),
+      projection_(projection),
+      currentPlane_(0)
+    {
+    }
+
+    void UpdateSize()
+    {
+      viewport_.UpdateSize();
+    }
+
+    void SetSlicer(int layerDepth,
+                   const boost::shared_ptr<IVolumeSlicer>& slicer,
+                   IObservable& loader,
+                   ILayerStyleConfigurator* configurator)
+    {
+      if (source_.get() != NULL)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls,
+                                        "Only one slicer can be registered");
+      }
+      
+      loader.RegisterObserverCallback(
+        new Callable<VolumeSlicerViewport, DicomVolumeImage::GeometryReadyMessage>
+        (*this, &VolumeSlicerViewport::Handle));
+
+      source_.reset(new VolumeSceneLayerSource(viewport_.GetScene(), layerDepth, slicer));
+
+      if (configurator != NULL)
+      {
+        source_->SetConfigurator(configurator);
+      }
+    }    
+
+    void Refresh()
+    {
+      if (source_.get() != NULL &&
+          currentPlane_ < planes_.size())
+      {
+        source_->Update(planes_[currentPlane_]);
+      }
+    }
+  };
+
+
+
+
+  class WebAssemblyOracle :
+    public IOracle,
+    public IObservable
+  {
+  private:
+    typedef std::map<std::string, std::string>  HttpHeaders;
+    
+    class FetchContext : public boost::noncopyable
+    {
+    private:
+      class Emitter : public IMessageEmitter
+      {
+      private:
+        WebAssemblyOracle&  oracle_;
+
+      public:
+        Emitter(WebAssemblyOracle&  oracle) :
+          oracle_(oracle)
+        {
+        }
+
+        virtual void EmitMessage(const IObserver& receiver,
+                                 const IMessage& message)
+        {
+          oracle_.EmitMessage(receiver, message);
+        }
+      };
+
+      Emitter                        emitter_;
+      const IObserver&               receiver_;
+      std::auto_ptr<IOracleCommand>  command_;
+      std::string                    expectedContentType_;
+
+    public:
+      FetchContext(WebAssemblyOracle& oracle,
+                   const IObserver& receiver,
+                   IOracleCommand* command,
+                   const std::string& expectedContentType) :
+        emitter_(oracle),
+        receiver_(receiver),
+        command_(command),
+        expectedContentType_(expectedContentType)
+      {
+        if (command == NULL)
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
+        }
+      }
+
+      const std::string& GetExpectedContentType() const
+      {
+        return expectedContentType_;
+      }
+
+      void EmitMessage(const IMessage& message)
+      {
+        emitter_.EmitMessage(receiver_, message);
+      }
+
+      IMessageEmitter& GetEmitter()
+      {
+        return emitter_;
+      }
+
+      const IObserver& GetReceiver() const
+      {
+        return receiver_;
+      }
+
+      IOracleCommand& GetCommand() const
+      {
+        return *command_;
+      }
+
+      template <typename T>
+      const T& GetTypedCommand() const
+      {
+        return dynamic_cast<T&>(*command_);
+      }
+    };
+    
+    static void FetchSucceeded(emscripten_fetch_t *fetch)
+    {
+      /**
+       * Firstly, make a local copy of the fetched information, and
+       * free data associated with the fetch.
+       **/
+      
+      std::auto_ptr<FetchContext> context(reinterpret_cast<FetchContext*>(fetch->userData));
+
+      std::string answer;
+      if (fetch->numBytes > 0)
+      {
+        answer.assign(fetch->data, fetch->numBytes);
+      }
+
+      /**
+       * TODO - HACK - As of emscripten-1.38.31, the fetch API does
+       * not contain a way to retrieve the HTTP headers of the
+       * answer. We make the assumption that the "Content-Type" header
+       * of the response is the same as the "Accept" header of the
+       * query. This should be fixed in future versions of emscripten.
+       * https://github.com/emscripten-core/emscripten/pull/8486
+       **/
+
+      HttpHeaders headers;
+      if (!context->GetExpectedContentType().empty())
+      {
+        headers["Content-Type"] = context->GetExpectedContentType();
+      }
+      
+      
+      emscripten_fetch_close(fetch);
+
+
+      /**
+       * Secondly, use the retrieved data.
+       **/
+
+      try
+      {
+        if (context.get() == NULL)
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
+        }
+        else
+        {
+          switch (context->GetCommand().GetType())
+          {
+            case IOracleCommand::Type_OrthancRestApi:
+            {
+              OrthancRestApiCommand::SuccessMessage message
+                (context->GetTypedCommand<OrthancRestApiCommand>(), headers, answer);
+              context->EmitMessage(message);
+              break;
+            }
+            
+            case IOracleCommand::Type_GetOrthancImage:
+            {
+              context->GetTypedCommand<GetOrthancImageCommand>().ProcessHttpAnswer
+                (context->GetEmitter(), context->GetReceiver(), answer, headers);
+              break;
+            }
+          
+            case IOracleCommand::Type_GetOrthancWebViewerJpeg:
+            {
+              context->GetTypedCommand<GetOrthancWebViewerJpegCommand>().ProcessHttpAnswer
+                (context->GetEmitter(), context->GetReceiver(), answer);
+              break;
+            }
+          
+            default:
+              LOG(ERROR) << "Command type not implemented by the WebAssembly Oracle: "
+                         << context->GetCommand().GetType();
+          }
+        }
+      }
+      catch (Orthanc::OrthancException& e)
+      {
+        LOG(ERROR) << "Error while processing a fetch answer in the oracle: " << e.What();
+      }
+    }
+
+    static void FetchFailed(emscripten_fetch_t *fetch)
+    {
+      std::auto_ptr<FetchContext> context(reinterpret_cast<FetchContext*>(fetch->userData));
+      
+      LOG(ERROR) << "Fetching " << fetch->url << " failed, HTTP failure status code: " << fetch->status;
+
+      /**
+       * TODO - The following code leads to an infinite recursion, at
+       * least with Firefox running on incognito mode => WHY?
+       **/      
+      //emscripten_fetch_close(fetch); // Also free data on failure.
+    }
+
+
+    class FetchCommand : public boost::noncopyable
+    {
+    private:
+      WebAssemblyOracle&             oracle_;
+      const IObserver&               receiver_;
+      std::auto_ptr<IOracleCommand>  command_;
+      Orthanc::HttpMethod            method_;
+      std::string                    uri_;
+      std::string                    body_;
+      HttpHeaders                    headers_;
+      unsigned int                   timeout_;
+      std::string                    expectedContentType_;
+
+    public:
+      FetchCommand(WebAssemblyOracle& oracle,
+                   const IObserver& receiver,
+                   IOracleCommand* command) :
+        oracle_(oracle),
+        receiver_(receiver),
+        command_(command),
+        method_(Orthanc::HttpMethod_Get),
+        timeout_(0)
+      {
+        if (command == NULL)
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
+        }
+      }
+
+      void SetMethod(Orthanc::HttpMethod method)
+      {
+        method_ = method;
+      }
+
+      void SetUri(const std::string& uri)
+      {
+        uri_ = uri;
+      }
+
+      void SetBody(std::string& body /* will be swapped */)
+      {
+        body_.swap(body);
+      }
+
+      void SetHttpHeaders(const HttpHeaders& headers)
+      {
+        headers_ = headers;
+      }
+
+      void SetTimeout(unsigned int timeout)
+      {
+        timeout_ = timeout;
+      }
+
+      void Execute()
+      {
+        if (command_.get() == NULL)
+        {
+          // Cannot call Execute() twice
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);          
+        }
+
+        emscripten_fetch_attr_t attr;
+        emscripten_fetch_attr_init(&attr);
+
+        const char* method;
+      
+        switch (method_)
+        {
+          case Orthanc::HttpMethod_Get:
+            method = "GET";
+            break;
+
+          case Orthanc::HttpMethod_Post:
+            method = "POST";
+            break;
+
+          case Orthanc::HttpMethod_Delete:
+            method = "DELETE";
+            break;
+
+          case Orthanc::HttpMethod_Put:
+            method = "PUT";
+            break;
+
+          default:
+            throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+        }
+
+        strcpy(attr.requestMethod, method);
+
+        attr.attributes = EMSCRIPTEN_FETCH_LOAD_TO_MEMORY;
+        attr.onsuccess = FetchSucceeded;
+        attr.onerror = FetchFailed;
+        attr.timeoutMSecs = timeout_ * 1000;
+
+        std::vector<const char*> headers;
+        headers.reserve(2 * headers_.size() + 1);
+
+        std::string expectedContentType;
+        
+        for (HttpHeaders::const_iterator it = headers_.begin(); it != headers_.end(); ++it)
+        {
+          std::string key;
+          Orthanc::Toolbox::ToLowerCase(key, it->first);
+          
+          if (key == "accept")
+          {
+            expectedContentType = it->second;
+          }
+
+          if (key != "accept-encoding")  // Web browsers forbid the modification of this HTTP header
+          {
+            headers.push_back(it->first.c_str());
+            headers.push_back(it->second.c_str());
+          }
+        }
+        
+        headers.push_back(NULL);  // Termination of the array of HTTP headers
+
+        attr.requestHeaders = &headers[0];
+
+        if (!body_.empty())
+        {
+          attr.requestDataSize = body_.size();
+          attr.requestData = body_.c_str();
+        }
+
+        // Must be the last call to prevent memory leak on error
+        attr.userData = new FetchContext(oracle_, receiver_, command_.release(), expectedContentType);
+        emscripten_fetch(&attr, uri_.c_str());
+      }        
+    };
+    
+    
+    void Execute(const IObserver& receiver,
+                 OrthancRestApiCommand* command)
+    {
+      FetchCommand fetch(*this, receiver, command);
+
+      fetch.SetMethod(command->GetMethod());
+      fetch.SetUri(command->GetUri());
+      fetch.SetHttpHeaders(command->GetHttpHeaders());
+      fetch.SetTimeout(command->GetTimeout());
+      
+      if (command->GetMethod() == Orthanc::HttpMethod_Put ||
+          command->GetMethod() == Orthanc::HttpMethod_Put)
+      {
+        std::string body;
+        command->SwapBody(body);
+        fetch.SetBody(body);
+      }
+      
+      fetch.Execute();
+    }
+    
+    
+    void Execute(const IObserver& receiver,
+                 GetOrthancImageCommand* command)
+    {
+      FetchCommand fetch(*this, receiver, command);
+
+      fetch.SetUri(command->GetUri());
+      fetch.SetHttpHeaders(command->GetHttpHeaders());
+      fetch.SetTimeout(command->GetTimeout());
+      
+      fetch.Execute();
+    }
+    
+    
+    void Execute(const IObserver& receiver,
+                 GetOrthancWebViewerJpegCommand* command)
+    {
+      FetchCommand fetch(*this, receiver, command);
+
+      fetch.SetUri(command->GetUri());
+      fetch.SetHttpHeaders(command->GetHttpHeaders());
+      fetch.SetTimeout(command->GetTimeout());
+      
+      fetch.Execute();
+    }
+
+    
+  public:
+    WebAssemblyOracle(MessageBroker& broker) :
+      IObservable(broker)
+    {
+    }
+    
+    virtual void Schedule(const IObserver& receiver,
+                          IOracleCommand* command)
+    {
+      std::auto_ptr<IOracleCommand> protection(command);
+
+      if (command == NULL)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
+      }
+
+      switch (command->GetType())
+      {
+        case IOracleCommand::Type_OrthancRestApi:
+          Execute(receiver, dynamic_cast<OrthancRestApiCommand*>(protection.release()));
+          break;
+        
+        case IOracleCommand::Type_GetOrthancImage:
+          Execute(receiver, dynamic_cast<GetOrthancImageCommand*>(protection.release()));
+          break;
+
+        case IOracleCommand::Type_GetOrthancWebViewerJpeg:
+          Execute(receiver, dynamic_cast<GetOrthancWebViewerJpegCommand*>(protection.release()));
+          break;          
+            
+        default:
+          LOG(ERROR) << "Command type not implemented by the WebAssembly Oracle: " << command->GetType();
+      }
+    }
+
+    virtual void Start()
+    {
+    }
+
+    virtual void Stop()
+    {
+    }
+  };
+}
+
+
+
+
+boost::shared_ptr<OrthancStone::DicomVolumeImage>  ct_(new OrthancStone::DicomVolumeImage);
+
+boost::shared_ptr<OrthancStone::OrthancSeriesVolumeProgressiveLoader>  loader_;
+
+std::auto_ptr<OrthancStone::VolumeSlicerViewport>  viewport1_;
+std::auto_ptr<OrthancStone::VolumeSlicerViewport>  viewport2_;
+std::auto_ptr<OrthancStone::VolumeSlicerViewport>  viewport3_;
+
+OrthancStone::MessageBroker  broker_;
+OrthancStone::WebAssemblyOracle  oracle_(broker_);
+
+
+EM_BOOL OnWindowResize(int eventType, const EmscriptenUiEvent *uiEvent, void *userData)
+{
+  try
+  {
+    if (viewport1_.get() != NULL)
+    {
+      viewport1_->UpdateSize();
+    }
+  
+    if (viewport2_.get() != NULL)
+    {
+      viewport2_->UpdateSize();
+    }
+  
+    if (viewport3_.get() != NULL)
+    {
+      viewport3_->UpdateSize();
+    }
+  }
+  catch (Orthanc::OrthancException& e)
+  {
+    LOG(ERROR) << "Exception while updating canvas size: " << e.What();
+  }
+  
+  return true;
+}
+
+
+
+
+EM_BOOL OnAnimationFrame(double time, void *userData)
+{
+  try
+  {
+    if (viewport1_.get() != NULL)
+    {
+      viewport1_->Refresh();
+    }
+  
+    if (viewport2_.get() != NULL)
+    {
+      viewport2_->Refresh();
+    }
+  
+    if (viewport3_.get() != NULL)
+    {
+      viewport3_->Refresh();
+    }
+
+    return true;
+  }
+  catch (Orthanc::OrthancException& e)
+  {
+    LOG(ERROR) << "Exception in the animation loop, stopping now: " << e.What();
+    return false;
+  }  
+}
+
+
+extern "C"
+{
+  int main(int argc, char const *argv[]) 
+  {
+    OrthancStone::StoneInitialize();
+    Orthanc::Logging::EnableInfoLevel(true);
+    // Orthanc::Logging::EnableTraceLevel(true);
+    EM_ASM(window.dispatchEvent(new CustomEvent("WebAssemblyLoaded")););
+  }
+
+  EMSCRIPTEN_KEEPALIVE
+  void Initialize()
+  {
+    try
+    {
+      loader_.reset(new OrthancStone::OrthancSeriesVolumeProgressiveLoader(ct_, oracle_, oracle_));
+    
+      viewport1_.reset(new OrthancStone::VolumeSlicerViewport(broker_, "mycanvas1", OrthancStone::VolumeProjection_Axial));
+      viewport1_->SetSlicer(0, loader_, *loader_, new OrthancStone::GrayscaleStyleConfigurator);
+      viewport1_->UpdateSize();
+
+      viewport2_.reset(new OrthancStone::VolumeSlicerViewport(broker_, "mycanvas2", OrthancStone::VolumeProjection_Coronal));
+      viewport2_->SetSlicer(0, loader_, *loader_, new OrthancStone::GrayscaleStyleConfigurator);
+      viewport2_->UpdateSize();
+
+      viewport3_.reset(new OrthancStone::VolumeSlicerViewport(broker_, "mycanvas3", OrthancStone::VolumeProjection_Sagittal));
+      viewport3_->SetSlicer(0, loader_, *loader_, new OrthancStone::GrayscaleStyleConfigurator);
+      viewport3_->UpdateSize();
+
+      emscripten_set_resize_callback("#window", NULL, false, OnWindowResize);
+    
+      emscripten_request_animation_frame_loop(OnAnimationFrame, NULL);
+
+      oracle_.Start();
+      loader_->LoadSeries("a04ecf01-79b2fc33-58239f7e-ad9db983-28e81afa");
+    }
+    catch (Orthanc::OrthancException& e)
+    {
+      LOG(ERROR) << "Exception during Initialize(): " << e.What();
+    }
+  }
+}