changeset 829:3a984741686f

Merge
author Benjamin Golinvaux <bgo@osimis.io>
date Wed, 29 May 2019 16:15:23 +0200
parents 28f99af358fa (current diff) 9a6c7a5dcb76 (diff)
children 171a486a0373
files Framework/Oracle/ThreadedOracle.h
diffstat 8 files changed, 673 insertions(+), 406 deletions(-) [+]
line wrap: on
line diff
--- a/Framework/Oracle/IOracle.h	Wed May 29 16:15:04 2019 +0200
+++ b/Framework/Oracle/IOracle.h	Wed May 29 16:15:23 2019 +0200
@@ -21,6 +21,7 @@
 
 #pragma once
 
+#include "../Messages/IObserver.h"
 #include "IOracleCommand.h"
 
 namespace OrthancStone
@@ -32,10 +33,6 @@
     {
     }
 
-    virtual void Start() = 0;
-
-    virtual void Stop() = 0;
-
     virtual void Schedule(const IObserver& receiver,
                           IOracleCommand* command) = 0;  // Takes ownership
   };
--- a/Framework/Oracle/SleepOracleCommand.h	Wed May 29 16:15:04 2019 +0200
+++ b/Framework/Oracle/SleepOracleCommand.h	Wed May 29 16:15:23 2019 +0200
@@ -35,7 +35,7 @@
     ORTHANC_STONE_DEFINE_ORIGIN_MESSAGE(__FILE__, __LINE__, TimeoutMessage, SleepOracleCommand);
 
     SleepOracleCommand(unsigned int milliseconds) : 
-    milliseconds_(milliseconds)
+      milliseconds_(milliseconds)
     {
     }
 
--- a/Framework/Oracle/ThreadedOracle.h	Wed May 29 16:15:04 2019 +0200
+++ b/Framework/Oracle/ThreadedOracle.h	Wed May 29 16:15:23 2019 +0200
@@ -81,9 +81,9 @@
 
     void SetSleepingTimeResolution(unsigned int milliseconds);
 
-    virtual void Start();
+    void Start();
 
-    virtual void Stop()
+    void Stop()
     {
       StopInternal();
     }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Oracle/WebAssemblyOracle.cpp	Wed May 29 16:15:23 2019 +0200
@@ -0,0 +1,466 @@
+/**
+ * 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 "WebAssemblyOracle.h"
+
+#include "SleepOracleCommand.h"
+
+#include <Core/OrthancException.h>
+#include <Core/Toolbox.h>
+
+#include <emscripten.h>
+#include <emscripten/html5.h>
+#include <emscripten/fetch.h>
+
+
+namespace OrthancStone
+{
+  class WebAssemblyOracle::TimeoutContext
+  {
+  private:
+    WebAssemblyOracle&                 oracle_;
+    const IObserver&                   receiver_;
+    std::auto_ptr<SleepOracleCommand>  command_;
+
+  public:
+    TimeoutContext(WebAssemblyOracle& oracle,
+                   const IObserver& receiver,
+                   IOracleCommand* command) :
+      oracle_(oracle),
+      receiver_(receiver)
+    {
+      if (command == NULL)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
+      }
+      else
+      {
+        command_.reset(dynamic_cast<SleepOracleCommand*>(command));
+      }
+    }
+
+    void EmitMessage()
+    {
+      SleepOracleCommand::TimeoutMessage message(*command_);
+      oracle_.EmitMessage(receiver_, message);
+    }
+
+    static void Callback(void *userData)
+    {
+      std::auto_ptr<TimeoutContext> context(reinterpret_cast<TimeoutContext*>(userData));
+      context->EmitMessage();
+    }
+  };
+    
+
+  class WebAssemblyOracle::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);
+    }
+  };
+
+
+  class WebAssemblyOracle::FetchContext : public boost::noncopyable
+  {
+  private:
+    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 SuccessCallback(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 FailureCallback(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 WebAssemblyOracle::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 = FetchContext::SuccessCallback;
+      attr.onerror = FetchContext::FailureCallback;
+      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 WebAssemblyOracle::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 WebAssemblyOracle::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 WebAssemblyOracle::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();
+  }
+
+
+
+  void WebAssemblyOracle::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;          
+            
+      case IOracleCommand::Type_Sleep:
+      {
+        unsigned int timeoutMS = dynamic_cast<SleepOracleCommand*>(command)->GetDelay();
+        emscripten_set_timeout(TimeoutContext::Callback, timeoutMS,
+                               new TimeoutContext(*this, receiver, protection.release()));
+        break;
+      }
+            
+      default:
+        LOG(ERROR) << "Command type not implemented by the WebAssembly Oracle: " << command->GetType();
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Oracle/WebAssemblyOracle.h	Wed May 29 16:15:23 2019 +0200
@@ -0,0 +1,71 @@
+/**
+ * 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/>.
+ **/
+
+
+#pragma once
+
+#if !defined(ORTHANC_ENABLE_WASM)
+#  error The macro ORTHANC_ENABLE_WASM must be defined
+#endif
+
+#if ORTHANC_ENABLE_WASM != 1
+#  error This file can only compiled for WebAssembly
+#endif
+
+#include "../Messages/IObservable.h"
+#include "GetOrthancImageCommand.h"
+#include "GetOrthancWebViewerJpegCommand.h"
+#include "IOracle.h"
+#include "OrthancRestApiCommand.h"
+
+
+namespace OrthancStone
+{
+  class WebAssemblyOracle :
+    public IOracle,
+    public IObservable
+  {
+  private:
+    typedef std::map<std::string, std::string>  HttpHeaders;
+    
+    class TimeoutContext;
+    class Emitter;
+    class FetchContext;
+    class FetchCommand;    
+    
+    void Execute(const IObserver& receiver,
+                 OrthancRestApiCommand* command);    
+    
+    void Execute(const IObserver& receiver,
+                 GetOrthancImageCommand* command);    
+    
+    void Execute(const IObserver& receiver,
+                 GetOrthancWebViewerJpegCommand* command);
+
+  public:
+    WebAssemblyOracle(MessageBroker& broker) :
+      IObservable(broker)
+    {
+    }
+    
+    virtual void Schedule(const IObserver& receiver,
+                          IOracleCommand* command);
+  };
+}
--- a/Framework/Scene2D/Scene2D.cpp	Wed May 29 16:15:04 2019 +0200
+++ b/Framework/Scene2D/Scene2D.cpp	Wed May 29 16:15:23 2019 +0200
@@ -102,8 +102,7 @@
   void Scene2D::SetLayer(int depth,
                          ISceneLayer* layer)  // Takes ownership
   {
-    LOG(INFO) << "SetLayer(" << depth << ", " <<
-      reinterpret_cast<intptr_t>(layer) << ")";
+    LOG(TRACE) << "SetLayer(" << depth << ", " << reinterpret_cast<intptr_t>(layer) << ")";
     std::auto_ptr<Item> item(new Item(layer, layerCounter_++));
 
     if (layer == NULL)
--- a/Resources/CMake/OrthancStoneConfiguration.cmake	Wed May 29 16:15:04 2019 +0200
+++ b/Resources/CMake/OrthancStoneConfiguration.cmake	Wed May 29 16:15:23 2019 +0200
@@ -373,6 +373,13 @@
 endif()
 
 
+if (ENABLE_WASM)
+  list(APPEND ORTHANC_STONE_SOURCES
+    ${ORTHANC_STONE_ROOT}/Framework/Oracle/WebAssemblyOracle.cpp
+    )
+endif()
+
+
 list(APPEND ORTHANC_STONE_SOURCES
   #${ORTHANC_STONE_ROOT}/Framework/Layers/SeriesFrameRendererFactory.cpp
   #${ORTHANC_STONE_ROOT}/Framework/Layers/SingleFrameRendererFactory.cpp
--- a/Samples/WebAssembly/BasicMPR.cpp	Wed May 29 16:15:04 2019 +0200
+++ b/Samples/WebAssembly/BasicMPR.cpp	Wed May 29 16:15:23 2019 +0200
@@ -26,6 +26,8 @@
 
 #include "../../Framework/Loaders/OrthancSeriesVolumeProgressiveLoader.h"
 #include "../../Framework/OpenGL/WebAssemblyOpenGLContext.h"
+#include "../../Framework/Oracle/SleepOracleCommand.h"
+#include "../../Framework/Oracle/WebAssemblyOracle.h"
 #include "../../Framework/Scene2D/GrayscaleStyleConfigurator.h"
 #include "../../Framework/Scene2D/OpenGLCompositor.h"
 #include "../../Framework/Scene2D/PanSceneTracker.h"
@@ -91,6 +93,11 @@
       compositor_.Refresh();
     }
 
+    void FitContent()
+    {
+      GetScene().FitContent(context_.GetCanvasWidth(), context_.GetCanvasHeight());
+    }
+
     const std::string& GetCanvasIdentifier() const
     {
       return context_.GetCanvasIdentifier();
@@ -303,6 +310,10 @@
       {
         planes_[z] = geometry.GetProjectionSlice(projection_, z);
       }
+
+      Refresh();
+
+      viewport_.FitContent();
     }
     
   public:
@@ -350,414 +361,36 @@
           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();
+        viewport_.Refresh();
       }
     }
 
-    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
+    void Scroll(int delta)
     {
-    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)
+      if (!planes_.empty())
       {
-        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);          
-        }
+        int tmp = static_cast<int>(currentPlane_) + delta;
+        unsigned int next;
 
-        emscripten_fetch_attr_t attr;
-        emscripten_fetch_attr_init(&attr);
-
-        const char* method;
-      
-        switch (method_)
+        if (tmp < 0)
+        {
+          next = 0;
+        }
+        else if (tmp >= static_cast<int>(planes_.size()))
         {
-          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);
+          next = planes_.size() - 1;
+        }
+        else
+        {
+          next = static_cast<size_t>(tmp);
         }
 
-        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)
+        if (next != currentPlane_)
         {
-          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());
-          }
+          currentPlane_ = next;
+          Refresh();
         }
-        
-        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()
-    {
     }
   };
 }
@@ -836,6 +469,94 @@
 }
 
 
+static bool ctrlDown_ = false;
+
+
+EM_BOOL OnMouseWheel(int eventType,
+                     const EmscriptenWheelEvent *wheelEvent,
+                     void *userData)
+{
+  try
+  {
+    if (userData != NULL)
+    {
+      int delta = 0;
+
+      if (wheelEvent->deltaY < 0)
+      {
+        delta = -1;
+      }
+           
+      if (wheelEvent->deltaY > 0)
+      {
+        delta = 1;
+      }
+
+      if (ctrlDown_)
+      {
+        delta *= 10;
+      }
+           
+      reinterpret_cast<OrthancStone::VolumeSlicerViewport*>(userData)->Scroll(delta);
+    }
+  }
+  catch (Orthanc::OrthancException& e)
+  {
+    LOG(ERROR) << "Exception in the wheel event: " << e.What();
+  }
+  
+  return true;
+}
+
+
+EM_BOOL OnKey(int eventType,
+              const EmscriptenKeyboardEvent *keyEvent,
+              void *userData)
+{
+  ctrlDown_ = keyEvent->ctrlKey;
+  return false;
+}
+
+
+
+
+namespace OrthancStone
+{
+  class TestSleep : public IObserver
+  {
+  private:
+    WebAssemblyOracle&  oracle_;
+
+    void Schedule()
+    {
+      oracle_.Schedule(*this, new OrthancStone::SleepOracleCommand(2000));
+    }
+    
+    void Handle(const SleepOracleCommand::TimeoutMessage& message)
+    {
+      LOG(INFO) << "TIMEOUT";
+      Schedule();
+    }
+    
+  public:
+    TestSleep(MessageBroker& broker,
+              WebAssemblyOracle& oracle) :
+      IObserver(broker),
+      oracle_(oracle)
+    {
+      oracle.RegisterObserverCallback(
+        new Callable<TestSleep, SleepOracleCommand::TimeoutMessage>
+        (*this, &TestSleep::Handle));
+
+      LOG(INFO) << "STARTING";
+      Schedule();
+    }
+  };
+
+  static TestSleep testSleep(broker_, oracle_);
+}
+
+
 extern "C"
 {
   int main(int argc, char const *argv[]) 
@@ -866,10 +587,16 @@
       viewport3_->UpdateSize();
 
       emscripten_set_resize_callback("#window", NULL, false, OnWindowResize);
+
+      emscripten_set_wheel_callback("mycanvas1", viewport1_.get(), false, OnMouseWheel);
+      emscripten_set_wheel_callback("mycanvas2", viewport2_.get(), false, OnMouseWheel);
+      emscripten_set_wheel_callback("mycanvas3", viewport3_.get(), false, OnMouseWheel);
+
+      emscripten_set_keydown_callback("#window", NULL, false, OnKey);
+      emscripten_set_keyup_callback("#window", NULL, false, OnKey);
     
       emscripten_request_animation_frame_loop(OnAnimationFrame, NULL);
 
-      oracle_.Start();
       loader_->LoadSeries("a04ecf01-79b2fc33-58239f7e-ad9db983-28e81afa");
     }
     catch (Orthanc::OrthancException& e)