changeset 820:270c31978df1

BasicMPR sample
author Sebastien Jodogne <s.jodogne@gmail.com>
date Wed, 29 May 2019 13:40:07 +0200
parents a68cd7ae8838
children 5c7b08bf84af
files Samples/Sdl/Loader.cpp Samples/WebAssembly/BasicMPR.cpp Samples/WebAssembly/BasicMPR.html Samples/WebAssembly/BasicScene.cpp Samples/WebAssembly/CMakeLists.txt Samples/WebAssembly/NOTES.txt Samples/WebAssembly/index.html
diffstat 7 files changed, 986 insertions(+), 16 deletions(-) [+]
line wrap: on
line diff
--- a/Samples/Sdl/Loader.cpp	Wed May 29 13:39:31 2019 +0200
+++ b/Samples/Sdl/Loader.cpp	Wed May 29 13:40:07 2019 +0200
@@ -166,9 +166,9 @@
   {
     printf("Geometry ready\n");
     
-    //plane_ = message.GetOrigin().GetGeometry().GetSagittalGeometry();
+    plane_ = message.GetOrigin().GetGeometry().GetSagittalGeometry();
     //plane_ = message.GetOrigin().GetGeometry().GetAxialGeometry();
-    plane_ = message.GetOrigin().GetGeometry().GetCoronalGeometry();
+    //plane_ = message.GetOrigin().GetGeometry().GetCoronalGeometry();
     plane_.SetOrigin(message.GetOrigin().GetGeometry().GetCoordinates(0.5f, 0.5f, 0.5f));
 
     Refresh();
--- /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();
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/WebAssembly/BasicMPR.html	Wed May 29 13:40:07 2019 +0200
@@ -0,0 +1,69 @@
+<!doctype html>
+<html lang="en-us">
+  <head>
+    <meta charset="utf-8">
+    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+
+    <!-- Disable pinch zoom on mobile devices -->
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
+    <meta name="HandheldFriendly" content="true" />
+    
+    
+    <title>Stone of Orthanc</title>
+
+    <style>
+      html, body {
+      width: 100%;
+      height: 100%;
+      margin: 0px;
+      border: 0;
+      overflow: hidden; /*  Disable scrollbars */
+      display: block;  /* No floating content on sides */
+      }
+
+      #mycanvas1 {
+      position:absolute;
+      left:0%;
+      top:0%;
+      background-color: red;
+      width: 50%;
+      height: 100%;
+      }
+
+      #mycanvas2 {
+      position:absolute;
+      left:50%;
+      top:0%;
+      background-color: green;
+      width: 50%;
+      height: 50%;
+      }
+
+      #mycanvas3 {
+      position:absolute;
+      left:50%;
+      top:50%;
+      background-color: blue;
+      width: 50%;
+      height: 50%;
+      }
+    </style>
+  </head>
+  <body>
+    <canvas id="mycanvas1" oncontextmenu="return false;"></canvas>
+    <canvas id="mycanvas2" oncontextmenu="return false;"></canvas>
+    <canvas id="mycanvas3" oncontextmenu="return false;"></canvas>
+
+    <script type="text/javascript">
+      if (!('WebAssembly' in window)) {
+      alert('Sorry, your browser does not support WebAssembly :(');
+      } else {
+      window.addEventListener('WebAssemblyLoaded', function() {
+      Module.ccall('Initialize', null, null, null);
+      });
+      }
+    </script>
+
+    <script type="text/javascript" async src="BasicMPR.js"></script>
+  </body>
+</html>
--- a/Samples/WebAssembly/BasicScene.cpp	Wed May 29 13:39:31 2019 +0200
+++ b/Samples/WebAssembly/BasicScene.cpp	Wed May 29 13:40:07 2019 +0200
@@ -113,14 +113,14 @@
     chain.push_back(ScenePoint2D(0 - 0.5, 2 - 0.5));
     chain.push_back(ScenePoint2D(2 - 0.5, 2 - 0.5));
     chain.push_back(ScenePoint2D(2 - 0.5, 0 - 0.5));
-    layer->AddChain(chain, true);
+    layer->AddChain(chain, true, 255, 0, 0);
 
     chain.clear();
     chain.push_back(ScenePoint2D(-5, -5));
     chain.push_back(ScenePoint2D(5, -5));
     chain.push_back(ScenePoint2D(5, 5));
     chain.push_back(ScenePoint2D(-5, 5));
-    layer->AddChain(chain, true);
+    layer->AddChain(chain, true, 0, 255, 0);
 
     double dy = 1.01;
     chain.clear();
@@ -128,9 +128,8 @@
     chain.push_back(ScenePoint2D(4, -4 + dy));
     chain.push_back(ScenePoint2D(-4, -4 + 2.0 * dy));
     chain.push_back(ScenePoint2D(4, 2));
-    layer->AddChain(chain, false);
+    layer->AddChain(chain, false, 0, 0, 255);
 
-    layer->SetColor(0,255, 255);
     scene->SetLayer(50, layer.release());
   }
 
--- a/Samples/WebAssembly/CMakeLists.txt	Wed May 29 13:39:31 2019 +0200
+++ b/Samples/WebAssembly/CMakeLists.txt	Wed May 29 13:40:07 2019 +0200
@@ -13,6 +13,7 @@
 set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s ERROR_ON_UNDEFINED_SYMBOLS=1")
 set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s ALLOW_MEMORY_GROWTH=1")
 set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s ASSERTIONS=1 -s DISABLE_EXCEPTION_CATCHING=0")
+#set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s EXIT_RUNTIME=1")
 
 
 #####################################################################
@@ -68,20 +69,40 @@
   ${ORTHANC_STONE_SOURCES}
   )
 
-add_executable(BasicScene
-  BasicScene.cpp
-  )
+
+if (OFF)
+  add_executable(BasicScene
+    BasicScene.cpp
+    )
+
+  target_link_libraries(BasicScene OrthancStone)
+
+  install(
+    TARGETS BasicScene
+    RUNTIME DESTINATION ${CMAKE_INSTALL_PREFIX}
+    )
+endif()
 
-target_link_libraries(BasicScene OrthancStone)
+
+if (ON)
+  add_executable(BasicMPR
+    BasicMPR.cpp
+    )
 
-install(
-  TARGETS BasicScene
-  RUNTIME DESTINATION ${CMAKE_INSTALL_PREFIX}
-  )
+  target_link_libraries(BasicMPR OrthancStone)
+
+  install(
+    TARGETS BasicMPR
+    RUNTIME DESTINATION ${CMAKE_INSTALL_PREFIX}
+    )
+endif()
+  
 
 install(
   FILES
+  ${CMAKE_CURRENT_BINARY_DIR}/BasicMPR.wasm
   ${CMAKE_CURRENT_BINARY_DIR}/BasicScene.wasm
+  ${CMAKE_SOURCE_DIR}/BasicMPR.html
   ${CMAKE_SOURCE_DIR}/BasicScene.html
   ${CMAKE_SOURCE_DIR}/Configuration.json
   ${CMAKE_SOURCE_DIR}/index.html
--- a/Samples/WebAssembly/NOTES.txt	Wed May 29 13:39:31 2019 +0200
+++ b/Samples/WebAssembly/NOTES.txt	Wed May 29 13:40:07 2019 +0200
@@ -1,7 +1,7 @@
 $ source ~/Downloads/emsdk/emsdk_env.sh
 $ cmake -G Ninja -DCMAKE_TOOLCHAIN_FILE=${EMSCRIPTEN}/cmake/Modules/Platform/Emscripten.cmake -DCMAKE_BUILD_TYPE=Release -DALLOW_DOWNLOADS=ON .. -DCMAKE_INSTALL_PREFIX=/tmp/stone
 $ ninja install
-$ sudo docker run -p 4242:4242 -p 8042:8042 --rm -v /tmp/stone:/root/stone:ro jodogne/orthanc-plugins:1.5.6 /root/stone/Configuration.json --verbose
+$ docker run -p 4242:4242 -p 8042:8042 --rm -v /tmp/stone:/root/stone:ro -v /tmp/stone-db/:/var/lib/orthanc/db/ jodogne/orthanc-plugins:1.5.6 /root/stone/Configuration.json --verbose
 
 notes BGO :
 source ~/apps/emsdk/emsdk_env.sh
@@ -11,4 +11,4 @@
 cmake -G Ninja -DCMAKE_TOOLCHAIN_FILE=${EMSCRIPTEN}/cmake/Modules/Platform/Emscripten.cmake -DCMAKE_BUILD_TYPE=Release -DALLOW_DOWNLOADS=ON .. -DCMAKE_INSTALL_PREFIX=/mnt/c/osi/dev/orthanc-stone/Samples/WebAssembly/installDir
 ninja install
 
-docker run -p 4242:4242 -p 8042:8042 --rm -v "C:/osi/dev/orthanc-stone/Samples/WebAssembly/installDir:/root/stone:ro" jodogne/orthanc-plugins:1.5.6 /root/stone/Configuration.json --verbose
\ No newline at end of file
+docker run -p 4242:4242 -p 8042:8042 --rm -v "C:/osi/dev/orthanc-stone/Samples/WebAssembly/installDir:/root/stone:ro" jodogne/orthanc-plugins:1.5.6 /root/stone/Configuration.json --verbose
--- a/Samples/WebAssembly/index.html	Wed May 29 13:39:31 2019 +0200
+++ b/Samples/WebAssembly/index.html	Wed May 29 13:40:07 2019 +0200
@@ -10,6 +10,7 @@
     <h1>Available samples</h1>
     <ul>
       <li><a href="BasicScene.html">Basic scene</a></li>
+      <li><a href="BasicMPR.html">Basic MPR display</a></li>
     </ul>
   </body>
 </html>