changeset 373:d6136a7e914d

making branch am-2 the new mainline
author Sebastien Jodogne <s.jodogne@gmail.com>
date Mon, 05 Nov 2018 10:06:18 +0100
parents fe4befe03935 (current diff) 17d1814c2fd4 (diff)
children 20a20babc02c 70256a53ff21
files Applications/BasicApplicationContext.cpp Applications/BasicApplicationContext.h Applications/IBasicApplication.cpp Applications/IBasicApplication.h Applications/Samples/SampleMainSdl.cpp Platforms/Generic/CMakeLists.txt Platforms/WebAssembly/CMakeLists.txt Platforms/WebAssembly/library.js
diffstat 205 files changed, 14732 insertions(+), 2606 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/.hgignore	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,7 @@
+CMakeLists.txt.user
+Platforms/Generic/ThirdPartyDownloads/
+Applications/Qt/archive/
+Applications/Samples/ThirdPartyDownloads/
+Applications/Samples/build-wasm/
+Applications/Samples/build-web/
+.vscode/
--- a/Applications/BasicApplicationContext.cpp	Mon Nov 05 10:04:56 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,144 +0,0 @@
-/**
- * Stone of Orthanc
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2018 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 "BasicApplicationContext.h"
-
-namespace OrthancStone
-{
-  void BasicApplicationContext::UpdateThread(BasicApplicationContext* that)
-  {
-    while (!that->stopped_)
-    {
-      {
-        ViewportLocker locker(*that);
-        locker.GetViewport().UpdateContent();
-      }
-      
-      boost::this_thread::sleep(boost::posix_time::milliseconds(that->updateDelay_));
-    }
-  }
-  
-
-  BasicApplicationContext::BasicApplicationContext(Orthanc::WebServiceParameters& orthanc) :
-    oracle_(viewportMutex_, 4),  // Use 4 threads to download
-    //oracle_(viewportMutex_, 1),  // Disable threading to be reproducible
-    webService_(oracle_, orthanc),
-    stopped_(true),
-    updateDelay_(100)   // By default, 100ms between each refresh of the content
-  {
-    srand(time(NULL)); 
-  }
-
-
-  BasicApplicationContext::~BasicApplicationContext()
-  {
-    for (Interactors::iterator it = interactors_.begin(); it != interactors_.end(); ++it)
-    {
-      assert(*it != NULL);
-      delete *it;
-    }
-
-    for (SlicedVolumes::iterator it = slicedVolumes_.begin(); it != slicedVolumes_.end(); ++it)
-    {
-      assert(*it != NULL);
-      delete *it;
-    }
-
-    for (VolumeLoaders::iterator it = volumeLoaders_.begin(); it != volumeLoaders_.end(); ++it)
-    {
-      assert(*it != NULL);
-      delete *it;
-    }
-  }
-
-
-  IWidget& BasicApplicationContext::SetCentralWidget(IWidget* widget)   // Takes ownership
-  {
-    viewport_.SetCentralWidget(widget);
-    return *widget;
-  }
-
-
-  ISlicedVolume& BasicApplicationContext::AddSlicedVolume(ISlicedVolume* volume)
-  {
-    if (volume == NULL)
-    {
-      throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
-    }
-    else
-    {
-      slicedVolumes_.push_back(volume);
-      return *volume;
-    }
-  }
-
-
-  IVolumeLoader& BasicApplicationContext::AddVolumeLoader(IVolumeLoader* loader)
-  {
-    if (loader == NULL)
-    {
-      throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
-    }
-    else
-    {
-      volumeLoaders_.push_back(loader);
-      return *loader;
-    }
-  }
-
-
-  IWorldSceneInteractor& BasicApplicationContext::AddInteractor(IWorldSceneInteractor* interactor)
-  {
-    if (interactor == NULL)
-    {
-      throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
-    }
-
-    interactors_.push_back(interactor);
-
-    return *interactor;
-  }
-
-
-  void BasicApplicationContext::Start()
-  {
-    oracle_.Start();
-
-    if (viewport_.HasUpdateContent())
-    {
-      stopped_ = false;
-      updateThread_ = boost::thread(UpdateThread, this);
-    }
-  }
-
-
-  void BasicApplicationContext::Stop()
-  {
-    stopped_ = true;
-    
-    if (updateThread_.joinable())
-    {
-      updateThread_.join();
-    }
-    
-    oracle_.Stop();
-  }
-}
--- a/Applications/BasicApplicationContext.h	Mon Nov 05 10:04:56 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,102 +0,0 @@
-/**
- * Stone of Orthanc
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2018 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
-
-#include "../Framework/Viewport/WidgetViewport.h"
-#include "../Framework/Volumes/ISlicedVolume.h"
-#include "../Framework/Volumes/IVolumeLoader.h"
-#include "../Framework/Widgets/IWorldSceneInteractor.h"
-#include "../Platforms/Generic/OracleWebService.h"
-
-#include <list>
-#include <boost/thread.hpp>
-
-namespace OrthancStone
-{
-  class BasicApplicationContext : public boost::noncopyable
-  {
-  private:
-    typedef std::list<ISlicedVolume*>          SlicedVolumes;
-    typedef std::list<IVolumeLoader*>          VolumeLoaders;
-    typedef std::list<IWorldSceneInteractor*>  Interactors;
-
-    static void UpdateThread(BasicApplicationContext* that);
-
-    Oracle              oracle_;
-    OracleWebService    webService_;
-    boost::mutex        viewportMutex_;
-    WidgetViewport      viewport_;
-    SlicedVolumes       slicedVolumes_;
-    VolumeLoaders       volumeLoaders_;
-    Interactors         interactors_;
-    boost::thread       updateThread_;
-    bool                stopped_;
-    unsigned int        updateDelay_;
-
-  public:
-    class ViewportLocker : public boost::noncopyable
-    {
-    private:
-      boost::mutex::scoped_lock  lock_;
-      IViewport&                 viewport_;
-
-    public:
-      ViewportLocker(BasicApplicationContext& that) :
-        lock_(that.viewportMutex_),
-        viewport_(that.viewport_)
-      {
-      }
-
-      IViewport& GetViewport() const
-      {
-        return viewport_;
-      }
-    };
-
-    
-    BasicApplicationContext(Orthanc::WebServiceParameters& orthanc);
-
-    ~BasicApplicationContext();
-
-    IWidget& SetCentralWidget(IWidget* widget);   // Takes ownership
-
-    IWebService& GetWebService()
-    {
-      return webService_;
-    }
-    
-    ISlicedVolume& AddSlicedVolume(ISlicedVolume* volume);
-
-    IVolumeLoader& AddVolumeLoader(IVolumeLoader* loader);
-
-    IWorldSceneInteractor& AddInteractor(IWorldSceneInteractor* interactor);
-
-    void Start();
-
-    void Stop();
-
-    void SetUpdateDelay(unsigned int delay)  // In milliseconds
-    {
-      updateDelay_ = delay;
-    }
-  };
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Commands/BaseCommandBuilder.cpp	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,49 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 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 "BaseCommandBuilder.h"
+#include "Core/OrthancException.h"
+#include <iostream>
+#include "Framework/StoneException.h"
+
+namespace OrthancStone
+{
+  ICommand* BaseCommandBuilder::CreateFromJson(const Json::Value& commandJson)
+  {
+    if (!commandJson.isObject() || !commandJson["command"].isString())
+    {
+      throw StoneException(ErrorCode_CommandJsonInvalidFormat);
+    }
+
+    if (commandJson["commandType"].isString() && commandJson["commandType"].asString() == "generic-no-arg-command")
+    {
+        printf("creating a simple command\n");
+        return new GenericNoArgCommand(commandJson["command"].asString().c_str());
+    }
+    else if (commandJson["commandType"].isString() && commandJson["commandType"].asString() == "generic-one-string-arg-command")
+    {
+        printf("creating a simple command\n");
+        return new GenericNoArgCommand(commandJson["command"].asString().c_str());
+    }
+
+    return NULL;
+  }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Commands/BaseCommandBuilder.h	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,38 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 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
+
+#include <map>
+#include <memory>
+
+#include "ICommand.h"
+#include "../../Applications/Commands/ICommandBuilder.h"
+
+// TODO: must be reworked completely (check trello)
+
+namespace OrthancStone
+{
+  class BaseCommandBuilder : public ICommandBuilder
+  {
+  public:
+    virtual ICommand* CreateFromJson(const Json::Value& commandJson);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Commands/BaseCommands.yml	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,10 @@
+SelectTool:
+  target: Application
+  toolName: string
+  comment: Selects the current application tool
+DownloadDicom:
+  target: LayerWidget
+  comment: Downloads the slice currently displayed in the LayerWidget
+Export:
+  target: IWidget
+  comment: Export the content of the widget
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Commands/ICommand.h	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,94 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 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
+
+#include <json/json.h>
+
+// TODO: must be reworked completely (check trello)
+
+namespace OrthancStone
+{
+  class ICommand  // TODO noncopyable
+  {
+  protected:
+    std::string name_;
+    ICommand(const std::string& name)
+      : name_(name)
+    {}
+  public:
+    virtual void Execute() = 0;
+//    virtual void Configure(const Json::Value& arguments) = 0;
+    const std::string& GetName() const
+    {
+      return name_;
+    }
+  };
+
+
+  template <typename TCommand>
+  class BaseCommand : public ICommand
+  {
+  protected:
+    BaseCommand(const std::string& name)
+      : ICommand(name)
+    {}
+
+  public:
+    static ICommand* Create() {
+      return new TCommand();
+    }
+
+    virtual void Configure(const Json::Value& arguments) {
+    }
+  };
+
+  class NoopCommand : public BaseCommand<NoopCommand>
+  {
+  public:
+    NoopCommand()
+      : BaseCommand("noop")
+    {}
+    virtual void Execute() {}
+  };
+
+  class GenericNoArgCommand : public BaseCommand<GenericNoArgCommand>
+  {
+  public:
+    GenericNoArgCommand(const std::string& name)
+      : BaseCommand(name)
+    {}
+    virtual void Execute() {} // TODO currently not used but this is not nice at all !
+  };
+
+  class GenericOneStringArgCommand : public BaseCommand<GenericOneStringArgCommand>
+  {
+    std::string argument_;
+  public:
+    GenericOneStringArgCommand(const std::string& name, const std::string& argument)
+      : BaseCommand(name)
+    {}
+
+    const std::string& GetArgument() const {return argument_;}
+    virtual void Execute() {} // TODO currently not used but this is not nice at all !
+  };
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Commands/ICommandBuilder.h	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,37 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 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
+
+#include <boost/noncopyable.hpp>
+#include <json/json.h>
+
+#include "ICommand.h"
+
+namespace OrthancStone
+{
+
+  class ICommandBuilder : public boost::noncopyable
+  {
+  public:
+    virtual ICommand* CreateFromJson(const Json::Value& commandJson) = 0;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Commands/ICommandExecutor.h	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,32 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 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
+
+#include <boost/noncopyable.hpp>
+
+namespace OrthancStone
+{
+  class ICommandExecutor : public boost::noncopyable
+  {
+
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Generic/NativeStoneApplicationContext.cpp	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,80 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 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 "NativeStoneApplicationContext.h"
+#include "../../Platforms/Generic/OracleWebService.h"
+
+namespace OrthancStone
+{
+  IWidget& NativeStoneApplicationContext::SetCentralWidget(IWidget* widget)   // Takes ownership
+  {
+    centralViewport_->SetCentralWidget(widget);
+    return *widget;
+  }
+
+
+  void NativeStoneApplicationContext::UpdateThread(NativeStoneApplicationContext* that)
+  {
+    while (!that->stopped_)
+    {
+      {
+        GlobalMutexLocker locker(*that);
+        that->GetCentralViewport().UpdateContent();
+      }
+      
+      boost::this_thread::sleep(boost::posix_time::milliseconds(that->updateDelayInMs_));
+    }
+  }
+  
+
+  NativeStoneApplicationContext::NativeStoneApplicationContext() :
+    centralViewport_(new OrthancStone::WidgetViewport()),
+    stopped_(true),
+    updateDelayInMs_(100)   // By default, 100ms between each refresh of the content
+  {
+    srand(time(NULL)); 
+  }
+
+
+  void NativeStoneApplicationContext::Start()
+  {
+    dynamic_cast<OracleWebService*>(webService_)->Start();
+
+    if (centralViewport_->HasUpdateContent())
+    {
+      stopped_ = false;
+      updateThread_ = boost::thread(UpdateThread, this);
+    }
+  }
+
+
+  void NativeStoneApplicationContext::Stop()
+  {
+    stopped_ = true;
+    
+    if (updateThread_.joinable())
+    {
+      updateThread_.join();
+    }
+    
+    dynamic_cast<OracleWebService*>(webService_)->Stop();
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Generic/NativeStoneApplicationContext.h	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,79 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 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
+
+#include "../../Framework/Viewport/WidgetViewport.h"
+#include "../../Framework/Volumes/ISlicedVolume.h"
+#include "../../Framework/Volumes/IVolumeLoader.h"
+
+#include <list>
+#include <boost/thread.hpp>
+#include "../StoneApplicationContext.h"
+
+namespace OrthancStone
+{
+  class NativeStoneApplicationContext : public StoneApplicationContext
+  {
+  private:
+
+    static void UpdateThread(NativeStoneApplicationContext* that);
+
+    boost::mutex                   globalMutex_;
+    std::auto_ptr<WidgetViewport>  centralViewport_;
+    boost::thread                  updateThread_;
+    bool                           stopped_;
+    unsigned int                   updateDelayInMs_;
+
+  public:
+    class GlobalMutexLocker: public boost::noncopyable
+    {
+      boost::mutex::scoped_lock  lock_;
+    public:
+      GlobalMutexLocker(NativeStoneApplicationContext& that):
+        lock_(that.globalMutex_)
+      {
+      }
+    };
+
+    NativeStoneApplicationContext();
+
+    virtual ~NativeStoneApplicationContext()
+    {
+    }
+
+    virtual IWidget& SetCentralWidget(IWidget* widget);   // Takes ownership
+
+    IViewport& GetCentralViewport() 
+    {
+      return *(centralViewport_.get());
+    }
+
+    void Start();
+
+    void Stop();
+
+    void SetUpdateDelay(unsigned int delayInMs)
+    {
+      updateDelayInMs_ = delayInMs;
+    }
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Generic/NativeStoneApplicationRunner.cpp	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,236 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 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/>.
+ **/
+
+
+#if ORTHANC_ENABLE_NATIVE != 1
+#error this file shall be included only with the ORTHANC_ENABLE_NATIVE set to 1
+#endif
+
+#include "NativeStoneApplicationRunner.h"
+#include "NativeStoneApplicationContext.h"
+#include <boost/program_options.hpp>
+
+#include "../../Framework/Toolbox/MessagingToolbox.h"
+
+#include <Core/Logging.h>
+#include <Core/HttpClient.h>
+#include <Core/Toolbox.h>
+#include <Plugins/Samples/Common/OrthancHttpConnection.h>
+#include "../../Platforms/Generic/OracleWebService.h"
+
+namespace OrthancStone
+{
+  // Anonymous namespace to avoid clashes against other compilation modules
+  namespace
+  {
+    class LogStatusBar : public IStatusBar
+    {
+    public:
+      virtual void ClearMessage()
+      {
+      }
+
+      virtual void SetMessage(const std::string& message)
+      {
+        LOG(WARNING) << message;
+      }
+    };
+  }
+
+  int NativeStoneApplicationRunner::Execute(int argc,
+                                            char* argv[])
+  {
+    /******************************************************************
+     * Initialize all the subcomponents of Orthanc Stone
+     ******************************************************************/
+
+    Orthanc::Logging::Initialize();
+    Orthanc::Toolbox::InitializeOpenSsl();
+    Orthanc::HttpClient::GlobalInitialize();
+
+    Initialize();
+
+    /******************************************************************
+     * Declare and parse the command-line options of the application
+     ******************************************************************/
+
+    boost::program_options::options_description options;
+
+    { // generic options
+      boost::program_options::options_description generic("Generic options");
+      generic.add_options()
+          ("help", "Display this help and exit")
+          ("verbose", "Be verbose in logs")
+          ("orthanc", boost::program_options::value<std::string>()->default_value("http://localhost:8042/"),
+           "URL to the Orthanc server")
+          ("username", "Username for the Orthanc server")
+          ("password", "Password for the Orthanc server")
+          ("https-verify", boost::program_options::value<bool>()->default_value(true), "Check HTTPS certificates")
+          ;
+
+      options.add(generic);
+    }
+
+    // platform specific options
+    DeclareCommandLineOptions(options);
+    
+    // application specific options
+    application_.DeclareStartupOptions(options);
+
+    boost::program_options::variables_map parameters;
+    bool error = false;
+
+    try
+    {
+      boost::program_options::store(boost::program_options::command_line_parser(argc, argv).
+                                    options(options).run(), parameters);
+      boost::program_options::notify(parameters);
+    }
+    catch (boost::program_options::error& e)
+    {
+      LOG(ERROR) << "Error while parsing the command-line arguments: " << e.what();
+      error = true;
+    }
+
+
+    /******************************************************************
+     * Configure the application with the command-line parameters
+     ******************************************************************/
+
+    if (error || parameters.count("help"))
+    {
+      std::cout << std::endl
+                << "Usage: " << argv[0] << " [OPTION]..."
+                << std::endl
+                << "Orthanc, lightweight, RESTful DICOM server for healthcare and medical research."
+                << std::endl << std::endl
+                << "Demonstration application of Orthanc Stone in native environment."
+                << std::endl;
+
+      std::cout << options << "\n";
+      return error ? -1 : 0;
+    }
+
+    if (parameters.count("https-verify") &&
+        !parameters["https-verify"].as<bool>())
+    {
+      LOG(WARNING) << "Turning off verification of HTTPS certificates (unsafe)";
+      Orthanc::HttpClient::ConfigureSsl(false, "");
+    }
+
+    if (parameters.count("verbose"))
+    {
+      Orthanc::Logging::EnableInfoLevel(true);
+    }
+
+    ParseCommandLineOptions(parameters);
+
+
+    bool success = true;
+    try
+    {
+      /****************************************************************
+       * Initialize the connection to the Orthanc server
+       ****************************************************************/
+
+      Orthanc::WebServiceParameters webServiceParameters;
+
+      if (parameters.count("orthanc"))
+      {
+        webServiceParameters.SetUrl(parameters["orthanc"].as<std::string>());
+      }
+
+      if (parameters.count("username") && parameters.count("password"))
+      {
+        webServiceParameters.SetCredentials(parameters["username"].as<std::string>(),
+            parameters["password"].as<std::string>());
+      }
+
+      LOG(WARNING) << "URL to the Orthanc REST API: " << webServiceParameters.GetUrl();
+
+      {
+        OrthancPlugins::OrthancHttpConnection orthanc(webServiceParameters);
+        if (!MessagingToolbox::CheckOrthancVersion(orthanc))
+        {
+          LOG(ERROR) << "Your version of Orthanc is incompatible with Stone of Orthanc, please upgrade";
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol);
+        }
+      }
+
+
+      /****************************************************************
+       * Initialize the application
+       ****************************************************************/
+
+      LOG(WARNING) << "Creating the widgets of the application";
+
+      LogStatusBar statusBar;
+
+      NativeStoneApplicationContext context;
+      Oracle oracle(4); // use 4 threads to download content
+      OracleWebService webService(broker_, oracle, webServiceParameters, context);
+      context.SetWebService(webService);
+
+      application_.Initialize(&context, statusBar, parameters);
+
+      {
+        NativeStoneApplicationContext::GlobalMutexLocker locker(context);
+        context.SetCentralWidget(application_.GetCentralWidget());
+        context.GetCentralViewport().SetStatusBar(statusBar);
+      }
+
+      std::string title = application_.GetTitle();
+      if (title.empty())
+      {
+        title = "Stone of Orthanc";
+      }
+
+      /****************************************************************
+       * Run the application
+       ****************************************************************/
+
+      Run(context, title, argc, argv);
+
+      /****************************************************************
+       * Finalize the application
+       ****************************************************************/
+
+      LOG(WARNING) << "The application is stopping";
+      application_.Finalize();
+    }
+    catch (Orthanc::OrthancException& e)
+    {
+      LOG(ERROR) << "EXCEPTION: " << e.What();
+      success = false;
+    }
+
+
+    /******************************************************************
+     * Finalize all the subcomponents of Orthanc Stone
+     ******************************************************************/
+
+    Finalize();
+    Orthanc::HttpClient::GlobalFinalize();
+    Orthanc::Toolbox::FinalizeOpenSsl();
+
+    return (success ? 0 : -1);
+  }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Generic/NativeStoneApplicationRunner.h	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,58 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 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
+
+#include "../IStoneApplication.h"
+
+#if ORTHANC_ENABLE_NATIVE != 1
+#error this file shall be included only with the ORTHANC_ENABLE_NATIVE set to 1
+#endif
+
+namespace OrthancStone
+{
+  class NativeStoneApplicationContext;
+
+  class NativeStoneApplicationRunner
+  {
+  protected:
+    MessageBroker&      broker_;
+    IStoneApplication&  application_;
+  public:
+
+    NativeStoneApplicationRunner(MessageBroker& broker,
+                                 IStoneApplication& application)
+      : broker_(broker),
+        application_(application)
+    {
+    }
+    int Execute(int argc,
+                char* argv[]);
+
+    virtual void Initialize() = 0;
+    virtual void DeclareCommandLineOptions(boost::program_options::options_description& options) = 0;
+    virtual void ParseCommandLineOptions(const boost::program_options::variables_map& parameters) = 0;
+
+    virtual void Run(NativeStoneApplicationContext& context, const std::string& title, int argc, char* argv[]) = 0;
+    virtual void Finalize() = 0;
+  };
+
+}
--- a/Applications/IBasicApplication.cpp	Mon Nov 05 10:04:56 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,300 +0,0 @@
-/**
- * Stone of Orthanc
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2018 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 "IBasicApplication.h"
-
-#include "../Framework/Toolbox/MessagingToolbox.h"
-#include "Sdl/SdlEngine.h"
-
-#include <Core/Logging.h>
-#include <Core/HttpClient.h>
-#include <Core/Toolbox.h>
-#include <Plugins/Samples/Common/OrthancHttpConnection.h>
-
-namespace OrthancStone
-{
-  // Anonymous namespace to avoid clashes against other compilation modules
-  namespace
-  {
-    class LogStatusBar : public IStatusBar
-    {
-    public:
-      virtual void ClearMessage()
-      {
-      }
-
-      virtual void SetMessage(const std::string& message)
-      {
-        LOG(WARNING) << message;
-      }
-    };
-  }
-
-
-#if ORTHANC_ENABLE_SDL == 1
-  static void DeclareSdlCommandLineOptions(boost::program_options::options_description& options)
-  {
-    // Declare the supported parameters
-    boost::program_options::options_description generic("Generic options");
-    generic.add_options()
-      ("help", "Display this help and exit")
-      ("verbose", "Be verbose in logs")
-      ("orthanc", boost::program_options::value<std::string>()->default_value("http://localhost:8042/"),
-       "URL to the Orthanc server")
-      ("username", "Username for the Orthanc server")
-      ("password", "Password for the Orthanc server")
-      ("https-verify", boost::program_options::value<bool>()->default_value(true), "Check HTTPS certificates")
-      ;
-
-    options.add(generic);
-
-    boost::program_options::options_description sdl("SDL options");
-    sdl.add_options()
-      ("width", boost::program_options::value<int>()->default_value(1024), "Initial width of the SDL window")
-      ("height", boost::program_options::value<int>()->default_value(768), "Initial height of the SDL window")
-      ("opengl", boost::program_options::value<bool>()->default_value(true), "Enable OpenGL in SDL")
-      ;
-
-    options.add(sdl);
-  }
-
-
-  int IBasicApplication::ExecuteWithSdl(IBasicApplication& application,
-                                        int argc, 
-                                        char* argv[])
-  {
-    /******************************************************************
-     * Initialize all the subcomponents of Orthanc Stone
-     ******************************************************************/
-
-    Orthanc::Logging::Initialize();
-    Orthanc::Toolbox::InitializeOpenSsl();
-    Orthanc::HttpClient::GlobalInitialize();
-    SdlWindow::GlobalInitialize();
-
-
-    /******************************************************************
-     * Declare and parse the command-line options of the application
-     ******************************************************************/
-
-    boost::program_options::options_description options;
-    DeclareSdlCommandLineOptions(options);   
-    application.DeclareCommandLineOptions(options);
-
-    boost::program_options::variables_map parameters;
-    bool error = false;
-
-    try
-    {
-      boost::program_options::store(boost::program_options::command_line_parser(argc, argv).
-                                    options(options).run(), parameters);
-      boost::program_options::notify(parameters);    
-    }
-    catch (boost::program_options::error& e)
-    {
-      LOG(ERROR) << "Error while parsing the command-line arguments: " << e.what();
-      error = true;
-    }
-
-
-    /******************************************************************
-     * Configure the application with the command-line parameters
-     ******************************************************************/
-
-    if (error || parameters.count("help")) 
-    {
-      std::cout << std::endl
-                << "Usage: " << argv[0] << " [OPTION]..."
-                << std::endl
-                << "Orthanc, lightweight, RESTful DICOM server for healthcare and medical research."
-                << std::endl << std::endl
-                << "Demonstration application of Orthanc Stone using SDL."
-                << std::endl;
-
-      std::cout << options << "\n";
-      return error ? -1 : 0;
-    }
-
-    if (parameters.count("https-verify") &&
-        !parameters["https-verify"].as<bool>())
-    {
-      LOG(WARNING) << "Turning off verification of HTTPS certificates (unsafe)";
-      Orthanc::HttpClient::ConfigureSsl(false, "");
-    }
-
-    if (parameters.count("verbose"))
-    {
-      Orthanc::Logging::EnableInfoLevel(true);
-    }
-
-    if (!parameters.count("width") ||
-        !parameters.count("height") ||
-        !parameters.count("opengl"))
-    {
-      LOG(ERROR) << "Parameter \"width\", \"height\" or \"opengl\" is missing";
-      return -1;
-    }
-
-    int w = parameters["width"].as<int>();
-    int h = parameters["height"].as<int>();
-    if (w <= 0 || h <= 0)
-    {
-      LOG(ERROR) << "Parameters \"width\" and \"height\" must be positive";
-      return -1;
-    }
-
-    unsigned int width = static_cast<unsigned int>(w);
-    unsigned int height = static_cast<unsigned int>(h);
-    LOG(WARNING) << "Initial display size: " << width << "x" << height;
-
-    bool opengl = parameters["opengl"].as<bool>();
-    if (opengl)
-    {
-      LOG(WARNING) << "OpenGL is enabled, disable it with option \"--opengl=off\" if the application crashes";
-    }
-    else
-    {
-      LOG(WARNING) << "OpenGL is disabled, enable it with option \"--opengl=on\" for best performance";
-    }
-
-    bool success = true;
-    try
-    {
-      /****************************************************************
-       * Initialize the connection to the Orthanc server
-       ****************************************************************/
-
-      Orthanc::WebServiceParameters webService;
-
-      if (parameters.count("orthanc"))
-      {
-        webService.SetUrl(parameters["orthanc"].as<std::string>());
-      }
-
-      std::string username, password;
-      
-      if (parameters.count("username"))
-      {
-        username = parameters["username"].as<std::string>();
-      }
-
-      if (parameters.count("password"))
-      {
-        password = parameters["password"].as<std::string>();
-      }
-
-      if (!username.empty() ||
-          !password.empty())
-      {
-        webService.SetCredentials(username, password);
-      }
-
-      LOG(WARNING) << "URL to the Orthanc REST API: " << webService.GetUrl();
-
-      {
-        OrthancPlugins::OrthancHttpConnection orthanc(webService);
-        if (!MessagingToolbox::CheckOrthancVersion(orthanc))
-        {
-          LOG(ERROR) << "Your version of Orthanc is incompatible with Stone of Orthanc, please upgrade";
-          throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol);
-        }
-      }
-
-
-      /****************************************************************
-       * Initialize the application
-       ****************************************************************/
-
-      LOG(WARNING) << "Creating the widgets of the application";
-
-      LogStatusBar statusBar;
-      BasicApplicationContext context(webService);
-
-      application.Initialize(context, statusBar, parameters);
-
-      {
-        BasicApplicationContext::ViewportLocker locker(context);
-        locker.GetViewport().SetStatusBar(statusBar);
-      }
-
-      std::string title = application.GetTitle();
-      if (title.empty())
-      {
-        title = "Stone of Orthanc";
-      }
-
-      {
-        /**************************************************************
-         * Run the application inside a SDL window
-         **************************************************************/
-
-        LOG(WARNING) << "Starting the application";
-
-        SdlWindow window(title.c_str(), width, height, opengl);
-        SdlEngine sdl(window, context);
-
-        {
-          BasicApplicationContext::ViewportLocker locker(context);
-          locker.GetViewport().Register(sdl);  // (*)
-        }
-
-        context.Start();
-        sdl.Run();
-
-        LOG(WARNING) << "Stopping the application";
-
-        // Don't move the "Stop()" command below out of the block,
-        // otherwise the application might crash, because the
-        // "SdlEngine" is an observer of the viewport (*) and the
-        // update thread started by "context.Start()" would call a
-        // destructed object (the "SdlEngine" is deleted with the
-        // lexical scope).
-        context.Stop();
-      }
-
-
-      /****************************************************************
-       * Finalize the application
-       ****************************************************************/
-
-      LOG(WARNING) << "The application has stopped";
-      application.Finalize();
-    }
-    catch (Orthanc::OrthancException& e)
-    {
-      LOG(ERROR) << "EXCEPTION: " << e.What();
-      success = false;
-    }
-
-
-    /******************************************************************
-     * Finalize all the subcomponents of Orthanc Stone
-     ******************************************************************/
-
-    SdlWindow::GlobalFinalize();
-    Orthanc::HttpClient::GlobalFinalize();
-    Orthanc::Toolbox::FinalizeOpenSsl();
-
-    return (success ? 0 : -1);
-  }
-#endif
-
-}
--- a/Applications/IBasicApplication.h	Mon Nov 05 10:04:56 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,57 +0,0 @@
-/**
- * Stone of Orthanc
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2018 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
-
-#include "BasicApplicationContext.h"
-
-#include <boost/program_options.hpp>
-
-#if ORTHANC_ENABLE_SDL == 1
-#  include <SDL.h>   // Necessary to avoid undefined reference to `SDL_main'
-#endif
-
-namespace OrthancStone
-{
-  class IBasicApplication : public boost::noncopyable
-  {
-  public:
-    virtual ~IBasicApplication()
-    {
-    }
-
-    virtual void DeclareCommandLineOptions(boost::program_options::options_description& options) = 0;
-
-    virtual std::string GetTitle() const = 0;
-
-    virtual void Initialize(BasicApplicationContext& context,
-                            IStatusBar& statusBar,
-                            const boost::program_options::variables_map& parameters) = 0;
-
-    virtual void Finalize() = 0;
-
-#if ORTHANC_ENABLE_SDL == 1
-    static int ExecuteWithSdl(IBasicApplication& application,
-                              int argc, 
-                              char* argv[]);
-#endif
-  };
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/IStoneApplication.h	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,74 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 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
+
+#include "StoneApplicationContext.h"
+#include <boost/program_options.hpp>
+#include "../Framework/Viewport/WidgetViewport.h"
+#include "json/json.h"
+#include "Commands/ICommand.h"
+#include "Commands/BaseCommandBuilder.h"
+
+
+namespace OrthancStone
+{
+#if ORTHANC_ENABLE_QT==1
+  class QStoneMainWindow;
+#endif
+
+  // a StoneApplication is an application that can actually be executed
+  // in multiple environments.  i.e: it can run natively integrated in a QtApplication
+  // or it can be executed as part of a WebPage when compiled into WebAssembly.
+  class IStoneApplication : public boost::noncopyable
+  {
+  protected:
+    StoneApplicationContext* context_;
+
+  public:
+    virtual ~IStoneApplication()
+    {
+    }
+
+    virtual void DeclareStartupOptions(boost::program_options::options_description& options) = 0;
+    virtual void Initialize(StoneApplicationContext* context,
+                            IStatusBar& statusBar,
+                            const boost::program_options::variables_map& parameters) = 0;
+#if ORTHANC_ENABLE_WASM==1
+    virtual void InitializeWasm() {}  // specific initialization when the app is running in WebAssembly.  This is called after the other Initialize()
+#endif
+#if ORTHANC_ENABLE_QT==1
+      virtual QStoneMainWindow* CreateQtMainWindow() = 0;
+#endif
+
+    virtual std::string GetTitle() const = 0;
+    virtual IWidget* GetCentralWidget() = 0;
+
+    virtual void Finalize() = 0;
+
+    virtual BaseCommandBuilder& GetCommandBuilder() = 0;
+
+    virtual void ExecuteCommand(ICommand& command)
+    {
+    }
+  };
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Qt/QCairoWidget.cpp	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,183 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 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 "QCairoWidget.h"
+
+#include <QPainter>
+#include <QPaintEvent>
+
+#include <stdexcept>
+
+QCairoWidget::QCairoWidget(QWidget *parent) :
+  QWidget(parent),
+  context_(NULL)
+{
+  setFocusPolicy(Qt::StrongFocus); // catch keyPressEvents
+}
+
+QCairoWidget::~QCairoWidget()
+{
+}
+
+void QCairoWidget::SetContext(OrthancStone::NativeStoneApplicationContext& context)
+{
+  context_ = &context;
+  context_->GetCentralViewport().Register(*this); // get notified each time the content of the central viewport changes
+}
+
+void QCairoWidget::paintEvent(QPaintEvent* /*event*/)
+{
+  QPainter painter(this);
+
+  if (image_.get() != NULL && context_ != NULL)
+  {
+    OrthancStone::NativeStoneApplicationContext::GlobalMutexLocker locker(*context_);
+    OrthancStone::IViewport& viewport = context_->GetCentralViewport();
+    Orthanc::ImageAccessor a;
+    surface_.GetWriteableAccessor(a);
+    viewport.Render(a);
+    painter.drawImage(0, 0, *image_);
+  }
+  else
+  {
+    painter.fillRect(rect(), Qt::red);
+  }
+}
+
+OrthancStone::KeyboardModifiers GetKeyboardModifiers(QInputEvent* event)
+{
+  Qt::KeyboardModifiers qtModifiers = event->modifiers();
+  int stoneModifiers = static_cast<int>(OrthancStone::KeyboardModifiers_None);
+  if ((qtModifiers & Qt::AltModifier) != 0)
+  {
+    stoneModifiers |= static_cast<int>(OrthancStone::KeyboardModifiers_Alt);
+  }
+  if ((qtModifiers & Qt::ControlModifier) != 0)
+  {
+    stoneModifiers |= static_cast<int>(OrthancStone::KeyboardModifiers_Control);
+  }
+  if ((qtModifiers & Qt::ShiftModifier) != 0)
+  {
+    stoneModifiers |= static_cast<int>(OrthancStone::KeyboardModifiers_Shift);
+  }
+  return static_cast<OrthancStone::KeyboardModifiers>(stoneModifiers);
+}
+
+void QCairoWidget::mousePressEvent(QMouseEvent* event)
+{
+  OrthancStone::KeyboardModifiers stoneModifiers = GetKeyboardModifiers(event);
+
+  OrthancStone::MouseButton button;
+
+  switch (event->button())
+  {
+    case Qt::LeftButton:
+      button = OrthancStone::MouseButton_Left;
+      break;
+
+    case Qt::RightButton:
+      button = OrthancStone::MouseButton_Right;
+      break;
+
+    case Qt::MiddleButton:
+      button = OrthancStone::MouseButton_Middle;
+      break;
+
+    default:
+      return;  // Unsupported button
+  }
+  context_->GetCentralViewport().MouseDown(button, event->pos().x(), event->pos().y(), stoneModifiers);
+}
+
+
+void QCairoWidget::mouseReleaseEvent(QMouseEvent* /*eventNotUsed*/)
+{
+  context_->GetCentralViewport().MouseLeave();
+}
+
+
+void QCairoWidget::mouseMoveEvent(QMouseEvent* event)
+{
+  context_->GetCentralViewport().MouseMove(event->pos().x(), event->pos().y());
+}
+
+
+void QCairoWidget::wheelEvent(QWheelEvent * event)
+{
+  OrthancStone::KeyboardModifiers stoneModifiers = GetKeyboardModifiers(event);
+
+  if (event->orientation() == Qt::Vertical)
+  {
+    if (event->delta() < 0)  // TODO: compare direction with SDL and make sure we send the same directions
+    {
+       context_->GetCentralViewport().MouseWheel(OrthancStone::MouseWheelDirection_Up, event->pos().x(), event->pos().y(), stoneModifiers);
+    }
+    else
+    {
+      context_->GetCentralViewport().MouseWheel(OrthancStone::MouseWheelDirection_Down, event->pos().x(), event->pos().y(), stoneModifiers);
+    }
+  }
+}
+
+void QCairoWidget::keyPressEvent(QKeyEvent *event)
+{
+  using namespace OrthancStone;
+
+  OrthancStone::KeyboardModifiers stoneModifiers = GetKeyboardModifiers(event);
+
+  OrthancStone::KeyboardKeys keyType = OrthancStone::KeyboardKeys_Generic;
+  char keyChar = event->text()[0].toLatin1();
+
+#define CASE_QT_KEY_TO_ORTHANC(qt, o) case qt: keyType = o; break;
+  if (keyChar == 0)
+  {
+    switch (event->key())
+    {
+      CASE_QT_KEY_TO_ORTHANC(Qt::Key_Up, KeyboardKeys_Up);
+      CASE_QT_KEY_TO_ORTHANC(Qt::Key_Down, KeyboardKeys_Down);
+      CASE_QT_KEY_TO_ORTHANC(Qt::Key_Left, KeyboardKeys_Left);
+      CASE_QT_KEY_TO_ORTHANC(Qt::Key_Right, KeyboardKeys_Right);
+    default:
+      break;
+    }
+  }
+  context_->GetCentralViewport().KeyPressed(keyType, keyChar, stoneModifiers);
+}
+
+
+void QCairoWidget::resizeEvent(QResizeEvent* event)
+{
+  grabGesture(Qt::PanGesture);
+  QWidget::resizeEvent(event);
+
+  if (event)
+  {
+    surface_.SetSize(event->size().width(), event->size().height());
+
+    image_.reset(new QImage(reinterpret_cast<uchar*>(surface_.GetBuffer()),
+                            event->size().width(), 
+                            event->size().height(),
+                            surface_.GetPitch(),
+                            QImage::Format_RGB32));
+
+    context_->GetCentralViewport().SetSize(event->size().width(), event->size().height());
+
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Qt/QCairoWidget.h	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,75 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 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
+
+#include "../../Framework/Widgets/CairoWidget.h"
+#include "../../Applications/Generic/NativeStoneApplicationContext.h"
+#include "../../Framework/Viewport/CairoSurface.h"
+
+#include <QWidget>
+#include <QGestureEvent>
+#include <memory>
+#include <cassert>
+
+class QCairoWidget : public QWidget, public OrthancStone::IViewport::IObserver
+{
+  Q_OBJECT
+
+private:
+  std::auto_ptr<QImage>         image_;
+  OrthancStone::CairoSurface    surface_;
+  OrthancStone::NativeStoneApplicationContext* context_;
+
+protected:
+  virtual void paintEvent(QPaintEvent *event);
+
+  virtual void resizeEvent(QResizeEvent *event);
+
+  virtual void mouseMoveEvent(QMouseEvent *event);
+
+  virtual void mousePressEvent(QMouseEvent *event);
+
+  virtual void mouseReleaseEvent(QMouseEvent *event);
+
+  virtual void wheelEvent(QWheelEvent *event);
+
+  virtual void keyPressEvent(QKeyEvent *event);
+
+public:
+  explicit QCairoWidget(QWidget *parent);
+ 
+  virtual ~QCairoWidget();
+
+  void SetContext(OrthancStone::NativeStoneApplicationContext& context);
+
+  virtual void OnViewportContentChanged(const OrthancStone::IViewport& /*sceneNotUsed*/)
+  {
+    update();  // schedule a repaint (handled by Qt)
+    emit ContentChanged();
+  }
+
+signals:
+
+  void ContentChanged();
+                                               
+public slots:
+
+};
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Qt/QStoneMainWindow.cpp	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,41 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 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 "QStoneMainWindow.h"
+
+namespace OrthancStone
+{
+
+  QStoneMainWindow::QStoneMainWindow(NativeStoneApplicationContext& context, QWidget *parent) :
+    QMainWindow(parent),
+    context_(context)
+  {
+  }
+
+  void QStoneMainWindow::SetCentralStoneWidget(QCairoWidget *centralWidget)
+  {
+    cairoCentralWidget_ = centralWidget;
+    cairoCentralWidget_->SetContext(context_);
+  }
+
+  QStoneMainWindow::~QStoneMainWindow()
+  {
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Qt/QStoneMainWindow.h	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,45 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 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
+
+#include <QMainWindow>
+
+#include "QCairoWidget.h"
+#include "../Generic/NativeStoneApplicationContext.h"
+
+namespace OrthancStone
+{
+  class QStoneMainWindow : public QMainWindow
+  {
+    Q_OBJECT
+
+  private:
+    OrthancStone::NativeStoneApplicationContext& context_;
+    QCairoWidget          *cairoCentralWidget_;
+
+  protected:  // you must inherit this class
+    QStoneMainWindow(NativeStoneApplicationContext& context, QWidget *parent = 0);
+    void SetCentralStoneWidget(QCairoWidget* centralWidget);
+  public:
+    virtual ~QStoneMainWindow();
+
+  };
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Qt/QtStoneApplicationRunner.cpp	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,67 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 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/>.
+ **/
+
+
+#if ORTHANC_ENABLE_QT != 1
+#error this file shall be included only with the ORTHANC_ENABLE_QT set to 1
+#endif
+
+#include "QtStoneApplicationRunner.h"
+#include <boost/program_options.hpp>
+#include <QApplication>
+
+#include "../../Framework/Toolbox/MessagingToolbox.h"
+
+#include <Core/Logging.h>
+#include <Core/HttpClient.h>
+#include <Core/Toolbox.h>
+#include <Plugins/Samples/Common/OrthancHttpConnection.h>
+#include "../../Platforms/Generic/OracleWebService.h"
+
+
+namespace OrthancStone
+{
+  void QtStoneApplicationRunner::Initialize()
+  {
+  }
+
+  void QtStoneApplicationRunner::DeclareCommandLineOptions(boost::program_options::options_description& options)
+  {
+  }
+
+  void QtStoneApplicationRunner::Run(NativeStoneApplicationContext& context, const std::string& title, int argc, char* argv[])
+  {
+    context.Start();
+
+    QApplication qtApplication(argc, argv);
+    window_.reset(application_.CreateQtMainWindow());
+
+    window_->show();
+    qtApplication.exec();
+
+    context.Stop();
+  }
+
+  void QtStoneApplicationRunner::Finalize()
+  {
+  }
+
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Qt/QtStoneApplicationRunner.h	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,54 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 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
+
+#include "../Generic/NativeStoneApplicationRunner.h"
+#include "QStoneMainWindow.h"
+
+#if ORTHANC_ENABLE_QT != 1
+#error this file shall be included only with the ORTHANC_ENABLE_QT set to 1
+#endif
+
+namespace OrthancStone
+{
+  class QtStoneApplicationRunner : public NativeStoneApplicationRunner
+  {
+  protected:
+    std::auto_ptr<QStoneMainWindow> window_;
+
+  public:
+    QtStoneApplicationRunner(MessageBroker& broker,
+                             IStoneApplication& application)
+      : NativeStoneApplicationRunner(broker, application)
+    {
+    }
+
+
+    virtual void Initialize();
+
+    virtual void DeclareCommandLineOptions(boost::program_options::options_description& options);
+    virtual void ParseCommandLineOptions(const boost::program_options::variables_map& parameters) {}
+    virtual void Run(NativeStoneApplicationContext& context, const std::string& title, int argc, char* argv[]);
+    virtual void Finalize();
+  };
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/CMakeLists.txt	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,222 @@
+# Usage (Linux):
+# to build the WASM samples
+# source ~/Downloads/emsdk/emsdk_env.sh && cmake -DCMAKE_TOOLCHAIN_FILE=${EMSCRIPTEN}/cmake/Modules/Platform/Emscripten.cmake -DCMAKE_BUILD_TYPE=Release -DSTONE_SOURCES_DIR=$currentDir/../../../orthanc-stone -DORTHANC_FRAMEWORK_SOURCE=path -DORTHANC_FRAMEWORK_ROOT=$currentDir/../../../orthanc -DALLOW_DOWNLOADS=ON .. -DENABLE_WASM=ON
+# to build the Qt samples
+
+cmake_minimum_required(VERSION 2.8.3)
+project(OrthancStone)
+
+include(../../Resources/CMake/OrthancStoneParameters.cmake)
+
+#set(ENABLE_DCMTK ON)
+
+set(ENABLE_SDL OFF CACHE BOOL "Target SDL Native application")
+set(ENABLE_QT OFF CACHE BOOL "Target Qt Native application")
+set(ENABLE_WASM OFF CACHE BOOL "Target WASM application")
+
+if (ENABLE_WASM)
+  #####################################################################
+  ## Configuration of the Emscripten compiler for WebAssembly target
+  #####################################################################
+
+  set(WASM_FLAGS "-s WASM=1")
+  set(WASM_MODULE_NAME "StoneFrameworkModule" CACHE STRING "Name of the WebAssembly module")
+  set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${WASM_FLAGS}")
+  set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${WASM_FLAGS}")
+  set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} --js-library ${STONE_SOURCES_DIR}/Applications/Samples/samples-library.js --js-library ${STONE_SOURCES_DIR}/Platforms/Wasm/WasmWebService.js --js-library ${STONE_SOURCES_DIR}/Platforms/Wasm/default-library.js  -s EXTRA_EXPORTED_RUNTIME_METHODS='[\"ccall\", \"cwrap\"]'")
+
+  # Handling of memory
+  #set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s ALLOW_MEMORY_GROWTH=1")  # Resize
+  #set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s TOTAL_MEMORY=536870912")  # 512MB
+  set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s EXPORT_NAME='\"${WASM_MODULE_NAME}\"' -s ALLOW_MEMORY_GROWTH=1 -s TOTAL_MEMORY=536870912 -s TOTAL_STACK=128000000")  # 512MB + resize
+  #set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s ALLOW_MEMORY_GROWTH=1 -s TOTAL_MEMORY=1073741824")  # 1GB + resize
+
+  # To debug exceptions
+  #set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s DEMANGLE_SUPPORT=1 -s ASSERTIONS=2")
+
+  add_definitions(-DORTHANC_ENABLE_WASM=1)
+  set(ORTHANC_SANDBOXED ON)
+
+elseif (ENABLE_QT OR ENABLE_SDL)
+
+  set(ENABLE_NATIVE ON)
+  set(ORTHANC_SANDBOXED OFF)
+  set(ENABLE_CRYPTO_OPTIONS ON)
+  set(ENABLE_GOOGLE_TEST ON)
+  set(ENABLE_WEB_CLIENT ON)
+
+endif()
+
+#####################################################################
+## Configuration for Orthanc
+#####################################################################
+
+# include(../../Resources/CMake/Version.cmake)
+
+if (ORTHANC_STONE_VERSION STREQUAL "mainline")
+  set(ORTHANC_FRAMEWORK_VERSION "mainline")
+  set(ORTHANC_FRAMEWORK_DEFAULT_SOURCE "hg")
+else()
+  set(ORTHANC_FRAMEWORK_VERSION "1.4.1")
+  set(ORTHANC_FRAMEWORK_DEFAULT_SOURCE "web")
+endif()
+
+set(ORTHANC_FRAMEWORK_SOURCE "${ORTHANC_FRAMEWORK_DEFAULT_SOURCE}" CACHE STRING "Source of the Orthanc source code (can be \"hg\", \"archive\", \"web\" or \"path\")")
+set(ORTHANC_FRAMEWORK_ARCHIVE "" CACHE STRING "Path to the Orthanc archive, if ORTHANC_FRAMEWORK_SOURCE is \"archive\"")
+set(ORTHANC_FRAMEWORK_ROOT "" CACHE STRING "Path to the Orthanc source directory, if ORTHANC_FRAMEWORK_SOURCE is \"path\"")
+
+
+#####################################################################
+## Build a static library containing the Orthanc Stone framework
+#####################################################################
+
+
+LIST(APPEND ORTHANC_BOOST_COMPONENTS program_options)
+
+include(../../Resources/CMake/OrthancStoneConfiguration.cmake)
+
+add_library(OrthancStone STATIC
+  ${ORTHANC_STONE_SOURCES}
+  )
+
+#####################################################################
+## Build all the sample applications
+#####################################################################
+
+include_directories(${ORTHANC_STONE_ROOT})
+
+# files common to all samples
+list(APPEND SAMPLE_APPLICATIONS_SOURCES
+  ${ORTHANC_STONE_ROOT}/Applications/Samples/SampleInteractor.h
+  ${ORTHANC_STONE_ROOT}/Applications/Samples/SampleApplicationBase.h
+  )
+
+if (ENABLE_QT)
+  list(APPEND SAMPLE_APPLICATIONS_SOURCES
+    ${ORTHANC_STONE_ROOT}/Applications/Samples/Qt/SampleQtApplicationRunner.h
+    ${ORTHANC_STONE_ROOT}/Applications/Samples/Qt/SampleMainWindow.cpp
+    ${ORTHANC_STONE_ROOT}/Applications/Samples/Qt/SampleMainWindowWithButtons.cpp
+    )
+
+  ORTHANC_QT_WRAP_UI(SAMPLE_APPLICATIONS_SOURCES
+    ${ORTHANC_STONE_ROOT}/Applications/Samples/Qt/SampleMainWindow.ui
+    ${ORTHANC_STONE_ROOT}/Applications/Samples/Qt/SampleMainWindowWithButtons.ui
+    )
+
+  ORTHANC_QT_WRAP_CPP(SAMPLE_APPLICATIONS_SOURCES
+    ${ORTHANC_STONE_ROOT}/Applications/Qt/QCairoWidget.h
+    ${ORTHANC_STONE_ROOT}/Applications/Qt/QStoneMainWindow.h
+    ${ORTHANC_STONE_ROOT}/Applications/Samples/Qt/SampleMainWindow.h
+    ${ORTHANC_STONE_ROOT}/Applications/Samples/Qt/SampleMainWindowWithButtons.h
+    )
+endif()
+
+if (ENABLE_NATIVE)
+  list(APPEND SAMPLE_APPLICATIONS_SOURCES
+    ${ORTHANC_STONE_ROOT}/Applications/Samples/SampleMainNative.cpp
+    )
+
+elseif (ENABLE_WASM)
+
+  list(APPEND SAMPLE_APPLICATIONS_SOURCES
+    ${ORTHANC_STONE_ROOT}/Applications/Samples/SampleMainWasm.cpp
+    ${STONE_WASM_SOURCES}
+    )
+endif()
+
+
+macro(BuildSingleFileSample Target Header Sample)
+  add_executable(${Target}
+    ${ORTHANC_STONE_ROOT}/Applications/Samples/${Header}
+    ${SAMPLE_APPLICATIONS_SOURCES}
+    )
+  set_target_properties(${Target} PROPERTIES COMPILE_DEFINITIONS ORTHANC_STONE_SAMPLE=${Sample})
+  target_link_libraries(${Target} OrthancStone)
+endmacro()
+
+#BuildSingleFileSample(OrthancStoneEmpty EmptyApplication.h 1)
+#BuildSingleFileSample(OrthancStoneTestPattern TestPatternApplication.h 2)
+BuildSingleFileSample(OrthancStoneSingleFrame SingleFrameApplication.h 3)
+#BuildSingleFileSample(OrthancStoneSingleVolume SingleVolumeApplication.h 4)
+#BuildSingleFileSample(OrthancStoneBasicPetCtFusion 5)
+#BuildSingleFileSample(OrthancStoneSynchronizedSeries 6)
+#BuildSingleFileSample(OrthancStoneLayoutPetCtFusion 7)
+BuildSingleFileSample(OrthancStoneSimpleViewerSingleFile SimpleViewerApplicationSingleFile.h 8)  # we keep that one just as a sample before we convert another sample to this pattern
+BuildSingleFileSample(OrthancStoneSingleFrameEditor SingleFrameEditorApplication.h 9)
+
+##### SimpleViewer sample (Qt and WASM only) #######
+
+if (ENABLE_QT OR ENABLE_WASM)
+
+    if (ENABLE_QT)
+      list(APPEND SIMPLE_VIEWER_APPLICATION_SOURCES
+        ${ORTHANC_STONE_ROOT}/Applications/Samples/SimpleViewer/Qt/SimpleViewerMainWindow.cpp
+        ${ORTHANC_STONE_ROOT}/Applications/Samples/SimpleViewer/Qt/SimpleViewerMainWindow.ui
+        ${ORTHANC_STONE_ROOT}/Applications/Samples/SimpleViewer/Qt/mainQt.cpp
+        )
+
+      ORTHANC_QT_WRAP_UI(SIMPLE_VIEWER_APPLICATION_SOURCES
+        ${ORTHANC_STONE_ROOT}/Applications/Samples/SimpleViewer/Qt/SimpleViewerMainWindow.ui
+        )
+
+      ORTHANC_QT_WRAP_CPP(SIMPLE_VIEWER_APPLICATION_SOURCES
+        ${ORTHANC_STONE_ROOT}/Applications/Qt/QCairoWidget.h
+        ${ORTHANC_STONE_ROOT}/Applications/Qt/QStoneMainWindow.h
+        ${ORTHANC_STONE_ROOT}/Applications/Samples/SimpleViewer/Qt/SimpleViewerMainWindow.h
+        )
+
+elseif (ENABLE_WASM)
+        list(APPEND SIMPLE_VIEWER_APPLICATION_SOURCES
+            ${ORTHANC_STONE_ROOT}/Applications/Samples/SimpleViewer/Wasm/mainWasm.cpp
+            ${ORTHANC_STONE_ROOT}/Applications/Samples/SimpleViewer/Wasm/SimpleViewerWasmApplicationAdapter.cpp
+            ${STONE_WASM_SOURCES}
+          )
+    endif()
+
+    add_executable(OrthancStoneSimpleViewer
+      ${ORTHANC_STONE_ROOT}/Applications/Samples/SimpleViewer/SimpleViewerApplication.cpp
+      ${ORTHANC_STONE_ROOT}/Applications/Samples/SimpleViewer/ThumbnailInteractor.cpp
+      ${ORTHANC_STONE_ROOT}/Applications/Samples/SimpleViewer/MainWidgetInteractor.cpp
+      ${ORTHANC_STONE_ROOT}/Applications/Samples/SimpleViewer/AppStatus.h
+      ${ORTHANC_STONE_ROOT}/Applications/Samples/SimpleViewer/Messages.h
+      ${SIMPLE_VIEWER_APPLICATION_SOURCES}
+      )
+    target_link_libraries(OrthancStoneSimpleViewer OrthancStone)
+
+endif()
+
+#####################################################################
+## Build the unit tests
+#####################################################################
+
+if (ENABLE_NATIVE)
+  add_executable(UnitTests
+    ${GOOGLE_TEST_SOURCES}
+    ${ORTHANC_STONE_ROOT}/UnitTestsSources/TestCommands.cpp
+    ${ORTHANC_STONE_ROOT}/UnitTestsSources/TestExceptions.cpp
+    ${ORTHANC_STONE_ROOT}/UnitTestsSources/TestMessageBroker.cpp
+    ${ORTHANC_STONE_ROOT}/UnitTestsSources/TestMessageBroker2.cpp
+    ${ORTHANC_STONE_ROOT}/UnitTestsSources/UnitTestsMain.cpp
+    )
+
+  target_link_libraries(UnitTests OrthancStone)
+endif()
+
+#####################################################################
+## Generate the documentation if Doxygen is present
+#####################################################################
+
+find_package(Doxygen)
+if (DOXYGEN_FOUND)
+  configure_file(
+    ${ORTHANC_STONE_ROOT}/Resources/OrthancStone.doxygen
+    ${CMAKE_CURRENT_BINARY_DIR}/OrthancStone.doxygen
+    @ONLY)
+
+  add_custom_target(doc
+    ${DOXYGEN_EXECUTABLE} ${CMAKE_CURRENT_BINARY_DIR}/OrthancStone.doxygen
+    COMMENT "Generating documentation with Doxygen" VERBATIM
+    )
+else()
+  message("Doxygen not found. The documentation will not be built.")
+endif()
--- a/Applications/Samples/EmptyApplication.h	Mon Nov 05 10:04:56 2018 +0100
+++ b/Applications/Samples/EmptyApplication.h	Mon Nov 05 10:06:18 2018 +0100
@@ -32,7 +32,7 @@
     class EmptyApplication : public SampleApplicationBase
     {
     public:
-      virtual void DeclareCommandLineOptions(boost::program_options::options_description& options)
+      virtual void DeclareStartupOptions(boost::program_options::options_description& options)
       {
         boost::program_options::options_description generic("Sample options");
         generic.add_options()
@@ -44,15 +44,14 @@
         options.add(generic);    
       }
 
-      virtual void Initialize(BasicApplicationContext& context,
-                              IStatusBar& statusBar,
+      virtual void Initialize(IStatusBar& statusBar,
                               const boost::program_options::variables_map& parameters)
       {
         int red = parameters["red"].as<int>();
         int green = parameters["green"].as<int>();
         int blue = parameters["blue"].as<int>();
 
-        context.SetCentralWidget(new EmptyWidget(red, green, blue));
+        context_->SetCentralWidget(new EmptyWidget(red, green, blue));
       }
     };
   }
--- a/Applications/Samples/LayoutPetCtFusionApplication.h	Mon Nov 05 10:04:56 2018 +0100
+++ b/Applications/Samples/LayoutPetCtFusionApplication.h	Mon Nov 05 10:06:18 2018 +0100
@@ -89,7 +89,7 @@
         {
           if (key == 's')
           {
-            that_.SetDefaultView();
+            that_.FitContent();
           }
         }
       };
@@ -109,11 +109,11 @@
       LayeredSceneWidget*  fusionSagittal_;
 
 
-      void SetDefaultView()
+      void FitContent()
       {
-        petAxial_->SetDefaultView();
-        petCoronal_->SetDefaultView();
-        petSagittal_->SetDefaultView();
+        petAxial_->FitContent();
+        petCoronal_->FitContent();
+        petSagittal_->FitContent();
       }
 
 
@@ -361,7 +361,7 @@
       virtual void NotifySizeChange(const WorldSceneWidget& source,
                                     ViewportGeometry& view)
       {
-        view.SetDefaultView();
+        view.FitContent();
       }
 
       virtual void NotifyViewChange(const WorldSceneWidget& source,
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/Qt/SampleMainWindow.cpp	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,50 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 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 "SampleMainWindow.h"
+
+/**
+ * Don't use "ui_MainWindow.h" instead of <ui_MainWindow.h> below, as
+ * this makes CMake unable to detect when the UI file changes.
+ **/
+#include <ui_SampleMainWindow.h>
+#include "../../Applications/Samples/SampleApplicationBase.h"
+
+namespace OrthancStone
+{
+  namespace Samples
+  {
+
+    SampleMainWindow::SampleMainWindow(OrthancStone::NativeStoneApplicationContext& context, OrthancStone::Samples::SampleSingleCanvasApplicationBase& stoneSampleApplication, QWidget *parent) :
+      QStoneMainWindow(context, parent),
+      ui_(new Ui::SampleMainWindow),
+      stoneSampleApplication_(stoneSampleApplication)
+    {
+      ui_->setupUi(this);
+      SetCentralStoneWidget(ui_->cairoCentralWidget);
+    }
+
+    SampleMainWindow::~SampleMainWindow()
+    {
+      delete ui_;
+    }
+
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/Qt/SampleMainWindow.h	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,50 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 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
+
+#include "../../Qt/QCairoWidget.h"
+#include "../../Qt/QStoneMainWindow.h"
+
+namespace Ui 
+{
+  class SampleMainWindow;
+}
+
+namespace OrthancStone
+{
+  namespace Samples
+  {
+
+    class SampleSingleCanvasApplicationBase;
+
+    class SampleMainWindow : public QStoneMainWindow
+    {
+      Q_OBJECT
+
+    private:
+      Ui::SampleMainWindow*   ui_;
+      SampleSingleCanvasApplicationBase&  stoneSampleApplication_;
+
+    public:
+      explicit SampleMainWindow(OrthancStone::NativeStoneApplicationContext& context, SampleSingleCanvasApplicationBase& stoneSampleApplication, QWidget *parent = 0);
+      ~SampleMainWindow();
+    };
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/Qt/SampleMainWindow.ui	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,84 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>SampleMainWindow</class>
+ <widget class="QMainWindow" name="SampleMainWindow">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>903</width>
+    <height>634</height>
+   </rect>
+  </property>
+  <property name="minimumSize">
+   <size>
+    <width>500</width>
+    <height>300</height>
+   </size>
+  </property>
+  <property name="baseSize">
+   <size>
+    <width>500</width>
+    <height>300</height>
+   </size>
+  </property>
+  <property name="windowTitle">
+   <string>Stone of Orthanc</string>
+  </property>
+  <property name="layoutDirection">
+   <enum>Qt::LeftToRight</enum>
+  </property>
+  <widget class="QWidget" name="centralwidget">
+   <property name="sizePolicy">
+    <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
+     <horstretch>0</horstretch>
+     <verstretch>0</verstretch>
+    </sizepolicy>
+   </property>
+   <property name="layoutDirection">
+    <enum>Qt::LeftToRight</enum>
+   </property>
+   <layout class="QVBoxLayout" name="verticalLayout_2" stretch="0">
+    <property name="sizeConstraint">
+     <enum>QLayout::SetDefaultConstraint</enum>
+    </property>
+    <item>
+     <widget class="QCairoWidget" name="cairoCentralWidget">
+      <property name="minimumSize">
+       <size>
+        <width>0</width>
+        <height>500</height>
+       </size>
+      </property>
+     </widget>
+    </item>
+   </layout>
+  </widget>
+  <widget class="QMenuBar" name="menubar">
+   <property name="geometry">
+    <rect>
+     <x>0</x>
+     <y>0</y>
+     <width>903</width>
+     <height>22</height>
+    </rect>
+   </property>
+   <widget class="QMenu" name="menuTest">
+    <property name="title">
+     <string>Test</string>
+    </property>
+   </widget>
+   <addaction name="menuTest"/>
+  </widget>
+  <widget class="QStatusBar" name="statusbar"/>
+ </widget>
+ <customwidgets>
+  <customwidget>
+   <class>QCairoWidget</class>
+   <extends>QGraphicsView</extends>
+   <header location="global">QCairoWidget.h</header>
+  </customwidget>
+ </customwidgets>
+ <resources/>
+ <connections/>
+</ui>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/Qt/SampleMainWindowWithButtons.cpp	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,93 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 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 "SampleMainWindow.h"
+
+/**
+ * Don't use "ui_MainWindow.h" instead of <ui_MainWindow.h> below, as
+ * this makes CMake unable to detect when the UI file changes.
+ **/
+#include <ui_SampleMainWindowWithButtons.h>
+#include "../../Applications/Samples/SampleApplicationBase.h"
+
+namespace OrthancStone
+{
+  namespace Samples
+  {
+
+    SampleMainWindowWithButtons::SampleMainWindowWithButtons(OrthancStone::NativeStoneApplicationContext& context, OrthancStone::Samples::SampleSingleCanvasWithButtonsApplicationBase& stoneSampleApplication, QWidget *parent) :
+      QStoneMainWindow(context, parent),
+      ui_(new Ui::SampleMainWindowWithButtons),
+      stoneSampleApplication_(stoneSampleApplication)
+    {
+      ui_->setupUi(this);
+      SetCentralStoneWidget(ui_->cairoCentralWidget);
+
+#if QT_VERSION >= 0x050000
+      connect(ui_->toolButton1, &QToolButton::clicked, this, &SampleMainWindowWithButtons::tool1Clicked);
+      connect(ui_->toolButton2, &QToolButton::clicked, this, &SampleMainWindowWithButtons::tool2Clicked);
+      connect(ui_->pushButton1, &QPushButton::clicked, this, &SampleMainWindowWithButtons::pushButton1Clicked);
+      connect(ui_->pushButton1, &QPushButton::clicked, this, &SampleMainWindowWithButtons::pushButton2Clicked);
+#else
+      connect(ui_->toolButton1, SIGNAL(clicked()), this, SLOT(tool1Clicked()));
+      connect(ui_->toolButton2, SIGNAL(clicked()), this, SLOT(tool2Clicked()));
+      connect(ui_->pushButton1, SIGNAL(clicked()), this, SLOT(pushButton1Clicked()));
+      connect(ui_->pushButton1, SIGNAL(clicked()), this, SLOT(pushButton2Clicked()));
+#endif
+
+      std::string pushButton1Name;
+      std::string pushButton2Name;
+      std::string tool1Name;
+      std::string tool2Name;
+      stoneSampleApplication_.GetButtonNames(pushButton1Name, pushButton2Name, tool1Name, tool2Name);
+
+      ui_->toolButton1->setText(QString::fromStdString(tool1Name));
+      ui_->toolButton2->setText(QString::fromStdString(tool2Name));
+      ui_->pushButton1->setText(QString::fromStdString(pushButton1Name));
+      ui_->pushButton2->setText(QString::fromStdString(pushButton2Name));
+    }
+
+    SampleMainWindowWithButtons::~SampleMainWindowWithButtons()
+    {
+      delete ui_;
+    }
+
+    void SampleMainWindowWithButtons::tool1Clicked()
+    {
+      stoneSampleApplication_.OnTool1Clicked();
+    }
+
+    void SampleMainWindowWithButtons::tool2Clicked()
+    {
+      stoneSampleApplication_.OnTool2Clicked();
+    }
+
+    void SampleMainWindowWithButtons::pushButton1Clicked()
+    {
+      stoneSampleApplication_.OnPushButton1Clicked();
+    }
+
+    void SampleMainWindowWithButtons::pushButton2Clicked()
+    {
+      stoneSampleApplication_.OnPushButton2Clicked();
+    }
+
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/Qt/SampleMainWindowWithButtons.h	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,56 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 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
+
+#include "../../Qt/QCairoWidget.h"
+#include "../../Qt/QStoneMainWindow.h"
+
+namespace Ui 
+{
+  class SampleMainWindowWithButtons;
+}
+
+namespace OrthancStone
+{
+  namespace Samples
+  {
+
+    class SampleSingleCanvasWithButtonsApplicationBase;
+
+    class SampleMainWindowWithButtons : public QStoneMainWindow
+    {
+      Q_OBJECT
+
+    private:
+      Ui::SampleMainWindowWithButtons*   ui_;
+      SampleSingleCanvasWithButtonsApplicationBase&  stoneSampleApplication_;
+
+    public:
+      explicit SampleMainWindowWithButtons(OrthancStone::NativeStoneApplicationContext& context, SampleSingleCanvasWithButtonsApplicationBase& stoneSampleApplication, QWidget *parent = 0);
+      ~SampleMainWindowWithButtons();
+
+    private slots:
+      void tool1Clicked();
+      void tool2Clicked();
+      void pushButton1Clicked();
+      void pushButton2Clicked();
+    };
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/Qt/SampleMainWindowWithButtons.ui	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,130 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>SampleMainWindowWithButtons</class>
+ <widget class="QMainWindow" name="SampleMainWindowWithButtons">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>903</width>
+    <height>634</height>
+   </rect>
+  </property>
+  <property name="minimumSize">
+   <size>
+    <width>500</width>
+    <height>300</height>
+   </size>
+  </property>
+  <property name="baseSize">
+   <size>
+    <width>500</width>
+    <height>300</height>
+   </size>
+  </property>
+  <property name="windowTitle">
+   <string>Stone of Orthanc</string>
+  </property>
+  <property name="layoutDirection">
+   <enum>Qt::LeftToRight</enum>
+  </property>
+  <widget class="QWidget" name="centralwidget">
+   <property name="sizePolicy">
+    <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
+     <horstretch>0</horstretch>
+     <verstretch>0</verstretch>
+    </sizepolicy>
+   </property>
+   <property name="layoutDirection">
+    <enum>Qt::LeftToRight</enum>
+   </property>
+   <layout class="QVBoxLayout" name="verticalLayout_2" stretch="0,0">
+    <property name="sizeConstraint">
+     <enum>QLayout::SetDefaultConstraint</enum>
+    </property>
+    <item>
+     <widget class="QCairoWidget" name="cairoCentralWidget">
+      <property name="minimumSize">
+       <size>
+        <width>0</width>
+        <height>500</height>
+       </size>
+      </property>
+     </widget>
+    </item>
+    <item>
+     <widget class="QGroupBox" name="horizontalGroupBox">
+      <property name="minimumSize">
+       <size>
+        <width>0</width>
+        <height>100</height>
+       </size>
+      </property>
+      <property name="maximumSize">
+       <size>
+        <width>16777215</width>
+        <height>100</height>
+       </size>
+      </property>
+      <layout class="QHBoxLayout" name="horizontalLayout">
+       <item>
+        <widget class="QToolButton" name="toolButton1">
+         <property name="text">
+          <string>tool1</string>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="QToolButton" name="toolButton2">
+         <property name="text">
+          <string>tool2</string>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="QPushButton" name="pushButton1">
+         <property name="text">
+          <string>action1</string>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="QPushButton" name="pushButton2">
+         <property name="text">
+          <string>action2</string>
+         </property>
+        </widget>
+       </item>
+      </layout>
+     </widget>
+    </item>
+   </layout>
+  </widget>
+  <widget class="QMenuBar" name="menubar">
+   <property name="geometry">
+    <rect>
+     <x>0</x>
+     <y>0</y>
+     <width>903</width>
+     <height>22</height>
+    </rect>
+   </property>
+   <widget class="QMenu" name="menuTest">
+    <property name="title">
+     <string>Test</string>
+    </property>
+   </widget>
+   <addaction name="menuTest"/>
+  </widget>
+  <widget class="QStatusBar" name="statusbar"/>
+ </widget>
+ <customwidgets>
+  <customwidget>
+   <class>QCairoWidget</class>
+   <extends>QGraphicsView</extends>
+   <header location="global">QCairoWidget.h</header>
+  </customwidget>
+ </customwidgets>
+ <resources/>
+ <connections/>
+</ui>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/Qt/SampleQtApplicationRunner.h	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,50 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 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
+
+#include "../../Qt/QtStoneApplicationRunner.h"
+
+#if ORTHANC_ENABLE_QT != 1
+#error this file shall be included only with the ORTHANC_ENABLE_QT set to 1
+#endif
+
+namespace OrthancStone
+{
+  namespace Samples
+  {
+    class SampleQtApplicationRunner : public OrthancStone::QtStoneApplicationRunner
+    {
+    protected:
+      virtual void InitializeMainWindow(OrthancStone::NativeStoneApplicationContext& context)
+      {
+        window_.reset(application_.CreateQtMainWindow());
+      }
+    public:
+      SampleQtApplicationRunner(MessageBroker& broker,
+                                SampleApplicationBase& application)
+        : OrthancStone::QtStoneApplicationRunner(broker, application)
+      {
+      }
+
+    };
+  }
+}
--- a/Applications/Samples/SampleApplicationBase.h	Mon Nov 05 10:04:56 2018 +0100
+++ b/Applications/Samples/SampleApplicationBase.h	Mon Nov 05 10:06:18 2018 +0100
@@ -13,7 +13,7 @@
  * 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/>.
  **/
@@ -21,27 +21,100 @@
 
 #pragma once
 
-#include "../IBasicApplication.h"
+#include "../../Applications/IStoneApplication.h"
+#include "../../Framework/Widgets/WorldSceneWidget.h"
+
+#if ORTHANC_ENABLE_WASM==1
+#include "../../Platforms/Wasm/WasmPlatformApplicationAdapter.h"
+#include "../../Platforms/Wasm/Defaults.h"
+#endif
+
+#if ORTHANC_ENABLE_QT==1
+#include "Qt/SampleMainWindow.h"
+#include "Qt/SampleMainWindowWithButtons.h"
+#endif
 
 namespace OrthancStone
 {
   namespace Samples
   {
-    class SampleApplicationBase : public IBasicApplication
+    class SampleApplicationBase : public IStoneApplication
     {
+    protected:
+      BaseCommandBuilder commandBuilder_;
+      WorldSceneWidget*  mainWidget_;   // ownership is transfered to the application context
+
     public:
+      virtual void Initialize(StoneApplicationContext* context,
+                              IStatusBar& statusBar,
+                              const boost::program_options::variables_map& parameters)
+      {
+      }
+
       virtual std::string GetTitle() const
       {
         return "Stone of Orthanc - Sample";
       }
 
-      virtual void DeclareCommandLineOptions(boost::program_options::options_description& options)
+      virtual BaseCommandBuilder& GetCommandBuilder() {return commandBuilder_;}
+
+      virtual void Finalize() {}
+      virtual IWidget* GetCentralWidget() {return mainWidget_;}
+
+#if ORTHANC_ENABLE_WASM==1
+      // default implementations for a single canvas named "canvas" in the HTML and an emtpy WasmApplicationAdapter
+
+      virtual void InitializeWasm()
       {
+        AttachWidgetToWasmViewport("canvas", mainWidget_);
       }
 
-      virtual void Finalize()
+      virtual WasmPlatformApplicationAdapter* CreateWasmApplicationAdapter(MessageBroker& broker)
       {
+        return new WasmPlatformApplicationAdapter(broker, *this);
       }
+#endif
+
+    };
+
+    // this application actually works in Qt and WASM
+    class SampleSingleCanvasWithButtonsApplicationBase : public SampleApplicationBase
+    {
+public:
+      virtual void OnPushButton1Clicked() {}
+      virtual void OnPushButton2Clicked() {}
+      virtual void OnTool1Clicked() {}
+      virtual void OnTool2Clicked() {}
+
+      virtual void GetButtonNames(std::string& pushButton1,
+                                  std::string& pushButton2,
+                                  std::string& tool1,
+                                  std::string& tool2
+                                  ) {
+        pushButton1 = "action1";
+        pushButton2 = "action2";
+        tool1 = "tool1";
+        tool2 = "tool2";
+      }
+
+#if ORTHANC_ENABLE_QT==1
+      virtual QStoneMainWindow* CreateQtMainWindow() {
+        return new SampleMainWindowWithButtons(dynamic_cast<OrthancStone::NativeStoneApplicationContext&>(*context_), *this);
+      }
+#endif
+
+    };
+
+    // this application actually works in SDL and WASM
+    class SampleSingleCanvasApplicationBase : public SampleApplicationBase
+    {
+public:
+
+#if ORTHANC_ENABLE_QT==1
+      virtual QStoneMainWindow* CreateQtMainWindow() {
+        return new SampleMainWindow(dynamic_cast<OrthancStone::NativeStoneApplicationContext&>(*context_), *this);
+      }
+#endif
     };
   }
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/SampleList.h	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,41 @@
+// The macro "ORTHANC_STONE_SAMPLE" must be set by the CMake script
+
+#if ORTHANC_STONE_SAMPLE == 1
+#include "EmptyApplication.h"
+typedef OrthancStone::Samples::EmptyApplication SampleApplication;
+
+#elif ORTHANC_STONE_SAMPLE == 2
+#include "TestPatternApplication.h"
+typedef OrthancStone::Samples::TestPatternApplication SampleApplication;
+
+#elif ORTHANC_STONE_SAMPLE == 3
+#include "SingleFrameApplication.h"
+typedef OrthancStone::Samples::SingleFrameApplication SampleApplication;
+
+#elif ORTHANC_STONE_SAMPLE == 4
+#include "SingleVolumeApplication.h"
+typedef OrthancStone::Samples::SingleVolumeApplication SampleApplication;
+
+#elif ORTHANC_STONE_SAMPLE == 5
+#include "BasicPetCtFusionApplication.h"
+typedef OrthancStone::Samples::BasicPetCtFusionApplication SampleApplication;
+
+#elif ORTHANC_STONE_SAMPLE == 6
+#include "SynchronizedSeriesApplication.h"
+typedef OrthancStone::Samples::SynchronizedSeriesApplication SampleApplication;
+
+#elif ORTHANC_STONE_SAMPLE == 7
+#include "LayoutPetCtFusionApplication.h"
+typedef OrthancStone::Samples::LayoutPetCtFusionApplication SampleApplication;
+
+#elif ORTHANC_STONE_SAMPLE == 8
+#include "SimpleViewerApplicationSingleFile.h"
+typedef OrthancStone::Samples::SimpleViewerApplication SampleApplication;
+
+#elif ORTHANC_STONE_SAMPLE == 9
+#include "SingleFrameEditorApplication.h"
+typedef OrthancStone::Samples::SingleFrameEditorApplication SampleApplication;
+
+#else
+#error Please set the ORTHANC_STONE_SAMPLE macro
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/SampleMainNative.cpp	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,44 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 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 "SampleList.h"
+#if ORTHANC_ENABLE_SDL==1
+#include "../Sdl/SdlStoneApplicationRunner.h"
+#endif
+#if ORTHANC_ENABLE_QT==1
+#include "Qt/SampleQtApplicationRunner.h"
+#endif
+#include "../../Framework/Messages/MessageBroker.h"
+
+int main(int argc, char* argv[]) 
+{
+  OrthancStone::MessageBroker broker;
+  SampleApplication sampleStoneApplication(broker);
+
+#if ORTHANC_ENABLE_SDL==1
+  OrthancStone::SdlStoneApplicationRunner sdlApplicationRunner(broker, sampleStoneApplication);
+  return sdlApplicationRunner.Execute(argc, argv);
+#endif
+#if ORTHANC_ENABLE_QT==1
+  OrthancStone::Samples::SampleQtApplicationRunner qtAppRunner(broker, sampleStoneApplication);
+  return qtAppRunner.Execute(argc, argv);
+#endif
+}
--- a/Applications/Samples/SampleMainSdl.cpp	Mon Nov 05 10:04:56 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,62 +0,0 @@
-/**
- * Stone of Orthanc
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2018 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/>.
- **/
-
-
-// The macro "ORTHANC_STONE_SAMPLE" must be set by the CMake script
-
-#if ORTHANC_STONE_SAMPLE == 1
-#include "EmptyApplication.h"
-typedef OrthancStone::Samples::EmptyApplication Application;
-
-#elif ORTHANC_STONE_SAMPLE == 2
-#include "TestPatternApplication.h"
-typedef OrthancStone::Samples::TestPatternApplication Application;
-
-#elif ORTHANC_STONE_SAMPLE == 3
-#include "SingleFrameApplication.h"
-typedef OrthancStone::Samples::SingleFrameApplication Application;
-
-#elif ORTHANC_STONE_SAMPLE == 4
-#include "SingleVolumeApplication.h"
-typedef OrthancStone::Samples::SingleVolumeApplication Application;
-
-#elif ORTHANC_STONE_SAMPLE == 5
-#include "BasicPetCtFusionApplication.h"
-typedef OrthancStone::Samples::BasicPetCtFusionApplication Application;
-
-#elif ORTHANC_STONE_SAMPLE == 6
-#include "SynchronizedSeriesApplication.h"
-typedef OrthancStone::Samples::SynchronizedSeriesApplication Application;
-
-#elif ORTHANC_STONE_SAMPLE == 7
-#include "LayoutPetCtFusionApplication.h"
-typedef OrthancStone::Samples::LayoutPetCtFusionApplication Application;
-
-#else
-#error Please set the ORTHANC_STONE_SAMPLE macro
-#endif
-
-
-int main(int argc, char* argv[]) 
-{
-  Application application;
-
-  return OrthancStone::IBasicApplication::ExecuteWithSdl(application, argc, argv);
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/SampleMainWasm.cpp	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,37 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 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 "Platforms/Wasm/WasmWebService.h"
+#include "Platforms/Wasm/WasmViewport.h"
+
+#include <emscripten/emscripten.h>
+
+#include "SampleList.h"
+
+
+OrthancStone::IStoneApplication* CreateUserApplication(OrthancStone::MessageBroker& broker) 
+{
+  return new SampleApplication(broker);
+}
+
+OrthancStone::WasmPlatformApplicationAdapter* CreateWasmApplicationAdapter(OrthancStone::MessageBroker& broker, OrthancStone::IStoneApplication* application)
+{
+  return dynamic_cast<SampleApplication*>(application)->CreateWasmApplicationAdapter(broker);
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/SimpleViewer/AppStatus.h	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,27 @@
+#pragma once
+
+#include <string>
+
+
+namespace SimpleViewer
+{
+  struct AppStatus
+  {
+    std::string patientId;
+    std::string studyDescription;
+    std::string currentInstanceIdInMainViewport;
+    // note: if you add members here, update the serialization code below and deserialization in simple-viewer.ts -> onAppStatusUpdated()
+
+
+    AppStatus()
+    {
+    }
+
+    void ToJson(Json::Value &output) const
+    {
+      output["patientId"] = patientId;
+      output["studyDescription"] = studyDescription;
+      output["currentInstanceIdInMainViewport"] = currentInstanceIdInMainViewport;
+    }
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/SimpleViewer/MainWidgetInteractor.cpp	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,111 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 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 "MainWidgetInteractor.h"
+
+#include "SimpleViewerApplication.h"
+
+namespace SimpleViewer {
+
+  IWorldSceneMouseTracker* MainWidgetInteractor::CreateMouseTracker(WorldSceneWidget& widget,
+                                                                    const ViewportGeometry& view,
+                                                                    MouseButton button,
+                                                                    KeyboardModifiers modifiers,
+                                                                    int viewportX,
+                                                                    int viewportY,
+                                                                    double x,
+                                                                    double y,
+                                                                    IStatusBar* statusBar)
+  {
+    if (button == MouseButton_Left)
+    {
+      if (application_.GetCurrentTool() == SimpleViewerApplication::Tools_LineMeasure)
+      {
+        return new LineMeasureTracker(statusBar, dynamic_cast<LayerWidget&>(widget).GetSlice(),
+                                      x, y, 255, 0, 0, application_.GetFont());
+      }
+      else if (application_.GetCurrentTool() == SimpleViewerApplication::Tools_CircleMeasure)
+      {
+        return new CircleMeasureTracker(statusBar, dynamic_cast<LayerWidget&>(widget).GetSlice(),
+                                        x, y, 255, 0, 0, application_.GetFont());
+      }
+      else if (application_.GetCurrentTool() == SimpleViewerApplication::Tools_Crop)
+      {
+        // TODO
+      }
+      else if (application_.GetCurrentTool() == SimpleViewerApplication::Tools_Windowing)
+      {
+        // TODO
+      }
+      else if (application_.GetCurrentTool() == SimpleViewerApplication::Tools_Zoom)
+      {
+        // TODO
+      }
+      else if (application_.GetCurrentTool() == SimpleViewerApplication::Tools_Pan)
+      {
+        // TODO
+      }
+
+    }
+    return NULL;
+  }
+
+  void MainWidgetInteractor::MouseOver(CairoContext& context,
+                                       WorldSceneWidget& widget,
+                                       const ViewportGeometry& view,
+                                       double x,
+                                       double y,
+                                       IStatusBar* statusBar)
+  {
+    if (statusBar != NULL)
+    {
+      Vector p = dynamic_cast<LayerWidget&>(widget).GetSlice().MapSliceToWorldCoordinates(x, y);
+
+      char buf[64];
+      sprintf(buf, "X = %.02f Y = %.02f Z = %.02f (in cm)",
+              p[0] / 10.0, p[1] / 10.0, p[2] / 10.0);
+      statusBar->SetMessage(buf);
+    }
+  }
+
+  void MainWidgetInteractor::MouseWheel(WorldSceneWidget& widget,
+                                        MouseWheelDirection direction,
+                                        KeyboardModifiers modifiers,
+                                        IStatusBar* statusBar)
+  {
+  }
+
+  void MainWidgetInteractor::KeyPressed(WorldSceneWidget& widget,
+                                        KeyboardKeys key,
+                                        char keyChar,
+                                        KeyboardModifiers modifiers,
+                                        IStatusBar* statusBar)
+  {
+    switch (keyChar)
+    {
+    case 's':
+      widget.FitContent();
+      break;
+
+    default:
+      break;
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/SimpleViewer/MainWidgetInteractor.h	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,73 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 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
+
+#include "Framework/Widgets/IWorldSceneInteractor.h"
+
+using namespace OrthancStone;
+
+namespace SimpleViewer {
+
+  class SimpleViewerApplication;
+
+  class MainWidgetInteractor : public IWorldSceneInteractor
+  {
+  private:
+    SimpleViewerApplication&  application_;
+
+  public:
+    MainWidgetInteractor(SimpleViewerApplication&  application) :
+      application_(application)
+    {
+    }
+
+    virtual IWorldSceneMouseTracker* CreateMouseTracker(WorldSceneWidget& widget,
+                                                        const ViewportGeometry& view,
+                                                        MouseButton button,
+                                                        KeyboardModifiers modifiers,
+                                                        int viewportX,
+                                                        int viewportY,
+                                                        double x,
+                                                        double y,
+                                                        IStatusBar* statusBar);
+
+    virtual void MouseOver(CairoContext& context,
+                           WorldSceneWidget& widget,
+                           const ViewportGeometry& view,
+                           double x,
+                           double y,
+                           IStatusBar* statusBar);
+
+    virtual void MouseWheel(WorldSceneWidget& widget,
+                            MouseWheelDirection direction,
+                            KeyboardModifiers modifiers,
+                            IStatusBar* statusBar);
+
+    virtual void KeyPressed(WorldSceneWidget& widget,
+                            KeyboardKeys key,
+                            char keyChar,
+                            KeyboardModifiers modifiers,
+                            IStatusBar* statusBar);
+  };
+
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/SimpleViewer/Messages.h	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,13 @@
+#pragma once
+
+#include "Framework/Messages/MessageType.h"
+
+namespace SimpleViewer
+{
+  enum SimpleViewerMessageType
+  {
+    SimpleViewerMessageType_First = OrthancStone::MessageType_CustomMessage,
+    SimpleViewerMessageType_AppStatusUpdated
+
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/SimpleViewer/Qt/SimpleViewerMainWindow.cpp	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,106 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 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 "SimpleViewerMainWindow.h"
+
+/**
+ * Don't use "ui_MainWindow.h" instead of <ui_MainWindow.h> below, as
+ * this makes CMake unable to detect when the UI file changes.
+ **/
+#include <ui_SimpleViewerMainWindow.h>
+#include "../SimpleViewerApplication.h"
+
+namespace SimpleViewer
+{
+
+  SimpleViewerMainWindow::SimpleViewerMainWindow(OrthancStone::NativeStoneApplicationContext& context, SimpleViewerApplication& stoneApplication, QWidget *parent) :
+    QStoneMainWindow(context, parent),
+    ui_(new Ui::SimpleViewerMainWindow),
+    stoneApplication_(stoneApplication)
+  {
+    ui_->setupUi(this);
+    SetCentralStoneWidget(ui_->cairoCentralWidget);
+
+#if QT_VERSION >= 0x050000
+    connect(ui_->toolButtonCrop, &QToolButton::clicked, this, &SimpleViewerMainWindow::cropClicked);
+    connect(ui_->pushButtonUndoCrop, &QToolButton::clicked, this, &SimpleViewerMainWindow::undoCropClicked);
+    connect(ui_->toolButtonLine, &QToolButton::clicked, this, &SimpleViewerMainWindow::lineClicked);
+    connect(ui_->toolButtonCircle, &QToolButton::clicked, this, &SimpleViewerMainWindow::circleClicked);
+    connect(ui_->toolButtonWindowing, &QToolButton::clicked, this, &SimpleViewerMainWindow::windowingClicked);
+    connect(ui_->pushButtonRotate, &QPushButton::clicked, this, &SimpleViewerMainWindow::rotateClicked);
+    connect(ui_->pushButtonInvert, &QPushButton::clicked, this, &SimpleViewerMainWindow::invertClicked);
+#else
+    connect(ui_->toolButtonCrop, SIGNAL(clicked()), this, SLOT(cropClicked()));
+    connect(ui_->toolButtonLine, SIGNAL(clicked()), this, SLOT(lineClicked()));
+    connect(ui_->toolButtonCircle, SIGNAL(clicked()), this, SLOT(circleClicked()));
+    connect(ui_->toolButtonWindowing, SIGNAL(clicked()), this, SLOT(windowingClicked()));
+    connect(ui_->pushButtonUndoCrop, SIGNAL(clicked()), this, SLOT(undoCropClicked()));
+    connect(ui_->pushButtonRotate, SIGNAL(clicked()), this, SLOT(rotateClicked()));
+    connect(ui_->pushButtonInvert, SIGNAL(clicked()), this, SLOT(invertClicked()));
+#endif
+  }
+
+  SimpleViewerMainWindow::~SimpleViewerMainWindow()
+  {
+    delete ui_;
+  }
+
+  void SimpleViewerMainWindow::cropClicked()
+  {
+    GenericNoArgCommand command("selectTool:crop");
+    stoneApplication_.ExecuteCommand(command);
+  }
+
+  void SimpleViewerMainWindow::undoCropClicked()
+  {
+    GenericNoArgCommand command("action:undo-crop");
+    stoneApplication_.ExecuteCommand(command);
+  }
+
+  void SimpleViewerMainWindow::lineClicked()
+  {
+    GenericNoArgCommand command("selectTool:line-measure");
+    stoneApplication_.ExecuteCommand(command);
+  }
+
+  void SimpleViewerMainWindow::circleClicked()
+  {
+    GenericNoArgCommand command("selectTool:circle-measure");
+    stoneApplication_.ExecuteCommand(command);
+  }
+
+  void SimpleViewerMainWindow::windowingClicked()
+  {
+    GenericNoArgCommand command("selectTool:windowing");
+    stoneApplication_.ExecuteCommand(command);
+  }
+
+  void SimpleViewerMainWindow::rotateClicked()
+  {
+    GenericNoArgCommand command("action:rotate");
+    stoneApplication_.ExecuteCommand(command);
+  }
+
+  void SimpleViewerMainWindow::invertClicked()
+  {
+    GenericNoArgCommand command("action:invert");
+    stoneApplication_.ExecuteCommand(command);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/SimpleViewer/Qt/SimpleViewerMainWindow.h	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,57 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 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
+
+#include <Applications/Qt/QCairoWidget.h>
+#include <Applications/Qt/QStoneMainWindow.h>
+
+namespace Ui 
+{
+  class SimpleViewerMainWindow;
+}
+
+using namespace OrthancStone;
+
+namespace SimpleViewer
+{
+  class SimpleViewerApplication;
+
+  class SimpleViewerMainWindow : public QStoneMainWindow
+  {
+    Q_OBJECT
+
+  private:
+    Ui::SimpleViewerMainWindow*   ui_;
+    SimpleViewerApplication&      stoneApplication_;
+
+  public:
+    explicit SimpleViewerMainWindow(OrthancStone::NativeStoneApplicationContext& context, SimpleViewerApplication& stoneApplication, QWidget *parent = 0);
+    ~SimpleViewerMainWindow();
+
+  private slots:
+    void cropClicked();
+    void undoCropClicked();
+    void rotateClicked();
+    void windowingClicked();
+    void lineClicked();
+    void circleClicked();
+    void invertClicked();
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/SimpleViewer/Qt/SimpleViewerMainWindow.ui	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,151 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>SimpleViewerMainWindow</class>
+ <widget class="QMainWindow" name="SimpleViewerMainWindow">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>903</width>
+    <height>634</height>
+   </rect>
+  </property>
+  <property name="minimumSize">
+   <size>
+    <width>500</width>
+    <height>300</height>
+   </size>
+  </property>
+  <property name="baseSize">
+   <size>
+    <width>500</width>
+    <height>300</height>
+   </size>
+  </property>
+  <property name="windowTitle">
+   <string>Stone of Orthanc</string>
+  </property>
+  <property name="layoutDirection">
+   <enum>Qt::LeftToRight</enum>
+  </property>
+  <widget class="QWidget" name="centralwidget">
+   <property name="sizePolicy">
+    <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
+     <horstretch>0</horstretch>
+     <verstretch>0</verstretch>
+    </sizepolicy>
+   </property>
+   <property name="layoutDirection">
+    <enum>Qt::LeftToRight</enum>
+   </property>
+   <layout class="QVBoxLayout" name="verticalLayout_2" stretch="0,0">
+    <property name="sizeConstraint">
+     <enum>QLayout::SetDefaultConstraint</enum>
+    </property>
+    <item>
+     <widget class="QCairoWidget" name="cairoCentralWidget">
+      <property name="minimumSize">
+       <size>
+        <width>0</width>
+        <height>500</height>
+       </size>
+      </property>
+     </widget>
+    </item>
+    <item>
+     <widget class="QGroupBox" name="horizontalGroupBox">
+      <property name="minimumSize">
+       <size>
+        <width>0</width>
+        <height>100</height>
+       </size>
+      </property>
+      <property name="maximumSize">
+       <size>
+        <width>16777215</width>
+        <height>100</height>
+       </size>
+      </property>
+      <layout class="QHBoxLayout" name="horizontalLayout">
+       <item>
+        <widget class="QToolButton" name="toolButtonWindowing">
+         <property name="text">
+          <string>windowing</string>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="QToolButton" name="toolButtonCrop">
+         <property name="text">
+          <string>crop</string>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="QPushButton" name="pushButtonUndoCrop">
+         <property name="text">
+          <string>undo crop</string>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="QToolButton" name="toolButtonLine">
+         <property name="text">
+          <string>line</string>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="QToolButton" name="toolButtonCircle">
+         <property name="text">
+          <string>circle</string>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="QPushButton" name="pushButtonRotate">
+         <property name="text">
+          <string>rotate</string>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="QPushButton" name="pushButtonInvert">
+         <property name="text">
+          <string>invert</string>
+         </property>
+        </widget>
+       </item>
+      </layout>
+     </widget>
+    </item>
+   </layout>
+  </widget>
+  <widget class="QMenuBar" name="menubar">
+   <property name="geometry">
+    <rect>
+     <x>0</x>
+     <y>0</y>
+     <width>903</width>
+     <height>22</height>
+    </rect>
+   </property>
+   <widget class="QMenu" name="menuTest">
+    <property name="title">
+     <string>Test</string>
+    </property>
+   </widget>
+   <addaction name="menuTest"/>
+  </widget>
+  <widget class="QStatusBar" name="statusbar"/>
+ </widget>
+ <customwidgets>
+  <customwidget>
+   <class>QCairoWidget</class>
+   <extends>QGraphicsView</extends>
+   <header location="global">QCairoWidget.h</header>
+  </customwidget>
+ </customwidgets>
+ <resources/>
+ <connections/>
+</ui>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/SimpleViewer/Qt/mainQt.cpp	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,14 @@
+#include "Applications/Qt/QtStoneApplicationRunner.h"
+
+#include "../SimpleViewerApplication.h"
+#include "Framework/Messages/MessageBroker.h"
+
+
+int main(int argc, char* argv[]) 
+{
+  OrthancStone::MessageBroker broker;
+  SimpleViewer::SimpleViewerApplication stoneApplication(broker);
+
+  OrthancStone::QtStoneApplicationRunner qtAppRunner(broker, stoneApplication);
+  return qtAppRunner.Execute(argc, argv);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/SimpleViewer/SimpleViewerApplication.cpp	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,242 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 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 "SimpleViewerApplication.h"
+
+#if ORTHANC_ENABLE_QT==1
+#include "Qt/SimpleViewerMainWindow.h"
+#endif
+
+#if ORTHANC_ENABLE_WASM==1
+#include <Platforms/Wasm/WasmViewport.h>
+#endif
+
+namespace SimpleViewer {
+
+  void SimpleViewerApplication::Initialize(StoneApplicationContext* context,
+                                           IStatusBar& statusBar,
+                                           const boost::program_options::variables_map& parameters)
+  {
+    using namespace OrthancStone;
+
+    context_ = context;
+    statusBar_ = &statusBar;
+
+    {// initialize viewports and layout
+      mainLayout_ = new LayoutWidget("main-layout");
+      mainLayout_->SetPadding(10);
+      mainLayout_->SetBackgroundCleared(true);
+      mainLayout_->SetBackgroundColor(0, 0, 0);
+      mainLayout_->SetHorizontal();
+
+      thumbnailsLayout_ = new LayoutWidget("thumbnail-layout");
+      thumbnailsLayout_->SetPadding(10);
+      thumbnailsLayout_->SetBackgroundCleared(true);
+      thumbnailsLayout_->SetBackgroundColor(50, 50, 50);
+      thumbnailsLayout_->SetVertical();
+
+      mainWidget_ = new LayerWidget(IObserver::broker_, "main-viewport");
+      //mainWidget_->RegisterObserver(*this);
+
+      // hierarchy
+      mainLayout_->AddWidget(thumbnailsLayout_);
+      mainLayout_->AddWidget(mainWidget_);
+
+      orthancApiClient_.reset(new OrthancApiClient(IObserver::broker_, context_->GetWebService()));
+
+      // sources
+      smartLoader_.reset(new SmartLoader(IObserver::broker_, *orthancApiClient_));
+      smartLoader_->SetImageQuality(SliceImageQuality_FullPam);
+
+      mainLayout_->SetTransmitMouseOver(true);
+      mainWidgetInteractor_.reset(new MainWidgetInteractor(*this));
+      mainWidget_->SetInteractor(*mainWidgetInteractor_);
+      thumbnailInteractor_.reset(new ThumbnailInteractor(*this));
+    }
+
+    statusBar.SetMessage("Use the key \"s\" to reinitialize the layout");
+    statusBar.SetMessage("Use the key \"n\" to go to next image in the main viewport");
+
+
+    if (parameters.count("studyId") < 1)
+    {
+      LOG(WARNING) << "The study ID is missing, will take the first studyId found in Orthanc";
+      orthancApiClient_->GetJsonAsync("/studies", new Callable<SimpleViewerApplication, OrthancApiClient::JsonResponseReadyMessage>(*this, &SimpleViewerApplication::OnStudyListReceived));
+    }
+    else
+    {
+      SelectStudy(parameters["studyId"].as<std::string>());
+    }
+  }
+
+
+  void SimpleViewerApplication::DeclareStartupOptions(boost::program_options::options_description& options)
+  {
+    boost::program_options::options_description generic("Sample options");
+    generic.add_options()
+        ("studyId", boost::program_options::value<std::string>(),
+         "Orthanc ID of the study")
+        ;
+
+    options.add(generic);
+  }
+
+  void SimpleViewerApplication::OnStudyListReceived(const OrthancApiClient::JsonResponseReadyMessage& message)
+  {
+    const Json::Value& response = message.Response;
+
+    if (response.isArray() && response.size() > 1)
+    {
+      SelectStudy(response[0].asString());
+    }
+  }
+  void SimpleViewerApplication::OnStudyReceived(const OrthancApiClient::JsonResponseReadyMessage& message)
+  {
+    const Json::Value& response = message.Response;
+
+    if (response.isObject() && response["Series"].isArray())
+    {
+      for (size_t i=0; i < response["Series"].size(); i++)
+      {
+        orthancApiClient_->GetJsonAsync("/series/" + response["Series"][(int)i].asString(), new Callable<SimpleViewerApplication, OrthancApiClient::JsonResponseReadyMessage>(*this, &SimpleViewerApplication::OnSeriesReceived));
+      }
+    }
+  }
+
+  void SimpleViewerApplication::OnSeriesReceived(const OrthancApiClient::JsonResponseReadyMessage& message)
+  {
+    const Json::Value& response = message.Response;
+
+    if (response.isObject() && response["Instances"].isArray() && response["Instances"].size() > 0)
+    {
+      // keep track of all instances IDs
+      const std::string& seriesId = response["ID"].asString();
+      seriesTags_[seriesId] = response;
+      instancesIdsPerSeriesId_[seriesId] = std::vector<std::string>();
+      for (size_t i = 0; i < response["Instances"].size(); i++)
+      {
+        const std::string& instanceId = response["Instances"][static_cast<int>(i)].asString();
+        instancesIdsPerSeriesId_[seriesId].push_back(instanceId);
+      }
+
+      // load the first instance in the thumbnail
+      LoadThumbnailForSeries(seriesId, instancesIdsPerSeriesId_[seriesId][0]);
+
+      // if this is the first thumbnail loaded, load the first instance in the mainWidget
+      if (mainWidget_->GetLayerCount() == 0)
+      {
+        smartLoader_->SetFrameInWidget(*mainWidget_, 0, instancesIdsPerSeriesId_[seriesId][0], 0);
+      }
+    }
+  }
+
+  void SimpleViewerApplication::LoadThumbnailForSeries(const std::string& seriesId, const std::string& instanceId)
+  {
+    LOG(INFO) << "Loading thumbnail for series " << seriesId;
+    LayerWidget* thumbnailWidget = new LayerWidget(IObserver::broker_, "thumbnail-series-" + seriesId);
+    thumbnails_.push_back(thumbnailWidget);
+    thumbnailsLayout_->AddWidget(thumbnailWidget);
+    thumbnailWidget->RegisterObserverCallback(new Callable<SimpleViewerApplication, LayerWidget::GeometryChangedMessage>(*this, &SimpleViewerApplication::OnWidgetGeometryChanged));
+    smartLoader_->SetFrameInWidget(*thumbnailWidget, 0, instanceId, 0);
+    thumbnailWidget->SetInteractor(*thumbnailInteractor_);
+  }
+
+  void SimpleViewerApplication::SelectStudy(const std::string& studyId)
+  {
+    orthancApiClient_->GetJsonAsync("/studies/" + studyId, new Callable<SimpleViewerApplication, OrthancApiClient::JsonResponseReadyMessage>(*this, &SimpleViewerApplication::OnStudyReceived));
+  }
+
+  void SimpleViewerApplication::OnWidgetGeometryChanged(const LayerWidget::GeometryChangedMessage& message)
+  {
+    message.origin_.FitContent();
+  }
+
+  void SimpleViewerApplication::SelectSeriesInMainViewport(const std::string& seriesId)
+  {
+    smartLoader_->SetFrameInWidget(*mainWidget_, 0, instancesIdsPerSeriesId_[seriesId][0], 0);
+  }
+
+
+
+  void SimpleViewerApplication::ExecuteCommand(ICommand& command)
+  {
+    statusBar_->SetMessage("received command: " + std::string(command.GetName()));
+    if (command.GetName() == "selectTool:circle-measure")
+    {
+      SelectTool(Tools_CircleMeasure);
+    }
+    else if (command.GetName() == "selectTool:line-measure")
+    {
+      SelectTool(Tools_LineMeasure);
+    }
+    else if (command.GetName() == "selectTool:crop")
+    {
+      SelectTool(Tools_Crop);
+    }
+    else if (command.GetName() == "selectTool:windowing")
+    {
+      SelectTool(Tools_Windowing);
+    }
+    else if (command.GetName() == "action:rotate")
+    {
+      ExecuteAction(Actions_Rotate);
+    }
+    else if (command.GetName() == "action:undo-crop")
+    {
+      ExecuteAction(Actions_UndoCrop);
+    }
+    else if (command.GetName() == "action:invert")
+    {
+      ExecuteAction(Actions_Invert);
+    }
+    else
+    {
+      command.Execute();
+    }
+  }
+
+  void SimpleViewerApplication::ExecuteAction(SimpleViewerApplication::Actions action)
+  {
+    // TODO
+  }
+
+  void SimpleViewerApplication::SelectTool(SimpleViewerApplication::Tools tool)
+  {
+    currentTool_ = tool;
+  }
+
+#if ORTHANC_ENABLE_QT==1
+  QStoneMainWindow* SimpleViewerApplication::CreateQtMainWindow()
+  {
+    return new SimpleViewerMainWindow(dynamic_cast<OrthancStone::NativeStoneApplicationContext&>(*context_), *this);
+  }
+#endif
+
+#if ORTHANC_ENABLE_WASM==1
+  void SimpleViewerApplication::InitializeWasm() {
+
+    AttachWidgetToWasmViewport("canvasThumbnails", thumbnailsLayout_);
+    AttachWidgetToWasmViewport("canvasMain", mainWidget_);
+  }
+#endif
+
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/SimpleViewer/SimpleViewerApplication.h	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,176 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 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
+
+#include "Applications/IStoneApplication.h"
+
+#include "Framework/Layers/OrthancFrameLayerSource.h"
+#include "Framework/Layers/CircleMeasureTracker.h"
+#include "Framework/Layers/LineMeasureTracker.h"
+#include "Framework/Widgets/LayerWidget.h"
+#include "Framework/Widgets/LayoutWidget.h"
+#include "Framework/Messages/IObserver.h"
+#include "Framework/SmartLoader.h"
+
+#if ORTHANC_ENABLE_WASM==1
+#include "Platforms/Wasm/WasmPlatformApplicationAdapter.h"
+#include "Platforms/Wasm/Defaults.h"
+#endif
+
+#if ORTHANC_ENABLE_QT==1
+#include "Qt/SimpleViewerMainWindow.h"
+#endif
+
+#include <Core/Images/Font.h>
+#include <Core/Logging.h>
+
+#include "ThumbnailInteractor.h"
+#include "MainWidgetInteractor.h"
+#include "AppStatus.h"
+#include "Messages.h"
+
+using namespace OrthancStone;
+
+
+namespace SimpleViewer
+{
+
+  class SimpleViewerApplication :
+      public IStoneApplication,
+      public IObserver,
+      public IObservable
+  {
+  public:
+
+    struct StatusUpdatedMessage : public BaseMessage<SimpleViewerMessageType_AppStatusUpdated>
+    {
+      const AppStatus& status_;
+
+      StatusUpdatedMessage(const AppStatus& status)
+        : BaseMessage(),
+          status_(status)
+      {
+      }
+    };
+
+    enum Tools {
+      Tools_LineMeasure,
+      Tools_CircleMeasure,
+      Tools_Crop,
+      Tools_Windowing,
+      Tools_Zoom,
+      Tools_Pan
+    };
+
+    enum Actions {
+      Actions_Rotate,
+      Actions_Invert,
+      Actions_UndoCrop
+    };
+
+  private:
+    Tools                           currentTool_;
+    std::unique_ptr<MainWidgetInteractor> mainWidgetInteractor_;
+    std::unique_ptr<ThumbnailInteractor>  thumbnailInteractor_;
+    LayoutWidget*                   mainLayout_;
+    LayoutWidget*                   thumbnailsLayout_;
+    LayerWidget*                    mainWidget_;
+    std::vector<LayerWidget*>       thumbnails_;
+    std::map<std::string, std::vector<std::string> > instancesIdsPerSeriesId_;
+    std::map<std::string, Json::Value> seriesTags_;
+    BaseCommandBuilder              commandBuilder_;
+
+    unsigned int                    currentInstanceIndex_;
+    OrthancStone::WidgetViewport*   wasmViewport1_;
+    OrthancStone::WidgetViewport*   wasmViewport2_;
+
+    IStatusBar*                     statusBar_;
+    std::unique_ptr<SmartLoader>    smartLoader_;
+    std::unique_ptr<OrthancApiClient>      orthancApiClient_;
+
+    Orthanc::Font                   font_;
+
+  public:
+    SimpleViewerApplication(MessageBroker& broker) :
+      IObserver(broker),
+      IObservable(broker),
+      currentTool_(Tools_LineMeasure),
+      mainLayout_(NULL),
+      currentInstanceIndex_(0),
+      wasmViewport1_(NULL),
+      wasmViewport2_(NULL)
+    {
+      font_.LoadFromResource(Orthanc::EmbeddedResources::FONT_UBUNTU_MONO_BOLD_16);
+    }
+
+    virtual void Finalize() {}
+    virtual IWidget* GetCentralWidget() {return mainLayout_;}
+
+    virtual void DeclareStartupOptions(boost::program_options::options_description& options);
+    virtual void Initialize(StoneApplicationContext* context,
+                            IStatusBar& statusBar,
+                            const boost::program_options::variables_map& parameters);
+
+    void OnStudyListReceived(const OrthancApiClient::JsonResponseReadyMessage& message);
+
+    void OnStudyReceived(const OrthancApiClient::JsonResponseReadyMessage& message);
+
+    void OnSeriesReceived(const OrthancApiClient::JsonResponseReadyMessage& message);
+
+    void LoadThumbnailForSeries(const std::string& seriesId, const std::string& instanceId);
+
+    void SelectStudy(const std::string& studyId);
+
+    void OnWidgetGeometryChanged(const LayerWidget::GeometryChangedMessage& message);
+
+    void SelectSeriesInMainViewport(const std::string& seriesId);
+
+    void SelectTool(Tools tool);
+    
+    Tools GetCurrentTool() const
+    {
+      return currentTool_;
+    }
+
+    const Orthanc::Font& GetFont() const
+    {
+      return font_;
+    }
+
+    void ExecuteAction(Actions action);
+
+    virtual std::string GetTitle() const {return "SimpleViewer";}
+    virtual void ExecuteCommand(ICommand& command);
+    virtual BaseCommandBuilder& GetCommandBuilder() {return commandBuilder_;}
+
+
+#if ORTHANC_ENABLE_WASM==1
+    virtual void InitializeWasm();
+#endif
+
+#if ORTHANC_ENABLE_QT==1
+    virtual QStoneMainWindow* CreateQtMainWindow();
+#endif
+  };
+
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/SimpleViewer/ThumbnailInteractor.cpp	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,45 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 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 "ThumbnailInteractor.h"
+
+#include "SimpleViewerApplication.h"
+
+namespace SimpleViewer {
+
+  IWorldSceneMouseTracker* ThumbnailInteractor::CreateMouseTracker(WorldSceneWidget& widget,
+                                                                   const ViewportGeometry& view,
+                                                                   MouseButton button,
+                                                                   KeyboardModifiers modifiers,
+                                                                   int viewportX,
+                                                                   int viewportY,
+                                                                   double x,
+                                                                   double y,
+                                                                   IStatusBar* statusBar)
+  {
+    if (button == MouseButton_Left)
+    {
+      statusBar->SetMessage("selected thumbnail " + widget.GetName());
+      std::string seriesId = widget.GetName().substr(strlen("thumbnail-series-"));
+      application_.SelectSeriesInMainViewport(seriesId);
+    }
+    return NULL;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/SimpleViewer/ThumbnailInteractor.h	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,76 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 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
+
+#include "Framework/Widgets/IWorldSceneInteractor.h"
+
+using namespace OrthancStone;
+
+namespace SimpleViewer {
+
+  class SimpleViewerApplication;
+
+  class ThumbnailInteractor : public IWorldSceneInteractor
+  {
+  private:
+    SimpleViewerApplication&  application_;
+  public:
+    ThumbnailInteractor(SimpleViewerApplication&  application) :
+      application_(application)
+    {
+    }
+
+    virtual IWorldSceneMouseTracker* CreateMouseTracker(WorldSceneWidget& widget,
+                                                        const ViewportGeometry& view,
+                                                        MouseButton button,
+                                                        KeyboardModifiers modifiers,
+                                                        int viewportX,
+                                                        int viewportY,
+                                                        double x,
+                                                        double y,
+                                                        IStatusBar* statusBar);
+
+    virtual void MouseOver(CairoContext& context,
+                           WorldSceneWidget& widget,
+                           const ViewportGeometry& view,
+                           double x,
+                           double y,
+                           IStatusBar* statusBar)
+    {}
+
+    virtual void MouseWheel(WorldSceneWidget& widget,
+                            MouseWheelDirection direction,
+                            KeyboardModifiers modifiers,
+                            IStatusBar* statusBar)
+    {}
+
+    virtual void KeyPressed(WorldSceneWidget& widget,
+                            KeyboardKeys key,
+                            char keyChar,
+                            KeyboardModifiers modifiers,
+                            IStatusBar* statusBar)
+    {}
+
+  };
+
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/SimpleViewer/Wasm/SimpleViewerWasmApplicationAdapter.cpp	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,51 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 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 "SimpleViewerWasmApplicationAdapter.h"
+
+namespace SimpleViewer
+{
+
+  SimpleViewerWasmApplicationAdapter::SimpleViewerWasmApplicationAdapter(MessageBroker &broker, SimpleViewerApplication &application)
+      : WasmPlatformApplicationAdapter(broker, application),
+        viewerApplication_(application)
+  {
+    application.RegisterObserverCallback(new Callable<SimpleViewerWasmApplicationAdapter, SimpleViewerApplication::StatusUpdatedMessage>(*this, &SimpleViewerWasmApplicationAdapter::OnStatusUpdated));
+  }
+
+  void SimpleViewerWasmApplicationAdapter::OnStatusUpdated(const SimpleViewerApplication::StatusUpdatedMessage &message)
+  {
+    Json::Value statusJson;
+    message.status_.ToJson(statusJson);
+
+    Json::Value event;
+    event["event"] = "appStatusUpdated";
+    event["data"] = statusJson;
+
+    Json::StreamWriterBuilder builder;
+    std::unique_ptr<Json::StreamWriter> writer(builder.newStreamWriter());
+    std::ostringstream outputStr;
+
+    writer->write(event, &outputStr);
+
+    NotifyStatusUpdateFromCppToWeb(outputStr.str());
+  }
+
+} // namespace SimpleViewer
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/SimpleViewer/Wasm/SimpleViewerWasmApplicationAdapter.h	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,43 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 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
+
+#include <string>
+#include <Framework/Messages/IObserver.h>
+#include <Platforms/Wasm/WasmPlatformApplicationAdapter.h>
+
+#include "../SimpleViewerApplication.h"
+
+namespace SimpleViewer {
+
+  class SimpleViewerWasmApplicationAdapter : public WasmPlatformApplicationAdapter
+    {
+      SimpleViewerApplication&  viewerApplication_;
+
+    public:
+      SimpleViewerWasmApplicationAdapter(MessageBroker& broker, SimpleViewerApplication& application);
+
+    private:
+      void OnStatusUpdated(const SimpleViewerApplication::StatusUpdatedMessage& message);
+
+    };
+
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/SimpleViewer/Wasm/mainWasm.cpp	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,38 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 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 "Platforms/Wasm/WasmWebService.h"
+#include "Platforms/Wasm/WasmViewport.h"
+
+#include <emscripten/emscripten.h>
+
+#include "../SimpleViewerApplication.h"
+#include "SimpleViewerWasmApplicationAdapter.h"
+
+
+OrthancStone::IStoneApplication* CreateUserApplication(OrthancStone::MessageBroker& broker) {
+  
+  return new SimpleViewer::SimpleViewerApplication(broker);
+}
+
+OrthancStone::WasmPlatformApplicationAdapter* CreateWasmApplicationAdapter(OrthancStone::MessageBroker& broker, IStoneApplication* application)
+{
+  return new SimpleViewer::SimpleViewerWasmApplicationAdapter(broker, *(dynamic_cast<SimpleViewer::SimpleViewerApplication*>(application)));
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/SimpleViewer/Wasm/simple-viewer.html	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,39 @@
+<!doctype html>
+
+<html lang="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>Simple Viewer</title>
+    <link href="styles.css" rel="stylesheet" />
+
+<body>
+  <div id="breadcrumb">
+    <span id="label-patient-id"></span>
+    <span id="label-study-description"></span>
+    <span id="label-series-description"></span>
+  </div>
+  <div>
+    <canvas id="canvasThumbnails" data-width-ratio="20" data-height-ratio="50"></canvas>
+    <canvas id="canvasMain" data-width-ratio="70" data-height-ratio="50"></canvas>
+  </div>
+  <div id="toolbox">
+    <button tool-selector="line-measure" class="tool-selector">line</button>
+    <button tool-selector="circle-measure" class="tool-selector">circle</button>
+    <button tool-selector="crop" class="tool-selector">crop</button>
+    <button tool-selector="windowing" class="tool-selector">windowing</button>
+    <button tool-selector="zoom" class="tool-selector">zoom</button>
+    <button tool-selector="pan" class="tool-selector">pan</button>
+    <button action-trigger="rotate-left" class="action-trigger">rotate left</button>
+    <button action-trigger="rotate-right" class="action-trigger">rotate right</button>
+    <button action-trigger="invert" class="action-trigger">invert</button>
+  </div>
+  <script type="text/javascript" src="app-simple-viewer.js"></script>
+</body>
+
+</html>
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/SimpleViewer/Wasm/simple-viewer.ts	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,75 @@
+///<reference path='../../../../Platforms/Wasm/wasm-application-runner.ts'/>
+
+InitializeWasmApplication("OrthancStoneSimpleViewer", "/orthanc");
+
+function SelectTool(toolName: string) {
+  var command = {
+    command: "selectTool:" + toolName,
+    commandType: "generic-no-arg-command",
+    args: {
+    }                                                                                                                       
+  };
+  SendMessageToStoneApplication(JSON.stringify(command));
+
+}
+
+function PerformAction(actionName: string) {
+  var command = {
+    command: "action:" + actionName,
+    commandType: "generic-no-arg-command",
+    args: {
+    }
+  };
+  SendMessageToStoneApplication(JSON.stringify(command));
+}
+
+class SimpleViewerUI {
+
+  private _labelPatientId: HTMLSpanElement;
+  private _labelStudyDescription: HTMLSpanElement;
+
+  public constructor() {
+    // install "SelectTool" handlers
+    document.querySelectorAll("[tool-selector]").forEach((e) => {
+      console.log(e);
+      (e as HTMLButtonElement).addEventListener("click", () => {
+        console.log(e);
+        SelectTool(e.attributes["tool-selector"].value);
+      });
+    });
+
+    // install "PerformAction" handlers
+    document.querySelectorAll("[action-trigger]").forEach((e) => {
+      (e as HTMLButtonElement).addEventListener("click", () => {
+        PerformAction(e.attributes["action-trigger"].value);
+      });
+    });
+
+    // connect all ui elements to members
+    this._labelPatientId = document.getElementById("label-patient-id") as HTMLSpanElement;
+    this._labelStudyDescription = document.getElementById("label-study-description") as HTMLSpanElement;
+  }
+
+  public onAppStatusUpdated(status: any) {
+    this._labelPatientId.innerText = status["patientId"];
+    this._labelStudyDescription.innerText = status["studyDescription"];
+    // this.highlighThumbnail(status["currentInstanceIdInMainViewport"]);
+  }
+
+}
+
+var ui = new SimpleViewerUI();
+
+// this method is called "from the C++ code" when the StoneApplication is updated.
+// it can be used to update the UI of the application
+function UpdateWebApplication(statusUpdateMessageString: string) {
+  console.log("updating web application: ", statusUpdateMessageString);
+  let statusUpdateMessage = JSON.parse(statusUpdateMessageString);
+
+  if ("event" in statusUpdateMessage) {
+    let eventName = statusUpdateMessage["event"];
+    if (eventName == "appStatusUpdated") {
+      ui.onAppStatusUpdated(statusUpdateMessage["data"]);
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/SimpleViewer/Wasm/styles.css	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,54 @@
+html, body {
+    width: 100%;
+    height: 100%;
+    margin: 0px;
+    border: 0;
+    overflow: hidden; /*  Disable scrollbars */
+    display: block;  /* No floating content on sides */
+    background-color: black;
+    color: white;
+    font-family: Arial, Helvetica, sans-serif;
+}
+
+canvas {
+    left:0px;
+    top:0px;
+}
+
+#canvas-group {
+    padding:5px;
+    background-color: grey;
+}
+
+#status-group {
+    padding:5px;
+}
+
+#worklist-group {
+    padding:5px;
+}
+
+.vsol-button {
+    height: 40px;
+}
+
+#thumbnails-group ul li {
+    display: inline;
+    list-style: none;
+}
+
+.thumbnail {
+    width: 100px;
+    height: 100px;
+    padding: 3px;
+}
+
+.thumbnail-selected {
+    border-width: 1px;
+    border-color: red;
+    border-style: solid;
+}
+
+#template-thumbnail-li {
+    display: none !important;
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/SimpleViewer/Wasm/tsconfig-simple-viewer.json	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,9 @@
+{
+    "extends" : "../../Web/tsconfig-samples",
+    "compilerOptions": {
+        "outFile": "../../build-web/simple-viewer/app-simple-viewer.js"
+    },
+    "include" : [
+        "simple-viewer.ts"
+    ]
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/SimpleViewerApplicationSingleFile.h	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,440 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 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
+
+#include "SampleApplicationBase.h"
+
+#include "../../Framework/Layers/OrthancFrameLayerSource.h"
+#include "../../Framework/Layers/CircleMeasureTracker.h"
+#include "../../Framework/Layers/LineMeasureTracker.h"
+#include "../../Framework/Widgets/LayerWidget.h"
+#include "../../Framework/Widgets/LayoutWidget.h"
+#include "../../Framework/Messages/IObserver.h"
+#include "../../Framework/SmartLoader.h"
+
+#if ORTHANC_ENABLE_WASM==1
+#include "../../Platforms/Wasm/WasmPlatformApplicationAdapter.h"
+#include "../../Platforms/Wasm/Defaults.h"
+#endif
+
+#include <Core/Images/Font.h>
+#include <Core/Logging.h>
+
+namespace OrthancStone
+{
+  namespace Samples
+  {
+    class SimpleViewerApplication :
+        public SampleSingleCanvasWithButtonsApplicationBase,
+        public IObserver
+    {
+    private:
+      class ThumbnailInteractor : public IWorldSceneInteractor
+      {
+      private:
+        SimpleViewerApplication&  application_;
+      public:
+        ThumbnailInteractor(SimpleViewerApplication&  application) :
+          application_(application)
+        {
+        }
+
+        virtual IWorldSceneMouseTracker* CreateMouseTracker(WorldSceneWidget& widget,
+                                                            const ViewportGeometry& view,
+                                                            MouseButton button,
+                                                            KeyboardModifiers modifiers,
+                                                            int viewportX,
+                                                            int viewportY,
+                                                            double x,
+                                                            double y,
+                                                            IStatusBar* statusBar)
+        {
+          if (button == MouseButton_Left)
+          {
+            statusBar->SetMessage("selected thumbnail " + widget.GetName());
+            std::string seriesId = widget.GetName().substr(strlen("thumbnail-series-"));
+            application_.SelectSeriesInMainViewport(seriesId);
+          }
+          return NULL;
+        }
+        virtual void MouseOver(CairoContext& context,
+                               WorldSceneWidget& widget,
+                               const ViewportGeometry& view,
+                               double x,
+                               double y,
+                               IStatusBar* statusBar)
+        {}
+
+        virtual void MouseWheel(WorldSceneWidget& widget,
+                                MouseWheelDirection direction,
+                                KeyboardModifiers modifiers,
+                                IStatusBar* statusBar)
+        {}
+
+        virtual void KeyPressed(WorldSceneWidget& widget,
+                                KeyboardKeys key,
+                                char keyChar,
+                                KeyboardModifiers modifiers,
+                                IStatusBar* statusBar)
+        {}
+
+      };
+
+      class MainWidgetInteractor : public IWorldSceneInteractor
+      {
+      private:
+        SimpleViewerApplication&  application_;
+        
+      public:
+        MainWidgetInteractor(SimpleViewerApplication&  application) :
+          application_(application)
+        {
+        }
+        
+        virtual IWorldSceneMouseTracker* CreateMouseTracker(WorldSceneWidget& widget,
+                                                            const ViewportGeometry& view,
+                                                            MouseButton button,
+                                                            KeyboardModifiers modifiers,
+                                                            int viewportX,
+                                                            int viewportY,
+                                                            double x,
+                                                            double y,
+                                                            IStatusBar* statusBar)
+        {
+          if (button == MouseButton_Left)
+          {
+            if (application_.currentTool_ == Tools_LineMeasure)
+            {
+              return new LineMeasureTracker(statusBar, dynamic_cast<LayerWidget&>(widget).GetSlice(),
+                                            x, y, 255, 0, 0, application_.GetFont());
+            }
+            else if (application_.currentTool_ == Tools_CircleMeasure)
+            {
+              return new CircleMeasureTracker(statusBar, dynamic_cast<LayerWidget&>(widget).GetSlice(),
+                                              x, y, 255, 0, 0, application_.GetFont());
+            }
+          }
+          return NULL;
+        }
+
+        virtual void MouseOver(CairoContext& context,
+                               WorldSceneWidget& widget,
+                               const ViewportGeometry& view,
+                               double x,
+                               double y,
+                               IStatusBar* statusBar)
+        {
+          if (statusBar != NULL)
+          {
+            Vector p = dynamic_cast<LayerWidget&>(widget).GetSlice().MapSliceToWorldCoordinates(x, y);
+            
+            char buf[64];
+            sprintf(buf, "X = %.02f Y = %.02f Z = %.02f (in cm)",
+                    p[0] / 10.0, p[1] / 10.0, p[2] / 10.0);
+            statusBar->SetMessage(buf);
+          }
+        }
+
+        virtual void MouseWheel(WorldSceneWidget& widget,
+                                MouseWheelDirection direction,
+                                KeyboardModifiers modifiers,
+                                IStatusBar* statusBar)
+        {
+        }
+
+        virtual void KeyPressed(WorldSceneWidget& widget,
+                                KeyboardKeys key,
+                                char keyChar,
+                                KeyboardModifiers modifiers,
+                                IStatusBar* statusBar)
+        {
+          switch (keyChar)
+          {
+          case 's':
+            widget.FitContent();
+            break;
+
+          case 'l':
+            application_.currentTool_ = Tools_LineMeasure;
+            break;
+
+          case 'c':
+            application_.currentTool_ = Tools_CircleMeasure;
+            break;
+
+          default:
+            break;
+          }
+        }
+      };
+
+
+#if ORTHANC_ENABLE_WASM==1
+      class SimpleViewerApplicationAdapter : public WasmPlatformApplicationAdapter
+      {
+        SimpleViewerApplication&  viewerApplication_;
+
+      public:
+        SimpleViewerApplicationAdapter(MessageBroker& broker, SimpleViewerApplication& application)
+          : WasmPlatformApplicationAdapter(broker, application),
+          viewerApplication_(application)
+        {
+
+        }
+
+        virtual void HandleMessageFromWeb(std::string& output, const std::string& input) {
+          if (input == "select-tool:line-measure")
+          {
+            viewerApplication_.currentTool_ = Tools_LineMeasure;
+            NotifyStatusUpdateFromCppToWeb("currentTool=line-measure");
+          }
+          else if (input == "select-tool:circle-measure")
+          {
+            viewerApplication_.currentTool_ = Tools_CircleMeasure;
+            NotifyStatusUpdateFromCppToWeb("currentTool=circle-measure");
+          }
+
+          output = "ok";
+        }
+
+        virtual void NotifyStatusUpdateFromCppToWeb(const std::string& statusUpdateMessage) {
+          UpdateStoneApplicationStatusFromCpp(statusUpdateMessage.c_str());
+        }
+
+      };
+#endif
+      enum Tools {
+        Tools_LineMeasure,
+        Tools_CircleMeasure
+      };
+
+      Tools                                currentTool_;
+      std::auto_ptr<MainWidgetInteractor>  mainWidgetInteractor_;
+      std::auto_ptr<ThumbnailInteractor>   thumbnailInteractor_;
+      LayoutWidget*                        mainLayout_;
+      LayoutWidget*                        thumbnailsLayout_;
+      std::vector<LayerWidget*>            thumbnails_;
+
+      std::map<std::string, std::vector<std::string> > instancesIdsPerSeriesId_;
+      std::map<std::string, Json::Value> seriesTags_;
+
+      unsigned int                         currentInstanceIndex_;
+      OrthancStone::WidgetViewport*        wasmViewport1_;
+      OrthancStone::WidgetViewport*        wasmViewport2_;
+
+      IStatusBar*                          statusBar_;
+      std::auto_ptr<SmartLoader>           smartLoader_;
+      std::auto_ptr<OrthancApiClient>      orthancApiClient_;
+
+      Orthanc::Font                        font_;
+
+    public:
+      SimpleViewerApplication(MessageBroker& broker) :
+        IObserver(broker),
+        currentTool_(Tools_LineMeasure),
+        mainLayout_(NULL),
+        currentInstanceIndex_(0),
+        wasmViewport1_(NULL),
+        wasmViewport2_(NULL)
+      {
+        font_.LoadFromResource(Orthanc::EmbeddedResources::FONT_UBUNTU_MONO_BOLD_16);
+//        DeclareIgnoredMessage(MessageType_Widget_ContentChanged);
+      }
+
+      virtual void DeclareStartupOptions(boost::program_options::options_description& options)
+      {
+        boost::program_options::options_description generic("Sample options");
+        generic.add_options()
+            ("studyId", boost::program_options::value<std::string>(),
+             "Orthanc ID of the study")
+            ;
+
+        options.add(generic);
+      }
+
+      virtual void Initialize(StoneApplicationContext* context,
+                              IStatusBar& statusBar,
+                              const boost::program_options::variables_map& parameters)
+      {
+        using namespace OrthancStone;
+
+        context_ = context;
+        statusBar_ = &statusBar;
+
+        {// initialize viewports and layout
+          mainLayout_ = new LayoutWidget("main-layout");
+          mainLayout_->SetPadding(10);
+          mainLayout_->SetBackgroundCleared(true);
+          mainLayout_->SetBackgroundColor(0, 0, 0);
+          mainLayout_->SetHorizontal();
+
+          thumbnailsLayout_ = new LayoutWidget("thumbnail-layout");
+          thumbnailsLayout_->SetPadding(10);
+          thumbnailsLayout_->SetBackgroundCleared(true);
+          thumbnailsLayout_->SetBackgroundColor(50, 50, 50);
+          thumbnailsLayout_->SetVertical();
+
+          mainWidget_ = new LayerWidget(broker_, "main-viewport");
+          //mainWidget_->RegisterObserver(*this);
+
+          // hierarchy
+          mainLayout_->AddWidget(thumbnailsLayout_);
+          mainLayout_->AddWidget(mainWidget_);
+
+          orthancApiClient_.reset(new OrthancApiClient(IObserver::broker_, context_->GetWebService()));
+
+          // sources
+          smartLoader_.reset(new SmartLoader(IObserver::broker_, *orthancApiClient_));
+          smartLoader_->SetImageQuality(SliceImageQuality_FullPam);
+
+          mainLayout_->SetTransmitMouseOver(true);
+          mainWidgetInteractor_.reset(new MainWidgetInteractor(*this));
+          mainWidget_->SetInteractor(*mainWidgetInteractor_);
+          thumbnailInteractor_.reset(new ThumbnailInteractor(*this));
+        }
+
+        statusBar.SetMessage("Use the key \"s\" to reinitialize the layout");
+        statusBar.SetMessage("Use the key \"n\" to go to next image in the main viewport");
+
+
+        if (parameters.count("studyId") < 1)
+        {
+          LOG(WARNING) << "The study ID is missing, will take the first studyId found in Orthanc";
+          orthancApiClient_->GetJsonAsync("/studies", new Callable<SimpleViewerApplication, OrthancApiClient::JsonResponseReadyMessage>(*this, &SimpleViewerApplication::OnStudyListReceived));
+        }
+        else
+        {
+          SelectStudy(parameters["studyId"].as<std::string>());
+        }
+      }
+
+      void OnStudyListReceived(const OrthancApiClient::JsonResponseReadyMessage& message)
+      {
+        const Json::Value& response = message.Response;
+
+        if (response.isArray() && response.size() > 1)
+        {
+          SelectStudy(response[0].asString());
+        }
+      }
+      void OnStudyReceived(const OrthancApiClient::JsonResponseReadyMessage& message)
+      {
+        const Json::Value& response = message.Response;
+
+        if (response.isObject() && response["Series"].isArray())
+        {
+          for (size_t i=0; i < response["Series"].size(); i++)
+          {
+            orthancApiClient_->GetJsonAsync("/series/" + response["Series"][(int)i].asString(), new Callable<SimpleViewerApplication, OrthancApiClient::JsonResponseReadyMessage>(*this, &SimpleViewerApplication::OnSeriesReceived));
+          }
+        }
+      }
+
+      void OnSeriesReceived(const OrthancApiClient::JsonResponseReadyMessage& message)
+      {
+        const Json::Value& response = message.Response;
+
+        if (response.isObject() && response["Instances"].isArray() && response["Instances"].size() > 0)
+        {
+          // keep track of all instances IDs
+          const std::string& seriesId = response["ID"].asString();
+          seriesTags_[seriesId] = response;
+          instancesIdsPerSeriesId_[seriesId] = std::vector<std::string>();
+          for (size_t i = 0; i < response["Instances"].size(); i++)
+          {
+            const std::string& instanceId = response["Instances"][static_cast<int>(i)].asString();
+            instancesIdsPerSeriesId_[seriesId].push_back(instanceId);
+          }
+
+          // load the first instance in the thumbnail
+          LoadThumbnailForSeries(seriesId, instancesIdsPerSeriesId_[seriesId][0]);
+
+          // if this is the first thumbnail loaded, load the first instance in the mainWidget
+          LayerWidget& widget = *dynamic_cast<LayerWidget*>(mainWidget_);
+          if (widget.GetLayerCount() == 0)
+          {
+            smartLoader_->SetFrameInWidget(widget, 0, instancesIdsPerSeriesId_[seriesId][0], 0);
+          }
+        }
+      }
+
+      void LoadThumbnailForSeries(const std::string& seriesId, const std::string& instanceId)
+      {
+        LOG(INFO) << "Loading thumbnail for series " << seriesId;
+        LayerWidget* thumbnailWidget = new LayerWidget(IObserver::broker_, "thumbnail-series-" + seriesId);
+        thumbnails_.push_back(thumbnailWidget);
+        thumbnailsLayout_->AddWidget(thumbnailWidget);
+        thumbnailWidget->RegisterObserverCallback(new Callable<SimpleViewerApplication, LayerWidget::GeometryChangedMessage>(*this, &SimpleViewerApplication::OnWidgetGeometryChanged));
+        smartLoader_->SetFrameInWidget(*thumbnailWidget, 0, instanceId, 0);
+        thumbnailWidget->SetInteractor(*thumbnailInteractor_);
+      }
+
+      void SelectStudy(const std::string& studyId)
+      {
+        orthancApiClient_->GetJsonAsync("/studies/" + studyId, new Callable<SimpleViewerApplication, OrthancApiClient::JsonResponseReadyMessage>(*this, &SimpleViewerApplication::OnStudyReceived));
+      }
+
+      void OnWidgetGeometryChanged(const LayerWidget::GeometryChangedMessage& message)
+      {
+        message.origin_.FitContent();
+      }
+
+      void SelectSeriesInMainViewport(const std::string& seriesId)
+      {
+        LayerWidget& widget = *dynamic_cast<LayerWidget*>(mainWidget_);
+        smartLoader_->SetFrameInWidget(widget, 0, instancesIdsPerSeriesId_[seriesId][0], 0);
+      }
+
+      const Orthanc::Font& GetFont() const
+      {
+        return font_;
+      }
+      
+      virtual void OnPushButton1Clicked() {}
+      virtual void OnPushButton2Clicked() {}
+      virtual void OnTool1Clicked() { currentTool_ = Tools_LineMeasure;}
+      virtual void OnTool2Clicked() { currentTool_ = Tools_CircleMeasure;}
+
+      virtual void GetButtonNames(std::string& pushButton1,
+                                  std::string& pushButton2,
+                                  std::string& tool1,
+                                  std::string& tool2
+                                  ) {
+        tool1 = "line";
+        tool2 = "circle";
+        pushButton1 = "action1";
+        pushButton2 = "action2";
+      }
+
+#if ORTHANC_ENABLE_WASM==1
+      virtual void InitializeWasm() {
+
+        AttachWidgetToWasmViewport("canvas", thumbnailsLayout_);
+        AttachWidgetToWasmViewport("canvas2", mainWidget_);
+      }
+#endif
+
+    };
+
+
+  }
+}
--- a/Applications/Samples/SingleFrameApplication.h	Mon Nov 05 10:04:56 2018 +0100
+++ b/Applications/Samples/SingleFrameApplication.h	Mon Nov 05 10:06:18 2018 +0100
@@ -33,8 +33,8 @@
   namespace Samples
   {
     class SingleFrameApplication :
-      public SampleApplicationBase,
-      private ILayerSource::IObserver
+      public SampleSingleCanvasApplicationBase,
+      public IObserver
     {
     private:
       class Interactor : public IWorldSceneInteractor
@@ -51,6 +51,9 @@
         virtual IWorldSceneMouseTracker* CreateMouseTracker(WorldSceneWidget& widget,
                                                             const ViewportGeometry& view,
                                                             MouseButton button,
+                                                            KeyboardModifiers modifiers,
+                                                            int viewportX,
+                                                            int viewportY,
                                                             double x,
                                                             double y,
                                                             IStatusBar* statusBar)
@@ -99,14 +102,15 @@
         }
 
         virtual void KeyPressed(WorldSceneWidget& widget,
-                                char key,
+                                KeyboardKeys key,
+                                char keyChar,
                                 KeyboardModifiers modifiers,
                                 IStatusBar* statusBar)
         {
-          switch (key)
+          switch (keyChar)
           {
             case 's':
-              widget.SetDefaultView();
+              widget.FitContent();
               break;
 
             default:
@@ -138,6 +142,12 @@
           }   
         }
       }
+
+
+      LayerWidget& GetMainWidget()
+      {
+        return *dynamic_cast<LayerWidget*>(mainWidget_);
+      }
       
 
       void SetSlice(size_t index)
@@ -148,7 +158,7 @@
           slice_ = index;
           
 #if 1
-          widget_->SetSlice(source_->GetSlice(slice_).GetGeometry());
+          GetMainWidget().SetSlice(source_->GetSlice(slice_).GetGeometry());
 #else
           // TEST for scene extents - Rotate the axes
           double a = 15.0 / 180.0 * M_PI;
@@ -169,52 +179,33 @@
       }
         
       
-      virtual void NotifyGeometryReady(const ILayerSource& source)
+      void OnMainWidgetGeometryReady(const ILayerSource::GeometryReadyMessage& message)
       {
         // Once the geometry of the series is downloaded from Orthanc,
-        // display its first slice, and adapt the viewport to fit this
+        // display its middle slice, and adapt the viewport to fit this
         // slice
-        if (source_ == &source)
+        if (source_ == &message.origin_)
         {
           SetSlice(source_->GetSliceCount() / 2);
         }
 
-        widget_->SetDefaultView();
-      }
-      
-      virtual void NotifyGeometryError(const ILayerSource& source)
-      {
+        GetMainWidget().FitContent();
       }
       
-      virtual void NotifyContentChange(const ILayerSource& source)
-      {
-      }
+      std::auto_ptr<Interactor>         mainWidgetInteractor_;
+      std::auto_ptr<OrthancApiClient>   orthancApiClient_;
+      const OrthancFrameLayerSource*    source_;
+      unsigned int                      slice_;
 
-      virtual void NotifySliceChange(const ILayerSource& source,
-                                     const Slice& slice)
-      {
-      }
- 
-      virtual void NotifyLayerReady(std::auto_ptr<ILayerRenderer>& layer,
-                                    const ILayerSource& source,
-                                    const CoordinateSystem3D& slice,
-                                    bool isError)
-      {
-      }
-
-      LayerWidget*                    widget_;
-      const OrthancFrameLayerSource*  source_;
-      unsigned int                    slice_;
-      
     public:
-      SingleFrameApplication() : 
-        widget_(NULL),
+      SingleFrameApplication(MessageBroker& broker) :
+        IObserver(broker),
         source_(NULL),
         slice_(0)
       {
       }
       
-      virtual void DeclareCommandLineOptions(boost::program_options::options_description& options)
+      virtual void DeclareStartupOptions(boost::program_options::options_description& options)
       {
         boost::program_options::options_description generic("Sample options");
         generic.add_options()
@@ -229,12 +220,14 @@
         options.add(generic);    
       }
 
-      virtual void Initialize(BasicApplicationContext& context,
+      virtual void Initialize(StoneApplicationContext* context,
                               IStatusBar& statusBar,
                               const boost::program_options::variables_map& parameters)
       {
         using namespace OrthancStone;
 
+        context_ = context;
+
         statusBar.SetMessage("Use the key \"s\" to reinitialize the layout");
 
         if (parameters.count("instance") != 1)
@@ -246,17 +239,14 @@
         std::string instance = parameters["instance"].as<std::string>();
         int frame = parameters["frame"].as<unsigned int>();
 
-        std::auto_ptr<LayerWidget> widget(new LayerWidget);
+        orthancApiClient_.reset(new OrthancApiClient(IObserver::broker_, context_->GetWebService()));
+        mainWidget_ = new LayerWidget(broker_, "main-widget");
 
-#if 1
-        std::auto_ptr<OrthancFrameLayerSource> layer
-          (new OrthancFrameLayerSource(context.GetWebService()));
-        //layer->SetImageQuality(SliceImageQuality_Jpeg50);
+        std::auto_ptr<OrthancFrameLayerSource> layer(new OrthancFrameLayerSource(broker_, *orthancApiClient_));
+        source_ = layer.get();
         layer->LoadFrame(instance, frame);
-        //layer->LoadSeries("6f1b492a-e181e200-44e51840-ef8db55e-af529ab6");
-        layer->Register(*this);
-        source_ = layer.get();
-        widget->AddLayer(layer.release());
+        layer->RegisterObserverCallback(new Callable<SingleFrameApplication, ILayerSource::GeometryReadyMessage>(*this, &SingleFrameApplication::OnMainWidgetGeometryReady));
+        GetMainWidget().AddLayer(layer.release());
 
         RenderStyle s;
 
@@ -265,53 +255,14 @@
           s.interpolation_ = ImageInterpolation_Bilinear;
         }
 
-        //s.drawGrid_ = true;
-        widget->SetLayerStyle(0, s);
-#else
-        // 0178023P**
-        // Extent of the CT layer: (-35.068 -20.368) => (34.932 49.632)
-        std::auto_ptr<OrthancFrameLayerSource> ct;
-        ct.reset(new OrthancFrameLayerSource(context.GetWebService()));
-        //ct->LoadInstance("c804a1a2-142545c9-33b32fe2-3df4cec0-a2bea6d6", 0);
-        //ct->LoadInstance("4bd4304f-47478948-71b24af2-51f4f1bc-275b6c1b", 0);  // BAD SLICE
-        //ct->SetImageQuality(SliceImageQuality_Jpeg50);
-        ct->LoadSeries("dd069910-4f090474-7d2bba07-e5c10783-f9e4fb1d");
-
-        ct->Register(*this);
-        widget->AddLayer(ct.release());
-
-        std::auto_ptr<OrthancFrameLayerSource> pet;
-        pet.reset(new OrthancFrameLayerSource(context.GetWebService()));
-        //pet->LoadInstance("a1c4dc6b-255d27f0-88069875-8daed730-2f5ee5c6", 0);
-        pet->LoadSeries("aabad2e7-80702b5d-e599d26c-4f13398e-38d58a9e");
-        pet->Register(*this);
-        source_ = pet.get();
-        widget->AddLayer(pet.release());
+        GetMainWidget().SetLayerStyle(0, s);
+        GetMainWidget().SetTransmitMouseOver(true);
 
-        {
-          RenderStyle s;
-          //s.drawGrid_ = true;
-          s.alpha_ = 1;
-          widget->SetLayerStyle(0, s);
-        }
-
-        {
-          RenderStyle s;
-          //s.drawGrid_ = true;
-          s.SetColor(255, 0, 0);  // Draw missing PET layer in red
-          s.alpha_ = 0.5;
-          s.applyLut_ = true;
-          s.lut_ = Orthanc::EmbeddedResources::COLORMAP_JET;
-          s.interpolation_ = ImageInterpolation_Bilinear;
-          widget->SetLayerStyle(1, s);
-        }
-#endif
-
-        widget_ = widget.get();
-        widget_->SetTransmitMouseOver(true);
-        widget_->SetInteractor(context.AddInteractor(new Interactor(*this)));
-        context.SetCentralWidget(widget.release());
+        mainWidgetInteractor_.reset(new Interactor(*this));
+        GetMainWidget().SetInteractor(*mainWidgetInteractor_);
       }
     };
+
+
   }
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/SingleFrameEditorApplication.h	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,2922 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 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
+
+#include "SampleApplicationBase.h"
+
+#include "../../Framework/Toolbox/ImageGeometry.h"
+#include "../../Framework/Toolbox/OrthancApiClient.h"
+#include "../../Framework/Toolbox/DicomFrameConverter.h"
+
+#include <Core/Images/FontRegistry.h>
+#include <Core/Images/Image.h>
+#include <Core/Images/ImageProcessing.h>
+#include <Core/Images/PamReader.h>
+#include <Core/Images/PamWriter.h>
+#include <Core/Images/PngWriter.h>
+#include <Core/Logging.h>
+#include <Core/Toolbox.h>
+#include <Plugins/Samples/Common/DicomDatasetReader.h>
+#include <Plugins/Samples/Common/FullOrthancDataset.h>
+
+#define EXPORT_USING_PAM  1
+
+
+#include <boost/math/constants/constants.hpp>
+
+namespace OrthancStone
+{
+  static Matrix CreateOffsetMatrix(double dx,
+                                   double dy)
+  {
+    Matrix m = LinearAlgebra::IdentityMatrix(3);
+    m(0, 2) = dx;
+    m(1, 2) = dy;
+    return m;
+  }
+      
+
+  static Matrix CreateScalingMatrix(double sx,
+                                    double sy)
+  {
+    Matrix m = LinearAlgebra::IdentityMatrix(3);
+    m(0, 0) = sx;
+    m(1, 1) = sy;
+    return m;
+  }
+      
+
+  static Matrix CreateRotationMatrix(double angle)
+  {
+    Matrix m;
+    const double v[] = { cos(angle), -sin(angle), 0,
+                         sin(angle), cos(angle), 0,
+                         0, 0, 1 };
+    LinearAlgebra::FillMatrix(m, 3, 3, v);
+    return m;
+  }
+      
+
+  class BitmapStack :
+    public IObserver,
+    public IObservable
+  {
+  public:
+    typedef OriginMessage<MessageType_Widget_GeometryChanged, BitmapStack> GeometryChangedMessage;
+    typedef OriginMessage<MessageType_Widget_ContentChanged, BitmapStack> ContentChangedMessage;
+
+
+    enum Corner
+    {
+      Corner_TopLeft,
+      Corner_TopRight,
+      Corner_BottomLeft,
+      Corner_BottomRight
+    };
+
+
+
+    class Bitmap : public boost::noncopyable
+    {
+    private:
+      size_t        index_;
+      bool          hasSize_;
+      unsigned int  width_;
+      unsigned int  height_;
+      bool          hasCrop_;
+      unsigned int  cropX_;
+      unsigned int  cropY_;
+      unsigned int  cropWidth_;
+      unsigned int  cropHeight_;
+      Matrix        transform_;
+      Matrix        transformInverse_;
+      double        pixelSpacingX_;
+      double        pixelSpacingY_;
+      double        panX_;
+      double        panY_;
+      double        angle_;
+      bool          resizeable_;
+
+
+    protected:
+      const Matrix& GetTransform() const
+      {
+        return transform_;
+      }
+
+
+    private:
+      static void ApplyTransform(double& x /* inout */,
+                                 double& y /* inout */,
+                                 const Matrix& transform)
+      {
+        Vector p;
+        LinearAlgebra::AssignVector(p, x, y, 1);
+
+        Vector q = LinearAlgebra::Product(transform, p);
+
+        if (!LinearAlgebra::IsNear(q[2], 1.0))
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+        }
+        else
+        {
+          x = q[0];
+          y = q[1];
+        }
+      }
+      
+      
+      void UpdateTransform()
+      {
+        transform_ = CreateScalingMatrix(pixelSpacingX_, pixelSpacingY_);
+
+        double centerX, centerY;
+        GetCenter(centerX, centerY);
+
+        transform_ = LinearAlgebra::Product(
+          CreateOffsetMatrix(panX_ + centerX, panY_ + centerY),
+          CreateRotationMatrix(angle_),
+          CreateOffsetMatrix(-centerX, -centerY),
+          transform_);
+
+        LinearAlgebra::InvertMatrix(transformInverse_, transform_);
+      }
+
+
+      void AddToExtent(Extent2D& extent,
+                       double x,
+                       double y) const
+      {
+        ApplyTransform(x, y, transform_);
+        extent.AddPoint(x, y);
+      }
+
+
+      void GetCornerInternal(double& x,
+                             double& y,
+                             Corner corner,
+                             unsigned int cropX,
+                             unsigned int cropY,
+                             unsigned int cropWidth,
+                             unsigned int cropHeight) const
+      {
+        double dx = static_cast<double>(cropX);
+        double dy = static_cast<double>(cropY);
+        double dwidth = static_cast<double>(cropWidth);
+        double dheight = static_cast<double>(cropHeight);
+
+        switch (corner)
+        {
+          case Corner_TopLeft:
+            x = dx;
+            y = dy;
+            break;
+
+          case Corner_TopRight:
+            x = dx + dwidth;
+            y = dy;
+            break;
+
+          case Corner_BottomLeft:
+            x = dx;
+            y = dy + dheight;
+            break;
+
+          case Corner_BottomRight:
+            x = dx + dwidth;
+            y = dy + dheight;
+            break;
+
+          default:
+            throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+        }
+
+        ApplyTransform(x, y, transform_);
+      }
+
+
+    public:
+      Bitmap(size_t index) :
+        index_(index),
+        hasSize_(false),
+        width_(0),
+        height_(0),
+        hasCrop_(false),
+        pixelSpacingX_(1),
+        pixelSpacingY_(1),
+        panX_(0),
+        panY_(0),
+        angle_(0),
+        resizeable_(false)
+      {
+        UpdateTransform();
+      }
+
+      virtual ~Bitmap()
+      {
+      }
+
+      size_t GetIndex() const
+      {
+        return index_;
+      }
+
+      void ResetCrop()
+      {
+        hasCrop_ = false;
+      }
+
+      void SetCrop(unsigned int x,
+                   unsigned int y,
+                   unsigned int width,
+                   unsigned int height)
+      {
+        if (!hasSize_)
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+        }
+        
+        if (x + width > width_ ||
+            y + height > height_)
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+        }
+        
+        hasCrop_ = true;
+        cropX_ = x;
+        cropY_ = y;
+        cropWidth_ = width;
+        cropHeight_ = height;
+
+        UpdateTransform();
+      }
+
+      void GetCrop(unsigned int& x,
+                   unsigned int& y,
+                   unsigned int& width,
+                   unsigned int& height) const
+      {
+        if (hasCrop_)
+        {
+          x = cropX_;
+          y = cropY_;
+          width = cropWidth_;
+          height = cropHeight_;
+        }
+        else 
+        {
+          x = 0;
+          y = 0;
+          width = width_;
+          height = height_;
+        }
+      }
+
+      void SetAngle(double angle)
+      {
+        angle_ = angle;
+        UpdateTransform();
+      }
+
+      double GetAngle() const
+      {
+        return angle_;
+      }
+
+      void SetSize(unsigned int width,
+                   unsigned int height)
+      {
+        if (hasSize_ &&
+            (width != width_ ||
+             height != height_))
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageSize);
+        }
+        
+        hasSize_ = true;
+        width_ = width;
+        height_ = height;
+
+        UpdateTransform();
+      }
+
+
+      unsigned int GetWidth() const
+      {
+        return width_;
+      }
+        
+
+      unsigned int GetHeight() const
+      {
+        return height_;
+      }       
+
+
+      void CheckSize(unsigned int width,
+                     unsigned int height)
+      {
+        if (hasSize_ &&
+            (width != width_ ||
+             height != height_))
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageSize);
+        }
+      }
+      
+
+      Extent2D GetExtent() const
+      {
+        Extent2D extent;
+       
+        unsigned int x, y, width, height;
+        GetCrop(x, y, width, height);
+
+        double dx = static_cast<double>(x);
+        double dy = static_cast<double>(y);
+        double dwidth = static_cast<double>(width);
+        double dheight = static_cast<double>(height);
+
+        AddToExtent(extent, dx, dy);
+        AddToExtent(extent, dx + dwidth, dy);
+        AddToExtent(extent, dx, dy + dheight);
+        AddToExtent(extent, dx + dwidth, dy + dheight);
+        
+        return extent;
+      }
+
+
+      bool Contains(double x,
+                    double y) const
+      {
+        ApplyTransform(x, y, transformInverse_);
+        
+        unsigned int cropX, cropY, cropWidth, cropHeight;
+        GetCrop(cropX, cropY, cropWidth, cropHeight);
+
+        return (x >= cropX && x <= cropX + cropWidth &&
+                y >= cropY && y <= cropY + cropHeight);
+      }
+
+
+      bool GetPixel(unsigned int& imageX,
+                    unsigned int& imageY,
+                    double sceneX,
+                    double sceneY) const
+      {
+        if (width_ == 0 ||
+            height_ == 0)
+        {
+          return false;
+        }
+        else
+        {
+          ApplyTransform(sceneX, sceneY, transformInverse_);
+        
+          int x = static_cast<int>(std::floor(sceneX));
+          int y = static_cast<int>(std::floor(sceneY));
+
+          if (x < 0)
+          {
+            imageX = 0;
+          }
+          else if (x >= static_cast<int>(width_))
+          {
+            imageX = width_;
+          }
+          else
+          {
+            imageX = static_cast<unsigned int>(x);
+          }
+
+          if (y < 0)
+          {
+            imageY = 0;
+          }
+          else if (y >= static_cast<int>(height_))
+          {
+            imageY = height_;
+          }
+          else
+          {
+            imageY = static_cast<unsigned int>(y);
+          }
+
+          return true;
+        }
+      }
+
+
+      void SetPan(double x,
+                  double y)
+      {
+        panX_ = x;
+        panY_ = y;
+        UpdateTransform();
+      }
+
+
+      void SetPixelSpacing(double x,
+                           double y)
+      {
+        pixelSpacingX_ = x;
+        pixelSpacingY_ = y;
+        UpdateTransform();
+      }
+
+      double GetPixelSpacingX() const
+      {
+        return pixelSpacingX_;
+      }   
+
+      double GetPixelSpacingY() const
+      {
+        return pixelSpacingY_;
+      }   
+
+      double GetPanX() const
+      {
+        return panX_;
+      }
+
+      double GetPanY() const
+      {
+        return panY_;
+      }
+
+      void GetCenter(double& centerX,
+                     double& centerY) const
+      {
+        centerX = static_cast<double>(width_) / 2.0;
+        centerY = static_cast<double>(height_) / 2.0;
+        ApplyTransform(centerX, centerY, transform_);
+      }
+
+
+      void DrawBorders(CairoContext& context,
+                       double zoom)
+      {
+        unsigned int cx, cy, width, height;
+        GetCrop(cx, cy, width, height);
+
+        double dx = static_cast<double>(cx);
+        double dy = static_cast<double>(cy);
+        double dwidth = static_cast<double>(width);
+        double dheight = static_cast<double>(height);
+
+        cairo_t* cr = context.GetObject();
+        cairo_set_line_width(cr, 2.0 / zoom);
+        
+        double x, y;
+        x = dx;
+        y = dy;
+        ApplyTransform(x, y, transform_);
+        cairo_move_to(cr, x, y);
+
+        x = dx + dwidth;
+        y = dy;
+        ApplyTransform(x, y, transform_);
+        cairo_line_to(cr, x, y);
+
+        x = dx + dwidth;
+        y = dy + dheight;
+        ApplyTransform(x, y, transform_);
+        cairo_line_to(cr, x, y);
+
+        x = dx;
+        y = dy + dheight;
+        ApplyTransform(x, y, transform_);
+        cairo_line_to(cr, x, y);
+
+        x = dx;
+        y = dy;
+        ApplyTransform(x, y, transform_);
+        cairo_line_to(cr, x, y);
+
+        cairo_stroke(cr);
+      }
+
+
+      static double Square(double x)
+      {
+        return x * x;
+      }
+
+
+      void GetCorner(double& x /* out */,
+                     double& y /* out */,
+                     Corner corner) const
+      {
+        unsigned int cropX, cropY, cropWidth, cropHeight;
+        GetCrop(cropX, cropY, cropWidth, cropHeight);
+        GetCornerInternal(x, y, corner, cropX, cropY, cropWidth, cropHeight);
+      }
+      
+      
+      bool LookupCorner(Corner& corner /* out */,
+                        double x,
+                        double y,
+                        double zoom,
+                        double viewportDistance) const
+      {
+        static const Corner CORNERS[] = {
+          Corner_TopLeft,
+          Corner_TopRight,
+          Corner_BottomLeft,
+          Corner_BottomRight
+        };
+        
+        unsigned int cropX, cropY, cropWidth, cropHeight;
+        GetCrop(cropX, cropY, cropWidth, cropHeight);
+
+        double threshold = Square(viewportDistance / zoom);
+        
+        for (size_t i = 0; i < 4; i++)
+        {
+          double cx, cy;
+          GetCornerInternal(cx, cy, CORNERS[i], cropX, cropY, cropWidth, cropHeight);
+
+          double d = Square(cx - x) + Square(cy - y);
+        
+          if (d <= threshold)
+          {
+            corner = CORNERS[i];
+            return true;
+          }
+        }
+        
+        return false;
+      }
+
+      bool IsResizeable() const
+      {
+        return resizeable_;
+      }
+
+      void SetResizeable(bool resizeable)
+      {
+        resizeable_ = resizeable;
+      }
+
+      virtual bool GetDefaultWindowing(float& center,
+                                       float& width) const
+      {
+        return false;
+      }
+
+      virtual void Render(Orthanc::ImageAccessor& buffer,
+                          const Matrix& viewTransform,
+                          ImageInterpolation interpolation) const = 0;
+
+      virtual bool GetRange(float& minValue,
+                            float& maxValue) const = 0;
+    }; 
+
+
+    class BitmapAccessor : public boost::noncopyable
+    {
+    private:
+      BitmapStack&  stack_;
+      size_t        index_;
+      Bitmap*       bitmap_;
+
+    public:
+      BitmapAccessor(BitmapStack& stack,
+                     size_t index) :
+        stack_(stack),
+        index_(index)
+      {
+        Bitmaps::iterator bitmap = stack.bitmaps_.find(index);
+        if (bitmap == stack.bitmaps_.end())
+        {
+          bitmap_ = NULL;
+        }
+        else
+        {
+          assert(bitmap->second != NULL);
+          bitmap_ = bitmap->second;
+        }
+      }
+
+      BitmapAccessor(BitmapStack& stack,
+                     double x,
+                     double y) :
+        stack_(stack),
+        index_(0)  // Dummy initialization
+      {
+        if (stack.LookupBitmap(index_, x, y))
+        {
+          Bitmaps::iterator bitmap = stack.bitmaps_.find(index_);
+          
+          if (bitmap == stack.bitmaps_.end())
+          {
+            throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+          }
+          else
+          {
+            assert(bitmap->second != NULL);
+            bitmap_ = bitmap->second;
+          }
+        }
+        else
+        {
+          bitmap_ = NULL;
+        }
+      }
+
+      void Invalidate()
+      {
+        bitmap_ = NULL;
+      }
+
+      bool IsValid() const
+      {
+        return bitmap_ != NULL;
+      }
+
+      BitmapStack& GetStack() const
+      {
+        if (IsValid())
+        {
+          return stack_;
+        }
+        else
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+        }
+      }
+
+      size_t GetIndex() const
+      {
+        if (IsValid())
+        {
+          return index_;
+        }
+        else
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+        }
+      }
+
+      Bitmap& GetBitmap() const
+      {
+        if (IsValid())
+        {
+          return *bitmap_;
+        }
+        else
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+        }
+      }    
+    };    
+
+
+    class AlphaBitmap : public Bitmap
+    {
+    private:
+      const BitmapStack&                     stack_;
+      std::auto_ptr<Orthanc::ImageAccessor>  alpha_;      // Grayscale8
+      bool                                   useWindowing_;
+      float                                  foreground_;
+
+    public:
+      AlphaBitmap(size_t index,
+                  const BitmapStack& stack) :
+        Bitmap(index),
+        stack_(stack),
+        useWindowing_(true),
+        foreground_(0)
+      {
+      }
+
+
+      void SetForegroundValue(float foreground)
+      {
+        useWindowing_ = false;
+        foreground_ = foreground;
+      }
+      
+      
+      void SetAlpha(Orthanc::ImageAccessor* image)
+      {
+        std::auto_ptr<Orthanc::ImageAccessor> raii(image);
+        
+        if (image == NULL)
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
+        }
+
+        if (image->GetFormat() != Orthanc::PixelFormat_Grayscale8)
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageFormat);
+        }
+
+        SetSize(image->GetWidth(), image->GetHeight());
+        alpha_ = raii;
+      }
+
+
+      void LoadText(const Orthanc::Font& font,
+                    const std::string& utf8)
+      {
+        SetAlpha(font.RenderAlpha(utf8));
+      }                   
+
+
+      virtual void Render(Orthanc::ImageAccessor& buffer,
+                          const Matrix& viewTransform,
+                          ImageInterpolation interpolation) const
+      {
+        if (buffer.GetFormat() != Orthanc::PixelFormat_Float32)
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageFormat);
+        }
+
+        unsigned int cropX, cropY, cropWidth, cropHeight;
+        GetCrop(cropX, cropY, cropWidth, cropHeight);
+
+        Matrix m = LinearAlgebra::Product(viewTransform,
+                                          GetTransform(),
+                                          CreateOffsetMatrix(cropX, cropY));
+
+        Orthanc::ImageAccessor cropped;
+        alpha_->GetRegion(cropped, cropX, cropY, cropWidth, cropHeight);
+        
+        Orthanc::Image tmp(Orthanc::PixelFormat_Grayscale8, buffer.GetWidth(), buffer.GetHeight(), false);
+        ApplyProjectiveTransform(tmp, cropped, m, interpolation, true /* clear */);
+
+        // Blit
+        const unsigned int width = buffer.GetWidth();
+        const unsigned int height = buffer.GetHeight();
+
+        float value = foreground_;
+        
+        if (useWindowing_)
+        {
+          float center, width;
+          if (stack_.GetWindowing(center, width))
+          {
+            value = center + width / 2.0f;
+          }
+        }
+        
+        for (unsigned int y = 0; y < height; y++)
+        {
+          float *q = reinterpret_cast<float*>(buffer.GetRow(y));
+          const uint8_t *p = reinterpret_cast<uint8_t*>(tmp.GetRow(y));
+
+          for (unsigned int x = 0; x < width; x++, p++, q++)
+          {
+            float a = static_cast<float>(*p) / 255.0f;
+            
+            *q = (a * value + (1.0f - a) * (*q));
+          }
+        }        
+      }
+
+      virtual bool GetRange(float& minValue,
+                            float& maxValue) const
+      {
+        if (useWindowing_)
+        {
+          return false;
+        }
+        else
+        {
+          minValue = 0;
+          maxValue = 0;
+
+          if (foreground_ < 0)
+          {
+            minValue = foreground_;
+          }
+
+          if (foreground_ > 0)
+          {
+            maxValue = foreground_;
+          }
+
+          return true;
+        }
+      }
+    };
+    
+    
+
+  private:
+    class DicomBitmap : public Bitmap
+    {
+    private:
+      std::auto_ptr<Orthanc::ImageAccessor>  source_;  // Content of PixelData
+      std::auto_ptr<DicomFrameConverter>     converter_;
+      std::auto_ptr<Orthanc::ImageAccessor>  converted_;  // Float32
+
+      static OrthancPlugins::DicomTag  ConvertTag(const Orthanc::DicomTag& tag)
+      {
+        return OrthancPlugins::DicomTag(tag.GetGroup(), tag.GetElement());
+      }
+      
+
+      void ApplyConverter()
+      {
+        if (source_.get() != NULL &&
+            converter_.get() != NULL)
+        {
+          converted_.reset(converter_->ConvertFrame(*source_));
+        }
+      }
+      
+    public:
+      DicomBitmap(size_t index) :
+        Bitmap(index)
+      {
+      }
+      
+      void SetDicomTags(const OrthancPlugins::FullOrthancDataset& dataset)
+      {
+        converter_.reset(new DicomFrameConverter);
+        converter_->ReadParameters(dataset);
+        ApplyConverter();
+
+        std::string tmp;
+        Vector pixelSpacing;
+        
+        if (dataset.GetStringValue(tmp, ConvertTag(Orthanc::DICOM_TAG_PIXEL_SPACING)) &&
+            LinearAlgebra::ParseVector(pixelSpacing, tmp) &&
+            pixelSpacing.size() == 2)
+        {
+          SetPixelSpacing(pixelSpacing[0], pixelSpacing[1]);
+        }
+
+        //SetPan(-0.5 * GetPixelSpacingX(), -0.5 * GetPixelSpacingY());
+      
+        OrthancPlugins::DicomDatasetReader reader(dataset);
+
+        unsigned int width, height;
+        if (!reader.GetUnsignedIntegerValue(width, ConvertTag(Orthanc::DICOM_TAG_COLUMNS)) ||
+            !reader.GetUnsignedIntegerValue(height, ConvertTag(Orthanc::DICOM_TAG_ROWS)))
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+        }
+        else
+        {
+          SetSize(width, height);
+        }
+      }
+
+      
+      void SetSourceImage(Orthanc::ImageAccessor* image)   // Takes ownership
+      {
+        std::auto_ptr<Orthanc::ImageAccessor> raii(image);
+        
+        if (image == NULL)
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
+        }
+
+        SetSize(image->GetWidth(), image->GetHeight());
+        
+        source_ = raii;
+        ApplyConverter();
+      }
+
+      
+      virtual void Render(Orthanc::ImageAccessor& buffer,
+                          const Matrix& viewTransform,
+                          ImageInterpolation interpolation) const
+      {
+        if (converted_.get() != NULL)
+        {
+          if (converted_->GetFormat() != Orthanc::PixelFormat_Float32)
+          {
+            throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+          }
+
+          unsigned int cropX, cropY, cropWidth, cropHeight;
+          GetCrop(cropX, cropY, cropWidth, cropHeight);
+
+          Matrix m = LinearAlgebra::Product(viewTransform,
+                                            GetTransform(),
+                                            CreateOffsetMatrix(cropX, cropY));
+
+          Orthanc::ImageAccessor cropped;
+          converted_->GetRegion(cropped, cropX, cropY, cropWidth, cropHeight);
+        
+          ApplyProjectiveTransform(buffer, cropped, m, interpolation, false);
+        }
+      }
+
+
+      virtual bool GetDefaultWindowing(float& center,
+                                       float& width) const
+      {
+        if (converter_.get() != NULL &&
+            converter_->HasDefaultWindow())
+        {
+          center = static_cast<float>(converter_->GetDefaultWindowCenter());
+          width = static_cast<float>(converter_->GetDefaultWindowWidth());
+          return true;
+        }
+        else
+        {
+          return false;
+        }
+      }
+
+
+      virtual bool GetRange(float& minValue,
+                            float& maxValue) const
+      {
+        if (converted_.get() != NULL)
+        {
+          if (converted_->GetFormat() != Orthanc::PixelFormat_Float32)
+          {
+            throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+          }
+
+          Orthanc::ImageProcessing::GetMinMaxFloatValue(minValue, maxValue, *converted_);
+          return true;
+        }
+        else
+        {
+          return false;
+        }
+      }
+    };
+
+
+
+
+    typedef std::map<size_t, Bitmap*>  Bitmaps;
+        
+    OrthancApiClient&  orthanc_;
+    size_t             countBitmaps_;
+    bool               hasWindowing_;
+    float              windowingCenter_;
+    float              windowingWidth_;
+    Bitmaps            bitmaps_;
+    bool               hasSelection_;
+    size_t             selectedBitmap_;
+
+  public:
+    BitmapStack(MessageBroker& broker,
+                OrthancApiClient& orthanc) :
+      IObserver(broker),
+      IObservable(broker),
+      orthanc_(orthanc),
+      countBitmaps_(0),
+      hasWindowing_(false),
+      windowingCenter_(0),  // Dummy initialization
+      windowingWidth_(0),   // Dummy initialization
+      hasSelection_(false),
+      selectedBitmap_(0)    // Dummy initialization
+    {
+    }
+
+
+    void Unselect()
+    {
+      hasSelection_ = false;
+    }
+
+
+    void Select(size_t bitmap)
+    {
+      hasSelection_ = true;
+      selectedBitmap_ = bitmap;
+    }
+
+
+    bool GetSelectedBitmap(size_t& bitmap) const
+    {
+      if (hasSelection_)
+      {
+        bitmap = selectedBitmap_;
+        return true;
+      }
+      else
+      {
+        return false;
+      }
+    }
+    
+    
+    virtual ~BitmapStack()
+    {
+      for (Bitmaps::iterator it = bitmaps_.begin(); it != bitmaps_.end(); it++)
+      {
+        assert(it->second != NULL);
+        delete it->second;
+      }
+    }
+
+
+    bool GetWindowing(float& center,
+                      float& width) const
+    {
+      if (hasWindowing_)
+      {
+        center = windowingCenter_;
+        width = windowingWidth_;
+        return true;
+      }
+      else
+      {
+        return false;
+      }
+    }
+
+
+    void GetWindowingWithDefault(float& center,
+                                 float& width) const
+    {
+      if (!GetWindowing(center, width))
+      {
+        center = 128;
+        width = 256;
+      }
+    }
+
+
+    void SetWindowing(float center,
+                      float width)
+
+    {
+      hasWindowing_ = true;
+      windowingCenter_ = center;
+      windowingWidth_ = width;
+
+      //EmitMessage(ContentChangedMessage(*this));
+    }
+    
+
+    Bitmap& LoadText(const Orthanc::Font& font,
+                     const std::string& utf8)
+    {
+      size_t bitmap = countBitmaps_++;
+
+      std::auto_ptr<AlphaBitmap>  alpha(new AlphaBitmap(bitmap, *this));
+      alpha->LoadText(font, utf8);
+
+      AlphaBitmap* ptr = alpha.get();
+      bitmaps_[bitmap] = alpha.release();
+
+      return *ptr;
+    }
+
+    
+    Bitmap& LoadTestBlock(unsigned int width,
+                          unsigned int height)
+    {
+      size_t bitmap = countBitmaps_++;
+
+      std::auto_ptr<AlphaBitmap>  alpha(new AlphaBitmap(bitmap, *this));
+
+      std::auto_ptr<Orthanc::Image>  block(new Orthanc::Image(Orthanc::PixelFormat_Grayscale8, width, height, false));
+
+      for (unsigned int padding = 0;
+           (width > 2 * padding) && (height > 2 * padding);
+           padding++)
+      {
+        uint8_t color;
+        if (255 > 10 * padding)
+        {
+          color = 255 - 10 * padding;
+        }
+        else
+        {
+          color = 0;
+        }
+
+        Orthanc::ImageAccessor region;
+        block->GetRegion(region, padding, padding, width - 2 * padding, height - 2 * padding);
+        Orthanc::ImageProcessing::Set(region, color);
+      }
+
+      alpha->SetAlpha(block.release());
+
+      AlphaBitmap* ptr = alpha.get();
+      bitmaps_[bitmap] = alpha.release();
+      
+      return *ptr;
+    }
+
+    
+    Bitmap& LoadFrame(const std::string& instance,
+                     unsigned int frame,
+                     bool httpCompression)
+    {
+      size_t bitmap = countBitmaps_++;
+
+      bitmaps_[bitmap] = new DicomBitmap(bitmap);
+      
+      {
+        IWebService::Headers headers;
+        std::string uri = "/instances/" + instance + "/tags";
+        orthanc_.GetBinaryAsync(uri, headers,
+                                new Callable<BitmapStack, OrthancApiClient::BinaryResponseReadyMessage>
+                                (*this, &BitmapStack::OnTagsReceived), NULL,
+                                new Orthanc::SingleValueObject<size_t>(bitmap));
+      }
+
+      {
+        IWebService::Headers headers;
+        headers["Accept"] = "image/x-portable-arbitrarymap";
+
+        if (httpCompression)
+        {
+          headers["Accept-Encoding"] = "gzip";
+        }
+        
+        std::string uri = "/instances/" + instance + "/frames/" + boost::lexical_cast<std::string>(frame) + "/image-uint16";
+        orthanc_.GetBinaryAsync(uri, headers,
+                                new Callable<BitmapStack, OrthancApiClient::BinaryResponseReadyMessage>
+                                (*this, &BitmapStack::OnFrameReceived), NULL,
+                                new Orthanc::SingleValueObject<size_t>(bitmap));
+      }
+
+      return *bitmaps_[bitmap];
+    }
+
+    
+    void OnTagsReceived(const OrthancApiClient::BinaryResponseReadyMessage& message)
+    {
+      size_t index = dynamic_cast<Orthanc::SingleValueObject<size_t>*>(message.Payload.get())->GetValue();
+
+      LOG(INFO) << "JSON received: " << message.Uri.c_str()
+                << " (" << message.AnswerSize << " bytes) for bitmap " << index;
+      
+      Bitmaps::iterator bitmap = bitmaps_.find(index);
+      if (bitmap != bitmaps_.end())
+      {
+        assert(bitmap->second != NULL);
+        
+        OrthancPlugins::FullOrthancDataset dicom(message.Answer, message.AnswerSize);
+        dynamic_cast<DicomBitmap*>(bitmap->second)->SetDicomTags(dicom);
+
+        float c, w;
+        if (!hasWindowing_ &&
+            bitmap->second->GetDefaultWindowing(c, w))
+        {
+          hasWindowing_ = true;
+          windowingCenter_ = c;
+          windowingWidth_ = w;
+        }
+
+        EmitMessage(GeometryChangedMessage(*this));
+      }
+    }
+    
+
+    void OnFrameReceived(const OrthancApiClient::BinaryResponseReadyMessage& message)
+    {
+      size_t index = dynamic_cast<Orthanc::SingleValueObject<size_t>*>(message.Payload.get())->GetValue();
+      
+      LOG(INFO) << "DICOM frame received: " << message.Uri.c_str()
+                << " (" << message.AnswerSize << " bytes) for bitmap " << index;
+      
+      Bitmaps::iterator bitmap = bitmaps_.find(index);
+      if (bitmap != bitmaps_.end())
+      {
+        assert(bitmap->second != NULL);
+
+        std::string content;
+        if (message.AnswerSize > 0)
+        {
+          content.assign(reinterpret_cast<const char*>(message.Answer), message.AnswerSize);
+        }
+        
+        std::auto_ptr<Orthanc::PamReader> reader(new Orthanc::PamReader);
+        reader->ReadFromMemory(content);
+        dynamic_cast<DicomBitmap*>(bitmap->second)->SetSourceImage(reader.release());
+
+        EmitMessage(ContentChangedMessage(*this));
+      }
+    }
+
+
+    Extent2D GetSceneExtent() const
+    {
+      Extent2D extent;
+
+      for (Bitmaps::const_iterator it = bitmaps_.begin();
+           it != bitmaps_.end(); ++it)
+      {
+        assert(it->second != NULL);
+        extent.Union(it->second->GetExtent());
+      }
+
+      return extent;
+    }
+    
+
+    void Render(Orthanc::ImageAccessor& buffer,
+                const Matrix& viewTransform,
+                ImageInterpolation interpolation) const
+    {
+      Orthanc::ImageProcessing::Set(buffer, 0);
+
+      // Render layers in the background-to-foreground order
+      for (size_t index = 0; index < countBitmaps_; index++)
+      {
+        Bitmaps::const_iterator it = bitmaps_.find(index);
+        if (it != bitmaps_.end())
+        {
+          assert(it->second != NULL);
+          it->second->Render(buffer, viewTransform, interpolation);
+        }
+      }
+    }
+
+
+    bool LookupBitmap(size_t& index /* out */,
+                      double x,
+                      double y) const
+    {
+      // Render layers in the foreground-to-background order
+      for (size_t i = countBitmaps_; i > 0; i--)
+      {
+        index = i - 1;
+        Bitmaps::const_iterator it = bitmaps_.find(index);
+        if (it != bitmaps_.end())
+        {
+          assert(it->second != NULL);
+          if (it->second->Contains(x, y))
+          {
+            return true;
+          }
+        }
+      }
+
+      return false;
+    }
+
+    void DrawControls(CairoContext& context,
+                      double zoom)
+    {
+      if (hasSelection_)
+      {
+        Bitmaps::const_iterator bitmap = bitmaps_.find(selectedBitmap_);
+        
+        if (bitmap != bitmaps_.end())
+        {
+          context.SetSourceColor(255, 0, 0);
+          //view.ApplyTransform(context);
+          bitmap->second->DrawBorders(context, zoom);
+        }
+      }
+    }
+
+
+    void GetRange(float& minValue,
+                  float& maxValue) const
+    {
+      bool first = true;
+      
+      for (Bitmaps::const_iterator it = bitmaps_.begin();
+           it != bitmaps_.end(); it++)
+      {
+        assert(it->second != NULL);
+
+        float a, b;
+        if (it->second->GetRange(a, b))
+        {
+          if (first)
+          {
+            minValue = a;
+            maxValue = b;
+            first = false;
+          }
+          else
+          {
+            minValue = std::min(a, minValue);
+            maxValue = std::max(b, maxValue);
+          }
+        }
+      }
+
+      if (first)
+      {
+        minValue = 0;
+        maxValue = 0;
+      }
+    }
+  };
+
+
+  class UndoRedoStack : public boost::noncopyable
+  {
+  public:
+    class ICommand : public boost::noncopyable
+    {
+    public:
+      virtual ~ICommand()
+      {
+      }
+      
+      virtual void Undo() const = 0;
+      
+      virtual void Redo() const = 0;
+    };
+
+  private:
+    typedef std::list<ICommand*>  Stack;
+
+    Stack            stack_;
+    Stack::iterator  current_;
+
+    void Clear(Stack::iterator from)
+    {
+      for (Stack::iterator it = from; it != stack_.end(); ++it)
+      {
+        assert(*it != NULL);
+        delete *it;
+      }
+
+      stack_.erase(from, stack_.end());
+    }
+
+  public:
+    UndoRedoStack() :
+      current_(stack_.end())
+    {
+    }
+    
+    ~UndoRedoStack()
+    {
+      Clear(stack_.begin());
+    }
+
+    void Add(ICommand* command)
+    {
+      if (command == NULL)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
+      }
+      
+      Clear(current_);
+
+      stack_.push_back(command);
+      current_ = stack_.end();
+    }
+
+    void Undo()
+    {
+      if (current_ != stack_.begin())
+      {
+        --current_;
+        
+        assert(*current_ != NULL);
+        (*current_)->Undo();
+      }
+    }
+
+    void Redo()
+    {
+      if (current_ != stack_.end())
+      {
+        assert(*current_ != NULL);
+        (*current_)->Redo();
+
+        ++current_;
+      }
+    }
+  };
+
+
+  class BitmapCommandBase : public UndoRedoStack::ICommand
+  {
+  private:
+    BitmapStack&  stack_;
+    size_t        bitmap_;
+
+  protected:
+    virtual void UndoInternal(BitmapStack::Bitmap& bitmap) const = 0;
+
+    virtual void RedoInternal(BitmapStack::Bitmap& bitmap) const = 0;
+
+  public:
+    BitmapCommandBase(BitmapStack& stack,
+                      size_t bitmap) :
+      stack_(stack),
+      bitmap_(bitmap)
+    {
+    }
+
+    BitmapCommandBase(const BitmapStack::BitmapAccessor& accessor) :
+      stack_(accessor.GetStack()),
+      bitmap_(accessor.GetIndex())
+    {
+    }
+
+    virtual void Undo() const
+    {
+      BitmapStack::BitmapAccessor accessor(stack_, bitmap_);
+
+      if (accessor.IsValid())
+      {
+        UndoInternal(accessor.GetBitmap());
+      }
+    }
+
+    virtual void Redo() const
+    {
+      BitmapStack::BitmapAccessor accessor(stack_, bitmap_);
+
+      if (accessor.IsValid())
+      {
+        RedoInternal(accessor.GetBitmap());
+      }
+    }
+  };
+
+
+  class RotateBitmapTracker : public IWorldSceneMouseTracker
+  {
+  private:
+    UndoRedoStack&               undoRedoStack_;
+    BitmapStack::BitmapAccessor  accessor_;
+    double                       centerX_;
+    double                       centerY_;
+    double                       originalAngle_;
+    double                       clickAngle_;
+    bool                         roundAngles_;
+
+    bool ComputeAngle(double& angle /* out */,
+                      double sceneX,
+                      double sceneY) const
+    {
+      Vector u;
+      LinearAlgebra::AssignVector(u, sceneX - centerX_, sceneY - centerY_);
+
+      double nu = boost::numeric::ublas::norm_2(u);
+
+      if (!LinearAlgebra::IsCloseToZero(nu))
+      {
+        u /= nu;
+        angle = atan2(u[1], u[0]);
+        return true;
+      }
+      else
+      {
+        return false;
+      }
+    }
+
+
+    class UndoRedoCommand : public BitmapCommandBase
+    {
+    private:
+      double  sourceAngle_;
+      double  targetAngle_;
+
+      static int ToDegrees(double angle)
+      {
+        return static_cast<int>(round(angle * 180.0 / boost::math::constants::pi<double>()));
+      }
+      
+    protected:
+      virtual void UndoInternal(BitmapStack::Bitmap& bitmap) const
+      {
+        LOG(INFO) << "Undo - Set angle to " << ToDegrees(sourceAngle_) << " degrees";
+        bitmap.SetAngle(sourceAngle_);
+      }
+
+      virtual void RedoInternal(BitmapStack::Bitmap& bitmap) const
+      {
+        LOG(INFO) << "Redo - Set angle to " << ToDegrees(sourceAngle_) << " degrees";
+        bitmap.SetAngle(targetAngle_);
+      }
+
+    public:
+      UndoRedoCommand(const RotateBitmapTracker& tracker) :
+        BitmapCommandBase(tracker.accessor_),
+        sourceAngle_(tracker.originalAngle_),
+        targetAngle_(tracker.accessor_.GetBitmap().GetAngle())
+      {
+      }
+    };
+
+      
+  public:
+    RotateBitmapTracker(UndoRedoStack& undoRedoStack,
+                        BitmapStack& stack,
+                        const ViewportGeometry& view,
+                        size_t bitmap,
+                        double x,
+                        double y,
+                        bool roundAngles) :
+      undoRedoStack_(undoRedoStack),
+      accessor_(stack, bitmap),
+      roundAngles_(roundAngles)
+    {
+      if (accessor_.IsValid())
+      {
+        accessor_.GetBitmap().GetCenter(centerX_, centerY_);
+        originalAngle_ = accessor_.GetBitmap().GetAngle();
+
+        double sceneX, sceneY;
+        view.MapDisplayToScene(sceneX, sceneY, x, y);
+
+        if (!ComputeAngle(clickAngle_, x, y))
+        {
+          accessor_.Invalidate();
+        }
+      }
+    }
+
+    virtual bool HasRender() const
+    {
+      return false;
+    }
+
+    virtual void Render(CairoContext& context,
+                        double zoom)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+    }
+
+    virtual void MouseUp()
+    {
+      if (accessor_.IsValid())
+      {
+        undoRedoStack_.Add(new UndoRedoCommand(*this));
+      }
+    }
+
+    virtual void MouseMove(int displayX,
+                           int displayY,
+                           double sceneX,
+                           double sceneY)
+    {
+      static const double ROUND_ANGLE = 15.0 / 180.0 * boost::math::constants::pi<double>(); 
+        
+      double angle;
+        
+      if (accessor_.IsValid() &&
+          ComputeAngle(angle, sceneX, sceneY))
+      {
+        angle = angle - clickAngle_ + originalAngle_;
+
+        if (roundAngles_)
+        {
+          angle = round(angle / ROUND_ANGLE) * ROUND_ANGLE;
+        }
+          
+        accessor_.GetBitmap().SetAngle(angle);
+      }
+    }
+  };
+    
+    
+  class MoveBitmapTracker : public IWorldSceneMouseTracker
+  {
+  private:
+    UndoRedoStack&               undoRedoStack_;
+    BitmapStack::BitmapAccessor  accessor_;
+    double                       clickX_;
+    double                       clickY_;
+    double                       panX_;
+    double                       panY_;
+    bool                         oneAxis_;
+
+    class UndoRedoCommand : public BitmapCommandBase
+    {
+    private:
+      double  sourceX_;
+      double  sourceY_;
+      double  targetX_;
+      double  targetY_;
+
+    protected:
+      virtual void UndoInternal(BitmapStack::Bitmap& bitmap) const
+      {
+        bitmap.SetPan(sourceX_, sourceY_);
+      }
+
+      virtual void RedoInternal(BitmapStack::Bitmap& bitmap) const
+      {
+        bitmap.SetPan(targetX_, targetY_);
+      }
+
+    public:
+      UndoRedoCommand(const MoveBitmapTracker& tracker) :
+        BitmapCommandBase(tracker.accessor_),
+        sourceX_(tracker.panX_),
+        sourceY_(tracker.panY_),
+        targetX_(tracker.accessor_.GetBitmap().GetPanX()),
+        targetY_(tracker.accessor_.GetBitmap().GetPanY())
+      {
+      }
+    };
+
+
+  public:
+    MoveBitmapTracker(UndoRedoStack& undoRedoStack,
+                      BitmapStack& stack,
+                      size_t bitmap,
+                      double x,
+                      double y,
+                      bool oneAxis) :
+      undoRedoStack_(undoRedoStack),
+      accessor_(stack, bitmap),
+      clickX_(x),
+      clickY_(y),
+      oneAxis_(oneAxis)
+    {
+      if (accessor_.IsValid())
+      {
+        panX_ = accessor_.GetBitmap().GetPanX();
+        panY_ = accessor_.GetBitmap().GetPanY();
+      }
+    }
+
+    virtual bool HasRender() const
+    {
+      return false;
+    }
+
+    virtual void Render(CairoContext& context,
+                        double zoom)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+    }
+
+    virtual void MouseUp()
+    {
+      if (accessor_.IsValid())
+      {
+        undoRedoStack_.Add(new UndoRedoCommand(*this));
+      }
+    }
+
+    virtual void MouseMove(int displayX,
+                           int displayY,
+                           double sceneX,
+                           double sceneY)
+    {
+      if (accessor_.IsValid())
+      {
+        double dx = sceneX - clickX_;
+        double dy = sceneY - clickY_;
+
+        if (oneAxis_)
+        {
+          if (fabs(dx) > fabs(dy))
+          {
+            accessor_.GetBitmap().SetPan(dx + panX_, panY_);
+          }
+          else
+          {
+            accessor_.GetBitmap().SetPan(panX_, dy + panY_);
+          }
+        }
+        else
+        {
+          accessor_.GetBitmap().SetPan(dx + panX_, dy + panY_);
+        }
+      }
+    }
+  };
+
+
+  class CropBitmapTracker : public IWorldSceneMouseTracker
+  {
+  private:
+    UndoRedoStack&               undoRedoStack_;
+    BitmapStack::BitmapAccessor  accessor_;
+    BitmapStack::Corner          corner_;
+    unsigned int                 cropX_;
+    unsigned int                 cropY_;
+    unsigned int                 cropWidth_;
+    unsigned int                 cropHeight_;
+
+    class UndoRedoCommand : public BitmapCommandBase
+    {
+    private:
+      unsigned int  sourceCropX_;
+      unsigned int  sourceCropY_;
+      unsigned int  sourceCropWidth_;
+      unsigned int  sourceCropHeight_;
+      unsigned int  targetCropX_;
+      unsigned int  targetCropY_;
+      unsigned int  targetCropWidth_;
+      unsigned int  targetCropHeight_;
+
+    protected:
+      virtual void UndoInternal(BitmapStack::Bitmap& bitmap) const
+      {
+        bitmap.SetCrop(sourceCropX_, sourceCropY_, sourceCropWidth_, sourceCropHeight_);
+      }
+
+      virtual void RedoInternal(BitmapStack::Bitmap& bitmap) const
+      {
+        bitmap.SetCrop(targetCropX_, targetCropY_, targetCropWidth_, targetCropHeight_);
+      }
+
+    public:
+      UndoRedoCommand(const CropBitmapTracker& tracker) :
+        BitmapCommandBase(tracker.accessor_),
+        sourceCropX_(tracker.cropX_),
+        sourceCropY_(tracker.cropY_),
+        sourceCropWidth_(tracker.cropWidth_),
+        sourceCropHeight_(tracker.cropHeight_)
+      {
+        tracker.accessor_.GetBitmap().GetCrop(targetCropX_, targetCropY_,
+                                              targetCropWidth_, targetCropHeight_);
+      }
+    };
+
+
+  public:
+    CropBitmapTracker(UndoRedoStack& undoRedoStack,
+                      BitmapStack& stack,
+                      const ViewportGeometry& view,
+                      size_t bitmap,
+                      double x,
+                      double y,
+                      BitmapStack::Corner corner) :
+      undoRedoStack_(undoRedoStack),
+      accessor_(stack, bitmap),
+      corner_(corner)
+    {
+      if (accessor_.IsValid())
+      {
+        accessor_.GetBitmap().GetCrop(cropX_, cropY_, cropWidth_, cropHeight_);          
+      }
+    }
+
+    virtual bool HasRender() const
+    {
+      return false;
+    }
+
+    virtual void Render(CairoContext& context,
+                        double zoom)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+    }
+
+    virtual void MouseUp()
+    {
+      if (accessor_.IsValid())
+      {
+        undoRedoStack_.Add(new UndoRedoCommand(*this));
+      }
+    }
+
+    virtual void MouseMove(int displayX,
+                           int displayY,
+                           double sceneX,
+                           double sceneY)
+    {
+      if (accessor_.IsValid())
+      {
+        unsigned int x, y;
+        
+        BitmapStack::Bitmap& bitmap = accessor_.GetBitmap();
+        if (bitmap.GetPixel(x, y, sceneX, sceneY))
+        {
+          unsigned int targetX, targetWidth;
+
+          if (corner_ == BitmapStack::Corner_TopLeft ||
+              corner_ == BitmapStack::Corner_BottomLeft)
+          {
+            targetX = std::min(x, cropX_ + cropWidth_);
+            targetWidth = cropX_ + cropWidth_ - targetX;
+          }
+          else
+          {
+            targetX = cropX_;
+            targetWidth = std::max(x, cropX_) - cropX_;
+          }
+
+          unsigned int targetY, targetHeight;
+
+          if (corner_ == BitmapStack::Corner_TopLeft ||
+              corner_ == BitmapStack::Corner_TopRight)
+          {
+            targetY = std::min(y, cropY_ + cropHeight_);
+            targetHeight = cropY_ + cropHeight_ - targetY;
+          }
+          else
+          {
+            targetY = cropY_;
+            targetHeight = std::max(y, cropY_) - cropY_;
+          }
+
+          bitmap.SetCrop(targetX, targetY, targetWidth, targetHeight);
+        }
+      }
+    }
+  };
+    
+    
+  class ResizeBitmapTracker : public IWorldSceneMouseTracker
+  {
+  private:
+    UndoRedoStack&               undoRedoStack_;
+    BitmapStack::BitmapAccessor  accessor_;
+    bool                         roundScaling_;
+    double                       originalSpacingX_;
+    double                       originalSpacingY_;
+    double                       originalPanX_;
+    double                       originalPanY_;
+    BitmapStack::Corner          oppositeCorner_;
+    double                       oppositeX_;
+    double                       oppositeY_;
+    double                       baseScaling_;
+
+    static double ComputeDistance(double x1,
+                                  double y1,
+                                  double x2,
+                                  double y2)
+    {
+      double dx = x1 - x2;
+      double dy = y1 - y2;
+      return sqrt(dx * dx + dy * dy);
+    }
+      
+    class UndoRedoCommand : public BitmapCommandBase
+    {
+    private:
+      double   sourceSpacingX_;
+      double   sourceSpacingY_;
+      double   sourcePanX_;
+      double   sourcePanY_;
+      double   targetSpacingX_;
+      double   targetSpacingY_;
+      double   targetPanX_;
+      double   targetPanY_;
+
+    protected:
+      virtual void UndoInternal(BitmapStack::Bitmap& bitmap) const
+      {
+        bitmap.SetPixelSpacing(sourceSpacingX_, sourceSpacingY_);
+        bitmap.SetPan(sourcePanX_, sourcePanY_);
+      }
+
+      virtual void RedoInternal(BitmapStack::Bitmap& bitmap) const
+      {
+        bitmap.SetPixelSpacing(targetSpacingX_, targetSpacingY_);
+        bitmap.SetPan(targetPanX_, targetPanY_);
+      }
+
+    public:
+      UndoRedoCommand(const ResizeBitmapTracker& tracker) :
+        BitmapCommandBase(tracker.accessor_),
+        sourceSpacingX_(tracker.originalSpacingX_),
+        sourceSpacingY_(tracker.originalSpacingY_),
+        sourcePanX_(tracker.originalPanX_),
+        sourcePanY_(tracker.originalPanY_),
+        targetSpacingX_(tracker.accessor_.GetBitmap().GetPixelSpacingX()),
+        targetSpacingY_(tracker.accessor_.GetBitmap().GetPixelSpacingY()),
+        targetPanX_(tracker.accessor_.GetBitmap().GetPanX()),
+        targetPanY_(tracker.accessor_.GetBitmap().GetPanY())
+      {
+      }
+    };
+
+
+  public:
+    ResizeBitmapTracker(UndoRedoStack& undoRedoStack,
+                        BitmapStack& stack,
+                        size_t bitmap,
+                        double x,
+                        double y,
+                        BitmapStack::Corner corner,
+                        bool roundScaling) :
+      undoRedoStack_(undoRedoStack),
+      accessor_(stack, bitmap),
+      roundScaling_(roundScaling)
+    {
+      if (accessor_.IsValid() &&
+          accessor_.GetBitmap().IsResizeable())
+      {
+        originalSpacingX_ = accessor_.GetBitmap().GetPixelSpacingX();
+        originalSpacingY_ = accessor_.GetBitmap().GetPixelSpacingY();
+        originalPanX_ = accessor_.GetBitmap().GetPanX();
+        originalPanY_ = accessor_.GetBitmap().GetPanY();
+
+        switch (corner)
+        {
+          case BitmapStack::Corner_TopLeft:
+            oppositeCorner_ = BitmapStack::Corner_BottomRight;
+            break;
+
+          case BitmapStack::Corner_TopRight:
+            oppositeCorner_ = BitmapStack::Corner_BottomLeft;
+            break;
+
+          case BitmapStack::Corner_BottomLeft:
+            oppositeCorner_ = BitmapStack::Corner_TopRight;
+            break;
+
+          case BitmapStack::Corner_BottomRight:
+            oppositeCorner_ = BitmapStack::Corner_TopLeft;
+            break;
+
+          default:
+            throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+        }
+
+        accessor_.GetBitmap().GetCorner(oppositeX_, oppositeY_, oppositeCorner_);
+
+        double d = ComputeDistance(x, y, oppositeX_, oppositeY_);
+        if (d >= std::numeric_limits<float>::epsilon())
+        {
+          baseScaling_ = 1.0 / d;
+        }
+        else
+        {
+          // Avoid division by zero in extreme cases
+          accessor_.Invalidate();
+        }
+      }
+    }
+
+    virtual bool HasRender() const
+    {
+      return false;
+    }
+
+    virtual void Render(CairoContext& context,
+                        double zoom)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+    }
+
+    virtual void MouseUp()
+    {
+      if (accessor_.IsValid() &&
+          accessor_.GetBitmap().IsResizeable())
+      {
+        undoRedoStack_.Add(new UndoRedoCommand(*this));
+      }
+    }
+
+    virtual void MouseMove(int displayX,
+                           int displayY,
+                           double sceneX,
+                           double sceneY)
+    {
+      static const double ROUND_SCALING = 0.1;
+        
+      if (accessor_.IsValid() &&
+          accessor_.GetBitmap().IsResizeable())
+      {
+        double scaling = ComputeDistance(oppositeX_, oppositeY_, sceneX, sceneY) * baseScaling_;
+
+        if (roundScaling_)
+        {
+          scaling = round(scaling / ROUND_SCALING) * ROUND_SCALING;
+        }
+          
+        BitmapStack::Bitmap& bitmap = accessor_.GetBitmap();
+        bitmap.SetPixelSpacing(scaling * originalSpacingX_,
+                               scaling * originalSpacingY_);
+
+        // Keep the opposite corner at a fixed location
+        double ox, oy;
+        bitmap.GetCorner(ox, oy, oppositeCorner_);
+        bitmap.SetPan(bitmap.GetPanX() + oppositeX_ - ox,
+                      bitmap.GetPanY() + oppositeY_ - oy);
+      }
+    }
+  };
+
+
+  class WindowingTracker : public IWorldSceneMouseTracker
+  {   
+  public:
+    enum Action
+    {
+      Action_IncreaseWidth,
+      Action_DecreaseWidth,
+      Action_IncreaseCenter,
+      Action_DecreaseCenter
+    };
+    
+  private:
+    UndoRedoStack&  undoRedoStack_;
+    BitmapStack&    stack_;
+    int             clickX_;
+    int             clickY_;
+    Action          leftAction_;
+    Action          rightAction_;
+    Action          upAction_;
+    Action          downAction_;
+    float           strength_;
+    float           sourceCenter_;
+    float           sourceWidth_;
+
+    static void ComputeAxisEffect(int& deltaCenter,
+                                  int& deltaWidth,
+                                  int delta,
+                                  Action actionNegative,
+                                  Action actionPositive)
+    {
+      if (delta < 0)
+      {
+        switch (actionNegative)
+        {
+          case Action_IncreaseWidth:
+            deltaWidth = -delta;
+            break;
+
+          case Action_DecreaseWidth:
+            deltaWidth = delta;
+            break;
+
+          case Action_IncreaseCenter:
+            deltaCenter = -delta;
+            break;
+
+          case Action_DecreaseCenter:
+            deltaCenter = delta;
+            break;
+
+          default:
+            throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+        }
+      }
+      else if (delta > 0)
+      {
+        switch (actionPositive)
+        {
+          case Action_IncreaseWidth:
+            deltaWidth = delta;
+            break;
+
+          case Action_DecreaseWidth:
+            deltaWidth = -delta;
+            break;
+
+          case Action_IncreaseCenter:
+            deltaCenter = delta;
+            break;
+
+          case Action_DecreaseCenter:
+            deltaCenter = -delta;
+            break;
+
+          default:
+            throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+        }
+      }
+    }
+    
+    
+    class UndoRedoCommand : public UndoRedoStack::ICommand
+    {
+    private:
+      BitmapStack&  stack_;
+      float         sourceCenter_;
+      float         sourceWidth_;
+      float         targetCenter_;
+      float         targetWidth_;
+
+    public:
+      UndoRedoCommand(const WindowingTracker& tracker) :
+        stack_(tracker.stack_),
+        sourceCenter_(tracker.sourceCenter_),
+        sourceWidth_(tracker.sourceWidth_)
+      {
+        stack_.GetWindowingWithDefault(targetCenter_, targetWidth_);
+      }
+
+      virtual void Undo() const
+      {
+        stack_.SetWindowing(sourceCenter_, sourceWidth_);
+      }
+      
+      virtual void Redo() const
+      {
+        stack_.SetWindowing(targetCenter_, targetWidth_);
+      }
+    };
+
+
+  public:
+    WindowingTracker(UndoRedoStack& undoRedoStack,
+                     BitmapStack& stack,
+                     int x,
+                     int y,
+                     Action leftAction,
+                     Action rightAction,
+                     Action upAction,
+                     Action downAction) :
+      undoRedoStack_(undoRedoStack),
+      stack_(stack),
+      clickX_(x),
+      clickY_(y),
+      leftAction_(leftAction),
+      rightAction_(rightAction),
+      upAction_(upAction),
+      downAction_(downAction)
+    {
+      stack_.GetWindowingWithDefault(sourceCenter_, sourceWidth_);
+
+      float minValue, maxValue;
+      stack.GetRange(minValue, maxValue);
+
+      assert(minValue <= maxValue);
+
+      float tmp;
+      
+      float delta = (maxValue - minValue);
+      if (delta <= 1)
+      {
+        tmp = 0;
+      }
+      else
+      {
+        tmp = log2(delta);
+      }
+
+      strength_ = tmp - 7;
+      if (strength_ < 1)
+      {
+        strength_ = 1;
+      }
+    }
+
+    virtual bool HasRender() const
+    {
+      return false;
+    }
+
+    virtual void Render(CairoContext& context,
+                        double zoom)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+    }
+
+    virtual void MouseUp()
+    {
+      undoRedoStack_.Add(new UndoRedoCommand(*this));
+    }
+
+
+    virtual void MouseMove(int displayX,
+                           int displayY,
+                           double sceneX,
+                           double sceneY)
+    {
+      // https://bitbucket.org/osimis/osimis-webviewer-plugin/src/master/frontend/src/app/viewport/image-plugins/windowing-viewport-tool.class.js
+
+      static const float SCALE = 1.0;
+      
+      int deltaCenter = 0;
+      int deltaWidth = 0;
+
+      ComputeAxisEffect(deltaCenter, deltaWidth, displayX - clickX_, leftAction_, rightAction_);
+      ComputeAxisEffect(deltaCenter, deltaWidth, displayY - clickY_, upAction_, downAction_);
+
+      float newCenter = sourceCenter_ + (deltaCenter / SCALE * strength_);
+      float newWidth = sourceWidth_ + (deltaWidth / SCALE * strength_);
+      stack_.SetWindowing(newCenter, newWidth);
+    }
+  };
+
+
+  class BitmapStackWidget :
+    public WorldSceneWidget,
+    public IObservable,
+    public IObserver
+  {
+  private:
+    BitmapStack&                   stack_;
+    std::auto_ptr<Orthanc::Image>  floatBuffer_;
+    std::auto_ptr<CairoSurface>    cairoBuffer_;
+    bool                           invert_;
+    ImageInterpolation             interpolation_;
+
+    virtual bool RenderInternal(unsigned int width,
+                                unsigned int height,
+                                ImageInterpolation interpolation)
+    {
+      float windowCenter, windowWidth;
+      stack_.GetWindowingWithDefault(windowCenter, windowWidth);
+      
+      float x0 = windowCenter - windowWidth / 2.0f;
+      float x1 = windowCenter + windowWidth / 2.0f;
+
+      if (windowWidth <= 0.001f)  // Avoid division by zero at (*)
+      {
+        return false;
+      }
+      else
+      {
+        if (floatBuffer_.get() == NULL ||
+            floatBuffer_->GetWidth() != width ||
+            floatBuffer_->GetHeight() != height)
+        {
+          floatBuffer_.reset(new Orthanc::Image(Orthanc::PixelFormat_Float32, width, height, false));
+        }
+
+        if (cairoBuffer_.get() == NULL ||
+            cairoBuffer_->GetWidth() != width ||
+            cairoBuffer_->GetHeight() != height)
+        {
+          cairoBuffer_.reset(new CairoSurface(width, height));
+        }
+
+        stack_.Render(*floatBuffer_, GetView().GetMatrix(), interpolation);
+        
+        // Conversion from Float32 to BGRA32 (cairo). Very similar to
+        // GrayscaleFrameRenderer => TODO MERGE?
+
+        Orthanc::ImageAccessor target;
+        cairoBuffer_->GetWriteableAccessor(target);
+
+        float scaling = 255.0f / (x1 - x0);
+        
+        for (unsigned int y = 0; y < height; y++)
+        {
+          const float* p = reinterpret_cast<const float*>(floatBuffer_->GetConstRow(y));
+          uint8_t* q = reinterpret_cast<uint8_t*>(target.GetRow(y));
+
+          for (unsigned int x = 0; x < width; x++, p++, q += 4)
+          {
+            uint8_t v = 0;
+            if (*p >= x1)
+            {
+              v = 255;
+            }
+            else if (*p <= x0)
+            {
+              v = 0;
+            }
+            else
+            {
+              // https://en.wikipedia.org/wiki/Linear_interpolation
+              v = static_cast<uint8_t>(scaling * (*p - x0));  // (*)
+            }
+
+            if (invert_)
+            {
+              v = 255 - v;
+            }
+
+            q[0] = v;
+            q[1] = v;
+            q[2] = v;
+            q[3] = 255;
+          }
+        }
+
+        return true;
+      }
+    }
+
+
+  protected:
+    virtual Extent2D GetSceneExtent()
+    {
+      return stack_.GetSceneExtent();
+    }
+
+    virtual bool RenderScene(CairoContext& context,
+                             const ViewportGeometry& view)
+    {
+      cairo_t* cr = context.GetObject();
+
+      if (RenderInternal(context.GetWidth(), context.GetHeight(), interpolation_))
+      {
+        // https://www.cairographics.org/FAQ/#paint_from_a_surface
+        cairo_save(cr);
+        cairo_identity_matrix(cr);
+        cairo_set_source_surface(cr, cairoBuffer_->GetObject(), 0, 0);
+        cairo_paint(cr);
+        cairo_restore(cr);
+      }
+      else
+      {
+        // https://www.cairographics.org/FAQ/#clear_a_surface
+        context.SetSourceColor(0, 0, 0);
+        cairo_paint(cr);
+      }
+
+      stack_.DrawControls(context, view.GetZoom());
+
+      return true;
+    }
+
+  public:
+    BitmapStackWidget(MessageBroker& broker,
+                      BitmapStack& stack,
+                      const std::string& name) :
+      WorldSceneWidget(name),
+      IObservable(broker),
+      IObserver(broker),
+      stack_(stack),
+      invert_(false),
+      interpolation_(ImageInterpolation_Nearest)
+    {
+      stack.RegisterObserverCallback(new Callable<BitmapStackWidget, BitmapStack::GeometryChangedMessage>(*this, &BitmapStackWidget::OnGeometryChanged));
+      stack.RegisterObserverCallback(new Callable<BitmapStackWidget, BitmapStack::ContentChangedMessage>(*this, &BitmapStackWidget::OnContentChanged));
+    }
+
+    BitmapStack& GetStack() const
+    {
+      return stack_;
+    }
+
+    void OnGeometryChanged(const BitmapStack::GeometryChangedMessage& message)
+    {
+      LOG(INFO) << "Geometry has changed";
+      FitContent();
+    }
+
+    void OnContentChanged(const BitmapStack::ContentChangedMessage& message)
+    {
+      LOG(INFO) << "Content has changed";
+      NotifyContentChanged();
+    }
+
+    void SetInvert(bool invert)
+    {
+      if (invert_ != invert)
+      {
+        invert_ = invert;
+        NotifyContentChanged();
+      }
+    }
+
+    void SwitchInvert()
+    {
+      invert_ = !invert_;
+      NotifyContentChanged();
+    }
+
+    bool IsInvert() const
+    {
+      return invert_;
+    }
+
+    void SetInterpolation(ImageInterpolation interpolation)
+    {
+      if (interpolation_ != interpolation)
+      {
+        interpolation_ = interpolation;
+        NotifyContentChanged();
+      }
+    }
+
+    ImageInterpolation GetInterpolation() const
+    {
+      return interpolation_;
+    }
+  };
+
+  
+  class BitmapStackInteractor :
+    public IWorldSceneInteractor,
+    public IObserver
+  {
+  private:
+    enum Tool
+    {
+      Tool_Move,
+      Tool_Rotate,
+      Tool_Crop,
+      Tool_Resize,
+      Tool_Windowing
+    };
+        
+
+    UndoRedoStack      undoRedoStack_;
+    Tool               tool_;
+    OrthancApiClient  *orthanc_;
+
+
+    static double GetHandleSize()
+    {
+      return 10.0;
+    }
+    
+      
+    static BitmapStackWidget& GetWidget(WorldSceneWidget& widget)
+    {
+      return dynamic_cast<BitmapStackWidget&>(widget);
+    }
+
+
+    static BitmapStack& GetStack(WorldSceneWidget& widget)
+    {
+      return GetWidget(widget).GetStack();
+    }
+    
+    
+  public:
+    BitmapStackInteractor(MessageBroker& broker) :
+      IObserver(broker),
+      tool_(Tool_Move),
+      orthanc_(NULL)
+    {
+    }
+    
+    virtual IWorldSceneMouseTracker* CreateMouseTracker(WorldSceneWidget& widget,
+                                                        const ViewportGeometry& view,
+                                                        MouseButton button,
+                                                        KeyboardModifiers modifiers,
+                                                        int viewportX,
+                                                        int viewportY,
+                                                        double x,
+                                                        double y,
+                                                        IStatusBar* statusBar)
+    {
+      if (button == MouseButton_Left)
+      {
+        size_t selected;
+
+        if (tool_ == Tool_Windowing)
+        {
+          return new WindowingTracker(undoRedoStack_, GetStack(widget),
+                                      viewportX, viewportY,
+                                      WindowingTracker::Action_DecreaseWidth,
+                                      WindowingTracker::Action_IncreaseWidth,
+                                      WindowingTracker::Action_DecreaseCenter,
+                                      WindowingTracker::Action_IncreaseCenter);
+        }
+        else if (!GetStack(widget).GetSelectedBitmap(selected))
+        {
+          size_t bitmap;
+          if (GetStack(widget).LookupBitmap(bitmap, x, y))
+          {
+            LOG(INFO) << "Click on bitmap " << bitmap;
+            GetStack(widget).Select(bitmap);
+          }
+
+          return NULL;
+        }
+        else if (tool_ == Tool_Crop ||
+                 tool_ == Tool_Resize)
+        {
+          BitmapStack::BitmapAccessor accessor(GetStack(widget), selected);
+          BitmapStack::Corner corner;
+          if (accessor.GetBitmap().LookupCorner(corner, x, y, view.GetZoom(), GetHandleSize()))
+          {
+            switch (tool_)
+            {
+              case Tool_Crop:
+                return new CropBitmapTracker(undoRedoStack_, GetStack(widget), view, selected, x, y, corner);
+
+              case Tool_Resize:
+                return new ResizeBitmapTracker(undoRedoStack_, GetStack(widget), selected, x, y, corner,
+                                               (modifiers & KeyboardModifiers_Shift));
+
+              default:
+                throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+            }
+          }
+          else
+          {
+            size_t bitmap;
+            
+            if (!GetStack(widget).LookupBitmap(bitmap, x, y) ||
+                bitmap != selected)
+            {
+              GetStack(widget).Unselect();
+            }
+            
+            return NULL;
+          }
+        }
+        else
+        {
+          size_t bitmap;
+
+          if (GetStack(widget).LookupBitmap(bitmap, x, y) &&
+              bitmap == selected)
+          {
+            switch (tool_)
+            {
+              case Tool_Move:
+                return new MoveBitmapTracker(undoRedoStack_, GetStack(widget), bitmap, x, y,
+                                             (modifiers & KeyboardModifiers_Shift));
+
+              case Tool_Rotate:
+                return new RotateBitmapTracker(undoRedoStack_, GetStack(widget), view, bitmap, x, y,
+                                               (modifiers & KeyboardModifiers_Shift));
+                
+              default:
+                break;
+            }
+
+            return NULL;
+          }
+          else
+          {
+            LOG(INFO) << "Click out of any bitmap";
+            GetStack(widget).Unselect();
+            return NULL;
+          }
+        }
+      }
+      else
+      {
+        return NULL;
+      }
+    }
+
+    virtual void MouseOver(CairoContext& context,
+                           WorldSceneWidget& widget,
+                           const ViewportGeometry& view,
+                           double x,
+                           double y,
+                           IStatusBar* statusBar)
+    {
+#if 0
+      if (statusBar != NULL)
+      {
+        char buf[64];
+        sprintf(buf, "X = %.02f Y = %.02f (in cm)", x / 10.0, y / 10.0);
+        statusBar->SetMessage(buf);
+      }
+#endif
+
+      size_t selected;
+      if (GetStack(widget).GetSelectedBitmap(selected) &&
+          (tool_ == Tool_Crop ||
+           tool_ == Tool_Resize))
+      {
+        BitmapStack::BitmapAccessor accessor(GetStack(widget), selected);
+        
+        BitmapStack::Corner corner;
+        if (accessor.GetBitmap().LookupCorner(corner, x, y, view.GetZoom(), GetHandleSize()))
+        {
+          accessor.GetBitmap().GetCorner(x, y, corner);
+          
+          double z = 1.0 / view.GetZoom();
+          
+          context.SetSourceColor(255, 0, 0);
+          cairo_t* cr = context.GetObject();
+          cairo_set_line_width(cr, 2.0 * z);
+          cairo_move_to(cr, x - GetHandleSize() * z, y - GetHandleSize() * z);
+          cairo_line_to(cr, x + GetHandleSize() * z, y - GetHandleSize() * z);
+          cairo_line_to(cr, x + GetHandleSize() * z, y + GetHandleSize() * z);
+          cairo_line_to(cr, x - GetHandleSize() * z, y + GetHandleSize() * z);
+          cairo_line_to(cr, x - GetHandleSize() * z, y - GetHandleSize() * z);
+          cairo_stroke(cr);
+        }
+      }
+    }
+
+    virtual void MouseWheel(WorldSceneWidget& widget,
+                            MouseWheelDirection direction,
+                            KeyboardModifiers modifiers,
+                            IStatusBar* statusBar)
+    {
+    }
+
+    virtual void KeyPressed(WorldSceneWidget& widget,
+                            KeyboardKeys key,
+                            char keyChar,
+                            KeyboardModifiers modifiers,
+                            IStatusBar* statusBar)
+    {
+      switch (keyChar)
+      {
+        case 'a':
+          widget.FitContent();
+          break;
+
+        case 'c':
+          tool_ = Tool_Crop;
+          break;
+
+        case 'e':
+        {
+          Orthanc::DicomMap tags;
+
+          // Minimal set of tags to generate a valid CR image
+          tags.SetValue(Orthanc::DICOM_TAG_ACCESSION_NUMBER, "NOPE", false);
+          tags.SetValue(Orthanc::DICOM_TAG_BODY_PART_EXAMINED, "PELVIS", false);
+          tags.SetValue(Orthanc::DICOM_TAG_INSTANCE_NUMBER, "1", false);
+          //tags.SetValue(Orthanc::DICOM_TAG_LATERALITY, "", false);
+          tags.SetValue(Orthanc::DICOM_TAG_MANUFACTURER, "OSIMIS", false);
+          tags.SetValue(Orthanc::DICOM_TAG_MODALITY, "CR", false);
+          tags.SetValue(Orthanc::DICOM_TAG_PATIENT_BIRTH_DATE, "20000101", false);
+          tags.SetValue(Orthanc::DICOM_TAG_PATIENT_ID, "hello", false);
+          tags.SetValue(Orthanc::DICOM_TAG_PATIENT_NAME, "HELLO^WORLD", false);
+          tags.SetValue(Orthanc::DICOM_TAG_PATIENT_ORIENTATION, "", false);
+          tags.SetValue(Orthanc::DICOM_TAG_PATIENT_SEX, "M", false);
+          tags.SetValue(Orthanc::DICOM_TAG_REFERRING_PHYSICIAN_NAME, "HOUSE^MD", false);
+          tags.SetValue(Orthanc::DICOM_TAG_SERIES_NUMBER, "1", false);
+          tags.SetValue(Orthanc::DICOM_TAG_SOP_CLASS_UID, "1.2.840.10008.5.1.4.1.1.1", false);
+          tags.SetValue(Orthanc::DICOM_TAG_STUDY_ID, "STUDY", false);
+          tags.SetValue(Orthanc::DICOM_TAG_VIEW_POSITION, "", false);
+
+          Export(GetWidget(widget), 0.1, 0.1, tags);
+          break;
+        }
+
+        case 'i':
+          GetWidget(widget).SwitchInvert();
+          break;
+        
+        case 'm':
+          tool_ = Tool_Move;
+          break;
+
+        case 'n':
+        {
+          switch (GetWidget(widget).GetInterpolation())
+          {
+            case ImageInterpolation_Nearest:
+              LOG(INFO) << "Switching to bilinear interpolation";
+              GetWidget(widget).SetInterpolation(ImageInterpolation_Bilinear);
+              break;
+              
+            case ImageInterpolation_Bilinear:
+              LOG(INFO) << "Switching to nearest neighbor interpolation";
+              GetWidget(widget).SetInterpolation(ImageInterpolation_Nearest);
+              break;
+
+            default:
+              throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+          }
+          
+          break;
+        }
+        
+        case 'r':
+          tool_ = Tool_Rotate;
+          break;
+
+        case 's':
+          tool_ = Tool_Resize;
+          break;
+
+        case 'w':
+          tool_ = Tool_Windowing;
+          break;
+
+        case 'y':
+          if (modifiers & KeyboardModifiers_Control)
+          {
+            undoRedoStack_.Redo();
+            widget.NotifyContentChanged();
+          }
+          break;
+
+        case 'z':
+          if (modifiers & KeyboardModifiers_Control)
+          {
+            undoRedoStack_.Undo();
+            widget.NotifyContentChanged();
+          }
+          break;
+        
+        default:
+          break;
+      }
+    }
+
+
+    void SetOrthanc(OrthancApiClient& orthanc)
+    {
+      orthanc_ = &orthanc;
+    }
+
+
+    void Export(const BitmapStackWidget& widget,
+                double pixelSpacingX,
+                double pixelSpacingY,
+                const Orthanc::DicomMap& dicom)
+    {
+      if (pixelSpacingX <= 0 ||
+          pixelSpacingY <= 0)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+      }
+      
+      if (orthanc_ == NULL)
+      {
+        return;
+      }
+      
+      LOG(WARNING) << "Exporting DICOM";
+
+      Extent2D extent = widget.GetStack().GetSceneExtent();
+
+      int w = std::ceil(extent.GetWidth() / pixelSpacingX);
+      int h = std::ceil(extent.GetHeight() / pixelSpacingY);
+
+      if (w < 0 || h < 0)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+      }
+
+      Orthanc::Image layers(Orthanc::PixelFormat_Float32,
+                            static_cast<unsigned int>(w),
+                            static_cast<unsigned int>(h), false);
+
+      Matrix view = LinearAlgebra::Product(
+        CreateScalingMatrix(1.0 / pixelSpacingX, 1.0 / pixelSpacingY),
+        CreateOffsetMatrix(-extent.GetX1(), -extent.GetY1()));
+      
+      widget.GetStack().Render(layers, view, widget.GetInterpolation());
+
+      Orthanc::Image rendered(Orthanc::PixelFormat_Grayscale16,
+                              layers.GetWidth(), layers.GetHeight(), false);
+      Orthanc::ImageProcessing::Convert(rendered, layers);
+
+      std::string base64;
+
+      {
+        std::string content;
+
+#if EXPORT_USING_PAM == 1
+        {
+          Orthanc::PamWriter writer;
+          writer.WriteToMemory(content, rendered);
+        }
+#else
+        {
+          Orthanc::PngWriter writer;
+          writer.WriteToMemory(content, rendered);
+        }
+#endif      
+
+        Orthanc::Toolbox::EncodeBase64(base64, content);
+      }
+
+      std::set<Orthanc::DicomTag> tags;
+      dicom.GetTags(tags);
+
+      Json::Value json = Json::objectValue;
+      json["Tags"] = Json::objectValue;
+           
+      for (std::set<Orthanc::DicomTag>::const_iterator
+             tag = tags.begin(); tag != tags.end(); ++tag)
+      {
+        const Orthanc::DicomValue& value = dicom.GetValue(*tag);
+        if (!value.IsNull() &&
+            !value.IsBinary())
+        {
+          json["Tags"][tag->Format()] = value.GetContent();
+        }
+      }
+
+      json["Tags"][Orthanc::DICOM_TAG_PHOTOMETRIC_INTERPRETATION.Format()] =
+        (widget.IsInvert() ? "MONOCHROME1" : "MONOCHROME2");
+
+
+      // WARNING: The order of PixelSpacing is Y/X
+      char buf[32];
+      sprintf(buf, "%0.8f\\%0.8f", pixelSpacingY, pixelSpacingX);
+      
+      json["Tags"][Orthanc::DICOM_TAG_PIXEL_SPACING.Format()] = buf;
+
+      float center, width;
+      if (widget.GetStack().GetWindowing(center, width))
+      {
+        json["Tags"][Orthanc::DICOM_TAG_WINDOW_CENTER.Format()] =
+          boost::lexical_cast<std::string>(lroundf(center));
+
+        json["Tags"][Orthanc::DICOM_TAG_WINDOW_WIDTH.Format()] =
+          boost::lexical_cast<std::string>(lroundf(width));
+      }
+
+#if EXPORT_USING_PAM == 1
+      json["Content"] = "data:" + std::string(Orthanc::MIME_PAM) + ";base64," + base64;
+#else
+      json["Content"] = "data:" + std::string(Orthanc::MIME_PNG) + ";base64," + base64;
+#endif
+
+      orthanc_->PostJsonAsyncExpectJson(
+        "/tools/create-dicom", json,
+        new Callable<BitmapStackInteractor, OrthancApiClient::JsonResponseReadyMessage>
+        (*this, &BitmapStackInteractor::OnDicomExported),
+        NULL, NULL);
+    }
+
+
+    void OnDicomExported(const OrthancApiClient::JsonResponseReadyMessage& message)
+    {
+      LOG(WARNING) << "DICOM export was successful:"
+                   << message.Response.toStyledString();
+    }
+  };
+
+  
+  
+  namespace Samples
+  {
+    class SingleFrameEditorApplication :
+      public SampleSingleCanvasApplicationBase,
+      public IObserver
+    {
+    private:
+      std::auto_ptr<OrthancApiClient>  orthancApiClient_;
+      std::auto_ptr<BitmapStack>       stack_;
+      BitmapStackInteractor            interactor_;
+
+    public:
+      SingleFrameEditorApplication(MessageBroker& broker) :
+        IObserver(broker),
+        interactor_(broker)
+      {
+      }
+      
+      virtual void DeclareStartupOptions(boost::program_options::options_description& options)
+      {
+        boost::program_options::options_description generic("Sample options");
+        generic.add_options()
+          ("instance", boost::program_options::value<std::string>(),
+           "Orthanc ID of the instance")
+          ("frame", boost::program_options::value<unsigned int>()->default_value(0),
+           "Number of the frame, for multi-frame DICOM instances")
+          ;
+
+        options.add(generic);
+      }
+
+      virtual void Initialize(StoneApplicationContext* context,
+                              IStatusBar& statusBar,
+                              const boost::program_options::variables_map& parameters)
+      {
+        using namespace OrthancStone;
+
+        context_ = context;
+
+        statusBar.SetMessage("Use the key \"a\" to reinitialize the layout");
+        statusBar.SetMessage("Use the key \"c\" to crop");
+        statusBar.SetMessage("Use the key \"e\" to export DICOM to the Orthanc server");
+        statusBar.SetMessage("Use the key \"f\" to switch full screen");
+        statusBar.SetMessage("Use the key \"i\" to invert contrast");
+        statusBar.SetMessage("Use the key \"m\" to move objects");
+        statusBar.SetMessage("Use the key \"n\" to switch between nearest neighbor and bilinear interpolation");
+        statusBar.SetMessage("Use the key \"r\" to rotate objects");
+        statusBar.SetMessage("Use the key \"s\" to resize objects (not applicable to DICOM bitmaps)");
+        statusBar.SetMessage("Use the key \"w\" to change windowing");
+        
+        statusBar.SetMessage("Use the key \"ctrl-z\" to undo action");
+        statusBar.SetMessage("Use the key \"ctrl-y\" to redo action");
+
+        if (parameters.count("instance") != 1)
+        {
+          LOG(ERROR) << "The instance ID is missing";
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+        }
+
+        std::string instance = parameters["instance"].as<std::string>();
+        int frame = parameters["frame"].as<unsigned int>();
+
+        orthancApiClient_.reset(new OrthancApiClient(IObserver::broker_, context_->GetWebService()));
+        interactor_.SetOrthanc(*orthancApiClient_);
+
+        Orthanc::FontRegistry fonts;
+        fonts.AddFromResource(Orthanc::EmbeddedResources::FONT_UBUNTU_MONO_BOLD_16);
+        
+        stack_.reset(new BitmapStack(IObserver::broker_, *orthancApiClient_));
+        stack_->LoadFrame(instance, frame, false); //.SetPan(200, 0);
+        //stack_->LoadFrame("61f3143e-96f34791-ad6bbb8d-62559e75-45943e1b", 0, false);
+
+        {
+          BitmapStack::Bitmap& bitmap = stack_->LoadText(fonts.GetFont(0), "Hello\nworld");
+          //dynamic_cast<BitmapStack::AlphaBitmap&>(bitmap).SetForegroundValue(256);
+          dynamic_cast<BitmapStack::AlphaBitmap&>(bitmap).SetResizeable(true);
+        }
+        
+        {
+          BitmapStack::Bitmap& bitmap = stack_->LoadTestBlock(100, 50);
+          //dynamic_cast<BitmapStack::AlphaBitmap&>(bitmap).SetForegroundValue(256);
+          dynamic_cast<BitmapStack::AlphaBitmap&>(bitmap).SetResizeable(true);
+          dynamic_cast<BitmapStack::AlphaBitmap&>(bitmap).SetPan(0, 200);
+        }
+        
+        
+        mainWidget_ = new BitmapStackWidget(IObserver::broker_, *stack_, "main-widget");
+        mainWidget_->SetTransmitMouseOver(true);
+        mainWidget_->SetInteractor(interactor_);
+
+        //stack_->SetWindowing(128, 256);
+      }
+    };
+  }
+}
--- a/Applications/Samples/SingleVolumeApplication.h	Mon Nov 05 10:04:56 2018 +0100
+++ b/Applications/Samples/SingleVolumeApplication.h	Mon Nov 05 10:06:18 2018 +0100
@@ -89,7 +89,7 @@
 
 
     public:
-      virtual void DeclareCommandLineOptions(boost::program_options::options_description& options)
+      virtual void DeclareStartupOptions(boost::program_options::options_description& options)
       {
         boost::program_options::options_description generic("Sample options");
         generic.add_options()
@@ -108,8 +108,7 @@
         options.add(generic);    
       }
 
-      virtual void Initialize(BasicApplicationContext& context,
-                              IStatusBar& statusBar,
+      virtual void Initialize(IStatusBar& statusBar,
                               const boost::program_options::variables_map& parameters)
       {
         using namespace OrthancStone;
@@ -147,8 +146,8 @@
           throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
         }
 
-        unsigned int threads = parameters["threads"].as<unsigned int>();
-        bool reverse = parameters["reverse"].as<bool>();
+        //unsigned int threads = parameters["threads"].as<unsigned int>();
+        //bool reverse = parameters["reverse"].as<bool>();
 
         std::string tmp = parameters["projection"].as<std::string>();
         Orthanc::Toolbox::ToLowerCase(tmp);
@@ -187,8 +186,8 @@
 
         widget->AddLayer(new VolumeImageSource(*volume));
 
-        context.AddInteractor(new Interactor(*volume, *widget, projection, 0));
-        context.AddSlicedVolume(volume.release());
+        context_->AddInteractor(new Interactor(*volume, *widget, projection, 0));
+        context_->AddSlicedVolume(volume.release());
 
         if (1)
         {
@@ -208,14 +207,14 @@
           widget->SetLayerStyle(0, s);
         }
 #else
-        std::auto_ptr<OrthancVolumeImage> ct(new OrthancVolumeImage(context.GetWebService(), false));
+        std::auto_ptr<OrthancVolumeImage> ct(new OrthancVolumeImage(context_->GetWebService(), false));
         //ct->ScheduleLoadSeries("15a6f44a-ac7b88fe-19c462d9-dddd918e-b01550d8");  // 0178023P
         //ct->ScheduleLoadSeries("dd069910-4f090474-7d2bba07-e5c10783-f9e4fb1d");
         //ct->ScheduleLoadSeries("a04ecf01-79b2fc33-58239f7e-ad9db983-28e81afa");  // IBA
         //ct->ScheduleLoadSeries("03677739-1d8bca40-db1daf59-d74ff548-7f6fc9c0");  // 0522c0001 TCIA
         ct->ScheduleLoadSeries("295e8a13-dfed1320-ba6aebb2-9a13e20f-1b3eb953");  // Captain
         
-        std::auto_ptr<OrthancVolumeImage> pet(new OrthancVolumeImage(context.GetWebService(), true));
+        std::auto_ptr<OrthancVolumeImage> pet(new OrthancVolumeImage(context_->GetWebService(), true));
         //pet->ScheduleLoadSeries("48d2997f-8e25cd81-dd715b64-bd79cdcc-e8fcee53");  // 0178023P
         //pet->ScheduleLoadSeries("aabad2e7-80702b5d-e599d26c-4f13398e-38d58a9e");
         //pet->ScheduleLoadInstance("830a69ff-8e4b5ee3-b7f966c8-bccc20fb-d322dceb"); // IBA 1
@@ -225,7 +224,7 @@
         pet->ScheduleLoadInstance("f080888c-0ab7528a-f7d9c28c-84980eb1-ff3b0ae6");  // Captain 1
         //pet->ScheduleLoadInstance("4f78055b-6499a2c5-1e089290-394acc05-3ec781c1");  // Captain 2
 
-        std::auto_ptr<StructureSetLoader> rtStruct(new StructureSetLoader(context.GetWebService()));
+        std::auto_ptr<StructureSetLoader> rtStruct(new StructureSetLoader(context_->GetWebService()));
         //rtStruct->ScheduleLoadInstance("c2ebc17b-6b3548db-5e5da170-b8ecab71-ea03add3");  // 0178023P
         //rtStruct->ScheduleLoadInstance("54460695-ba3885ee-ddf61ac0-f028e31d-a6e474d9");  // IBA
         //rtStruct->ScheduleLoadInstance("17cd032b-ad92a438-ca05f06a-f9e96668-7e3e9e20");  // 0522c0001 TCIA
@@ -235,12 +234,12 @@
         widget->AddLayer(new VolumeImageSource(*pet));
         widget->AddLayer(new DicomStructureSetRendererFactory(*rtStruct));
         
-        context.AddInteractor(new Interactor(*pet, *widget, projection, 1));
-        //context.AddInteractor(new VolumeImageInteractor(*ct, *widget, projection));
+        context_->AddInteractor(new Interactor(*pet, *widget, projection, 1));
+        //context_->AddInteractor(new VolumeImageInteractor(*ct, *widget, projection));
 
-        context.AddSlicedVolume(ct.release());
-        context.AddSlicedVolume(pet.release());
-        context.AddVolumeLoader(rtStruct.release());
+        context_->AddSlicedVolume(ct.release());
+        context_->AddSlicedVolume(pet.release());
+        context_->AddVolumeLoader(rtStruct.release());
 
         {
           RenderStyle s;
@@ -272,7 +271,7 @@
         statusBar.SetMessage("Use the keys \"c\" to draw circles");
 
         widget->SetTransmitMouseOver(true);
-        context.SetCentralWidget(widget.release());
+        context_->SetCentralWidget(widget.release());
       }
     };
   }
--- a/Applications/Samples/TestPatternApplication.h	Mon Nov 05 10:04:56 2018 +0100
+++ b/Applications/Samples/TestPatternApplication.h	Mon Nov 05 10:06:18 2018 +0100
@@ -34,7 +34,7 @@
     class TestPatternApplication : public SampleApplicationBase
     {
     public:
-      virtual void DeclareCommandLineOptions(boost::program_options::options_description& options)
+      virtual void DeclareStartupOptions(boost::program_options::options_description& options)
       {
         boost::program_options::options_description generic("Sample options");
         generic.add_options()
@@ -44,8 +44,7 @@
         options.add(generic);    
       }
 
-      virtual void Initialize(BasicApplicationContext& context,
-                              IStatusBar& statusBar,
+      virtual void Initialize(IStatusBar& statusBar,
                               const boost::program_options::variables_map& parameters)
       {
         using namespace OrthancStone;
@@ -56,8 +55,8 @@
         layout->AddWidget(new TestCairoWidget(parameters["animate"].as<bool>()));
         layout->AddWidget(new TestWorldSceneWidget(parameters["animate"].as<bool>()));
 
-        context.SetCentralWidget(layout.release());
-        context.SetUpdateDelay(25);  // If animation, update the content each 25ms
+        context_->SetCentralWidget(layout.release());
+        context_->SetUpdateDelay(25);  // If animation, update the content each 25ms
       }
     };
   }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/Web/index.html	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,24 @@
+<!doctype html>
+
+<html lang="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>Wasm Samples</title>
+    <link href="samples-styles.css" rel="stylesheet" />
+
+<body>
+    <ul>
+      <li><a href="simple-viewer/simple-viewer.html">Simple Viewer Project (you may add ?studyId=XXX in the url)</a></li>
+      <li><a href="single-frame.html?instance=XXX">Single frame application (you must replace XXX by a valid instance id in the url)</a></li>
+      <li><a href="single-frame-editor.html?instance=XXX">Single frame editor application (you must replace XXX by a valid instance id in the url)</a></li>
+      <li><a href="simple-viewer-single-file.html">Simple Viewer Single file (to be replaced by other samples)</a></li>
+    </ul>
+</body>
+
+</html>
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/Web/simple-viewer-single-file.html	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,34 @@
+<!doctype html>
+
+<html lang="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>Simple Viewer</title>
+    <link href="samples-styles.css" rel="stylesheet" />
+
+<body>
+  <div id="breadcrumb">
+    <span id="patient-id"></span>
+    <span id="study-description"></span>
+    <span id="series-description"></span>
+  </div>
+  <div>
+    <canvas id="canvas" data-width-ratio="20" data-height-ratio="50"></canvas>
+    <canvas id="canvas2" data-width-ratio="70" data-height-ratio="50"></canvas>
+  </div>
+  <div id="toolbox">
+    <input tool-selector="line-measure" type="radio" name="radio-tool-selector" class="tool-selector">line
+    <input tool-selector="circle-measure" type="radio" name="radio-tool-selector" class="tool-selector">circle
+    <button action-trigger="action1" class="action-trigger">action1</button>
+    <button action-trigger="action2" class="action-trigger">action2</button>
+  </div>
+  <script type="text/javascript" src="app-simple-viewer-single-file.js"></script>
+</body>
+
+</html>
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/Web/simple-viewer-single-file.ts	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,51 @@
+///<reference path='../../../Platforms/Wasm/wasm-application-runner.ts'/>
+
+InitializeWasmApplication("OrthancStoneSimpleViewerSingleFile", "/orthanc");
+
+function SelectTool(toolName: string) {
+    var command = {
+        command: "selectTool",
+        args: {
+            toolName: toolName
+        }
+    };
+    SendMessageToStoneApplication(JSON.stringify(command));
+
+}
+
+function PerformAction(commandName: string) {
+    var command = {
+        command: commandName,
+        commandType: "simple",
+        args: {}
+    };
+    SendMessageToStoneApplication(JSON.stringify(command));
+}
+
+//initializes the buttons
+//-----------------------
+// install "SelectTool" handlers
+document.querySelectorAll("[tool-selector]").forEach((e) => {
+    console.log(e);
+    (e as HTMLInputElement).addEventListener("click", () => {
+        console.log(e);
+        SelectTool(e.attributes["tool-selector"].value);
+    });
+});
+
+// install "PerformAction" handlers
+document.querySelectorAll("[action-trigger]").forEach((e) => {
+    (e as HTMLInputElement).addEventListener("click", () => {
+        PerformAction(e.attributes["action-trigger"].value);
+    });
+});
+
+// this method is called "from the C++ code" when the StoneApplication is updated.
+// it can be used to update the UI of the application
+function UpdateWebApplication(statusUpdateMessage: string) {
+  console.log(statusUpdateMessage);
+  
+  if (statusUpdateMessage.startsWith("series-description=")) {
+      document.getElementById("series-description").innerText = statusUpdateMessage.split("=")[1];
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/Web/simple-viewer-single-file.tsconfig.json	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,9 @@
+{
+    "extends" : "./tsconfig-samples",
+    "compilerOptions": {
+        "outFile": "../build-web/app-simple-viewer-single-file.js"
+    },
+    "include" : [
+        "simple-viewer-single-file.ts"
+    ]
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/Web/single-frame-editor.html	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,22 @@
+<!doctype html>
+
+<html lang="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>Simple Viewer</title>
+    <link href="samples-styles.css" rel="stylesheet" />
+
+<body>
+  <div>
+    <canvas id="canvas" data-width-ratio="100" data-height-ratio="100"></canvas>
+  </div>
+  <script type="text/javascript" src="app-single-frame-editor.js"></script>
+</body>
+
+</html>
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/Web/single-frame-editor.ts	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,3 @@
+///<reference path='../../../Platforms/Wasm/wasm-application-runner.ts'/>
+
+InitializeWasmApplication("OrthancStoneSingleFrameEditor", "/orthanc");
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/Web/single-frame-editor.tsconfig.json	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,9 @@
+{
+    "extends" : "./tsconfig-samples",
+    "compilerOptions": {
+        "outFile": "../build-web/app-single-frame-editor.js"
+    },
+    "include" : [
+        "single-frame-editor.ts"
+    ]
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/Web/single-frame.html	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,22 @@
+<!doctype html>
+
+<html lang="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>Simple Viewer</title>
+    <link href="samples-styles.css" rel="stylesheet" />
+
+<body>
+  <div>
+    <canvas id="canvas" data-width-ratio="100" data-height-ratio="100"></canvas>
+  </div>
+  <script type="text/javascript" src="app-single-frame.js"></script>
+</body>
+
+</html>
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/Web/single-frame.ts	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,3 @@
+///<reference path='../../../Platforms/Wasm/wasm-application-runner.ts'/>
+
+InitializeWasmApplication("OrthancStoneSingleFrame", "/orthanc");
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/Web/single-frame.tsconfig.json	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,9 @@
+{
+    "extends" : "./tsconfig-samples",
+    "compilerOptions": {
+        "outFile": "../build-web/app-single-frame.js"
+    },
+    "include" : [
+        "single-frame.ts"
+    ]
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/Web/tsconfig-samples.json	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,11 @@
+{
+    "extends" : "../../../Platforms/Wasm/tsconfig-stone",
+    "compilerOptions": {
+        "sourceMap": false,
+        "lib" : [
+            "es2017",
+            "dom",
+            "dom.iterable"
+        ]
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/build-wasm.sh	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,17 @@
+#!/bin/bash
+
+set -e
+
+currentDir=$(pwd)
+samplesRootDir=$(pwd)
+
+mkdir -p $samplesRootDir/build-wasm
+cd $samplesRootDir/build-wasm
+
+source ~/Downloads/emsdk/emsdk_env.sh
+cmake -DCMAKE_TOOLCHAIN_FILE=${EMSCRIPTEN}/cmake/Modules/Platform/Emscripten.cmake -DCMAKE_BUILD_TYPE=Release -DSTONE_SOURCES_DIR=$currentDir/../../../orthanc-stone -DORTHANC_FRAMEWORK_SOURCE=path -DORTHANC_FRAMEWORK_ROOT=$currentDir/../../../orthanc -DALLOW_DOWNLOADS=ON .. -DENABLE_WASM=ON
+make -j 5
+
+echo "-- building the web application -- "
+cd $currentDir
+./build-web.sh
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/build-web.sh	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,43 @@
+#!/bin/bash
+
+set -e
+
+# this script currently assumes that the wasm code has been built on its side and is availabie in build-wasm/
+
+currentDir=$(pwd)
+samplesRootDir=$(pwd)
+
+outputDir=$samplesRootDir/build-web/
+mkdir -p $outputDir
+
+# files used by all single files samples
+cp $samplesRootDir/Web/index.html $outputDir
+cp $samplesRootDir/Web/samples-styles.css $outputDir
+
+# build simple-viewer-single-file (obsolete project)
+cp $samplesRootDir/Web/simple-viewer-single-file.html $outputDir
+tsc --allowJs --project $samplesRootDir/Web/simple-viewer-single-file.tsconfig.json
+cp $currentDir/build-wasm/OrthancStoneSimpleViewerSingleFile.js  $outputDir
+cp $currentDir/build-wasm/OrthancStoneSimpleViewerSingleFile.wasm  $outputDir
+
+# build single-frame
+cp $samplesRootDir/Web/single-frame.html $outputDir
+tsc --allowJs --project $samplesRootDir/Web/single-frame.tsconfig.json
+cp $currentDir/build-wasm/OrthancStoneSingleFrame.js  $outputDir
+cp $currentDir/build-wasm/OrthancStoneSingleFrame.wasm  $outputDir
+
+# build single-frame-editor
+cp $samplesRootDir/Web/single-frame-editor.html $outputDir
+tsc --allowJs --project $samplesRootDir/Web/single-frame-editor.tsconfig.json
+cp $currentDir/build-wasm/OrthancStoneSingleFrameEditor.js  $outputDir
+cp $currentDir/build-wasm/OrthancStoneSingleFrameEditor.wasm  $outputDir
+
+# build simple-viewer project
+mkdir -p $outputDir/simple-viewer/
+cp $samplesRootDir/SimpleViewer/Wasm/simple-viewer.html $outputDir/simple-viewer/
+cp $samplesRootDir/SimpleViewer/Wasm/styles.css $outputDir/simple-viewer/
+tsc --allowJs --project $samplesRootDir/SimpleViewer/Wasm/tsconfig-simple-viewer.json
+cp $currentDir/build-wasm/OrthancStoneSimpleViewer.js  $outputDir/simple-viewer/
+cp $currentDir/build-wasm/OrthancStoneSimpleViewer.wasm  $outputDir/simple-viewer/
+
+cd $currentDir
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/nginx.local.conf	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,44 @@
+# Local config to serve the WASM samples static files and reverse proxy Orthanc.
+# Uses port 9977 instead of 80.
+
+# `events` section is mandatory
+events {
+  worker_connections 1024; # Default: 1024
+}
+
+http {
+
+  # prevent nginx sync issues on OSX
+  proxy_buffering off;
+
+  server {
+    listen 9977 default_server;
+    client_max_body_size 4G;
+
+    # location may have to be adjusted depending on your OS and nginx install
+    include /etc/nginx/mime.types;
+    # if not in your system mime.types, add this line to support WASM:
+    # types {
+    #    application/wasm                      wasm; 
+    # }
+
+    # serve WASM static files
+    root build-web/;
+    location / {
+	}
+
+    # reverse proxy orthanc
+	location /orthanc/ {
+		rewrite /orthanc(.*) $1 break;
+		proxy_pass http://127.0.0.1:8042;
+		proxy_set_header Host $http_host;
+		proxy_set_header my-auth-header good-token;
+		proxy_request_buffering off;
+		proxy_max_temp_file_size 0;
+		client_max_body_size 0;
+	}
+
+
+  }
+  
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/samples-library.js	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,8 @@
+// this file contains the JS method you want to expose to C++ code
+
+// mergeInto(LibraryManager.library, {
+//    ScheduleRedraw: function() {
+//      ScheduleRedraw();
+//    }
+//  });
+  
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/tsconfig-stone.json	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,7 @@
+{
+    "include" : [
+        "../../Platforms/Wasm/stone-framework-loader.ts",
+        "../../Platforms/Wasm/wasm-application-runner.ts",
+        "../../Platforms/Wasm/wasm-viewport.ts"
+    ]
+}
--- a/Applications/Sdl/SdlEngine.cpp	Mon Nov 05 10:04:56 2018 +0100
+++ b/Applications/Sdl/SdlEngine.cpp	Mon Nov 05 10:06:18 2018 +0100
@@ -13,7 +13,7 @@
  * 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/>.
  **/
@@ -29,21 +29,20 @@
 
 namespace OrthancStone
 {
-  void SdlEngine::SetSize(BasicApplicationContext::ViewportLocker& locker,
-                          unsigned int width,
+  void SdlEngine::SetSize(unsigned int width,
                           unsigned int height)
   {
-    locker.GetViewport().SetSize(width, height);
+    context_.GetCentralViewport().SetSize(width, height);
     surface_.SetSize(width, height);
   }
-    
+
 
   void SdlEngine::RenderFrame()
   {
     if (viewportChanged_)
     {
-      BasicApplicationContext::ViewportLocker locker(context_);
-      surface_.Render(locker.GetViewport());
+      NativeStoneApplicationContext::GlobalMutexLocker locker(context_);
+      surface_.Render(context_.GetCentralViewport());
 
       viewportChanged_ = false;
     }
@@ -99,7 +98,7 @@
 
 
   SdlEngine::SdlEngine(SdlWindow& window,
-                       BasicApplicationContext& context) :
+                       NativeStoneApplicationContext& context) :
     window_(window),
     context_(context),
     surface_(window),
@@ -119,9 +118,9 @@
     const uint8_t* keyboardState = SDL_GetKeyboardState(&scancodeCount);
 
     {
-      BasicApplicationContext::ViewportLocker locker(context_);
-      SetSize(locker, window_.GetWidth(), window_.GetHeight());
-      locker.GetViewport().SetDefaultView();
+      NativeStoneApplicationContext::GlobalMutexLocker locker(context_);
+      SetSize(window_.GetWidth(), window_.GetHeight());
+      context_.GetCentralViewport().FitContent();
     }
     
     bool stop = false;
@@ -134,9 +133,9 @@
       while (!stop &&
              SDL_PollEvent(&event))
       {
-        BasicApplicationContext::ViewportLocker locker(context_);
+        NativeStoneApplicationContext::GlobalMutexLocker locker(context_);
 
-        if (event.type == SDL_QUIT) 
+        if (event.type == SDL_QUIT)
         {
           stop = true;
           break;
@@ -147,48 +146,48 @@
 
           switch (event.button.button)
           {
-            case SDL_BUTTON_LEFT:
-              locker.GetViewport().MouseDown(MouseButton_Left, event.button.x, event.button.y, modifiers);
-              break;
+          case SDL_BUTTON_LEFT:
+            context_.GetCentralViewport().MouseDown(MouseButton_Left, event.button.x, event.button.y, modifiers);
+            break;
             
-            case SDL_BUTTON_RIGHT:
-              locker.GetViewport().MouseDown(MouseButton_Right, event.button.x, event.button.y, modifiers);
-              break;
+          case SDL_BUTTON_RIGHT:
+            context_.GetCentralViewport().MouseDown(MouseButton_Right, event.button.x, event.button.y, modifiers);
+            break;
             
-            case SDL_BUTTON_MIDDLE:
-              locker.GetViewport().MouseDown(MouseButton_Middle, event.button.x, event.button.y, modifiers);
-              break;
+          case SDL_BUTTON_MIDDLE:
+            context_.GetCentralViewport().MouseDown(MouseButton_Middle, event.button.x, event.button.y, modifiers);
+            break;
 
-            default:
-              break;
+          default:
+            break;
           }
         }
         else if (event.type == SDL_MOUSEMOTION)
         {
-          locker.GetViewport().MouseMove(event.button.x, event.button.y);
+          context_.GetCentralViewport().MouseMove(event.button.x, event.button.y);
         }
         else if (event.type == SDL_MOUSEBUTTONUP)
         {
-          locker.GetViewport().MouseUp();
+          context_.GetCentralViewport().MouseUp();
         }
         else if (event.type == SDL_WINDOWEVENT)
         {
           switch (event.window.event)
           {
-            case SDL_WINDOWEVENT_LEAVE:
-              locker.GetViewport().MouseLeave();
-              break;
+          case SDL_WINDOWEVENT_LEAVE:
+            context_.GetCentralViewport().MouseLeave();
+            break;
 
-            case SDL_WINDOWEVENT_ENTER:
-              locker.GetViewport().MouseEnter();
-              break;
+          case SDL_WINDOWEVENT_ENTER:
+            context_.GetCentralViewport().MouseEnter();
+            break;
 
-            case SDL_WINDOWEVENT_SIZE_CHANGED:
-              SetSize(locker, event.window.data1, event.window.data2);
-              break;
+          case SDL_WINDOWEVENT_SIZE_CHANGED:
+            SetSize(event.window.data1, event.window.data2);
+            break;
 
-            default:
-              break;
+          default:
+            break;
           }
         }
         else if (event.type == SDL_MOUSEWHEEL)
@@ -200,11 +199,11 @@
 
           if (event.wheel.y > 0)
           {
-            locker.GetViewport().MouseWheel(MouseWheelDirection_Up, x, y, modifiers);
+            context_.GetCentralViewport().MouseWheel(MouseWheelDirection_Up, x, y, modifiers);
           }
           else if (event.wheel.y < 0)
           {
-            locker.GetViewport().MouseWheel(MouseWheelDirection_Down, x, y, modifiers);
+            context_.GetCentralViewport().MouseWheel(MouseWheelDirection_Down, x, y, modifiers);
           }
         }
         else if (event.type == SDL_KEYDOWN &&
@@ -214,53 +213,61 @@
 
           switch (event.key.keysym.sym)
           {
-            case SDLK_a:    locker.GetViewport().KeyPressed('a', modifiers);  break;
-            case SDLK_b:    locker.GetViewport().KeyPressed('b', modifiers);  break;
-            case SDLK_c:    locker.GetViewport().KeyPressed('c', modifiers);  break;
-            case SDLK_d:    locker.GetViewport().KeyPressed('d', modifiers);  break;
-            case SDLK_e:    locker.GetViewport().KeyPressed('e', modifiers);  break;
-            case SDLK_f:    window_.ToggleMaximize();                         break;
-            case SDLK_g:    locker.GetViewport().KeyPressed('g', modifiers);  break;
-            case SDLK_h:    locker.GetViewport().KeyPressed('h', modifiers);  break;
-            case SDLK_i:    locker.GetViewport().KeyPressed('i', modifiers);  break;
-            case SDLK_j:    locker.GetViewport().KeyPressed('j', modifiers);  break;
-            case SDLK_k:    locker.GetViewport().KeyPressed('k', modifiers);  break;
-            case SDLK_l:    locker.GetViewport().KeyPressed('l', modifiers);  break;
-            case SDLK_m:    locker.GetViewport().KeyPressed('m', modifiers);  break;
-            case SDLK_n:    locker.GetViewport().KeyPressed('n', modifiers);  break;
-            case SDLK_o:    locker.GetViewport().KeyPressed('o', modifiers);  break;
-            case SDLK_p:    locker.GetViewport().KeyPressed('p', modifiers);  break;
-            case SDLK_q:    stop = true;                                      break;
-            case SDLK_r:    locker.GetViewport().KeyPressed('r', modifiers);  break;
-            case SDLK_s:    locker.GetViewport().KeyPressed('s', modifiers);  break;
-            case SDLK_t:    locker.GetViewport().KeyPressed('t', modifiers);  break;
-            case SDLK_u:    locker.GetViewport().KeyPressed('u', modifiers);  break;
-            case SDLK_v:    locker.GetViewport().KeyPressed('v', modifiers);  break;
-            case SDLK_w:    locker.GetViewport().KeyPressed('w', modifiers);  break;
-            case SDLK_x:    locker.GetViewport().KeyPressed('x', modifiers);  break;
-            case SDLK_y:    locker.GetViewport().KeyPressed('y', modifiers);  break;
-            case SDLK_z:    locker.GetViewport().KeyPressed('z', modifiers);  break;
-            case SDLK_KP_0: locker.GetViewport().KeyPressed('0', modifiers);  break;
-            case SDLK_KP_1: locker.GetViewport().KeyPressed('1', modifiers);  break;
-            case SDLK_KP_2: locker.GetViewport().KeyPressed('2', modifiers);  break;
-            case SDLK_KP_3: locker.GetViewport().KeyPressed('3', modifiers);  break;
-            case SDLK_KP_4: locker.GetViewport().KeyPressed('4', modifiers);  break;
-            case SDLK_KP_5: locker.GetViewport().KeyPressed('5', modifiers);  break;
-            case SDLK_KP_6: locker.GetViewport().KeyPressed('6', modifiers);  break;
-            case SDLK_KP_7: locker.GetViewport().KeyPressed('7', modifiers);  break;
-            case SDLK_KP_8: locker.GetViewport().KeyPressed('8', modifiers);  break;
-            case SDLK_KP_9: locker.GetViewport().KeyPressed('9', modifiers);  break;
+          case SDLK_a:    context_.GetCentralViewport().KeyPressed(KeyboardKeys_Generic, 'a', modifiers);  break;
+          case SDLK_b:    context_.GetCentralViewport().KeyPressed(KeyboardKeys_Generic, 'b', modifiers);  break;
+          case SDLK_c:    context_.GetCentralViewport().KeyPressed(KeyboardKeys_Generic, 'c', modifiers);  break;
+          case SDLK_d:    context_.GetCentralViewport().KeyPressed(KeyboardKeys_Generic, 'd', modifiers);  break;
+          case SDLK_e:    context_.GetCentralViewport().KeyPressed(KeyboardKeys_Generic, 'e', modifiers);  break;
+          case SDLK_f:    window_.ToggleMaximize();                         break;
+          case SDLK_g:    context_.GetCentralViewport().KeyPressed(KeyboardKeys_Generic, 'g', modifiers);  break;
+          case SDLK_h:    context_.GetCentralViewport().KeyPressed(KeyboardKeys_Generic, 'h', modifiers);  break;
+          case SDLK_i:    context_.GetCentralViewport().KeyPressed(KeyboardKeys_Generic, 'i', modifiers);  break;
+          case SDLK_j:    context_.GetCentralViewport().KeyPressed(KeyboardKeys_Generic, 'j', modifiers);  break;
+          case SDLK_k:    context_.GetCentralViewport().KeyPressed(KeyboardKeys_Generic, 'k', modifiers);  break;
+          case SDLK_l:    context_.GetCentralViewport().KeyPressed(KeyboardKeys_Generic, 'l', modifiers);  break;
+          case SDLK_m:    context_.GetCentralViewport().KeyPressed(KeyboardKeys_Generic, 'm', modifiers);  break;
+          case SDLK_n:    context_.GetCentralViewport().KeyPressed(KeyboardKeys_Generic, 'n', modifiers);  break;
+          case SDLK_o:    context_.GetCentralViewport().KeyPressed(KeyboardKeys_Generic, 'o', modifiers);  break;
+          case SDLK_p:    context_.GetCentralViewport().KeyPressed(KeyboardKeys_Generic, 'p', modifiers);  break;
+          case SDLK_q:    stop = true;                                      break;
+          case SDLK_r:    context_.GetCentralViewport().KeyPressed(KeyboardKeys_Generic, 'r', modifiers);  break;
+          case SDLK_s:    context_.GetCentralViewport().KeyPressed(KeyboardKeys_Generic, 's', modifiers);  break;
+          case SDLK_t:    context_.GetCentralViewport().KeyPressed(KeyboardKeys_Generic, 't', modifiers);  break;
+          case SDLK_u:    context_.GetCentralViewport().KeyPressed(KeyboardKeys_Generic, 'u', modifiers);  break;
+          case SDLK_v:    context_.GetCentralViewport().KeyPressed(KeyboardKeys_Generic, 'v', modifiers);  break;
+          case SDLK_w:    context_.GetCentralViewport().KeyPressed(KeyboardKeys_Generic, 'w', modifiers);  break;
+          case SDLK_x:    context_.GetCentralViewport().KeyPressed(KeyboardKeys_Generic, 'x', modifiers);  break;
+          case SDLK_y:    context_.GetCentralViewport().KeyPressed(KeyboardKeys_Generic, 'y', modifiers);  break;
+          case SDLK_z:    context_.GetCentralViewport().KeyPressed(KeyboardKeys_Generic, 'z', modifiers);  break;
+          case SDLK_KP_0: context_.GetCentralViewport().KeyPressed(KeyboardKeys_Generic, '0', modifiers);  break;
+          case SDLK_KP_1: context_.GetCentralViewport().KeyPressed(KeyboardKeys_Generic, '1', modifiers);  break;
+          case SDLK_KP_2: context_.GetCentralViewport().KeyPressed(KeyboardKeys_Generic, '2', modifiers);  break;
+          case SDLK_KP_3: context_.GetCentralViewport().KeyPressed(KeyboardKeys_Generic, '3', modifiers);  break;
+          case SDLK_KP_4: context_.GetCentralViewport().KeyPressed(KeyboardKeys_Generic, '4', modifiers);  break;
+          case SDLK_KP_5: context_.GetCentralViewport().KeyPressed(KeyboardKeys_Generic, '5', modifiers);  break;
+          case SDLK_KP_6: context_.GetCentralViewport().KeyPressed(KeyboardKeys_Generic, '6', modifiers);  break;
+          case SDLK_KP_7: context_.GetCentralViewport().KeyPressed(KeyboardKeys_Generic, '7', modifiers);  break;
+          case SDLK_KP_8: context_.GetCentralViewport().KeyPressed(KeyboardKeys_Generic, '8', modifiers);  break;
+          case SDLK_KP_9: context_.GetCentralViewport().KeyPressed(KeyboardKeys_Generic, '9', modifiers);  break;
 
-            case SDLK_PLUS:
-            case SDLK_KP_PLUS:
-              locker.GetViewport().KeyPressed('+', modifiers);  break;
+          case SDLK_PLUS:
+          case SDLK_KP_PLUS:
+            context_.GetCentralViewport().KeyPressed(KeyboardKeys_Generic, '+', modifiers);  break;
+
+          case SDLK_MINUS:
+          case SDLK_KP_MINUS:
+            context_.GetCentralViewport().KeyPressed(KeyboardKeys_Generic, '-', modifiers);  break;
 
-            case SDLK_MINUS:
-            case SDLK_KP_MINUS:
-              locker.GetViewport().KeyPressed('-', modifiers);  break;
-
-            default:
-              break;
+          case SDLK_RIGHT:
+            context_.GetCentralViewport().KeyPressed(KeyboardKeys_Right, 0, modifiers);  break;
+          case SDLK_LEFT:
+            context_.GetCentralViewport().KeyPressed(KeyboardKeys_Left, 0, modifiers);  break;
+          case SDLK_UP:
+            context_.GetCentralViewport().KeyPressed(KeyboardKeys_Up, 0, modifiers);  break;
+          case SDLK_DOWN:
+            context_.GetCentralViewport().KeyPressed(KeyboardKeys_Down, 0, modifiers);  break;
+          default:
+            break;
           }
         }
       }
--- a/Applications/Sdl/SdlEngine.h	Mon Nov 05 10:04:56 2018 +0100
+++ b/Applications/Sdl/SdlEngine.h	Mon Nov 05 10:06:18 2018 +0100
@@ -24,7 +24,7 @@
 #if ORTHANC_ENABLE_SDL == 1
 
 #include "SdlCairoSurface.h"
-#include "../BasicApplicationContext.h"
+#include "../Generic/NativeStoneApplicationContext.h"
 
 namespace OrthancStone
 {
@@ -32,12 +32,11 @@
   {
   private:
     SdlWindow&                window_;
-    BasicApplicationContext&  context_;
+    NativeStoneApplicationContext&  context_;
     SdlCairoSurface           surface_;
     bool                      viewportChanged_;
 
-    void SetSize(BasicApplicationContext::ViewportLocker& locker,
-                 unsigned int width,
+    void SetSize(unsigned int width,
                  unsigned int height);
     
     void RenderFrame();
@@ -47,11 +46,11 @@
 
   public:
     SdlEngine(SdlWindow& window,
-              BasicApplicationContext& context);
+              NativeStoneApplicationContext& context);
   
     virtual ~SdlEngine();
 
-    virtual void NotifyChange(const IViewport& viewport)
+    virtual void OnViewportContentChanged(const IViewport& viewport)
     {
       viewportChanged_ = true;
     }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Sdl/SdlStoneApplicationRunner.cpp	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,127 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 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/>.
+ **/
+
+
+#if ORTHANC_ENABLE_SDL != 1
+#error this file shall be included only with the ORTHANC_ENABLE_SDL set to 1
+#endif
+
+#include "SdlStoneApplicationRunner.h"
+#include <boost/program_options.hpp>
+
+#include "../../Framework/Toolbox/MessagingToolbox.h"
+#include "SdlEngine.h"
+
+#include <Core/Logging.h>
+#include <Core/HttpClient.h>
+#include <Core/Toolbox.h>
+#include <Plugins/Samples/Common/OrthancHttpConnection.h>
+#include "../../Platforms/Generic/OracleWebService.h"
+
+namespace OrthancStone
+{
+  void SdlStoneApplicationRunner::Initialize()
+  {
+    SdlWindow::GlobalInitialize();
+  }
+
+  void SdlStoneApplicationRunner::DeclareCommandLineOptions(boost::program_options::options_description& options)
+  {
+    boost::program_options::options_description sdl("SDL options");
+    sdl.add_options()
+        ("width", boost::program_options::value<int>()->default_value(1024), "Initial width of the SDL window")
+        ("height", boost::program_options::value<int>()->default_value(768), "Initial height of the SDL window")
+        ("opengl", boost::program_options::value<bool>()->default_value(true), "Enable OpenGL in SDL")
+        ;
+
+    options.add(sdl);
+  }
+
+  void SdlStoneApplicationRunner::ParseCommandLineOptions(const boost::program_options::variables_map& parameters)
+  {
+    if (!parameters.count("width") ||
+        !parameters.count("height") ||
+        !parameters.count("opengl"))
+    {
+      LOG(ERROR) << "Parameter \"width\", \"height\" or \"opengl\" is missing";
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+
+    int w = parameters["width"].as<int>();
+    int h = parameters["height"].as<int>();
+    if (w <= 0 || h <= 0)
+    {
+      LOG(ERROR) << "Parameters \"width\" and \"height\" must be positive";
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+
+    width_ = static_cast<unsigned int>(w);
+    height_ = static_cast<unsigned int>(h);
+    LOG(WARNING) << "Initial display size: " << width_ << "x" << height_;
+
+    enableOpenGl_ = parameters["opengl"].as<bool>();
+    if (enableOpenGl_)
+    {
+      LOG(WARNING) << "OpenGL is enabled, disable it with option \"--opengl=off\" if the application crashes";
+    }
+    else
+    {
+      LOG(WARNING) << "OpenGL is disabled, enable it with option \"--opengl=on\" for best performance";
+    }
+
+  }
+
+  void SdlStoneApplicationRunner::Run(NativeStoneApplicationContext& context, const std::string& title, int argc, char* argv[])
+  {
+    /**************************************************************
+     * Run the application inside a SDL window
+     **************************************************************/
+
+    LOG(WARNING) << "Starting the application";
+
+    SdlWindow window(title.c_str(), width_, height_, enableOpenGl_);
+    SdlEngine sdl(window, context);
+
+    {
+      NativeStoneApplicationContext::GlobalMutexLocker locker(context);
+      context.GetCentralViewport().Register(sdl);  // (*)
+    }
+
+    context.Start();
+    sdl.Run();
+
+    LOG(WARNING) << "Stopping the application";
+
+    // Don't move the "Stop()" command below out of the block,
+    // otherwise the application might crash, because the
+    // "SdlEngine" is an observer of the viewport (*) and the
+    // update thread started by "context.Start()" would call a
+    // destructed object (the "SdlEngine" is deleted with the
+    // lexical scope).
+    context.Stop();
+  }
+
+  void SdlStoneApplicationRunner::Finalize()
+  {
+    SdlWindow::GlobalFinalize();
+  }
+
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Sdl/SdlStoneApplicationRunner.h	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,53 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 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
+
+#include "../Generic/NativeStoneApplicationRunner.h"
+
+#if ORTHANC_ENABLE_SDL != 1
+#error this file shall be included only with the ORTHANC_ENABLE_SDL set to 1
+#endif
+
+#include <SDL.h>   // Necessary to avoid undefined reference to `SDL_main'
+
+namespace OrthancStone
+{
+  class SdlStoneApplicationRunner : public NativeStoneApplicationRunner
+  {
+    unsigned int width_;
+    unsigned int height_;
+    bool enableOpenGl_;
+  public:
+    SdlStoneApplicationRunner(MessageBroker& broker,
+                                 IStoneApplication& application)
+      : NativeStoneApplicationRunner(broker, application)
+    {
+    }
+
+    virtual void Initialize();
+    virtual void DeclareCommandLineOptions(boost::program_options::options_description& options);
+    virtual void Run(NativeStoneApplicationContext& context, const std::string& title, int argc, char* argv[]);
+    virtual void ParseCommandLineOptions(const boost::program_options::variables_map& parameters);
+    virtual void Finalize();
+  };
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/StoneApplicationContext.cpp	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,26 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 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 "StoneApplicationContext.h"
+
+namespace OrthancStone
+{
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/StoneApplicationContext.h	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,56 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 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
+
+#include "../Framework/Toolbox/IWebService.h"
+#include "../Framework/Viewport/WidgetViewport.h"
+
+#include <list>
+
+namespace OrthancStone
+{
+  // a StoneApplicationContext contains the services that a StoneApplication
+  // uses and that depends on the environment in which the Application executes.
+  // I.e, the StoneApplicationContext provides a WebService interface such that
+  // the StoneApplication can perform HTTP requests.  In a WASM environment,
+  // the WebService is provided by the browser while, in a native environment,
+  // the WebService is provided by the OracleWebService (a C++ Http client)
+  class StoneApplicationContext : public boost::noncopyable
+  {
+
+  protected:
+    IWebService* webService_;
+  public:
+    StoneApplicationContext()
+      : webService_(NULL)
+    {
+    }
+
+    IWebService& GetWebService() {return *webService_;}
+    void SetWebService(IWebService& webService)
+    {
+      webService_ = &webService;
+    }
+
+    virtual ~StoneApplicationContext() {}
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Wasm/StartupParametersBuilder.cpp	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,43 @@
+#include "StartupParametersBuilder.h"
+
+namespace OrthancStone
+{
+    void StartupParametersBuilder::Clear() {
+        startupParameters_.clear();
+    }
+
+    void StartupParametersBuilder::SetStartupParameter(const char* name, const char* value) {
+        startupParameters_.push_back(std::make_tuple(name, value));
+    }
+
+    void StartupParametersBuilder::GetStartupParameters(boost::program_options::variables_map& parameters, const boost::program_options::options_description& options) {
+        
+        const char* argv[startupParameters_.size() + 1];
+        int argCounter = 0;
+        argv[0] = "Toto.exe";
+        argCounter++;
+
+        std::string cmdLine = "";
+        for (StartupParameters::const_iterator it = startupParameters_.begin(); it != startupParameters_.end(); it++) {
+            char* arg = new char[128];
+            snprintf(arg, 128, "--%s=%s", std::get<0>(*it).c_str(), std::get<1>(*it).c_str());
+            argv[argCounter] = arg;
+            cmdLine = cmdLine + " --" + std::get<0>(*it) + "=" + std::get<1>(*it);
+            argCounter++;
+        }
+
+        printf("simulated cmdLine = %s\n", cmdLine.c_str());
+
+        try
+        {
+            boost::program_options::store(boost::program_options::command_line_parser(argCounter, argv).
+                                            options(options).run(), parameters);
+            boost::program_options::notify(parameters);
+        }
+        catch (boost::program_options::error& e)
+        {
+            printf("Error while parsing the command-line arguments: %s\n", e.what());
+        }
+
+    }
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Wasm/StartupParametersBuilder.h	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,50 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 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
+
+#include <boost/program_options.hpp>
+#include <tuple>
+
+#if ORTHANC_ENABLE_SDL == 1
+#error this file shall be included only with the ORTHANC_ENABLE_SDL set to 0
+#endif
+
+namespace OrthancStone
+{
+  // This class is used to generate boost program options from a dico.
+  // In a Wasm context, startup options are passed as URI arguments that
+  // are then passed to this class as a dico.
+  // This class regenerates a fake command-line and parses it to produce
+  // the same output as if the app was started at command-line.
+  class StartupParametersBuilder
+  {
+    typedef std::list<std::tuple<std::string, std::string>> StartupParameters;
+    StartupParameters startupParameters_;
+
+  public:
+
+    void Clear();
+    void SetStartupParameter(const char* name, const char* value);
+    void GetStartupParameters(boost::program_options::variables_map& parameters_, const boost::program_options::options_description& options);
+  };
+
+}
--- a/Framework/Layers/CircleMeasureTracker.cpp	Mon Nov 05 10:04:56 2018 +0100
+++ b/Framework/Layers/CircleMeasureTracker.cpp	Mon Nov 05 10:06:18 2018 +0100
@@ -24,8 +24,6 @@
 
 #include "CircleMeasureTracker.h"
 
-#include "../Viewport/CairoFont.h"
-
 #include <stdio.h>
 
 namespace OrthancStone
@@ -37,14 +35,14 @@
                                              uint8_t red,
                                              uint8_t green,
                                              uint8_t blue,
-                                             unsigned int fontSize) :
+                                             const Orthanc::Font& font) :
     statusBar_(statusBar),
     slice_(slice),
     x1_(x),
     y1_(y),
     x2_(x),
     y2_(y),
-    fontSize_(fontSize)
+    font_(font)
   {
     color_[0] = red;
     color_[1] = green;
@@ -73,12 +71,7 @@
     cairo_stroke(cr);
     cairo_restore(cr);
 
-    if (fontSize_ != 0)
-    {
-      cairo_move_to(cr, x, y);
-      CairoFont font("sans-serif", CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_NORMAL);
-      font.Draw(context, FormatRadius(), static_cast<double>(fontSize_) / zoom);
-    }
+    context.DrawText(font_, FormatRadius(), x, y, BitmapAnchor_Center);
   }
     
 
@@ -97,7 +90,9 @@
     return buf;
   }
 
-  void CircleMeasureTracker::MouseMove(double x,
+  void CircleMeasureTracker::MouseMove(int displayX,
+                                       int displayY,
+                                       double x,
                                        double y)
   {
     x2_ = x;
--- a/Framework/Layers/CircleMeasureTracker.h	Mon Nov 05 10:04:56 2018 +0100
+++ b/Framework/Layers/CircleMeasureTracker.h	Mon Nov 05 10:06:18 2018 +0100
@@ -26,19 +26,21 @@
 #include "../Viewport/IStatusBar.h"
 #include "../Toolbox/CoordinateSystem3D.h"
 
+#include <Core/Images/Font.h>
+
 namespace OrthancStone
 {
   class CircleMeasureTracker : public IWorldSceneMouseTracker
   {
   private:
-    IStatusBar*         statusBar_;
-    CoordinateSystem3D  slice_;
-    double              x1_;
-    double              y1_;
-    double              x2_;
-    double              y2_;
-    uint8_t             color_[3];
-    unsigned int        fontSize_;
+    IStatusBar*           statusBar_;
+    CoordinateSystem3D    slice_;
+    double                x1_;
+    double                y1_;
+    double                x2_;
+    double                y2_;
+    uint8_t               color_[3];
+    const Orthanc::Font&  font_;
 
   public:
     CircleMeasureTracker(IStatusBar* statusBar,
@@ -48,8 +50,13 @@
                          uint8_t red,
                          uint8_t green,
                          uint8_t blue,
-                         unsigned int fontSize);
+                         const Orthanc::Font& font);
     
+    virtual bool HasRender() const
+    {
+      return true;
+    }
+
     virtual void Render(CairoContext& context,
                         double zoom);
     
@@ -62,7 +69,9 @@
       // Possibly create a new landmark "volume" with the circle in subclasses
     }
 
-    virtual void MouseMove(double x,
+    virtual void MouseMove(int displayX,
+                           int displayY,
+                           double x,
                            double y);
   };
 }
--- a/Framework/Layers/DicomStructureSetRendererFactory.h	Mon Nov 05 10:04:56 2018 +0100
+++ b/Framework/Layers/DicomStructureSetRendererFactory.h	Mon Nov 05 10:06:18 2018 +0100
@@ -37,21 +37,22 @@
     {
       LayerSourceBase::NotifyGeometryReady();
     }
-      
+
     virtual void NotifyGeometryError(const IVolumeLoader& loader)
     {
       LayerSourceBase::NotifyGeometryError();
     }
-      
+
     virtual void NotifyContentChange(const IVolumeLoader& loader)
     {
       LayerSourceBase::NotifyContentChange();
     }
-    
+
     StructureSetLoader& loader_;
 
   public:
-    DicomStructureSetRendererFactory(StructureSetLoader& loader) :
+    DicomStructureSetRendererFactory(MessageBroker& broker, StructureSetLoader& loader) :
+      LayerSourceBase(broker),
       loader_(loader)
     {
       loader_.Register(*this);
--- a/Framework/Layers/GrayscaleFrameRenderer.cpp	Mon Nov 05 10:04:56 2018 +0100
+++ b/Framework/Layers/GrayscaleFrameRenderer.cpp	Mon Nov 05 10:06:18 2018 +0100
@@ -27,6 +27,8 @@
 {
   CairoSurface* GrayscaleFrameRenderer::GenerateDisplay(const RenderStyle& style)
   {
+    assert(frame_->GetFormat() == Orthanc::PixelFormat_Float32);
+
     std::auto_ptr<CairoSurface> result;
 
     float windowCenter, windowWidth;
@@ -82,7 +84,7 @@
             v = static_cast<uint8_t>(255.0f * (*p - x0) / (x1 - x0));
           }
 
-          if (style.reverse_)
+          if (style.reverse_ ^ (photometric_ == Orthanc::PhotometricInterpretation_Monochrome1))
           {
             v = 255 - v;
           }
@@ -119,14 +121,15 @@
     FrameRenderer(frameSlice, pixelSpacingX, pixelSpacingY, isFullQuality),
     frame_(frame),
     defaultWindowCenter_(converter.GetDefaultWindowCenter()),
-    defaultWindowWidth_(converter.GetDefaultWindowWidth())
+    defaultWindowWidth_(converter.GetDefaultWindowWidth()),
+    photometric_(converter.GetPhotometricInterpretation())
   {
     if (frame == NULL)
     {
       throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
     }
 
-    converter.ConvertFrame(frame_);
+    converter.ConvertFrameInplace(frame_);
     assert(frame_.get() != NULL);
 
     if (frame_->GetFormat() != Orthanc::PixelFormat_Float32)
--- a/Framework/Layers/GrayscaleFrameRenderer.h	Mon Nov 05 10:04:56 2018 +0100
+++ b/Framework/Layers/GrayscaleFrameRenderer.h	Mon Nov 05 10:06:18 2018 +0100
@@ -32,6 +32,7 @@
     std::auto_ptr<Orthanc::ImageAccessor>   frame_;  // In Float32
     float                                   defaultWindowCenter_;
     float                                   defaultWindowWidth_;
+    Orthanc::PhotometricInterpretation      photometric_;
 
   protected:
     virtual CairoSurface* GenerateDisplay(const RenderStyle& style);
--- a/Framework/Layers/ILayerSource.h	Mon Nov 05 10:04:56 2018 +0100
+++ b/Framework/Layers/ILayerSource.h	Mon Nov 05 10:06:18 2018 +0100
@@ -13,7 +13,7 @@
  * 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/>.
  **/
@@ -23,48 +23,77 @@
 
 #include "ILayerRenderer.h"
 #include "../Toolbox/Slice.h"
+#include "../../Framework/Messages/IObservable.h"
+#include "../../Framework/Messages/IMessage.h"
+#include "Core/Images/Image.h"
+#include <boost/shared_ptr.hpp>
 
 namespace OrthancStone
 {
-  class ILayerSource : public boost::noncopyable
+  class ILayerSource : public IObservable
   {
   public:
-    class IObserver : public boost::noncopyable
+
+    typedef OriginMessage<MessageType_LayerSource_GeometryReady, ILayerSource> GeometryReadyMessage;
+    typedef OriginMessage<MessageType_LayerSource_GeometryError, ILayerSource> GeometryErrorMessage;
+    typedef OriginMessage<MessageType_LayerSource_ContentChanged, ILayerSource> ContentChangedMessage;
+
+    struct SliceChangedMessage : public OriginMessage<MessageType_LayerSource_SliceChanged, ILayerSource>
     {
-    public:
-      virtual ~IObserver()
+      const Slice& slice_;
+      SliceChangedMessage(ILayerSource& origin, const Slice& slice)
+        : OriginMessage(origin),
+          slice_(slice)
       {
       }
+    };
 
-      // Triggered as soon as the source has enough information to
-      // answer to "GetExtent()"
-      virtual void NotifyGeometryReady(const ILayerSource& source) = 0;
-      
-      virtual void NotifyGeometryError(const ILayerSource& source) = 0;
-      
-      // Triggered if the content of several slices in the source
-      // volume has changed
-      virtual void NotifyContentChange(const ILayerSource& source) = 0;
+    struct LayerReadyMessage : public OriginMessage<MessageType_LayerSource_LayerReady, ILayerSource>
+    {
+      std::auto_ptr<ILayerRenderer>& renderer_;
+      const CoordinateSystem3D& slice_;
+      bool isError_;
 
-      // Triggered if the content of some individual slice in the
-      // source volume has changed
-      virtual void NotifySliceChange(const ILayerSource& source,
-                                     const Slice& slice) = 0;
- 
-      // The layer must be deleted by the observer that releases the
-      // std::auto_ptr
-      virtual void NotifyLayerReady(std::auto_ptr<ILayerRenderer>& layer,
-                                    const ILayerSource& source,
-                                    const CoordinateSystem3D& slice,
-                                    bool isError) = 0;  // TODO Shouldn't this be separate as NotifyLayerError?
+      LayerReadyMessage(ILayerSource& origin,
+                        std::auto_ptr<ILayerRenderer>& layer,
+                        const CoordinateSystem3D& slice,
+                        bool isError  // TODO Shouldn't this be separate as NotifyLayerError?
+                        )
+        : OriginMessage(origin),
+          renderer_(layer),
+          slice_(slice),
+          isError_(isError)
+      {
+      }
+    };
+
+    struct ImageReadyMessage : public OriginMessage<MessageType_LayerSource_ImageReady, ILayerSource>
+    {
+      boost::shared_ptr<Orthanc::ImageAccessor> image_;
+      SliceImageQuality                         imageQuality_;
+      const Slice&                              slice_;
+
+      ImageReadyMessage(ILayerSource& origin,
+                        boost::shared_ptr<Orthanc::ImageAccessor> image,
+                        SliceImageQuality imageQuality,
+                        const Slice& slice
+                        )
+        : OriginMessage(origin),
+          image_(image),
+          imageQuality_(imageQuality),
+          slice_(slice)
+      {
+      }
     };
     
+    ILayerSource(MessageBroker& broker)
+      : IObservable(broker)
+    {}
+
     virtual ~ILayerSource()
     {
     }
 
-    virtual void Register(IObserver& observer) = 0;
-
     virtual bool GetExtent(std::vector<Vector>& points,
                            const CoordinateSystem3D& viewportSlice) = 0;
 
--- a/Framework/Layers/LayerSourceBase.cpp	Mon Nov 05 10:04:56 2018 +0100
+++ b/Framework/Layers/LayerSourceBase.cpp	Mon Nov 05 10:06:18 2018 +0100
@@ -25,63 +25,39 @@
 
 namespace OrthancStone
 {
-  namespace
-  {
-    class LayerReadyFunctor : public boost::noncopyable
-    {
-    private:
-      std::auto_ptr<ILayerRenderer>  layer_;
-      const CoordinateSystem3D&      slice_;
-      bool                           isError_;
-      
-    public:
-      LayerReadyFunctor(ILayerRenderer* layer,
-                        const CoordinateSystem3D& slice,
-                        bool isError) :
-        layer_(layer),
-        slice_(slice),
-        isError_(isError)
-      {
-      }
-
-      void operator() (ILayerSource::IObserver& observer,
-                       const ILayerSource& source)
-      {
-        observer.NotifyLayerReady(layer_, source, slice_, isError_);
-      }
-    };
-  }
-
   void LayerSourceBase::NotifyGeometryReady()
   {
-    observers_.Apply(*this, &IObserver::NotifyGeometryReady);
+    EmitMessage(ILayerSource::GeometryReadyMessage(*this));
   }
     
   void LayerSourceBase::NotifyGeometryError()
   {
-    observers_.Apply(*this, &IObserver::NotifyGeometryError);
-  }  
+    EmitMessage(ILayerSource::GeometryErrorMessage(*this));
+  }
     
   void LayerSourceBase::NotifyContentChange()
   {
-    observers_.Apply(*this, &IObserver::NotifyContentChange);
+    EmitMessage(ILayerSource::ContentChangedMessage(*this));
   }
 
   void LayerSourceBase::NotifySliceChange(const Slice& slice)
   {
-    observers_.Apply(*this, &IObserver::NotifySliceChange, slice);
+    EmitMessage(ILayerSource::SliceChangedMessage(*this, slice));
   }
 
   void LayerSourceBase::NotifyLayerReady(ILayerRenderer* layer,
                                          const CoordinateSystem3D& slice,
                                          bool isError)
   {
-    LayerReadyFunctor functor(layer, slice, isError);
-    observers_.Notify(*this, functor);
+    std::auto_ptr<ILayerRenderer> renderer(layer);
+    EmitMessage(ILayerSource::LayerReadyMessage(*this, renderer, slice, isError));
   }
 
-  void LayerSourceBase::Register(IObserver& observer)
+  void LayerSourceBase::NotifyImageReady(boost::shared_ptr<Orthanc::ImageAccessor> image,
+                                         SliceImageQuality imageQuality,
+                                         const Slice& slice)
   {
-    observers_.Register(observer);
+    EmitMessage(ILayerSource::ImageReadyMessage(*this, image, imageQuality, slice));
   }
+
 }
--- a/Framework/Layers/LayerSourceBase.h	Mon Nov 05 10:04:56 2018 +0100
+++ b/Framework/Layers/LayerSourceBase.h	Mon Nov 05 10:06:18 2018 +0100
@@ -13,7 +13,7 @@
  * 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/>.
  **/
@@ -26,13 +26,10 @@
 
 namespace OrthancStone
 {
+  class SmartLoader;
+
   class LayerSourceBase : public ILayerSource
   {
-  private:
-    typedef ObserversRegistry<ILayerSource, IObserver>  Observers;
-
-    Observers  observers_;
-
   protected:
     void NotifyGeometryReady();
     
@@ -46,7 +43,15 @@
                           const CoordinateSystem3D& slice,
                           bool isError);
 
-  public:
-    virtual void Register(IObserver& observer);
+    void NotifyImageReady(boost::shared_ptr<Orthanc::ImageAccessor> image,
+                          SliceImageQuality imageQuality,
+                          const Slice& slice);
+
+    LayerSourceBase(MessageBroker& broker)
+      : ILayerSource(broker)
+    {
+    }
+
+    friend class SmartLoader;
   };
 }
--- a/Framework/Layers/LineMeasureTracker.cpp	Mon Nov 05 10:04:56 2018 +0100
+++ b/Framework/Layers/LineMeasureTracker.cpp	Mon Nov 05 10:06:18 2018 +0100
@@ -21,8 +21,6 @@
 
 #include "LineMeasureTracker.h"
 
-#include "../Viewport/CairoFont.h"
-
 #include <stdio.h>
 
 namespace OrthancStone
@@ -34,14 +32,14 @@
                                          uint8_t red,
                                          uint8_t green,
                                          uint8_t blue,
-                                         unsigned int fontSize) :
+                                         const Orthanc::Font& font) :
     statusBar_(statusBar),
     slice_(slice),
     x1_(x),
     y1_(y),
     x2_(x),
     y2_(y),
-    fontSize_(fontSize)
+    font_(font)
   {
     color_[0] = red;
     color_[1] = green;
@@ -60,11 +58,13 @@
     cairo_line_to(cr, x2_, y2_);
     cairo_stroke(cr);
 
-    if (fontSize_ != 0)
+    if (y2_ - y1_ < 0)
     {
-      cairo_move_to(cr, x2_, y2_ - static_cast<double>(fontSize_) / zoom);
-      CairoFont font("sans-serif", CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_NORMAL);
-      font.Draw(context, FormatLength(), static_cast<double>(fontSize_) / zoom);
+      context.DrawText(font_, FormatLength(), x2_, y2_ - 5, BitmapAnchor_BottomCenter);
+    }
+    else
+    {
+      context.DrawText(font_, FormatLength(), x2_, y2_ + 5, BitmapAnchor_TopCenter);
     }
   }
     
@@ -84,7 +84,9 @@
     return buf;
   }
 
-  void LineMeasureTracker::MouseMove(double x,
+  void LineMeasureTracker::MouseMove(int displayX,
+                                     int displayY,
+                                     double x,
                                      double y)
   {
     x2_ = x;
--- a/Framework/Layers/LineMeasureTracker.h	Mon Nov 05 10:04:56 2018 +0100
+++ b/Framework/Layers/LineMeasureTracker.h	Mon Nov 05 10:06:18 2018 +0100
@@ -31,14 +31,15 @@
   class LineMeasureTracker : public IWorldSceneMouseTracker
   {
   private:
-    IStatusBar*         statusBar_;
-    CoordinateSystem3D  slice_;
-    double              x1_;
-    double              y1_;
-    double              x2_;
-    double              y2_;
-    uint8_t             color_[3];
-    unsigned int        fontSize_;
+    IStatusBar*           statusBar_;
+    CoordinateSystem3D    slice_;
+    double                x1_;
+    double                y1_;
+    double                x2_;
+    double                y2_;
+    uint8_t               color_[3];
+    unsigned int          fontSize_;
+    const Orthanc::Font&  font_;
 
   public:
     LineMeasureTracker(IStatusBar* statusBar,
@@ -48,8 +49,13 @@
                        uint8_t red,
                        uint8_t green,
                        uint8_t blue,
-                       unsigned int fontSize);
-    
+                       const Orthanc::Font& font);
+
+    virtual bool HasRender() const
+    {
+      return true;
+    }
+
     virtual void Render(CairoContext& context,
                         double zoom);
     
@@ -62,7 +68,9 @@
       // Possibly create a new landmark "volume" with the line in subclasses
     }
 
-    virtual void MouseMove(double x,
+    virtual void MouseMove(int displayX,
+                           int displayY,
+                           double x,
                            double y);
   };
 }
--- a/Framework/Layers/OrthancFrameLayerSource.cpp	Mon Nov 05 10:04:56 2018 +0100
+++ b/Framework/Layers/OrthancFrameLayerSource.cpp	Mon Nov 05 10:06:18 2018 +0100
@@ -31,9 +31,10 @@
 
 namespace OrthancStone
 {
-  void OrthancFrameLayerSource::NotifyGeometryReady(const OrthancSlicesLoader& loader)
+
+  void OrthancFrameLayerSource::OnSliceGeometryReady(const OrthancSlicesLoader::SliceGeometryReadyMessage& message)
   {
-    if (loader.GetSliceCount() > 0)
+    if (message.origin_.GetSliceCount() > 0)
     {
       LayerSourceBase::NotifyGeometryReady();
     }
@@ -43,35 +44,41 @@
     }
   }
 
-  void OrthancFrameLayerSource::NotifyGeometryError(const OrthancSlicesLoader& loader)
+  void OrthancFrameLayerSource::OnSliceGeometryError(const OrthancSlicesLoader::SliceGeometryErrorMessage& message)
   {
     LayerSourceBase::NotifyGeometryError();
   }
 
-  void OrthancFrameLayerSource::NotifySliceImageReady(const OrthancSlicesLoader& loader,
-                                                      unsigned int sliceIndex,
-                                                      const Slice& slice,
-                                                      std::auto_ptr<Orthanc::ImageAccessor>& image,
-                                                      SliceImageQuality quality)
+  void OrthancFrameLayerSource::OnSliceImageReady(const OrthancSlicesLoader::SliceImageReadyMessage& message)
   {
-    bool isFull = (quality == SliceImageQuality_Full);
-    LayerSourceBase::NotifyLayerReady(FrameRenderer::CreateRenderer(image.release(), slice, isFull),
-                                      slice.GetGeometry(), false);
+    // first notify that the image is ready (targeted to, i.e: an image cache)
+    LayerSourceBase::NotifyImageReady(message.image_, message.effectiveQuality_, message.slice_);
+
+    // then notify that the layer is ready for render
+    bool isFull = (message.effectiveQuality_ == SliceImageQuality_FullPng || message.effectiveQuality_ == SliceImageQuality_FullPam);
+    std::auto_ptr<Orthanc::ImageAccessor> accessor(new Orthanc::ImageAccessor());
+    message.image_->GetReadOnlyAccessor(*accessor);
+
+    LayerSourceBase::NotifyLayerReady(FrameRenderer::CreateRenderer(accessor.release(), message.slice_, isFull),
+                                      message.slice_.GetGeometry(), false);
+
   }
 
-  void OrthancFrameLayerSource::NotifySliceImageError(const OrthancSlicesLoader& loader,
-                                                      unsigned int sliceIndex,
-                                                      const Slice& slice,
-                                                      SliceImageQuality quality)
+  void OrthancFrameLayerSource::OnSliceImageError(const OrthancSlicesLoader::SliceImageErrorMessage& message)
   {
-    LayerSourceBase::NotifyLayerReady(NULL, slice.GetGeometry(), true);
+    LayerSourceBase::NotifyLayerReady(NULL, message.slice_.GetGeometry(), true);
   }
 
-
-  OrthancFrameLayerSource::OrthancFrameLayerSource(IWebService& orthanc) :
-    loader_(*this, orthanc),
-    quality_(SliceImageQuality_Full)
+  OrthancFrameLayerSource::OrthancFrameLayerSource(MessageBroker& broker, OrthancApiClient& orthanc) :
+    LayerSourceBase(broker),
+    IObserver(broker),
+    loader_(broker, orthanc),
+    quality_(SliceImageQuality_FullPng)
   {
+    loader_.RegisterObserverCallback(new Callable<OrthancFrameLayerSource, OrthancSlicesLoader::SliceGeometryReadyMessage>(*this, &OrthancFrameLayerSource::OnSliceGeometryReady));
+    loader_.RegisterObserverCallback(new Callable<OrthancFrameLayerSource, OrthancSlicesLoader::SliceGeometryErrorMessage>(*this, &OrthancFrameLayerSource::OnSliceGeometryError));
+    loader_.RegisterObserverCallback(new Callable<OrthancFrameLayerSource, OrthancSlicesLoader::SliceImageReadyMessage>(*this, &OrthancFrameLayerSource::OnSliceImageReady));
+    loader_.RegisterObserverCallback(new Callable<OrthancFrameLayerSource, OrthancSlicesLoader::SliceImageErrorMessage>(*this, &OrthancFrameLayerSource::OnSliceImageError));
   }
 
   
@@ -98,6 +105,7 @@
                                           const CoordinateSystem3D& viewportSlice)
   {
     size_t index;
+
     if (loader_.IsGeometryReady() &&
         loader_.LookupSlice(index, viewportSlice))
     {
@@ -115,17 +123,10 @@
   {
     size_t index;
 
-    if (loader_.IsGeometryReady())
+    if (loader_.IsGeometryReady() &&
+        loader_.LookupSlice(index, viewportSlice))
     {
-      if (loader_.LookupSlice(index, viewportSlice))
-      {
-        loader_.ScheduleLoadSliceImage(index, quality_);
-      }
-      else
-      {
-        Slice slice;
-        LayerSourceBase::NotifyLayerReady(NULL, slice.GetGeometry(), true);
-      }
+      loader_.ScheduleLoadSliceImage(index, quality_);
     }
   }
 }
--- a/Framework/Layers/OrthancFrameLayerSource.h	Mon Nov 05 10:04:56 2018 +0100
+++ b/Framework/Layers/OrthancFrameLayerSource.h	Mon Nov 05 10:06:18 2018 +0100
@@ -24,34 +24,24 @@
 #include "LayerSourceBase.h"
 #include "../Toolbox/IWebService.h"
 #include "../Toolbox/OrthancSlicesLoader.h"
+#include "../Toolbox/OrthancApiClient.h"
 
 namespace OrthancStone
 {  
+  // this class is in charge of loading a Frame.
+  // once it's been loaded (first the geometry and then the image),
+  // messages are sent to observers so they can use it
   class OrthancFrameLayerSource :
     public LayerSourceBase,
-    private OrthancSlicesLoader::ICallback
+    public IObserver
+    //private OrthancSlicesLoader::ISliceLoaderObserver
   {
   private:
     OrthancSlicesLoader  loader_;
     SliceImageQuality    quality_;
 
-    virtual void NotifyGeometryReady(const OrthancSlicesLoader& loader);
-
-    virtual void NotifyGeometryError(const OrthancSlicesLoader& loader);
-
-    virtual void NotifySliceImageReady(const OrthancSlicesLoader& loader,
-                                       unsigned int sliceIndex,
-                                       const Slice& slice,
-                                       std::auto_ptr<Orthanc::ImageAccessor>& image,
-                                       SliceImageQuality quality);
-
-    virtual void NotifySliceImageError(const OrthancSlicesLoader& loader,
-                                       unsigned int sliceIndex,
-                                       const Slice& slice,
-                                       SliceImageQuality quality);
-
   public:
-    OrthancFrameLayerSource(IWebService& orthanc);
+    OrthancFrameLayerSource(MessageBroker& broker, OrthancApiClient& orthanc);
 
     void LoadSeries(const std::string& seriesId);
 
@@ -65,6 +55,11 @@
       quality_ = quality;
     }
 
+    SliceImageQuality GetImageQuality() const
+    {
+      return quality_;
+    }
+
     size_t GetSliceCount() const
     {
       return loader_.GetSliceCount();
@@ -79,5 +74,11 @@
                            const CoordinateSystem3D& viewportSlice);
 
     virtual void ScheduleLayerCreation(const CoordinateSystem3D& viewportSlice);
+
+protected:
+    void OnSliceGeometryReady(const OrthancSlicesLoader::SliceGeometryReadyMessage& message);
+    void OnSliceGeometryError(const OrthancSlicesLoader::SliceGeometryErrorMessage& message);
+    void OnSliceImageReady(const OrthancSlicesLoader::SliceImageReadyMessage& message);
+    void OnSliceImageError(const OrthancSlicesLoader::SliceImageErrorMessage& message);
   };
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Messages/ICallable.h	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,92 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 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
+
+#include "IMessage.h"
+
+#include <boost/noncopyable.hpp>
+
+namespace OrthancStone {
+
+  class IObserver;
+
+  // This is referencing an object and member function that can be notified
+  // by an IObservable.  The object must derive from IO
+  // The member functions must be of type "void Function(const IMessage& message)" or reference a derived class of IMessage
+  class ICallable : public boost::noncopyable
+  {
+  public:
+    virtual ~ICallable()
+    {
+    }
+
+    virtual void Apply(const IMessage& message) = 0;
+
+    virtual MessageType GetMessageType() const = 0;
+    virtual IObserver* GetObserver() const = 0;
+  };
+
+  template <typename TMessage>
+  class MessageHandler: public ICallable
+  {
+  };
+
+
+  template <typename TObserver,
+            typename TMessage>
+  class Callable : public MessageHandler<TMessage>
+  {
+  private:
+    typedef void (TObserver::* MemberFunction) (const TMessage&);
+
+    TObserver&      observer_;
+    MemberFunction  function_;
+
+  public:
+    Callable(TObserver& observer,
+             MemberFunction function) :
+      observer_(observer),
+      function_(function)
+    {
+    }
+
+    void ApplyInternal(const TMessage& message)
+    {
+      (observer_.*function_) (message);
+    }
+
+    virtual void Apply(const IMessage& message)
+    {
+      ApplyInternal(dynamic_cast<const TMessage&>(message));
+    }
+
+    virtual MessageType GetMessageType() const
+    {
+      return static_cast<MessageType>(TMessage::Type);
+    }
+
+    virtual IObserver* GetObserver() const
+    {
+      return &observer_;
+    }
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Messages/IMessage.h	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,87 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 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
+
+#include "MessageType.h"
+
+#include <boost/noncopyable.hpp>
+
+namespace OrthancStone {
+
+
+  // base message that are exchanged between IObservable and IObserver
+  struct IMessage : public boost::noncopyable
+  {
+    int messageType_;
+  protected:
+    IMessage(const int& messageType)
+      : messageType_(messageType)
+    {}
+  public:
+    virtual ~IMessage() {}
+
+    virtual int GetType() const {return messageType_;}
+  };
+
+
+  // base class to derive from to implement your own messages
+  // it handles the message type for you
+  template <int type>
+  struct BaseMessage : public IMessage
+  {
+    enum
+    {
+      Type = type
+    };
+
+    BaseMessage()
+      : IMessage(static_cast<int>(Type))
+    {}
+  };
+
+  // simple message implementation when no payload is needed
+  // sample usage:
+  // typedef NoPayloadMessage<MessageType_LayerSource_GeometryReady> GeometryReadyMessage;
+  template <int type>
+  struct NoPayloadMessage : public BaseMessage<type>
+  {
+    NoPayloadMessage()
+      : BaseMessage<type>()
+    {}
+
+  };
+
+  // simple message implementation when no payload is needed but the origin is required
+  // sample usage:
+  // typedef OriginMessage<MessageType_SliceLoader_GeometryError, OrthancSlicesLoader> SliceGeometryErrorMessage;
+  template <int type, typename TOrigin>
+  struct OriginMessage : public BaseMessage<type>
+  {
+    TOrigin& origin_;
+    OriginMessage(TOrigin& origin)
+      : BaseMessage<type>(),
+        origin_(origin)
+    {}
+
+  };
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Messages/IObservable.h	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,110 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 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
+
+#include <set>
+#include <assert.h>
+#include <algorithm>
+#include <iostream>
+#include <map>
+
+
+#include "MessageBroker.h"
+#include "MessageType.h"
+#include "ICallable.h"
+#include "IObserver.h"
+#include "MessageForwarder.h"
+
+namespace OrthancStone {
+
+
+  class IObservable : public boost::noncopyable
+  {
+  protected:
+    MessageBroker&                     broker_;
+
+    typedef std::map<int, std::set<ICallable*> >   Callables;
+    Callables                         callables_;
+
+    typedef std::set<IMessageForwarder*>      Forwarders;
+    Forwarders                        forwarders_;
+
+  public:
+
+    IObservable(MessageBroker& broker)
+      : broker_(broker)
+    {
+    }
+    virtual ~IObservable()
+    {
+      // delete all callables (this will also unregister them from the broker)
+      for (Callables::const_iterator it = callables_.begin();
+           it != callables_.end(); ++it)
+      {
+        for (std::set<ICallable*>::const_iterator
+               it2 = it->second.begin(); it2 != it->second.end(); ++it2)
+        {
+          delete *it2;
+        }
+      }
+
+      // unregister the forwarders but don't delete them (they'll be deleted by the observable they are observing as any other callable)
+      for (Forwarders::iterator it = forwarders_.begin();
+           it != forwarders_.end(); ++it)
+      {
+        IMessageForwarder* fw = *it;
+        broker_.Unregister(dynamic_cast<IObserver&>(*fw));
+      }
+    }
+
+    void RegisterObserverCallback(ICallable* callable)
+    {
+      MessageType messageType = callable->GetMessageType();
+
+      callables_[messageType].insert(callable);
+    }
+
+    void EmitMessage(const IMessage& message)
+    {
+      Callables::const_iterator found = callables_.find(message.GetType());
+
+      if (found != callables_.end())
+      {
+        for (std::set<ICallable*>::const_iterator
+               it = found->second.begin(); it != found->second.end(); ++it)
+        {
+          if (broker_.IsActive((*it)->GetObserver()))
+          {
+            (*it)->Apply(message);
+          }
+        }
+      }
+    }
+
+    void RegisterForwarder(IMessageForwarder* forwarder)
+    {
+      forwarders_.insert(forwarder);
+    }
+
+  };
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Messages/IObserver.h	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,51 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 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
+
+#include "MessageBroker.h"
+#include "IMessage.h"
+#include <set>
+#include <assert.h>
+
+namespace OrthancStone {
+
+  class IObservable;
+
+  class IObserver : public boost::noncopyable
+  {
+  protected:
+    MessageBroker&                    broker_;
+
+  public:
+    IObserver(MessageBroker& broker)
+      : broker_(broker)
+    {
+      broker_.Register(*this);
+    }
+
+    virtual ~IObserver()
+    {
+      broker_.Unregister(*this);
+    }
+  };
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Messages/MessageBroker.h	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,60 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 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
+
+#include "boost/noncopyable.hpp"
+#include <set>
+
+namespace OrthancStone
+{
+  class IObserver;
+  class IObservable;
+
+  /*
+   * This is a central message broker.  It keeps track of all observers and knows
+   * when an observer is deleted.
+   * This way, it can prevent an observable to send a message to a deleted observer.
+   */
+  class MessageBroker : public boost::noncopyable
+  {
+
+    std::set<IObserver*> activeObservers_;  // the list of observers that are currently alive (that have not been deleted)
+
+  public:
+
+    void Register(IObserver& observer)
+    {
+      activeObservers_.insert(&observer);
+    }
+
+    void Unregister(IObserver& observer)
+    {
+      activeObservers_.erase(&observer);
+    }
+
+    bool IsActive(IObserver* observer)
+    {
+      return activeObservers_.find(observer) != activeObservers_.end();
+    }
+  };
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Messages/MessageForwarder.cpp	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,38 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 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 "MessageForwarder.h"
+
+#include "IObservable.h"
+
+namespace OrthancStone
+{
+
+  void IMessageForwarder::ForwardMessageInternal(const IMessage& message)
+  {
+    emitter_.EmitMessage(message);
+  }
+
+  void IMessageForwarder::RegisterForwarderInEmitter()
+  {
+    emitter_.RegisterForwarder(this);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Messages/MessageForwarder.h	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,87 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 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
+
+#include "ICallable.h"
+#include "IObserver.h"
+
+#include <boost/noncopyable.hpp>
+
+namespace OrthancStone
+{
+
+  class IObservable;
+
+  class IMessageForwarder : public IObserver
+  {
+    IObservable& emitter_;
+  public:
+    IMessageForwarder(MessageBroker& broker, IObservable& emitter)
+      : IObserver(broker),
+        emitter_(emitter)
+    {}
+    virtual ~IMessageForwarder() {}
+
+  protected:
+    void ForwardMessageInternal(const IMessage& message);
+    void RegisterForwarderInEmitter();
+
+  };
+
+  /* When an Observer (B) simply needs to re-emit a message it has received, instead of implementing
+   * a specific member function to forward the message, it can create a MessageForwarder.
+   * The MessageForwarder will re-emit the message "in the name of (B)"
+   *
+   * Consider the chain where
+   * A is an observable
+   * |
+   * B is an observer of A and observable
+   * |
+   * C is an observer of B and knows that B is re-emitting many messages from A
+   *
+   * instead of implementing a callback, B will create a MessageForwarder that will emit the messages in his name:
+   * A.RegisterObserverCallback(new MessageForwarder<A::MessageType>(broker, *this)  // where this is B
+   *
+   * in C:
+   * B.RegisterObserverCallback(new Callable<C, A:MessageTyper>(*this, &B::MyCallback))   // where this is C
+   */
+  template<typename TMessage>
+  class MessageForwarder : public IMessageForwarder, public Callable<MessageForwarder<TMessage>, TMessage>
+  {
+  public:
+    MessageForwarder(MessageBroker& broker,
+                     IObservable& emitter // the object that will emit the messages to forward
+                     )
+      : IMessageForwarder(broker, emitter),
+        Callable<MessageForwarder<TMessage>, TMessage>(*this, &MessageForwarder::ForwardMessage)
+    {
+      RegisterForwarderInEmitter();
+    }
+
+protected:
+    void ForwardMessage(const TMessage& message)
+    {
+      ForwardMessageInternal(message);
+    }
+
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Messages/MessageType.h	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,59 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 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
+
+namespace OrthancStone {
+
+  enum MessageType
+  {
+    MessageType_Widget_GeometryChanged,
+    MessageType_Widget_ContentChanged,
+
+    MessageType_LayerSource_GeometryReady,   // instance tags have been loaded
+    MessageType_LayerSource_GeometryError,
+    MessageType_LayerSource_ContentChanged,
+    MessageType_LayerSource_SliceChanged,
+    MessageType_LayerSource_ImageReady,      // instance pixels data have been loaded
+    MessageType_LayerSource_LayerReady,      // layer is ready to be rendered
+
+    MessageType_SliceLoader_GeometryReady,
+    MessageType_SliceLoader_GeometryError,
+    MessageType_SliceLoader_ImageReady,
+    MessageType_SliceLoader_ImageError,
+
+    MessageType_HttpRequestSuccess,
+    MessageType_HttpRequestError,
+
+    MessageType_OrthancApi_InternalGetJsonResponseReady,
+    MessageType_OrthancApi_InternalGetJsonResponseError,
+
+    MessageType_OrthancApi_GenericGetJson_Ready,
+    MessageType_OrthancApi_GenericGetBinary_Ready,
+    MessageType_OrthancApi_GenericHttpError_Ready,
+    MessageType_OrthancApi_GenericEmptyResponse_Ready,
+
+    // used in unit tests only
+    MessageType_Test1,
+    MessageType_Test2,
+
+    MessageType_CustomMessage // Custom messages ids ust be greater than this (this one must remain in last position)
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Messages/Promise.h	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,88 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 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
+
+#include "MessageBroker.h"
+#include "ICallable.h"
+#include "IMessage.h"
+
+#include <boost/noncopyable.hpp>
+#include <memory>
+
+namespace OrthancStone {
+
+  class Promise : public boost::noncopyable
+  {
+  protected:
+    MessageBroker&                    broker_;
+
+    std::auto_ptr<ICallable> successCallable_;
+    std::auto_ptr<ICallable> failureCallable_;
+
+  public:
+    Promise(MessageBroker& broker)
+      : broker_(broker)
+    {
+    }
+
+    void Success(const IMessage& message)
+    {
+      // check the target is still alive in the broker
+      if (broker_.IsActive(successCallable_->GetObserver()))
+      {
+        successCallable_->Apply(message);
+      }
+    }
+
+    void Failure(const IMessage& message)
+    {
+      // check the target is still alive in the broker
+      if (broker_.IsActive(failureCallable_->GetObserver()))
+      {
+        failureCallable_->Apply(message);
+      }
+    }
+
+    Promise& Then(ICallable* successCallable)
+    {
+      if (successCallable_.get() != NULL)
+      {
+        // TODO: throw throw new "Promise may only have a single success target"
+      }
+      successCallable_.reset(successCallable);
+      return *this;
+    }
+
+    Promise& Else(ICallable* failureCallable)
+    {
+      if (failureCallable_.get() != NULL)
+      {
+        // TODO: throw throw new "Promise may only have a single failure target"
+      }
+      failureCallable_.reset(failureCallable);
+      return *this;
+    }
+
+  };
+
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/SmartLoader.cpp	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,272 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 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 "SmartLoader.h"
+#include "Layers/OrthancFrameLayerSource.h"
+#include "Messages/MessageForwarder.h"
+#include "Core/Images/Image.h"
+#include "Framework/Widgets/LayerWidget.h"
+#include "Framework/StoneException.h"
+#include "Framework/Layers/FrameRenderer.h"
+#include "Core/Logging.h"
+
+namespace OrthancStone
+{
+  enum CachedSliceStatus
+  {
+    CachedSliceStatus_ScheduledToLoad,
+    CachedSliceStatus_GeometryLoaded,
+    CachedSliceStatus_ImageLoaded
+  };
+
+  class SmartLoader::CachedSlice : public LayerSourceBase
+  {
+  public:
+    unsigned int                    sliceIndex_;
+    std::auto_ptr<Slice>            slice_;
+    boost::shared_ptr<Orthanc::ImageAccessor>   image_;
+    SliceImageQuality               effectiveQuality_;
+    CachedSliceStatus               status_;
+
+  public:
+    CachedSlice(MessageBroker& broker) :
+    LayerSourceBase(broker)
+    {
+    }
+
+    virtual ~CachedSlice()
+    {
+    }
+
+    virtual bool GetExtent(std::vector<Vector>& points,
+                           const CoordinateSystem3D& viewportSlice)
+    {
+      // TODO: viewportSlice is not used !!!!
+      slice_->GetExtent(points);
+      return true;
+    }
+
+    virtual void ScheduleLayerCreation(const CoordinateSystem3D& viewportSlice)
+    {
+      // TODO: viewportSlice is not used !!!!
+
+      // it has already been loaded -> trigger the "layer ready" message immediately otherwise, do nothing now.  The LayerReady will be triggered
+      // once the LayerSource is ready
+      if (status_ == CachedSliceStatus_ImageLoaded)
+      {
+        LOG(WARNING) << "ScheduleLayerCreation for CachedSlice (image is loaded): " << slice_->GetOrthancInstanceId();
+        bool isFull = (effectiveQuality_ == SliceImageQuality_FullPng || effectiveQuality_ == SliceImageQuality_FullPam);
+        std::auto_ptr<Orthanc::ImageAccessor> accessor(new Orthanc::ImageAccessor());
+        image_->GetReadOnlyAccessor(*accessor);
+        LayerSourceBase::NotifyLayerReady(FrameRenderer::CreateRenderer(accessor.release(), *slice_, isFull),
+                                          slice_->GetGeometry(), false);
+      }
+      else
+      {
+        LOG(WARNING) << "ScheduleLayerCreation for CachedSlice (image is not loaded yet): " << slice_->GetOrthancInstanceId();
+      }
+    }
+
+    CachedSlice* Clone() const
+    {
+      CachedSlice* output = new CachedSlice(broker_);
+      output->sliceIndex_ = sliceIndex_;
+      output->slice_.reset(slice_->Clone());
+      output->image_ = image_;
+      output->effectiveQuality_ = effectiveQuality_;
+      output->status_ = status_;
+
+      return output;
+    }
+
+  };
+
+
+  SmartLoader::SmartLoader(MessageBroker& broker,  
+                           OrthancApiClient& orthancApiClient) :
+    IObservable(broker),
+    IObserver(broker),
+    imageQuality_(SliceImageQuality_FullPam),
+    orthancApiClient_(orthancApiClient)
+  {
+  }
+
+  void SmartLoader::SetFrameInWidget(LayerWidget& layerWidget, 
+                                     size_t layerIndex, 
+                                     const std::string& instanceId, 
+                                     unsigned int frame)
+  {
+    // TODO: check if this frame has already been loaded or is already being loaded.
+    // - if already loaded: create a "clone" that will emit the GeometryReady/ImageReady messages "immediately"
+    //   (it can not be immediate because Observers needs to register first and this is done after this method returns)
+    // - if currently loading, we need to return an object that will observe the existing LayerSource and forward
+    //   the messages to its observables
+    // in both cases, we must be carefull about objects lifecycle !!!
+
+    std::auto_ptr<ILayerSource> layerSource;
+    std::string sliceKeyId = instanceId + ":" + boost::lexical_cast<std::string>(frame);
+    SmartLoader::CachedSlice* cachedSlice = NULL;
+
+    if (cachedSlices_.find(sliceKeyId) != cachedSlices_.end()) // && cachedSlices_[sliceKeyId]->status_ == CachedSliceStatus_Loaded)
+    {
+      layerSource.reset(cachedSlices_[sliceKeyId]->Clone());
+      cachedSlice = dynamic_cast<SmartLoader::CachedSlice*>(layerSource.get());
+    }
+    else
+    {
+      layerSource.reset(new OrthancFrameLayerSource(IObserver::broker_, orthancApiClient_));
+      dynamic_cast<OrthancFrameLayerSource*>(layerSource.get())->SetImageQuality(imageQuality_);
+      layerSource->RegisterObserverCallback(new Callable<SmartLoader, ILayerSource::GeometryReadyMessage>(*this, &SmartLoader::OnLayerGeometryReady));
+      layerSource->RegisterObserverCallback(new Callable<SmartLoader, ILayerSource::ImageReadyMessage>(*this, &SmartLoader::OnImageReady));
+      layerSource->RegisterObserverCallback(new Callable<SmartLoader, ILayerSource::LayerReadyMessage>(*this, &SmartLoader::OnLayerReady));
+      dynamic_cast<OrthancFrameLayerSource*>(layerSource.get())->LoadFrame(instanceId, frame);
+    }
+
+    // make sure that the widget registers the events before we trigger them
+    if (layerWidget.GetLayerCount() == layerIndex)
+    {
+      layerWidget.AddLayer(layerSource.release());
+    }
+    else if (layerWidget.GetLayerCount() > layerIndex)
+    {
+      layerWidget.ReplaceLayer(layerIndex, layerSource.release());
+    }
+    else
+    {
+      throw StoneException(ErrorCode_CanOnlyAddOneLayerAtATime);
+    }
+
+    if (cachedSlice != NULL)
+    {
+      cachedSlice->NotifyGeometryReady();
+    }
+
+  }
+
+  void SmartLoader::PreloadSlice(const std::string instanceId, 
+                                 unsigned int frame)
+  {
+    // TODO: reactivate -> need to be able to ScheduleLayerLoading in ILayerSource without calling ScheduleLayerCreation
+    return;
+    // TODO: check if it is already in the cache
+
+
+
+    // create the slice in the cache with "empty" data
+    boost::shared_ptr<CachedSlice> cachedSlice(new CachedSlice(IObserver::broker_));
+    cachedSlice->slice_.reset(new Slice(instanceId, frame));
+    cachedSlice->status_ = CachedSliceStatus_ScheduledToLoad;
+    std::string sliceKeyId = instanceId + ":" + boost::lexical_cast<std::string>(frame);
+
+    LOG(WARNING) << "Will preload: " << sliceKeyId;
+
+    cachedSlices_[sliceKeyId] = boost::shared_ptr<CachedSlice>(cachedSlice);
+
+    std::auto_ptr<ILayerSource> layerSource(new OrthancFrameLayerSource(IObserver::broker_, orthancApiClient_));
+
+    dynamic_cast<OrthancFrameLayerSource*>(layerSource.get())->SetImageQuality(imageQuality_);
+    layerSource->RegisterObserverCallback(new Callable<SmartLoader, ILayerSource::GeometryReadyMessage>(*this, &SmartLoader::OnLayerGeometryReady));
+    layerSource->RegisterObserverCallback(new Callable<SmartLoader, ILayerSource::ImageReadyMessage>(*this, &SmartLoader::OnImageReady));
+    layerSource->RegisterObserverCallback(new Callable<SmartLoader, ILayerSource::LayerReadyMessage>(*this, &SmartLoader::OnLayerReady));
+    dynamic_cast<OrthancFrameLayerSource*>(layerSource.get())->LoadFrame(instanceId, frame);
+
+    // keep a ref to the LayerSource until the slice is fully loaded and saved to cache
+    preloadingInstances_[sliceKeyId] = boost::shared_ptr<ILayerSource>(layerSource.release());
+  }
+
+
+//  void PreloadStudy(const std::string studyId)
+//  {
+//    /* TODO */
+//  }
+
+//  void PreloadSeries(const std::string seriesId)
+//  {
+//    /* TODO */
+//  }
+
+
+  void SmartLoader::OnLayerGeometryReady(const ILayerSource::GeometryReadyMessage& message)
+  {
+    OrthancFrameLayerSource& source = dynamic_cast<OrthancFrameLayerSource&>(message.origin_);
+
+    // save/replace the slice in cache
+    const Slice& slice = source.GetSlice(0); // TODO handle GetSliceCount()
+    std::string sliceKeyId = (slice.GetOrthancInstanceId() + ":" + 
+                              boost::lexical_cast<std::string>(slice.GetFrame()));
+
+    LOG(WARNING) << "Geometry ready: " << sliceKeyId;
+
+    boost::shared_ptr<CachedSlice> cachedSlice(new CachedSlice(IObserver::broker_));
+    cachedSlice->slice_.reset(slice.Clone());
+    cachedSlice->effectiveQuality_ = source.GetImageQuality();
+    cachedSlice->status_ = CachedSliceStatus_GeometryLoaded;
+
+    cachedSlices_[sliceKeyId] = boost::shared_ptr<CachedSlice>(cachedSlice);
+
+    // re-emit original Layer message to observers
+    EmitMessage(message);
+  }
+
+
+  void SmartLoader::OnImageReady(const ILayerSource::ImageReadyMessage& message)
+  {
+    OrthancFrameLayerSource& source = dynamic_cast<OrthancFrameLayerSource&>(message.origin_);
+
+    // save/replace the slice in cache
+    const Slice& slice = source.GetSlice(0); // TODO handle GetSliceCount() ?
+    std::string sliceKeyId = (slice.GetOrthancInstanceId() + ":" + 
+                              boost::lexical_cast<std::string>(slice.GetFrame()));
+
+    LOG(WARNING) << "Image ready: " << sliceKeyId;
+
+    boost::shared_ptr<CachedSlice> cachedSlice(new CachedSlice(IObserver::broker_));
+    cachedSlice->image_ = message.image_;
+    cachedSlice->effectiveQuality_ = message.imageQuality_;
+    cachedSlice->slice_.reset(message.slice_.Clone());
+    cachedSlice->status_ = CachedSliceStatus_ImageLoaded;
+
+    cachedSlices_[sliceKeyId] = cachedSlice;
+
+    // re-emit original Layer message to observers
+    EmitMessage(message);
+  }
+
+
+  void SmartLoader::OnLayerReady(const ILayerSource::LayerReadyMessage& message)
+  {
+    OrthancFrameLayerSource& source = dynamic_cast<OrthancFrameLayerSource&>(message.origin_);
+    const Slice& slice = source.GetSlice(0); // TODO handle GetSliceCount() ?
+    std::string sliceKeyId = (slice.GetOrthancInstanceId() + ":" + 
+                              boost::lexical_cast<std::string>(slice.GetFrame()));
+
+    LOG(WARNING) << "Layer ready: " << sliceKeyId;
+
+    // remove the slice from the preloading slices now that it has been fully loaded and it is referenced in the cache
+    if (preloadingInstances_.find(sliceKeyId) != preloadingInstances_.end())
+    {
+      preloadingInstances_.erase(sliceKeyId);
+    }
+
+    // re-emit original Layer message to observers
+    EmitMessage(message);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/SmartLoader.h	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,67 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 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
+#include <map>
+
+#include "Layers/ILayerSource.h"
+#include "Messages/IObservable.h"
+#include "Toolbox/OrthancApiClient.h"
+
+namespace OrthancStone
+{
+  class LayerWidget;
+
+  class SmartLoader : public IObservable, public IObserver
+  {
+    class CachedSlice;
+
+  protected:
+    typedef std::map<std::string, boost::shared_ptr<SmartLoader::CachedSlice> > CachedSlices;
+    CachedSlices cachedSlices_;
+
+    typedef std::map<std::string, boost::shared_ptr<ILayerSource> > PreloadingInstances;
+    PreloadingInstances preloadingInstances_;
+
+    SliceImageQuality     imageQuality_;
+    OrthancApiClient&     orthancApiClient_;
+
+  public:
+    SmartLoader(MessageBroker& broker, OrthancApiClient& orthancApiClient);  // TODO: add maxPreloadStorageSizeInBytes
+
+//    void PreloadStudy(const std::string studyId);
+//    void PreloadSeries(const std::string seriesId);
+    void PreloadSlice(const std::string instanceId, unsigned int frame);
+
+    void SetImageQuality(SliceImageQuality imageQuality) { imageQuality_ = imageQuality; }
+
+    void SetFrameInWidget(LayerWidget& layerWidget, size_t layerIndex, const std::string& instanceId, unsigned int frame);
+
+    void GetFirstInstanceIdForSeries(std::string& output, const std::string& seriesId);
+
+  private:
+    void OnLayerGeometryReady(const ILayerSource::GeometryReadyMessage& message);
+    void OnImageReady(const ILayerSource::ImageReadyMessage& message);
+    void OnLayerReady(const ILayerSource::LayerReadyMessage& message);
+
+  };
+
+}
--- a/Framework/StoneEnumerations.cpp	Mon Nov 05 10:04:56 2018 +0100
+++ b/Framework/StoneEnumerations.cpp	Mon Nov 05 10:06:18 2018 +0100
@@ -72,4 +72,66 @@
         throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
     }
   }
+
+  
+  void ComputeAnchorTranslation(double& deltaX,
+                                double& deltaY,
+                                BitmapAnchor anchor,
+                                unsigned int bitmapWidth,
+                                unsigned int bitmapHeight)
+  {
+    double dw = static_cast<double>(bitmapWidth);
+    double dh = static_cast<double>(bitmapHeight);
+
+    switch (anchor)
+    {
+      case BitmapAnchor_TopLeft:
+        deltaX = 0;
+        deltaY = 0;
+        break;
+        
+      case BitmapAnchor_TopCenter:
+        deltaX = -dw / 2.0;
+        deltaY = 0;
+        break;
+        
+      case BitmapAnchor_TopRight:
+        deltaX = -dw;
+        deltaY = 0;
+        break;
+        
+      case BitmapAnchor_CenterLeft:
+        deltaX = 0;
+        deltaY = -dh / 2.0;
+        break;
+        
+      case BitmapAnchor_Center:
+        deltaX = -dw / 2.0;
+        deltaY = -dh / 2.0;
+        break;
+        
+      case BitmapAnchor_CenterRight:
+        deltaX = -dw;
+        deltaY = -dh / 2.0;
+        break;
+        
+      case BitmapAnchor_BottomLeft:
+        deltaX = 0;
+        deltaY = -dh;
+        break;
+        
+      case BitmapAnchor_BottomCenter:
+        deltaX = -dw / 2.0;
+        deltaY = -dh;
+        break;
+        
+      case BitmapAnchor_BottomRight:
+        deltaX = -dw;
+        deltaY = -dh;
+        break;
+        
+      default:
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }    
+  }
 }
--- a/Framework/StoneEnumerations.h	Mon Nov 05 10:04:56 2018 +0100
+++ b/Framework/StoneEnumerations.h	Mon Nov 05 10:06:18 2018 +0100
@@ -75,12 +75,26 @@
     KeyboardModifiers_Alt = (1 << 2)
   };
 
+  enum KeyboardKeys
+  {
+    KeyboardKeys_Generic = 0,
+
+    // let's use the same ids as in javascript to avoid some conversion in WASM: https://css-tricks.com/snippets/javascript/javascript-keycodes/
+    KeyboardKeys_Left = 37,
+    KeyboardKeys_Up = 38,
+    KeyboardKeys_Right = 39,
+    KeyboardKeys_Down = 40
+  };
+
   enum SliceImageQuality
   {
-    SliceImageQuality_Full,
+    SliceImageQuality_FullPng,  // smaller to transmit but longer to generate on Orthanc side (better choice when on low bandwidth)
+    SliceImageQuality_FullPam,  // bigger to transmit but faster to generate on Orthanc side (better choice when on localhost or LAN)
     SliceImageQuality_Jpeg50,
     SliceImageQuality_Jpeg90,
-    SliceImageQuality_Jpeg95
+    SliceImageQuality_Jpeg95,
+
+    SliceImageQuality_InternalRaw   // downloads the raw pixels data as they are stored in the DICOM file (internal use only)
   };
 
   enum SopClassUid
@@ -88,6 +102,19 @@
     SopClassUid_RTDose
   };
 
+  enum BitmapAnchor
+  {
+    BitmapAnchor_BottomLeft,
+    BitmapAnchor_BottomCenter,
+    BitmapAnchor_BottomRight,
+    BitmapAnchor_CenterLeft,
+    BitmapAnchor_Center,
+    BitmapAnchor_CenterRight,
+    BitmapAnchor_TopLeft,
+    BitmapAnchor_TopCenter,
+    BitmapAnchor_TopRight
+  };
+
   bool StringToSopClassUid(SopClassUid& result,
                            const std::string& source);
 
@@ -96,4 +123,10 @@
                         ImageWindowing windowing,
                         float defaultCenter,
                         float defaultWidth);
+
+  void ComputeAnchorTranslation(double& deltaX /* out */,
+                                double& deltaY /* out */,
+                                BitmapAnchor anchor,
+                                unsigned int bitmapWidth,
+                                unsigned int bitmapHeight);
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/StoneException.h	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,115 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 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
+
+#include "Core/OrthancException.h"
+#include <boost/lexical_cast.hpp>
+
+namespace OrthancStone
+{
+  enum ErrorCode
+  {
+    ErrorCode_Success,
+    ErrorCode_OrthancError, // this StoneException is actually an OrthancException with an Orthanc error code
+    ErrorCode_ApplicationException, // this StoneException is specific to an application (and should have its own internal error code)
+    ErrorCode_NotImplemented, // case not implemented
+
+    ErrorCode_PromiseSingleSuccessHandler, // a Promise can only have a single success handler
+    ErrorCode_PromiseSingleFailureHandler, // a Promise can only have a single failure handler
+
+    ErrorCode_CanOnlyAddOneLayerAtATime,
+    ErrorCode_CommandJsonInvalidFormat,
+    ErrorCode_Last
+  };
+
+
+
+  class StoneException
+  {
+  protected:
+    OrthancStone::ErrorCode     errorCode_;
+
+  public:
+    explicit StoneException(ErrorCode errorCode) :
+      errorCode_(errorCode)
+    {
+    }
+
+    ErrorCode GetErrorCode() const
+    {
+      return errorCode_;
+    }
+
+    virtual const char* What() const
+    {
+      return "TODO: EnumerationToString for StoneException";
+    }
+  };
+
+  class StoneOrthancException : public StoneException
+  {
+  protected:
+    Orthanc::OrthancException&  orthancException_;
+
+  public:
+    explicit StoneOrthancException(Orthanc::OrthancException& orthancException) :
+      StoneException(ErrorCode_OrthancError),
+      orthancException_(orthancException)
+    {
+    }
+
+    Orthanc::ErrorCode GetOrthancErrorCode() const
+    {
+      return orthancException_.GetErrorCode();
+    }
+
+    virtual const char* What() const
+    {
+      return orthancException_.What();
+    }
+  };
+
+  class StoneApplicationException : public StoneException
+  {
+  protected:
+    int applicationErrorCode_;
+
+  public:
+    explicit StoneApplicationException(int applicationErrorCode) :
+      StoneException(ErrorCode_ApplicationException),
+      applicationErrorCode_(applicationErrorCode)
+    {
+    }
+
+    int GetApplicationErrorCode() const
+    {
+      return applicationErrorCode_;
+    }
+
+    virtual const char* What() const
+    {
+      return boost::lexical_cast<std::string>(applicationErrorCode_).c_str();
+    }
+  };
+
+}
+
--- a/Framework/Toolbox/DicomFrameConverter.cpp	Mon Nov 05 10:04:56 2018 +0100
+++ b/Framework/Toolbox/DicomFrameConverter.cpp	Mon Nov 05 10:06:18 2018 +0100
@@ -30,6 +30,19 @@
 
 namespace OrthancStone
 {
+  static const Orthanc::DicomTag IMAGE_TAGS[] =
+  {
+    Orthanc::DICOM_TAG_BITS_STORED,
+    Orthanc::DICOM_TAG_DOSE_GRID_SCALING,
+    Orthanc::DICOM_TAG_PHOTOMETRIC_INTERPRETATION,
+    Orthanc::DICOM_TAG_PIXEL_REPRESENTATION,
+    Orthanc::DICOM_TAG_RESCALE_INTERCEPT,
+    Orthanc::DICOM_TAG_RESCALE_SLOPE,
+    Orthanc::DICOM_TAG_WINDOW_CENTER,
+    Orthanc::DICOM_TAG_WINDOW_WIDTH
+  };
+
+  
   void DicomFrameConverter::SetDefaultParameters()
   {
     isSigned_ = true;
@@ -37,6 +50,7 @@
     hasRescale_ = false;
     rescaleIntercept_ = 0;
     rescaleSlope_ = 1;
+    hasDefaultWindow_ = false;
     defaultWindowCenter_ = 128;
     defaultWindowWidth_ = 256;
     expectedPixelFormat_ = Orthanc::PixelFormat_Grayscale16;
@@ -53,6 +67,7 @@
         c.size() > 0 && 
         w.size() > 0)
     {
+      hasDefaultWindow_ = true;
       defaultWindowCenter_ = static_cast<float>(c[0]);
       defaultWindowWidth_ = static_cast<float>(w[0]);
     }
@@ -113,6 +128,8 @@
       // Type 1 tag, must be present
       throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
     }
+
+    photometric_ = Orthanc::StringToPhotometricInterpretation(photometric.c_str());
     
     isColor_ = (photometric != "MONOCHROME1" &&
                 photometric != "MONOCHROME2");
@@ -137,8 +154,27 @@
     }
   }
 
+  
+  void DicomFrameConverter::ReadParameters(const OrthancPlugins::IDicomDataset& dicom)
+  {
+    Orthanc::DicomMap converted;
 
-  void DicomFrameConverter::ConvertFrame(std::auto_ptr<Orthanc::ImageAccessor>& source) const
+    for (size_t i = 0; i < sizeof(IMAGE_TAGS) / sizeof(Orthanc::DicomTag); i++)
+    {
+      OrthancPlugins::DicomTag tag(IMAGE_TAGS[i].GetGroup(), IMAGE_TAGS[i].GetElement());
+    
+      std::string value;
+      if (dicom.GetStringValue(value, tag))
+      {
+        converted.SetValue(IMAGE_TAGS[i], value, false);
+      }
+    }
+
+    ReadParameters(converted);
+  }
+    
+
+  void DicomFrameConverter::ConvertFrameInplace(std::auto_ptr<Orthanc::ImageAccessor>& source) const
   {
     assert(sizeof(float) == 4);
 
@@ -147,7 +183,24 @@
       throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
     }
 
-    Orthanc::PixelFormat sourceFormat = source->GetFormat();
+    if (source->GetFormat() == GetExpectedPixelFormat() &&
+        source->GetFormat() == Orthanc::PixelFormat_RGB24)
+    {
+      // No conversion has to be done, check out (*)
+      return;
+    }
+    else
+    {
+      source.reset(ConvertFrame(*source));
+    }
+  }
+
+
+  Orthanc::ImageAccessor* DicomFrameConverter::ConvertFrame(const Orthanc::ImageAccessor& source) const
+  {
+    assert(sizeof(float) == 4);
+
+    Orthanc::PixelFormat sourceFormat = source.GetFormat();
 
     if (sourceFormat != GetExpectedPixelFormat())
     {
@@ -156,27 +209,32 @@
 
     if (sourceFormat == Orthanc::PixelFormat_RGB24)
     {
-      // No conversion has to be done
-      return;
+      // This is the case of a color image. No conversion has to be done (*)
+      std::auto_ptr<Orthanc::Image> converted(new Orthanc::Image(Orthanc::PixelFormat_RGB24, 
+                                                                 source.GetWidth(), 
+                                                                 source.GetHeight(),
+                                                                 false));
+      Orthanc::ImageProcessing::Copy(*converted, source);
+      return converted.release();
     }
-
-    assert(sourceFormat == Orthanc::PixelFormat_Grayscale16 ||
-           sourceFormat == Orthanc::PixelFormat_Grayscale32 ||
-           sourceFormat == Orthanc::PixelFormat_SignedGrayscale16);
+    else
+    {
+      assert(sourceFormat == Orthanc::PixelFormat_Grayscale16 ||
+             sourceFormat == Orthanc::PixelFormat_Grayscale32 ||
+             sourceFormat == Orthanc::PixelFormat_SignedGrayscale16);
 
-    // This is the case of a grayscale frame. Convert it to Float32.
-    std::auto_ptr<Orthanc::Image> converted(new Orthanc::Image(Orthanc::PixelFormat_Float32, 
-                                                               source->GetWidth(), 
-                                                               source->GetHeight(),
-                                                               false));
-    Orthanc::ImageProcessing::Convert(*converted, *source);
+      // This is the case of a grayscale frame. Convert it to Float32.
+      std::auto_ptr<Orthanc::Image> converted(new Orthanc::Image(Orthanc::PixelFormat_Float32, 
+                                                                 source.GetWidth(), 
+                                                                 source.GetHeight(),
+                                                                 false));
+      Orthanc::ImageProcessing::Convert(*converted, source);
 
-    source.reset(NULL);  // We don't need the source frame anymore
-
-    // Correct rescale slope/intercept if need be
-    ApplyRescale(*converted, sourceFormat != Orthanc::PixelFormat_Grayscale32);
+      // Correct rescale slope/intercept if need be
+      ApplyRescale(*converted, sourceFormat != Orthanc::PixelFormat_Grayscale32);
       
-    source = converted;
+      return converted.release();
+    }
   }
 
 
--- a/Framework/Toolbox/DicomFrameConverter.h	Mon Nov 05 10:04:56 2018 +0100
+++ b/Framework/Toolbox/DicomFrameConverter.h	Mon Nov 05 10:06:18 2018 +0100
@@ -21,6 +21,7 @@
 
 #pragma once
 
+#include <Plugins/Samples/Common/IDicomDataset.h>
 #include <Core/DicomFormat/DicomMap.h>
 #include <Core/Images/ImageAccessor.h>
 
@@ -43,10 +44,12 @@
     bool    hasRescale_;
     double  rescaleIntercept_;
     double  rescaleSlope_;
+    bool    hasDefaultWindow_;
     double  defaultWindowCenter_;
     double  defaultWindowWidth_;
-
-    Orthanc::PixelFormat  expectedPixelFormat_;
+    
+    Orthanc::PhotometricInterpretation  photometric_;
+    Orthanc::PixelFormat                expectedPixelFormat_;
 
     void SetDefaultParameters();
 
@@ -61,8 +64,20 @@
       return expectedPixelFormat_;
     }
 
+    Orthanc::PhotometricInterpretation GetPhotometricInterpretation() const
+    {
+      return photometric_;
+    }
+
     void ReadParameters(const Orthanc::DicomMap& dicom);
 
+    void ReadParameters(const OrthancPlugins::IDicomDataset& dicom);
+
+    bool HasDefaultWindow() const
+    {
+      return hasDefaultWindow_;
+    }
+    
     double GetDefaultWindowCenter() const
     {
       return defaultWindowCenter_;
@@ -83,7 +98,9 @@
       return rescaleSlope_;
     }
 
-    void ConvertFrame(std::auto_ptr<Orthanc::ImageAccessor>& source) const;
+    void ConvertFrameInplace(std::auto_ptr<Orthanc::ImageAccessor>& source) const;
+
+    Orthanc::ImageAccessor* ConvertFrame(const Orthanc::ImageAccessor& source) const;
 
     void ApplyRescale(Orthanc::ImageAccessor& image,
                       bool useDouble) const;
--- a/Framework/Toolbox/GeometryToolbox.cpp	Mon Nov 05 10:04:56 2018 +0100
+++ b/Framework/Toolbox/GeometryToolbox.cpp	Mon Nov 05 10:06:18 2018 +0100
@@ -260,8 +260,9 @@
         }
         else
         {
-          spacingX = v[0];
-          spacingY = v[1];
+          // WARNING: X/Y are swapped (Y comes first)
+          spacingX = v[1];
+          spacingY = v[0];
         }
       }
       else
--- a/Framework/Toolbox/IWebService.h	Mon Nov 05 10:04:56 2018 +0100
+++ b/Framework/Toolbox/IWebService.h	Mon Nov 05 10:06:18 2018 +0100
@@ -13,7 +13,7 @@
  * 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/>.
  **/
@@ -22,41 +22,86 @@
 #pragma once
 
 #include <Core/IDynamicObject.h>
-
+#include "../../Framework/Messages/IObserver.h"
+#include "../../Framework/Messages/ICallable.h"
 #include <string>
+#include <map>
+#include <Core/Logging.h>
 
 namespace OrthancStone
 {
-  class IWebService : public boost::noncopyable
+  // The IWebService performs HTTP requests.
+  // Since applications can run in native or WASM environment and, since
+  // in a WASM environment, the WebService is asynchronous, the IWebservice
+  // also implements an asynchronous interface: you must schedule a request
+  // and you'll be notified when the response/error is ready.
+  class IWebService
   {
+  protected:
+    MessageBroker& broker_;
   public:
-    class ICallback : public boost::noncopyable
-    {
-    public:
-      virtual ~ICallback()
-      {
-      }
+    typedef std::map<std::string, std::string> Headers;
 
-      virtual void NotifyError(const std::string& uri,
-                               Orthanc::IDynamicObject* payload) = 0;
+    struct HttpRequestSuccessMessage: public BaseMessage<MessageType_HttpRequestSuccess>
+    {
+      const std::string& uri_;
+      const void* answer_;
+      size_t answerSize_;
+      Orthanc::IDynamicObject* payload_;
+      HttpRequestSuccessMessage(const std::string& uri,
+                                const void* answer,
+                                size_t answerSize,
+                                Orthanc::IDynamicObject* payload)
+        : BaseMessage(),
+          uri_(uri),
+          answer_(answer),
+          answerSize_(answerSize),
+          payload_(payload)
+      {}
+    };
 
-      virtual void NotifySuccess(const std::string& uri,
-                                 const void* answer,
-                                 size_t answerSize,
-                                 Orthanc::IDynamicObject* payload) = 0;
+    struct HttpRequestErrorMessage: public BaseMessage<MessageType_HttpRequestError>
+    {
+      const std::string& uri_;
+      Orthanc::IDynamicObject* payload_;
+      HttpRequestErrorMessage(const std::string& uri,
+                              Orthanc::IDynamicObject* payload)
+        : BaseMessage(),
+          uri_(uri),
+          payload_(payload)
+      {}
     };
-    
+
+
+
+    IWebService(MessageBroker& broker)
+      : broker_(broker)
+    {}
+
     virtual ~IWebService()
     {
     }
 
-    virtual void ScheduleGetRequest(ICallback& callback,
-                                    const std::string& uri,
-                                    Orthanc::IDynamicObject* payload) = 0;
+    virtual void GetAsync(const std::string& uri,
+                          const Headers& headers,
+                          Orthanc::IDynamicObject* payload,
+                          MessageHandler<IWebService::HttpRequestSuccessMessage>* successCallback,
+                          MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback = NULL,
+                          unsigned int timeoutInSeconds = 60) = 0;
 
-    virtual void SchedulePostRequest(ICallback& callback,
-                                     const std::string& uri,
-                                     const std::string& body,
-                                     Orthanc::IDynamicObject* payload) = 0;
+    virtual void PostAsync(const std::string& uri,
+                           const Headers& headers,
+                           const std::string& body,
+                           Orthanc::IDynamicObject* payload,
+                           MessageHandler<IWebService::HttpRequestSuccessMessage>* successCallback,
+                           MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback = NULL,
+                           unsigned int timeoutInSeconds = 60) = 0;
+
+    virtual void DeleteAsync(const std::string& uri,
+                             const Headers& headers,
+                             Orthanc::IDynamicObject* payload,
+                             MessageHandler<IWebService::HttpRequestSuccessMessage>* successCallback,
+                             MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback = NULL,
+                             unsigned int timeoutInSeconds = 60) = 0;
   };
 }
--- a/Framework/Toolbox/ImageGeometry.cpp	Mon Nov 05 10:04:56 2018 +0100
+++ b/Framework/Toolbox/ImageGeometry.cpp	Mon Nov 05 10:06:18 2018 +0100
@@ -152,10 +152,6 @@
       {
         *p = value;
       }
-      else
-      {
-        Reader::Traits::SetZero(*p);
-      }
 
       if (HasOffsetX)
       {
@@ -174,7 +170,8 @@
             ImageInterpolation Interpolation>
   static void ApplyAffineInternal(Orthanc::ImageAccessor& target,
                                   const Orthanc::ImageAccessor& source,
-                                  const Matrix& a)
+                                  const Matrix& a,
+                                  bool clear)
   {
     assert(target.GetFormat() == Format &&
            source.GetFormat() == Format);
@@ -182,13 +179,16 @@
     typedef SubpixelReader<Format, Interpolation>  Reader;
     typedef typename Reader::PixelType             PixelType;
 
-    if (Format == Orthanc::PixelFormat_RGB24)
+    if (clear)
     {
-      Orthanc::ImageProcessing::Set(target, 0, 0, 0, 255);
-    }
-    else
-    {
-      Orthanc::ImageProcessing::Set(target, 0);
+      if (Format == Orthanc::PixelFormat_RGB24)
+      {
+        Orthanc::ImageProcessing::Set(target, 0, 0, 0, 255);
+      }
+      else
+      {
+        Orthanc::ImageProcessing::Set(target, 0);
+      }
     }
 
     Matrix inva;
@@ -260,7 +260,8 @@
                             double a21,
                             double a22,
                             double b2,
-                            ImageInterpolation interpolation)
+                            ImageInterpolation interpolation,
+                            bool clear)
   {
     if (source.GetFormat() != target.GetFormat())
     {
@@ -292,12 +293,12 @@
         {
           case ImageInterpolation_Nearest:
             ApplyAffineInternal<Orthanc::PixelFormat_Grayscale8, 
-                                ImageInterpolation_Nearest>(target, source, a);
+                                ImageInterpolation_Nearest>(target, source, a, clear);
             break;
 
           case ImageInterpolation_Bilinear:
             ApplyAffineInternal<Orthanc::PixelFormat_Grayscale8, 
-                                ImageInterpolation_Bilinear>(target, source, a);
+                                ImageInterpolation_Bilinear>(target, source, a, clear);
             break;
 
           default:
@@ -310,12 +311,12 @@
         {
           case ImageInterpolation_Nearest:
             ApplyAffineInternal<Orthanc::PixelFormat_Grayscale16, 
-                                ImageInterpolation_Nearest>(target, source, a);
+                                ImageInterpolation_Nearest>(target, source, a, clear);
             break;
 
           case ImageInterpolation_Bilinear:
             ApplyAffineInternal<Orthanc::PixelFormat_Grayscale16, 
-                                ImageInterpolation_Bilinear>(target, source, a);
+                                ImageInterpolation_Bilinear>(target, source, a, clear);
             break;
 
           default:
@@ -328,12 +329,30 @@
         {
           case ImageInterpolation_Nearest:
             ApplyAffineInternal<Orthanc::PixelFormat_SignedGrayscale16, 
-                                ImageInterpolation_Nearest>(target, source, a);
+                                ImageInterpolation_Nearest>(target, source, a, clear);
             break;
 
           case ImageInterpolation_Bilinear:
             ApplyAffineInternal<Orthanc::PixelFormat_SignedGrayscale16, 
-                                ImageInterpolation_Bilinear>(target, source, a);
+                                ImageInterpolation_Bilinear>(target, source, a, clear);
+            break;
+
+          default:
+            throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+        }
+        break;
+
+      case Orthanc::PixelFormat_Float32:
+        switch (interpolation)
+        {
+          case ImageInterpolation_Nearest:
+            ApplyAffineInternal<Orthanc::PixelFormat_Float32, 
+                                ImageInterpolation_Nearest>(target, source, a, clear);
+            break;
+
+          case ImageInterpolation_Bilinear:
+            ApplyAffineInternal<Orthanc::PixelFormat_Float32, 
+                                ImageInterpolation_Bilinear>(target, source, a, clear);
             break;
 
           default:
@@ -346,7 +365,7 @@
         {
           case ImageInterpolation_Nearest:
             ApplyAffineInternal<Orthanc::PixelFormat_RGB24, 
-                                ImageInterpolation_Nearest>(target, source, a);
+                                ImageInterpolation_Nearest>(target, source, a, clear);
             break;
 
           default:
@@ -412,10 +431,6 @@
           { 
             reader.GetValue(*p, sourceX, sourceY);
           }
-          else
-          {
-            Reader::Traits::SetZero(*p);
-          }
 
           p++;
         }
@@ -429,7 +444,8 @@
   void ApplyProjectiveTransform(Orthanc::ImageAccessor& target,
                                 const Orthanc::ImageAccessor& source,
                                 const Matrix& a,
-                                ImageInterpolation interpolation)
+                                ImageInterpolation interpolation,
+                                bool clear)
   {
     if (source.GetFormat() != target.GetFormat())
     {
@@ -463,18 +479,21 @@
         ApplyAffineTransform(target, source, 
                              a(0, 0) / w, a(0, 1) / w, a(0, 2) / w,
                              a(1, 0) / w, a(1, 1) / w, a(1, 2) / w,
-                             interpolation);
+                             interpolation, clear);
         return;
       }
     }
 
-    if (target.GetFormat() == Orthanc::PixelFormat_RGB24)
+    if (clear)
     {
-      Orthanc::ImageProcessing::Set(target, 0, 0, 0, 255);
-    }
-    else
-    {
-      Orthanc::ImageProcessing::Set(target, 0);
+      if (target.GetFormat() == Orthanc::PixelFormat_RGB24)
+      {
+        Orthanc::ImageProcessing::Set(target, 0, 0, 0, 255);
+      }
+      else
+      {
+        Orthanc::ImageProcessing::Set(target, 0);
+      }
     }
 
     Matrix inva;
@@ -539,6 +558,24 @@
         }
         break;
 
+      case Orthanc::PixelFormat_Float32:
+        switch (interpolation)
+        {
+          case ImageInterpolation_Nearest:
+            ApplyProjectiveInternal<Orthanc::PixelFormat_Float32, 
+                                    ImageInterpolation_Nearest>(target, source, a, inva);
+            break;
+
+          case ImageInterpolation_Bilinear:
+            ApplyProjectiveInternal<Orthanc::PixelFormat_Float32, 
+                                    ImageInterpolation_Bilinear>(target, source, a, inva);
+            break;
+
+          default:
+            throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+        }
+        break;
+
       case Orthanc::PixelFormat_RGB24:
         switch (interpolation)
         {
--- a/Framework/Toolbox/ImageGeometry.h	Mon Nov 05 10:04:56 2018 +0100
+++ b/Framework/Toolbox/ImageGeometry.h	Mon Nov 05 10:06:18 2018 +0100
@@ -50,10 +50,12 @@
                             double a21,
                             double a22,
                             double b2,
-                            ImageInterpolation interpolation);
+                            ImageInterpolation interpolation,
+                            bool clear);
 
   void ApplyProjectiveTransform(Orthanc::ImageAccessor& target,
                                 const Orthanc::ImageAccessor& source,
                                 const Matrix& a,
-                                ImageInterpolation interpolation);
+                                ImageInterpolation interpolation,
+                                bool clear);
 }
--- a/Framework/Toolbox/MessagingToolbox.cpp	Mon Nov 05 10:04:56 2018 +0100
+++ b/Framework/Toolbox/MessagingToolbox.cpp	Mon Nov 05 10:06:18 2018 +0100
@@ -31,6 +31,7 @@
 
 #include <boost/lexical_cast.hpp>
 #include <json/reader.h>
+#include <json/writer.h>
 
 namespace OrthancStone
 {
@@ -114,6 +115,12 @@
                           target);
     }
 
+    void JsonToString(std::string& target,
+                      const Json::Value& source)
+    {
+      Json::FastWriter writer;
+      target = writer.write(source);
+    }
 
     static void ParseJsonException(Json::Value& target,
                                    const std::string& source)
--- a/Framework/Toolbox/MessagingToolbox.h	Mon Nov 05 10:04:56 2018 +0100
+++ b/Framework/Toolbox/MessagingToolbox.h	Mon Nov 05 10:06:18 2018 +0100
@@ -13,7 +13,7 @@
  * 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/>.
  **/
@@ -38,6 +38,10 @@
                    const void* content,
                    size_t size);
 
+    void JsonToString(std::string& target,
+                      const Json::Value& source);
+
+
     void RestApiGet(Json::Value& target,
                     OrthancPlugins::IOrthancConnection& orthanc,
                     const std::string& uri);
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Toolbox/OrthancApiClient.cpp	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,232 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 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 "OrthancApiClient.h"
+
+#include "MessagingToolbox.h"
+#include <Core/OrthancException.h>
+#include "Framework/Toolbox/MessagingToolbox.h"
+
+namespace OrthancStone {
+
+  OrthancApiClient::OrthancApiClient(MessageBroker &broker, IWebService &orthanc)
+    : IObservable(broker),
+      orthanc_(orthanc)
+  {
+  }
+
+  // performs the translation between IWebService messages and OrthancApiClient messages
+  // TODO: handle destruction of this object (with shared_ptr ?::delete_later ???)
+  class HttpResponseToJsonConverter : public IObserver, IObservable
+  {
+  private:
+    std::auto_ptr<MessageHandler<OrthancApiClient::JsonResponseReadyMessage> > orthancApiSuccessCallback_;
+    std::auto_ptr<MessageHandler<OrthancApiClient::HttpErrorMessage> > orthancApiFailureCallback_;
+
+  public:
+    HttpResponseToJsonConverter(MessageBroker& broker,
+                                MessageHandler<OrthancApiClient::JsonResponseReadyMessage>* orthancApiSuccessCallback,
+                                MessageHandler<OrthancApiClient::HttpErrorMessage>* orthancApiFailureCallback)
+      : IObserver(broker),
+        IObservable(broker),
+        orthancApiSuccessCallback_(orthancApiSuccessCallback),
+        orthancApiFailureCallback_(orthancApiFailureCallback)
+    {
+    }
+
+    void ConvertResponseToJson(const IWebService::HttpRequestSuccessMessage& message)
+    {
+      Json::Value response;
+      if (MessagingToolbox::ParseJson(response, message.answer_, message.answerSize_))
+      {
+        if (orthancApiSuccessCallback_.get() != NULL)
+        {
+          orthancApiSuccessCallback_->Apply(OrthancApiClient::JsonResponseReadyMessage(message.uri_, response, message.payload_));
+        }
+      }
+      else if (orthancApiFailureCallback_.get() != NULL)
+      {
+        orthancApiFailureCallback_->Apply(OrthancApiClient::HttpErrorMessage(message.uri_, message.payload_));
+      }
+
+      delete this; // hack untill we find someone to take ownership of this object (https://isocpp.org/wiki/faq/freestore-mgmt#delete-this)
+    }
+
+    void ConvertError(const IWebService::HttpRequestErrorMessage& message)
+    {
+      if (orthancApiFailureCallback_.get() != NULL)
+      {
+        orthancApiFailureCallback_->Apply(OrthancApiClient::HttpErrorMessage(message.uri_));
+      }
+
+      delete this; // hack untill we find someone to take ownership of this object (https://isocpp.org/wiki/faq/freestore-mgmt#delete-this)
+    }
+  };
+
+  // performs the translation between IWebService messages and OrthancApiClient messages
+  // TODO: handle destruction of this object (with shared_ptr ?::delete_later ???)
+  class HttpResponseToBinaryConverter : public IObserver, IObservable
+  {
+  private:
+    std::auto_ptr<MessageHandler<OrthancApiClient::BinaryResponseReadyMessage> > orthancApiSuccessCallback_;
+    std::auto_ptr<MessageHandler<OrthancApiClient::HttpErrorMessage> > orthancApiFailureCallback_;
+
+  public:
+    HttpResponseToBinaryConverter(MessageBroker& broker,
+                                  MessageHandler<OrthancApiClient::BinaryResponseReadyMessage>* orthancApiSuccessCallback,
+                                  MessageHandler<OrthancApiClient::HttpErrorMessage>* orthancApiFailureCallback)
+      : IObserver(broker),
+        IObservable(broker),
+        orthancApiSuccessCallback_(orthancApiSuccessCallback),
+        orthancApiFailureCallback_(orthancApiFailureCallback)
+    {
+    }
+
+    void ConvertResponseToBinary(const IWebService::HttpRequestSuccessMessage& message)
+    {
+      if (orthancApiSuccessCallback_.get() != NULL)
+      {
+        orthancApiSuccessCallback_->Apply(OrthancApiClient::BinaryResponseReadyMessage(message.uri_, message.answer_, message.answerSize_, message.payload_));
+      }
+      else if (orthancApiFailureCallback_.get() != NULL)
+      {
+        orthancApiFailureCallback_->Apply(OrthancApiClient::HttpErrorMessage(message.uri_, message.payload_));
+      }
+
+      delete this; // hack untill we find someone to take ownership of this object (https://isocpp.org/wiki/faq/freestore-mgmt#delete-this)
+    }
+
+    void ConvertError(const IWebService::HttpRequestErrorMessage& message)
+    {
+      if (orthancApiFailureCallback_.get() != NULL)
+      {
+        orthancApiFailureCallback_->Apply(OrthancApiClient::HttpErrorMessage(message.uri_));
+      }
+
+      delete this; // hack untill we find someone to take ownership of this object (https://isocpp.org/wiki/faq/freestore-mgmt#delete-this)
+    }
+  };
+
+  // performs the translation between IWebService messages and OrthancApiClient messages
+  // TODO: handle destruction of this object (with shared_ptr ?::delete_later ???)
+  class HttpResponseToEmptyConverter : public IObserver, IObservable
+  {
+  private:
+    std::auto_ptr<MessageHandler<OrthancApiClient::EmptyResponseReadyMessage> > orthancApiSuccessCallback_;
+    std::auto_ptr<MessageHandler<OrthancApiClient::HttpErrorMessage> > orthancApiFailureCallback_;
+
+  public:
+    HttpResponseToEmptyConverter(MessageBroker& broker,
+                                  MessageHandler<OrthancApiClient::EmptyResponseReadyMessage>* orthancApiSuccessCallback,
+                                  MessageHandler<OrthancApiClient::HttpErrorMessage>* orthancApiFailureCallback)
+      : IObserver(broker),
+        IObservable(broker),
+        orthancApiSuccessCallback_(orthancApiSuccessCallback),
+        orthancApiFailureCallback_(orthancApiFailureCallback)
+    {
+    }
+
+    void ConvertResponseToEmpty(const IWebService::HttpRequestSuccessMessage& message)
+    {
+      if (orthancApiSuccessCallback_.get() != NULL)
+      {
+        orthancApiSuccessCallback_->Apply(OrthancApiClient::EmptyResponseReadyMessage(message.uri_, message.payload_));
+      }
+      else if (orthancApiFailureCallback_.get() != NULL)
+      {
+        orthancApiFailureCallback_->Apply(OrthancApiClient::HttpErrorMessage(message.uri_, message.payload_));
+      }
+
+      delete this; // hack untill we find someone to take ownership of this object (https://isocpp.org/wiki/faq/freestore-mgmt#delete-this)
+    }
+
+    void ConvertError(const IWebService::HttpRequestErrorMessage& message)
+    {
+      if (orthancApiFailureCallback_.get() != NULL)
+      {
+        orthancApiFailureCallback_->Apply(OrthancApiClient::HttpErrorMessage(message.uri_));
+      }
+
+      delete this; // hack untill we find someone to take ownership of this object (https://isocpp.org/wiki/faq/freestore-mgmt#delete-this)
+    }
+  };
+
+
+  void OrthancApiClient::GetJsonAsync(const std::string& uri,
+                                      MessageHandler<JsonResponseReadyMessage>* successCallback,
+                                      MessageHandler<HttpErrorMessage>* failureCallback,
+                                      Orthanc::IDynamicObject* payload)
+  {
+    HttpResponseToJsonConverter* converter = new HttpResponseToJsonConverter(broker_, successCallback, failureCallback);  // it is currently deleting itself after being used
+    orthanc_.GetAsync(uri, IWebService::Headers(), payload,
+                      new Callable<HttpResponseToJsonConverter, IWebService::HttpRequestSuccessMessage>(*converter, &HttpResponseToJsonConverter::ConvertResponseToJson),
+                      new Callable<HttpResponseToJsonConverter, IWebService::HttpRequestErrorMessage>(*converter, &HttpResponseToJsonConverter::ConvertError));
+
+  }
+
+  void OrthancApiClient::GetBinaryAsync(const std::string& uri,
+                                        const IWebService::Headers& headers,
+                                        MessageHandler<BinaryResponseReadyMessage>* successCallback,
+                                        MessageHandler<HttpErrorMessage>* failureCallback,
+                                        Orthanc::IDynamicObject* payload)
+  {
+    HttpResponseToBinaryConverter* converter = new HttpResponseToBinaryConverter(broker_, successCallback, failureCallback);  // it is currently deleting itself after being used
+    orthanc_.GetAsync(uri, headers, payload,
+                      new Callable<HttpResponseToBinaryConverter, IWebService::HttpRequestSuccessMessage>(*converter, &HttpResponseToBinaryConverter::ConvertResponseToBinary),
+                      new Callable<HttpResponseToBinaryConverter, IWebService::HttpRequestErrorMessage>(*converter, &HttpResponseToBinaryConverter::ConvertError));
+  }
+
+  void OrthancApiClient::PostBinaryAsyncExpectJson(const std::string& uri,
+                                                   const std::string& body,
+                                                   MessageHandler<JsonResponseReadyMessage>* successCallback,
+                                                   MessageHandler<HttpErrorMessage>* failureCallback,
+                                                   Orthanc::IDynamicObject* payload)
+  {
+    HttpResponseToJsonConverter* converter = new HttpResponseToJsonConverter(broker_, successCallback, failureCallback);  // it is currently deleting itself after being used
+    orthanc_.PostAsync(uri, IWebService::Headers(), body, payload,
+                       new Callable<HttpResponseToJsonConverter, IWebService::HttpRequestSuccessMessage>(*converter, &HttpResponseToJsonConverter::ConvertResponseToJson),
+                       new Callable<HttpResponseToJsonConverter, IWebService::HttpRequestErrorMessage>(*converter, &HttpResponseToJsonConverter::ConvertError));
+
+  }
+
+  void OrthancApiClient::PostJsonAsyncExpectJson(const std::string& uri,
+                                                 const Json::Value& data,
+                                                 MessageHandler<JsonResponseReadyMessage>* successCallback,
+                                                 MessageHandler<HttpErrorMessage>* failureCallback,
+                                                 Orthanc::IDynamicObject* payload)
+  {
+    std::string body;
+    MessagingToolbox::JsonToString(body, data);
+    return PostBinaryAsyncExpectJson(uri, body, successCallback, failureCallback, payload);
+  }
+
+  void OrthancApiClient::DeleteAsync(const std::string& uri,
+                                     MessageHandler<EmptyResponseReadyMessage>* successCallback,
+                                     MessageHandler<HttpErrorMessage>* failureCallback,
+                                     Orthanc::IDynamicObject* payload)
+  {
+    HttpResponseToEmptyConverter* converter = new HttpResponseToEmptyConverter(broker_, successCallback, failureCallback);  // it is currently deleting itself after being used
+    orthanc_.DeleteAsync(uri, IWebService::Headers(), payload,
+                       new Callable<HttpResponseToEmptyConverter, IWebService::HttpRequestSuccessMessage>(*converter, &HttpResponseToEmptyConverter::ConvertResponseToEmpty),
+                       new Callable<HttpResponseToEmptyConverter, IWebService::HttpRequestErrorMessage>(*converter, &HttpResponseToEmptyConverter::ConvertError));
+  }
+
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Toolbox/OrthancApiClient.h	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,167 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 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
+
+#include <boost/shared_ptr.hpp>
+#include <json/json.h>
+
+#include "IWebService.h"
+#include "../Messages/IObservable.h"
+#include "../Messages/Promise.h"
+
+namespace OrthancStone
+{
+  class OrthancApiClient:
+      public IObservable
+  {
+  public:
+
+    struct JsonResponseReadyMessage : public BaseMessage<MessageType_OrthancApi_GenericGetJson_Ready>
+    {
+      Json::Value   Response;
+      std::string   Uri;
+      std::auto_ptr<Orthanc::IDynamicObject>  Payload;
+
+      JsonResponseReadyMessage(const std::string& uri,
+                               const Json::Value& response,
+                               Orthanc::IDynamicObject*  payload = NULL)
+        : BaseMessage(),
+          Response(response),
+          Uri(uri),
+          Payload(payload)
+      {
+      }
+    };
+
+    struct EmptyResponseReadyMessage : public BaseMessage<MessageType_OrthancApi_GenericEmptyResponse_Ready>
+    {
+      std::string   Uri;
+      std::auto_ptr<Orthanc::IDynamicObject>  Payload;
+
+      EmptyResponseReadyMessage(const std::string& uri,
+                                Orthanc::IDynamicObject*  payload = NULL)
+        : BaseMessage(),
+          Uri(uri),
+          Payload(payload)
+      {
+      }
+    };
+
+    struct HttpErrorMessage : public BaseMessage<MessageType_OrthancApi_GenericHttpError_Ready>
+    {
+      std::string   Uri;
+      std::auto_ptr<Orthanc::IDynamicObject>  Payload;
+
+      HttpErrorMessage(const std::string& uri,
+                       Orthanc::IDynamicObject*  payload = NULL)
+        : BaseMessage(),
+          Uri(uri),
+          Payload(payload)
+      {
+      }
+    };
+
+    struct BinaryResponseReadyMessage : public BaseMessage<MessageType_OrthancApi_GenericGetBinary_Ready>
+    {
+      const void* Answer;
+      size_t AnswerSize;
+      std::string   Uri;
+      std::auto_ptr<Orthanc::IDynamicObject>  Payload;
+
+      BinaryResponseReadyMessage(const std::string& uri,
+                                 const void* answer,
+                                 size_t answerSize,
+                                 Orthanc::IDynamicObject*  payload = NULL)
+        : BaseMessage(),
+          Answer(answer),
+          AnswerSize(answerSize),
+          Uri(uri),
+          Payload(payload)
+      {
+      }
+    };
+
+
+
+  public:
+
+    enum Mode
+    {
+      Mode_GetJson
+    };
+
+  protected:
+    IWebService&                      orthanc_;
+
+  public:
+    OrthancApiClient(MessageBroker& broker,
+                     IWebService& orthanc);
+    virtual ~OrthancApiClient() {}
+
+    // schedule a GET request expecting a JSON response.
+    void GetJsonAsync(const std::string& uri,
+                      MessageHandler<JsonResponseReadyMessage>* successCallback,
+                      MessageHandler<HttpErrorMessage>* failureCallback = NULL,
+                      Orthanc::IDynamicObject* payload = NULL);
+
+    // schedule a GET request expecting a binary response.
+    void GetBinaryAsync(const std::string& uri,
+                        const std::string& contentType,
+                        MessageHandler<BinaryResponseReadyMessage>* successCallback,
+                        MessageHandler<HttpErrorMessage>* failureCallback = NULL,
+                        Orthanc::IDynamicObject* payload = NULL)
+    {
+      IWebService::Headers headers;
+      headers["Accept"] = contentType;
+      GetBinaryAsync(uri, headers, successCallback, failureCallback, payload);
+    }
+
+    // schedule a GET request expecting a binary response.
+    void GetBinaryAsync(const std::string& uri,
+                        const IWebService::Headers& headers,
+                        MessageHandler<BinaryResponseReadyMessage>* successCallback,
+                        MessageHandler<HttpErrorMessage>* failureCallback = NULL,
+                        Orthanc::IDynamicObject* payload = NULL);
+
+    // schedule a POST request expecting a JSON response.
+    void PostBinaryAsyncExpectJson(const std::string& uri,
+                                   const std::string& body,
+                                   MessageHandler<JsonResponseReadyMessage>* successCallback,
+                                   MessageHandler<HttpErrorMessage>* failureCallback = NULL,
+                                   Orthanc::IDynamicObject* payload = NULL);
+
+    // schedule a POST request expecting a JSON response.
+    void PostJsonAsyncExpectJson(const std::string& uri,
+                                 const Json::Value& data,
+                                 MessageHandler<JsonResponseReadyMessage>* successCallback,
+                                 MessageHandler<HttpErrorMessage>* failureCallback = NULL,
+                                 Orthanc::IDynamicObject* payload = NULL);
+
+    // schedule a DELETE request expecting an empty response.
+    void DeleteAsync(const std::string& uri,
+                     MessageHandler<EmptyResponseReadyMessage>* successCallback,
+                     MessageHandler<HttpErrorMessage>* failureCallback = NULL,
+                     Orthanc::IDynamicObject* payload = NULL);
+
+
+  };
+}
--- a/Framework/Toolbox/OrthancSlicesLoader.cpp	Mon Nov 05 10:04:56 2018 +0100
+++ b/Framework/Toolbox/OrthancSlicesLoader.cpp	Mon Nov 05 10:06:18 2018 +0100
@@ -13,7 +13,7 @@
  * 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/>.
  **/
@@ -29,6 +29,7 @@
 #include <Core/Images/ImageProcessing.h>
 #include <Core/Images/JpegReader.h>
 #include <Core/Images/PngReader.h>
+#include <Core/Images/PamReader.h>
 #include <Core/Logging.h>
 #include <Core/OrthancException.h>
 #include <Core/Toolbox.h>
@@ -47,10 +48,10 @@
 static std::string base64_decode(const std::string &in)
 {
   std::string out;
-
+  
   std::vector<int> T(256,-1);
-  for (int i=0; i<64; i++) T["ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"[i]] = i; 
-
+  for (int i=0; i<64; i++) T["ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"[i]] = i;
+  
   int val=0, valb=-8;
   for (size_t i = 0; i < in.size(); i++) {
     unsigned char c = in[i];
@@ -117,18 +118,13 @@
       assert(mode_ == Mode_FrameGeometry);
       return frame_;
     }
-      
+
     const std::string& GetInstanceId() const
     {
       assert(mode_ == Mode_FrameGeometry ||
              mode_ == Mode_InstanceGeometry);
       return instanceId_;
     }
-      
-    static Operation* DownloadSeriesGeometry()
-    {
-      return new Operation(Mode_SeriesGeometry);
-    }
 
     static Operation* DownloadInstanceGeometry(const std::string& instanceId)
     {
@@ -163,103 +159,21 @@
       std::auto_ptr<Operation> tmp(new Operation(Mode_LoadRawImage));
       tmp->sliceIndex_ = sliceIndex;
       tmp->slice_ = &slice;
-      tmp->quality_ = SliceImageQuality_Full;
+      tmp->quality_ = SliceImageQuality_InternalRaw;
       return tmp.release();
     }
-  };
-    
-
-  class OrthancSlicesLoader::WebCallback : public IWebService::ICallback
-  {
-  private:
-    OrthancSlicesLoader&  that_;
-
-  public:
-    WebCallback(OrthancSlicesLoader&  that) :
-      that_(that)
-    {
-    }
 
-    virtual void NotifySuccess(const std::string& uri,
-                               const void* answer,
-                               size_t answerSize,
-                               Orthanc::IDynamicObject* payload)
+    static Operation* DownloadDicomFile(const Slice&  slice)
     {
-      std::auto_ptr<Operation> operation(dynamic_cast<Operation*>(payload));
-
-      switch (operation->GetMode())
-      {
-        case Mode_SeriesGeometry:
-          that_.ParseSeriesGeometry(answer, answerSize);
-          break;
-
-        case Mode_InstanceGeometry:
-          that_.ParseInstanceGeometry(operation->GetInstanceId(), answer, answerSize);
-          break;
-
-        case Mode_FrameGeometry:
-          that_.ParseFrameGeometry(operation->GetInstanceId(),
-                                   operation->GetFrame(), answer, answerSize);
-          break;
-
-        case Mode_LoadImage:
-          switch (operation->GetQuality())
-          {
-            case SliceImageQuality_Full:
-              that_.ParseSliceImagePng(*operation, answer, answerSize);
-              break;
-
-            case SliceImageQuality_Jpeg50:
-            case SliceImageQuality_Jpeg90:
-            case SliceImageQuality_Jpeg95:
-              that_.ParseSliceImageJpeg(*operation, answer, answerSize);
-              break;
-
-            default:
-              throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
-          }
-                
-          break;
-
-        case Mode_LoadRawImage:
-          that_.ParseSliceRawImage(*operation, answer, answerSize);
-          break;
-
-        default:
-          throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
-      }
+      std::auto_ptr<Operation> tmp(new Operation(Mode_LoadDicomFile));
+      tmp->slice_ = &slice;
+      return tmp.release();
     }
 
-    virtual void NotifyError(const std::string& uri,
-                             Orthanc::IDynamicObject* payload)
-    {
-      std::auto_ptr<Operation> operation(dynamic_cast<Operation*>(payload));
-      LOG(ERROR) << "Cannot download " << uri;
-
-      switch (operation->GetMode())
-      {
-        case Mode_FrameGeometry:
-        case Mode_SeriesGeometry:
-          that_.userCallback_.NotifyGeometryError(that_);
-          that_.state_ = State_Error;
-          break;
-
-        case Mode_LoadImage:
-          that_.userCallback_.NotifySliceImageError(that_, operation->GetSliceIndex(),
-                                                    operation->GetSlice(),
-                                                    operation->GetQuality());
-          break;
-          
-        default:
-          throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
-      }
-    }     
   };
 
-
-  
   void OrthancSlicesLoader::NotifySliceImageSuccess(const Operation& operation,
-                                                    std::auto_ptr<Orthanc::ImageAccessor>& image) const
+                                                    boost::shared_ptr<Orthanc::ImageAccessor> image)
   {
     if (image.get() == NULL)
     {
@@ -267,19 +181,19 @@
     }
     else
     {
-      userCallback_.NotifySliceImageReady
-        (*this, operation.GetSliceIndex(), operation.GetSlice(), image, operation.GetQuality());
+      OrthancSlicesLoader::SliceImageReadyMessage msg(operation.GetSliceIndex(), operation.GetSlice(), image, operation.GetQuality());
+      EmitMessage(msg);
     }
   }
-
+  
   
-  void OrthancSlicesLoader::NotifySliceImageError(const Operation& operation) const
+  void OrthancSlicesLoader::NotifySliceImageError(const Operation& operation)
   {
-    userCallback_.NotifySliceImageError
-      (*this, operation.GetSliceIndex(), operation.GetSlice(), operation.GetQuality());
+    OrthancSlicesLoader::SliceImageErrorMessage msg(operation.GetSliceIndex(), operation.GetSlice(), operation.GetQuality());
+    EmitMessage(msg);
   }
-
-
+  
+  
   void OrthancSlicesLoader::SortAndFinalizeSlices()
   {
     bool ok = false;
@@ -295,41 +209,44 @@
         ok = true;
       }
     }
-
+    
     state_ = State_GeometryReady;
-
+    
     if (ok)
     {
       LOG(INFO) << "Loaded a series with " << slices_.GetSliceCount() << " slice(s)";
-      userCallback_.NotifyGeometryReady(*this);
+      EmitMessage(SliceGeometryReadyMessage(*this));
     }
     else
     {
       LOG(ERROR) << "This series is empty";
-      userCallback_.NotifyGeometryError(*this);
+      EmitMessage(SliceGeometryErrorMessage(*this));
     }
   }
+  
+  void OrthancSlicesLoader::OnGeometryError(const OrthancApiClient::HttpErrorMessage& message)
+  {
+    EmitMessage(SliceGeometryErrorMessage(*this));
+    state_ = State_Error;
+  }
 
-  
-  void OrthancSlicesLoader::ParseSeriesGeometry(const void* answer,
-                                                size_t size)
+  void OrthancSlicesLoader::OnSliceImageError(const OrthancApiClient::HttpErrorMessage& message)
   {
-    Json::Value series;
-    if (!MessagingToolbox::ParseJson(series, answer, size) ||
-        series.type() != Json::objectValue)
-    {
-      userCallback_.NotifyGeometryError(*this);
-      return;
-    }
+    NotifySliceImageError(dynamic_cast<const Operation&>(*(message.Payload)));
+    state_ = State_Error;
+  }
 
+  void OrthancSlicesLoader::ParseSeriesGeometry(const OrthancApiClient::JsonResponseReadyMessage& message)
+  {
+    Json::Value series = message.Response;
     Json::Value::Members instances = series.getMemberNames();
-
+    
     slices_.Reserve(instances.size());
-
+    
     for (size_t i = 0; i < instances.size(); i++)
     {
       OrthancPlugins::FullOrthancDataset dataset(series[instances[i]]);
-
+      
       Orthanc::DicomMap dicom;
       MessagingToolbox::ConvertDataset(dicom, dataset);
       
@@ -338,7 +255,7 @@
       {
         frames = 1;
       }
-
+      
       for (unsigned int frame = 0; frame < frames; frame++)
       {
         std::auto_ptr<Slice> slice(new Slice);
@@ -352,28 +269,20 @@
         }
       }
     }
-
+    
     SortAndFinalizeSlices();
   }
-
-
-  void OrthancSlicesLoader::ParseInstanceGeometry(const std::string& instanceId,
-                                                  const void* answer,
-                                                  size_t size)
+  
+  void OrthancSlicesLoader::ParseInstanceGeometry(const OrthancApiClient::JsonResponseReadyMessage& message)
   {
-    Json::Value tags;
-    if (!MessagingToolbox::ParseJson(tags, answer, size) ||
-        tags.type() != Json::objectValue)
-    {
-      userCallback_.NotifyGeometryError(*this);
-      return;
-    }
+    Json::Value tags = message.Response;
+    const std::string& instanceId = dynamic_cast<OrthancSlicesLoader::Operation*>(message.Payload.get())->GetInstanceId();
 
     OrthancPlugins::FullOrthancDataset dataset(tags);
-
+    
     Orthanc::DicomMap dicom;
     MessagingToolbox::ConvertDataset(dicom, dataset);
-      
+
     unsigned int frames;
     if (!dicom.ParseUnsignedInteger32(frames, Orthanc::DICOM_TAG_NUMBER_OF_FRAMES))
     {
@@ -381,7 +290,7 @@
     }
     
     LOG(INFO) << "Instance " << instanceId << " contains " << frames << " frame(s)";
-
+    
     for (unsigned int frame = 0; frame < frames; frame++)
     {
       std::auto_ptr<Slice> slice(new Slice);
@@ -392,60 +301,92 @@
       else
       {
         LOG(WARNING) << "Skipping invalid multi-frame instance " << instanceId;
-        userCallback_.NotifyGeometryError(*this);
+        EmitMessage(SliceGeometryErrorMessage(*this));
         return;
       }
     }
-
+    
     SortAndFinalizeSlices();
   }
-
-
-  void OrthancSlicesLoader::ParseFrameGeometry(const std::string& instanceId,
-                                               unsigned int frame,
-                                               const void* answer,
-                                               size_t size)
+  
+  
+  void OrthancSlicesLoader::ParseFrameGeometry(const OrthancApiClient::JsonResponseReadyMessage& message)
   {
-    Json::Value tags;
-    if (!MessagingToolbox::ParseJson(tags, answer, size) ||
-        tags.type() != Json::objectValue)
-    {
-      userCallback_.NotifyGeometryError(*this);
-      return;
-    }
+    Json::Value tags = message.Response;
+    const std::string& instanceId = dynamic_cast<OrthancSlicesLoader::Operation*>(message.Payload.get())->GetInstanceId();
+    unsigned int frame = dynamic_cast<OrthancSlicesLoader::Operation*>(message.Payload.get())->GetFrame();
 
     OrthancPlugins::FullOrthancDataset dataset(tags);
-
+    
     state_ = State_GeometryReady;
-
+    
     Orthanc::DicomMap dicom;
     MessagingToolbox::ConvertDataset(dicom, dataset);
-
+    
     std::auto_ptr<Slice> slice(new Slice);
     if (slice->ParseOrthancFrame(dicom, instanceId, frame))
     {
-      LOG(INFO) << "Loaded instance " << instanceId;
+      LOG(INFO) << "Loaded instance geometry " << instanceId;
       slices_.AddSlice(slice.release());
-      userCallback_.NotifyGeometryReady(*this);
+      EmitMessage(SliceGeometryReadyMessage(*this));
     }
     else
     {
       LOG(WARNING) << "Skipping invalid instance " << instanceId;
-      userCallback_.NotifyGeometryError(*this);
+      EmitMessage(SliceGeometryErrorMessage(*this));
     }
   }
-
+  
+  
+  void OrthancSlicesLoader::ParseSliceImagePng(const OrthancApiClient::BinaryResponseReadyMessage& message)
+  {
+    const Operation& operation = dynamic_cast<const OrthancSlicesLoader::Operation&>(*message.Payload.get());
+    boost::shared_ptr<Orthanc::ImageAccessor>  image;
+    
+    try
+    {
+      image.reset(new Orthanc::PngReader);
+      dynamic_cast<Orthanc::PngReader&>(*image).ReadFromMemory(message.Answer, message.AnswerSize);
+    }
+    catch (Orthanc::OrthancException&)
+    {
+      NotifySliceImageError(operation);
+      return;
+    }
+    
+    if (image->GetWidth() != operation.GetSlice().GetWidth() ||
+        image->GetHeight() != operation.GetSlice().GetHeight())
+    {
+      NotifySliceImageError(operation);
+      return;
+    }
 
-  void OrthancSlicesLoader::ParseSliceImagePng(const Operation& operation,
-                                               const void* answer,
-                                               size_t size)
+    if (operation.GetSlice().GetConverter().GetExpectedPixelFormat() ==
+        Orthanc::PixelFormat_SignedGrayscale16)
+    {
+      if (image->GetFormat() == Orthanc::PixelFormat_Grayscale16)
+      {
+        image->SetFormat(Orthanc::PixelFormat_SignedGrayscale16);
+      }
+      else
+      {
+        NotifySliceImageError(operation);
+        return;
+      }
+    }
+    
+    NotifySliceImageSuccess(operation, image);
+  }
+  
+  void OrthancSlicesLoader::ParseSliceImagePam(const OrthancApiClient::BinaryResponseReadyMessage& message)
   {
-    std::auto_ptr<Orthanc::ImageAccessor>  image;
+    const Operation& operation = dynamic_cast<const OrthancSlicesLoader::Operation&>(*message.Payload.get());
+    boost::shared_ptr<Orthanc::ImageAccessor>  image;
 
     try
     {
-      image.reset(new Orthanc::PngReader);
-      dynamic_cast<Orthanc::PngReader&>(*image).ReadFromMemory(answer, size);
+      image.reset(new Orthanc::PamReader);
+      dynamic_cast<Orthanc::PamReader&>(*image).ReadFromMemory(std::string(reinterpret_cast<const char*>(message.Answer), message.AnswerSize));
     }
     catch (Orthanc::OrthancException&)
     {
@@ -459,7 +400,7 @@
       NotifySliceImageError(operation);
       return;
     }
-      
+
     if (operation.GetSlice().GetConverter().GetExpectedPixelFormat() ==
         Orthanc::PixelFormat_SignedGrayscale16)
     {
@@ -475,23 +416,22 @@
     }
 
     NotifySliceImageSuccess(operation, image);
-  } 
+  }
+
 
-  
-  void OrthancSlicesLoader::ParseSliceImageJpeg(const Operation& operation,
-                                                const void* answer,
-                                                size_t size)
+  void OrthancSlicesLoader::ParseSliceImageJpeg(const OrthancApiClient::JsonResponseReadyMessage& message)
   {
-    Json::Value encoded;
-    if (!MessagingToolbox::ParseJson(encoded, answer, size) ||
-        encoded.type() != Json::objectValue ||
+    const Operation& operation = dynamic_cast<const OrthancSlicesLoader::Operation&>(*message.Payload.get());
+
+    Json::Value encoded = message.Response;
+    if (encoded.type() != Json::objectValue ||
         !encoded.isMember("Orthanc") ||
         encoded["Orthanc"].type() != Json::objectValue)
     {
       NotifySliceImageError(operation);
       return;
     }
-
+    
     Json::Value& info = encoded["Orthanc"];
     if (!info.isMember("PixelData") ||
         !info.isMember("Stretched") ||
@@ -504,30 +444,30 @@
       NotifySliceImageError(operation);
       return;
     }
-
+    
     bool isSigned = false;
     bool isStretched = info["Stretched"].asBool();
-
+    
     if (info.isMember("IsSigned"))
     {
       if (info["IsSigned"].type() != Json::booleanValue)
       {
         NotifySliceImageError(operation);
         return;
-      }          
+      }
       else
       {
         isSigned = info["IsSigned"].asBool();
       }
     }
-
-    std::auto_ptr<Orthanc::ImageAccessor> reader;
-
+    
+    boost::shared_ptr<Orthanc::ImageAccessor> reader;
+    
     {
       std::string jpeg;
       //Orthanc::Toolbox::DecodeBase64(jpeg, info["PixelData"].asString());
       jpeg = base64_decode(info["PixelData"].asString());
-
+      
       try
       {
         reader.reset(new Orthanc::JpegReader);
@@ -539,10 +479,10 @@
         return;
       }
     }
-
+    
     Orthanc::PixelFormat expectedFormat =
-      operation.GetSlice().GetConverter().GetExpectedPixelFormat();
-
+        operation.GetSlice().GetConverter().GetExpectedPixelFormat();
+    
     if (reader->GetFormat() == Orthanc::PixelFormat_RGB24)  // This is a color image
     {
       if (expectedFormat != Orthanc::PixelFormat_RGB24)
@@ -550,7 +490,7 @@
         NotifySliceImageError(operation);
         return;
       }
-
+      
       if (isSigned || isStretched)
       {
         NotifySliceImageError(operation);
@@ -562,13 +502,13 @@
         return;
       }
     }
-
+    
     if (reader->GetFormat() != Orthanc::PixelFormat_Grayscale8)
     {
       NotifySliceImageError(operation);
       return;
     }
-
+    
     if (!isStretched)
     {
       if (expectedFormat != reader->GetFormat())
@@ -582,10 +522,10 @@
         return;
       }
     }
-
+    
     int32_t stretchLow = 0;
     int32_t stretchHigh = 0;
-
+    
     if (!info.isMember("StretchLow") ||
         !info.isMember("StretchHigh") ||
         info["StretchLow"].type() != Json::intValue ||
@@ -594,10 +534,10 @@
       NotifySliceImageError(operation);
       return;
     }
-
+    
     stretchLow = info["StretchLow"].asInt();
     stretchHigh = info["StretchHigh"].asInt();
-
+    
     if (stretchLow < -32768 ||
         stretchHigh > 65535 ||
         (stretchLow < 0 && stretchHigh > 32767))
@@ -606,22 +546,22 @@
       NotifySliceImageError(operation);
       return;
     }
-
+    
     // Decode a grayscale JPEG 8bpp image coming from the Web viewer
-    std::auto_ptr<Orthanc::ImageAccessor> image
-      (new Orthanc::Image(expectedFormat, reader->GetWidth(), reader->GetHeight(), false));
-     
+    boost::shared_ptr<Orthanc::ImageAccessor> image
+        (new Orthanc::Image(expectedFormat, reader->GetWidth(), reader->GetHeight(), false));
+
     Orthanc::ImageProcessing::Convert(*image, *reader);
-    reader.reset(NULL);
-
+    reader.reset();
+    
     float scaling = static_cast<float>(stretchHigh - stretchLow) / 255.0f;
-
+    
     if (!LinearAlgebra::IsCloseToZero(scaling))
     {
       float offset = static_cast<float>(stretchLow) / scaling;
       Orthanc::ImageProcessing::ShiftScale(*image, offset, scaling, true);
     }
-
+    
     NotifySliceImageSuccess(operation, image);
   }
 
@@ -641,25 +581,23 @@
       {
         throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageFormat);
       }
-
+      
       buffer_.swap(buffer);  // The source buffer is now empty
-
+      
       void* data = (buffer_.empty() ? NULL : &buffer_[0]);
-
+      
       AssignWritable(format, width, height,
                      Orthanc::GetBytesPerPixel(format) * width, data);
     }
   };
-
   
-  void OrthancSlicesLoader::ParseSliceRawImage(const Operation& operation,
-                                               const void* answer,
-                                               size_t size)
+  void OrthancSlicesLoader::ParseSliceRawImage(const OrthancApiClient::BinaryResponseReadyMessage& message)
   {
+    const Operation& operation = dynamic_cast<const OrthancSlicesLoader::Operation&>(*message.Payload.get());
     Orthanc::GzipCompressor compressor;
-
+    
     std::string raw;
-    compressor.Uncompress(raw, answer, size);
+    compressor.Uncompress(raw, message.Answer, message.AnswerSize);
     
     const Orthanc::DicomImageInformation& info = operation.GetSlice().GetImageInformation();
     
@@ -673,10 +611,10 @@
     {
       // This is the case of RT-DOSE (uint32_t values)
       
-      std::auto_ptr<Orthanc::ImageAccessor> image
-        (new StringImage(Orthanc::PixelFormat_Grayscale32, info.GetWidth(),
-                         info.GetHeight(), raw));
-
+      boost::shared_ptr<Orthanc::ImageAccessor> image
+          (new StringImage(Orthanc::PixelFormat_Grayscale32, info.GetWidth(),
+                           info.GetHeight(), raw));
+      
       // TODO - Only for big endian
       for (unsigned int y = 0; y < image->GetHeight(); y++)
       {
@@ -686,7 +624,7 @@
           *p = le32toh(*p);
         }
       }
-
+      
       NotifySliceImageSuccess(operation, image);
     }
     else if (info.GetBitsAllocated() == 16 &&
@@ -697,31 +635,31 @@
              info.GetPhotometricInterpretation() == Orthanc::PhotometricInterpretation_Monochrome2 &&
              raw.size() == info.GetWidth() * info.GetHeight() * 2)
     {
-      std::auto_ptr<Orthanc::ImageAccessor> image
-        (new StringImage(Orthanc::PixelFormat_Grayscale16, info.GetWidth(),
-                         info.GetHeight(), raw));
-
+      boost::shared_ptr<Orthanc::ImageAccessor> image
+          (new StringImage(Orthanc::PixelFormat_Grayscale16, info.GetWidth(),
+                           info.GetHeight(), raw));
+      
       // TODO - Big endian ?
-
+      
       NotifySliceImageSuccess(operation, image);
     }
     else
     {
       throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
     }
-        
+
   }
-
-
-  OrthancSlicesLoader::OrthancSlicesLoader(ICallback& callback,
-                                           IWebService& orthanc) :
-    webCallback_(new WebCallback(*this)),
-    userCallback_(callback),
+  
+  
+  OrthancSlicesLoader::OrthancSlicesLoader(MessageBroker& broker,
+                                           OrthancApiClient& orthanc) :
+    IObservable(broker),
+    IObserver(broker),
     orthanc_(orthanc),
     state_(State_Initialization)
   {
   }
-
+  
   
   void OrthancSlicesLoader::ScheduleLoadSeries(const std::string& seriesId)
   {
@@ -732,12 +670,13 @@
     else
     {
       state_ = State_LoadingGeometry;
-      std::string uri = "/series/" + seriesId + "/instances-tags";
-      orthanc_.ScheduleGetRequest(*webCallback_, uri, Operation::DownloadSeriesGeometry());
+      orthanc_.GetJsonAsync("/series/" + seriesId + "/instances-tags",
+                            new Callable<OrthancSlicesLoader, OrthancApiClient::JsonResponseReadyMessage>(*this, &OrthancSlicesLoader::ParseSeriesGeometry),
+                            new Callable<OrthancSlicesLoader, OrthancApiClient::HttpErrorMessage>(*this, &OrthancSlicesLoader::OnGeometryError),
+                            NULL);
     }
   }
-
-
+  
   void OrthancSlicesLoader::ScheduleLoadInstance(const std::string& instanceId)
   {
     if (state_ != State_Initialization)
@@ -747,16 +686,17 @@
     else
     {
       state_ = State_LoadingGeometry;
-
+      
       // Tag "3004-000c" is "Grid Frame Offset Vector", which is
       // mandatory to read RT DOSE, but is too long to be returned by default
-      std::string uri = "/instances/" + instanceId + "/tags?ignore-length=3004-000c";
-      orthanc_.ScheduleGetRequest
-        (*webCallback_, uri, Operation::DownloadInstanceGeometry(instanceId));
+      orthanc_.GetJsonAsync("/instances/" + instanceId + "/tags?ignore-length=3004-000c",
+                            new Callable<OrthancSlicesLoader, OrthancApiClient::JsonResponseReadyMessage>(*this, &OrthancSlicesLoader::ParseInstanceGeometry),
+                            new Callable<OrthancSlicesLoader, OrthancApiClient::HttpErrorMessage>(*this, &OrthancSlicesLoader::OnGeometryError),
+                            Operation::DownloadInstanceGeometry(instanceId));
     }
   }
   
-
+  
   void OrthancSlicesLoader::ScheduleLoadFrame(const std::string& instanceId,
                                               unsigned int frame)
   {
@@ -767,29 +707,31 @@
     else
     {
       state_ = State_LoadingGeometry;
-      std::string uri = "/instances/" + instanceId + "/tags";
-      orthanc_.ScheduleGetRequest
-        (*webCallback_, uri, Operation::DownloadFrameGeometry(instanceId, frame));
+
+      orthanc_.GetJsonAsync("/instances/" + instanceId + "/tags",
+                            new Callable<OrthancSlicesLoader, OrthancApiClient::JsonResponseReadyMessage>(*this, &OrthancSlicesLoader::ParseFrameGeometry),
+                            new Callable<OrthancSlicesLoader, OrthancApiClient::HttpErrorMessage>(*this, &OrthancSlicesLoader::OnGeometryError),
+                            Operation::DownloadFrameGeometry(instanceId, frame));
     }
   }
   
-
+  
   bool OrthancSlicesLoader::IsGeometryReady() const
   {
     return state_ == State_GeometryReady;
   }
-
-
+  
+  
   size_t OrthancSlicesLoader::GetSliceCount() const
   {
     if (state_ != State_GeometryReady)
     {
       throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
     }
-
+    
     return slices_.GetSliceCount();
   }
-
+  
   
   const Slice& OrthancSlicesLoader::GetSlice(size_t index) const
   {
@@ -797,11 +739,11 @@
     {
       throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
     }
-
+    
     return slices_.GetSlice(index);
   }
   
-
+  
   bool OrthancSlicesLoader::LookupSlice(size_t& index,
                                         const CoordinateSystem3D& plane) const
   {
@@ -809,76 +751,111 @@
     {
       throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
     }
-
+    
     return slices_.LookupSlice(index, plane);
   }
   
-
+  
   void OrthancSlicesLoader::ScheduleSliceImagePng(const Slice& slice,
                                                   size_t index)
   {
-    std::string uri = ("/instances/" + slice.GetOrthancInstanceId() + "/frames/" + 
+    std::string uri = ("/instances/" + slice.GetOrthancInstanceId() + "/frames/" +
+                       boost::lexical_cast<std::string>(slice.GetFrame()));
+    
+    switch (slice.GetConverter().GetExpectedPixelFormat())
+    {
+    case Orthanc::PixelFormat_RGB24:
+      uri += "/preview";
+      break;
+      
+    case Orthanc::PixelFormat_Grayscale16:
+      uri += "/image-uint16";
+      break;
+      
+    case Orthanc::PixelFormat_SignedGrayscale16:
+      uri += "/image-int16";
+      break;
+      
+    default:
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+    
+    orthanc_.GetBinaryAsync(uri, "image/png",
+                            new Callable<OrthancSlicesLoader, OrthancApiClient::BinaryResponseReadyMessage>(*this, &OrthancSlicesLoader::ParseSliceImagePng),
+                            new Callable<OrthancSlicesLoader, OrthancApiClient::HttpErrorMessage>(*this, &OrthancSlicesLoader::OnSliceImageError),
+                            Operation::DownloadSliceImage(index, slice, SliceImageQuality_FullPng));
+  }
+  
+  void OrthancSlicesLoader::ScheduleSliceImagePam(const Slice& slice,
+                                                  size_t index)
+  {
+    std::string uri = ("/instances/" + slice.GetOrthancInstanceId() + "/frames/" +
                        boost::lexical_cast<std::string>(slice.GetFrame()));
 
     switch (slice.GetConverter().GetExpectedPixelFormat())
     {
-      case Orthanc::PixelFormat_RGB24:
-        uri += "/preview";
-        break;
+    case Orthanc::PixelFormat_RGB24:
+      uri += "/preview";
+      break;
 
-      case Orthanc::PixelFormat_Grayscale16:
-        uri += "/image-uint16";
-        break;
+    case Orthanc::PixelFormat_Grayscale16:
+      uri += "/image-uint16";
+      break;
 
-      case Orthanc::PixelFormat_SignedGrayscale16:
-        uri += "/image-int16";
-        break;
+    case Orthanc::PixelFormat_SignedGrayscale16:
+      uri += "/image-int16";
+      break;
 
-      default:
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    default:
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
     }
 
-    orthanc_.ScheduleGetRequest(*webCallback_, uri,
-                                Operation::DownloadSliceImage(index, slice, SliceImageQuality_Full));
+    orthanc_.GetBinaryAsync(uri, "image/x-portable-arbitrarymap",
+                            new Callable<OrthancSlicesLoader, OrthancApiClient::BinaryResponseReadyMessage>(*this, &OrthancSlicesLoader::ParseSliceImagePam),
+                            new Callable<OrthancSlicesLoader, OrthancApiClient::HttpErrorMessage>(*this, &OrthancSlicesLoader::OnSliceImageError),
+                            Operation::DownloadSliceImage(index, slice, SliceImageQuality_FullPam));
   }
 
 
+  
   void OrthancSlicesLoader::ScheduleSliceImageJpeg(const Slice& slice,
                                                    size_t index,
                                                    SliceImageQuality quality)
   {
     unsigned int value;
-
+    
     switch (quality)
     {
-      case SliceImageQuality_Jpeg50:
-        value = 50;
-        break;
-    
-      case SliceImageQuality_Jpeg90:
-        value = 90;
-        break;
-    
-      case SliceImageQuality_Jpeg95:
-        value = 95;
-        break;
+    case SliceImageQuality_Jpeg50:
+      value = 50;
+      break;
 
-      default:
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    case SliceImageQuality_Jpeg90:
+      value = 90;
+      break;
+
+    case SliceImageQuality_Jpeg95:
+      value = 95;
+      break;
+      
+    default:
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
     }
     
     // This requires the official Web viewer plugin to be installed!
-    std::string uri = ("/web-viewer/instances/jpeg" + 
-                       boost::lexical_cast<std::string>(value) + 
-                       "-" + slice.GetOrthancInstanceId() + "_" + 
+    std::string uri = ("/web-viewer/instances/jpeg" +
+                       boost::lexical_cast<std::string>(value) +
+                       "-" + slice.GetOrthancInstanceId() + "_" +
                        boost::lexical_cast<std::string>(slice.GetFrame()));
-      
-    orthanc_.ScheduleGetRequest(*webCallback_, uri,
-                                Operation::DownloadSliceImage(index, slice, quality));
+
+    orthanc_.GetJsonAsync(uri,
+                          new Callable<OrthancSlicesLoader, OrthancApiClient::JsonResponseReadyMessage>(*this, &OrthancSlicesLoader::ParseSliceImageJpeg),
+                          new Callable<OrthancSlicesLoader, OrthancApiClient::HttpErrorMessage>(*this, &OrthancSlicesLoader::OnSliceImageError),
+                          Operation::DownloadSliceImage(index, slice, quality));
   }
-
-
-
+  
+  
+  
   void OrthancSlicesLoader::ScheduleLoadSliceImage(size_t index,
                                                    SliceImageQuality quality)
   {
@@ -886,26 +863,31 @@
     {
       throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
     }
-
+    
     const Slice& slice = GetSlice(index);
-
+    
     if (slice.HasOrthancDecoding())
     {
-      if (quality == SliceImageQuality_Full)
+      switch (quality)
       {
+      case SliceImageQuality_FullPng:
         ScheduleSliceImagePng(slice, index);
-      }
-      else
-      {
+        break;
+      case SliceImageQuality_FullPam:
+        ScheduleSliceImagePam(slice, index);
+        break;
+      default:
         ScheduleSliceImageJpeg(slice, index, quality);
       }
     }
     else
     {
-      std::string uri = ("/instances/" + slice.GetOrthancInstanceId() + "/frames/" + 
+      std::string uri = ("/instances/" + slice.GetOrthancInstanceId() + "/frames/" +
                          boost::lexical_cast<std::string>(slice.GetFrame()) + "/raw.gz");
-      orthanc_.ScheduleGetRequest(*webCallback_, uri,
-                                  Operation::DownloadSliceRawImage(index, slice));
+      orthanc_.GetBinaryAsync(uri, IWebService::Headers(),
+                              new Callable<OrthancSlicesLoader, OrthancApiClient::BinaryResponseReadyMessage>(*this, &OrthancSlicesLoader::ParseSliceRawImage),
+                              new Callable<OrthancSlicesLoader, OrthancApiClient::HttpErrorMessage>(*this, &OrthancSlicesLoader::OnSliceImageError),
+                              Operation::DownloadSliceRawImage(index, slice));
     }
   }
 }
--- a/Framework/Toolbox/OrthancSlicesLoader.h	Mon Nov 05 10:04:56 2018 +0100
+++ b/Framework/Toolbox/OrthancSlicesLoader.h	Mon Nov 05 10:06:18 2018 +0100
@@ -13,7 +13,7 @@
  * 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/>.
  **/
@@ -24,35 +24,56 @@
 #include "IWebService.h"
 #include "SlicesSorter.h"
 #include "../StoneEnumerations.h"
+#include "../Messages/IObservable.h"
+#include <boost/shared_ptr.hpp>
+#include "OrthancApiClient.h"
+#include "Core/Images/Image.h"
 
-#include <boost/shared_ptr.hpp>
 
 namespace OrthancStone
 {
-  class OrthancSlicesLoader : public boost::noncopyable
+  class OrthancSlicesLoader : public IObservable, public IObserver
   {
   public:
-    class ICallback : public boost::noncopyable
+
+    typedef OriginMessage<MessageType_SliceLoader_GeometryReady, OrthancSlicesLoader> SliceGeometryReadyMessage;
+    typedef OriginMessage<MessageType_SliceLoader_GeometryError, OrthancSlicesLoader> SliceGeometryErrorMessage;
+
+    struct SliceImageReadyMessage : public BaseMessage<MessageType_SliceLoader_ImageReady>
     {
-    public:
-      virtual ~ICallback()
+      unsigned int sliceIndex_;
+      const Slice& slice_;
+      boost::shared_ptr<Orthanc::ImageAccessor> image_;
+      SliceImageQuality effectiveQuality_;
+
+      SliceImageReadyMessage(unsigned int sliceIndex,
+                             const Slice& slice,
+                             boost::shared_ptr<Orthanc::ImageAccessor> image,
+                             SliceImageQuality effectiveQuality)
+        : BaseMessage(),
+          sliceIndex_(sliceIndex),
+          slice_(slice),
+          image_(image),
+          effectiveQuality_(effectiveQuality)
       {
       }
+    };
 
-      virtual void NotifyGeometryReady(const OrthancSlicesLoader& loader) = 0;
-
-      virtual void NotifyGeometryError(const OrthancSlicesLoader& loader) = 0;
+    struct SliceImageErrorMessage : public BaseMessage<MessageType_SliceLoader_ImageError>
+    {
+      const Slice& slice_;
+      unsigned int sliceIndex_;
+      SliceImageQuality effectiveQuality_;
 
-      virtual void NotifySliceImageReady(const OrthancSlicesLoader& loader,
-                                         unsigned int sliceIndex,
-                                         const Slice& slice,
-                                         std::auto_ptr<Orthanc::ImageAccessor>& image,
-                                         SliceImageQuality effectiveQuality) = 0;
-
-      virtual void NotifySliceImageError(const OrthancSlicesLoader& loader,
-                                         unsigned int sliceIndex,
-                                         const Slice& slice,
-                                         SliceImageQuality quality) = 0;
+      SliceImageErrorMessage(unsigned int sliceIndex,
+                             const Slice& slice,
+                             SliceImageQuality effectiveQuality)
+        : BaseMessage(),
+          slice_(slice),
+          sliceIndex_(sliceIndex),
+          effectiveQuality_(effectiveQuality)
+      {
+      }
     };
     
   private:
@@ -70,51 +91,44 @@
       Mode_InstanceGeometry,
       Mode_FrameGeometry,
       Mode_LoadImage,
-      Mode_LoadRawImage
+      Mode_LoadRawImage,
+      Mode_LoadDicomFile
     };
 
     class Operation;
-    class WebCallback;
 
-    boost::shared_ptr<WebCallback>  webCallback_;  // This is a PImpl pattern
-
-    ICallback&    userCallback_;
-    IWebService&  orthanc_;
+    OrthancApiClient&  orthanc_;
     State         state_;
     SlicesSorter  slices_;
 
     void NotifySliceImageSuccess(const Operation& operation,
-                                 std::auto_ptr<Orthanc::ImageAccessor>& image) const;
-  
-    void NotifySliceImageError(const Operation& operation) const;
-    
-    void ParseSeriesGeometry(const void* answer,
-                             size_t size);
+                                 boost::shared_ptr<Orthanc::ImageAccessor> image);
+
+    void NotifySliceImageError(const Operation& operation);
 
-    void ParseInstanceGeometry(const std::string& instanceId,
-                               const void* answer,
-                               size_t size);
+    void OnGeometryError(const OrthancApiClient::HttpErrorMessage& message);
+    void OnSliceImageError(const OrthancApiClient::HttpErrorMessage& message);
+
+    void ParseSeriesGeometry(const OrthancApiClient::JsonResponseReadyMessage& message);
 
-    void ParseFrameGeometry(const std::string& instanceId,
-                            unsigned int frame,
-                            const void* answer,
-                            size_t size);
+    void ParseInstanceGeometry(const OrthancApiClient::JsonResponseReadyMessage& message);
+
+    void ParseFrameGeometry(const OrthancApiClient::JsonResponseReadyMessage& message);
+
+    void ParseSliceImagePng(const OrthancApiClient::BinaryResponseReadyMessage& message);
 
-    void ParseSliceImagePng(const Operation& operation,
-                            const void* answer,
-                            size_t size);
+    void ParseSliceImagePam(const OrthancApiClient::BinaryResponseReadyMessage& message);
 
-    void ParseSliceImageJpeg(const Operation& operation,
-                             const void* answer,
-                             size_t size);
+    void ParseSliceImageJpeg(const OrthancApiClient::JsonResponseReadyMessage& message);
 
-    void ParseSliceRawImage(const Operation& operation,
-                            const void* answer,
-                            size_t size);
+    void ParseSliceRawImage(const OrthancApiClient::BinaryResponseReadyMessage& message);
 
     void ScheduleSliceImagePng(const Slice& slice,
                                size_t index);
-    
+
+    void ScheduleSliceImagePam(const Slice& slice,
+                               size_t index);
+
     void ScheduleSliceImageJpeg(const Slice& slice,
                                 size_t index,
                                 SliceImageQuality quality);
@@ -122,8 +136,9 @@
     void SortAndFinalizeSlices();
     
   public:
-    OrthancSlicesLoader(ICallback& callback,
-                        IWebService& orthanc);
+    OrthancSlicesLoader(MessageBroker& broker,
+                        //ISliceLoaderObserver& callback,
+                        OrthancApiClient& orthancApi);
 
     void ScheduleLoadSeries(const std::string& seriesId);
 
@@ -143,5 +158,7 @@
 
     void ScheduleLoadSliceImage(size_t index,
                                 SliceImageQuality requestedQuality);
+
+
   };
 }
--- a/Framework/Toolbox/ShearWarpProjectiveTransform.cpp	Mon Nov 05 10:04:56 2018 +0100
+++ b/Framework/Toolbox/ShearWarpProjectiveTransform.cpp	Mon Nov 05 10:06:18 2018 +0100
@@ -476,7 +476,7 @@
         ApplyAffineTransform(*intermediate, reader.GetAccessor(),
                              a11, 0,   b1,
                              0,   a22, b2,
-                             shearInterpolation);
+                             shearInterpolation, true);
       }
       
 
@@ -582,7 +582,7 @@
     }
 
     // (5.b) Apply the projective transform to the image
-    ApplyProjectiveTransform(target, *intermediate, warp, warpInterpolation);
+    ApplyProjectiveTransform(target, *intermediate, warp, warpInterpolation, true);
   }
 
 
--- a/Framework/Toolbox/Slice.cpp	Mon Nov 05 10:04:56 2018 +0100
+++ b/Framework/Toolbox/Slice.cpp	Mon Nov 05 10:06:18 2018 +0100
@@ -45,6 +45,28 @@
       return false;
     }
   }
+
+  Slice* Slice::Clone() const
+  {
+    std::auto_ptr<Slice> target(new Slice());
+
+    target->type_ = type_;
+    target->orthancInstanceId_ = orthancInstanceId_;
+    target->sopClassUid_ = sopClassUid_;
+    target->frame_ = frame_;
+    target->frameCount_ = frameCount_;
+    target->geometry_ = geometry_;
+    target->pixelSpacingX_ = pixelSpacingX_;
+    target->pixelSpacingY_ = pixelSpacingY_;
+    target->thickness_ = thickness_;
+    target->width_ = width_;
+    target->height_ = height_;
+    target->converter_ = converter_;
+    if (imageInformation_.get() != NULL)
+      target->imageInformation_.reset(imageInformation_->Clone());
+
+    return target.release();
+  }
   
   bool Slice::ComputeRTDoseGeometry(const Orthanc::DicomMap& dataset,
                                     unsigned int frame)
--- a/Framework/Toolbox/Slice.h	Mon Nov 05 10:04:56 2018 +0100
+++ b/Framework/Toolbox/Slice.h	Mon Nov 05 10:06:18 2018 +0100
@@ -61,6 +61,16 @@
   public:
     Slice() :
       type_(Type_Invalid)
+    {
+    }
+
+
+    // this constructor is used to reference, i.e, a slice that is being loaded
+    Slice(const std::string& orthancInstanceId,
+          unsigned int frame) :
+      type_(Type_Invalid),
+      orthancInstanceId_(orthancInstanceId),
+      frame_(frame)
     {        
     }
 
@@ -136,5 +146,7 @@
     void GetExtent(std::vector<Vector>& points) const;
 
     const Orthanc::DicomImageInformation& GetImageInformation() const;
+
+    Slice* Clone() const;
   };
 }
--- a/Framework/Toolbox/ViewportGeometry.cpp	Mon Nov 05 10:04:56 2018 +0100
+++ b/Framework/Toolbox/ViewportGeometry.cpp	Mon Nov 05 10:06:18 2018 +0100
@@ -121,7 +121,19 @@
   }
 
 
-  void ViewportGeometry::SetDefaultView()
+  void ViewportGeometry::MapPixelCenterToScene(double& sceneX,
+                                               double& sceneY,
+                                               int x,
+                                               int y) const
+  {
+    // Take the center of the pixel
+    MapDisplayToScene(sceneX, sceneY,
+                      static_cast<double>(x) + 0.5,
+                      static_cast<double>(y) + 0.5);
+  }
+
+
+  void ViewportGeometry::FitContent()
   {
     if (width_ > 0 &&
         height_ > 0 &&
@@ -167,4 +179,22 @@
     zoom_ = zoom;
     ComputeTransform();
   }
+
+
+  Matrix ViewportGeometry::GetMatrix() const
+  {
+    Matrix m(3, 3);
+
+    m(0, 0) = transform_.xx;
+    m(0, 1) = transform_.xy;
+    m(0, 2) = transform_.x0;
+    m(1, 0) = transform_.yx;
+    m(1, 1) = transform_.yy;
+    m(1, 2) = transform_.y0;
+    m(2, 0) = 0;
+    m(2, 1) = 0;
+    m(2, 2) = 1;
+    
+    return m;
+  }
 }
--- a/Framework/Toolbox/ViewportGeometry.h	Mon Nov 05 10:04:56 2018 +0100
+++ b/Framework/Toolbox/ViewportGeometry.h	Mon Nov 05 10:06:18 2018 +0100
@@ -22,7 +22,8 @@
 #pragma once
 
 #include "../Viewport/CairoContext.h"
-#include "../Toolbox/Extent2D.h"
+#include "Extent2D.h"
+#include "LinearAlgebra.h"
 
 namespace OrthancStone
 {
@@ -63,6 +64,11 @@
                            double x,
                            double y) const;
 
+    void MapPixelCenterToScene(double& sceneX /* out */,
+                               double& sceneY /* out */,
+                               int x,
+                               int y) const;
+
     void MapSceneToDisplay(int& displayX /* out */,
                            int& displayY /* out */,
                            double x,
@@ -83,7 +89,7 @@
       return zoom_;
     }
 
-    void SetDefaultView();
+    void FitContent();
 
     void ApplyTransform(CairoContext& context) const;
 
@@ -94,5 +100,7 @@
                 double y);
 
     void SetZoom(double zoom);
+
+    Matrix GetMatrix() const;
   };
 }
--- a/Framework/Viewport/CairoContext.cpp	Mon Nov 05 10:04:56 2018 +0100
+++ b/Framework/Viewport/CairoContext.cpp	Mon Nov 05 10:06:18 2018 +0100
@@ -24,9 +24,12 @@
 #include <Core/Logging.h>
 #include <Core/OrthancException.h>
 
+
 namespace OrthancStone
 {
-  CairoContext::CairoContext(CairoSurface& surface)
+  CairoContext::CairoContext(CairoSurface& surface) :
+    width_(surface.GetWidth()),
+    height_(surface.GetHeight())
   {
     context_ = cairo_create(surface.GetObject());
     if (!context_)
@@ -56,4 +59,88 @@
                          static_cast<float>(green) / 255.0f,
                          static_cast<float>(blue) / 255.0f);
   }
+
+
+  class CairoContext::AlphaSurface : public boost::noncopyable
+  {
+  private:
+    cairo_surface_t  *surface_;
+
+  public:
+    AlphaSurface(unsigned int width,
+                 unsigned int height)
+    {
+      surface_ = cairo_image_surface_create(CAIRO_FORMAT_A8, width, height);
+      
+      if (!surface_)
+      {
+        // Should never occur
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+      }
+
+      if (cairo_surface_status(surface_) != CAIRO_STATUS_SUCCESS)
+      {
+        LOG(ERROR) << "Cannot create a Cairo surface";
+        cairo_surface_destroy(surface_);
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+      }
+    }
+    
+    ~AlphaSurface()
+    {
+      cairo_surface_destroy(surface_);
+    }
+
+    void GetAccessor(Orthanc::ImageAccessor& target)
+    {
+      target.AssignWritable(Orthanc::PixelFormat_Grayscale8,
+                            cairo_image_surface_get_width(surface_),
+                            cairo_image_surface_get_height(surface_),
+                            cairo_image_surface_get_stride(surface_),
+                            cairo_image_surface_get_data(surface_));
+    }
+
+    void Blit(cairo_t* cr,
+              double x,
+              double y)
+    {
+      cairo_surface_mark_dirty(surface_);
+      cairo_mask_surface(cr, surface_, x, y);
+      cairo_fill(cr);
+    }
+  };
+
+
+  void CairoContext::DrawText(const Orthanc::Font& font,
+                              const std::string& text,
+                              double x,
+                              double y,
+                              BitmapAnchor anchor)
+  {
+    // Render a bitmap containing the text
+    unsigned int width, height;
+    font.ComputeTextExtent(width, height, text);
+    
+    AlphaSurface surface(width, height);
+
+    Orthanc::ImageAccessor accessor;
+    surface.GetAccessor(accessor);
+    font.Draw(accessor, text, 0, 0, 255);
+
+    // Correct the text location given the anchor location
+    double deltaX, deltaY;
+    ComputeAnchorTranslation(deltaX, deltaY, anchor, width, height);
+
+    // Cancel zoom/rotation before blitting the text onto the surface
+    double pixelX = x;
+    double pixelY = y;
+    cairo_user_to_device(context_, &pixelX, &pixelY);
+
+    cairo_save(context_);
+    cairo_identity_matrix(context_);
+
+    // Blit the text bitmap
+    surface.Blit(context_, pixelX + deltaX, pixelY + deltaY);
+    cairo_restore(context_);
+  }
 }
--- a/Framework/Viewport/CairoContext.h	Mon Nov 05 10:04:56 2018 +0100
+++ b/Framework/Viewport/CairoContext.h	Mon Nov 05 10:06:18 2018 +0100
@@ -22,6 +22,9 @@
 #pragma once
 
 #include "CairoSurface.h"
+#include "../StoneEnumerations.h"
+
+#include <Core/Images/Font.h>
 
 namespace OrthancStone
 {
@@ -29,7 +32,11 @@
   class CairoContext : public boost::noncopyable
   {
   private:
-    cairo_t* context_;
+    class AlphaSurface;
+    
+    cairo_t*      context_;
+    unsigned int  width_;
+    unsigned int  height_;
 
   public:
     CairoContext(CairoSurface& surface);
@@ -41,6 +48,16 @@
       return context_;
     }
 
+    unsigned int GetWidth() const
+    {
+      return width_;
+    }
+
+    unsigned int GetHeight() const
+    {
+      return height_;
+    }
+
     void SetSourceColor(uint8_t red,
                         uint8_t green,
                         uint8_t blue);
@@ -49,5 +66,11 @@
     {
       SetSourceColor(color[0], color[1], color[2]);
     }
+
+    void DrawText(const Orthanc::Font& font,
+                  const std::string& text,
+                  double x,
+                  double y,
+                  BitmapAnchor anchor);      
   };
 }
--- a/Framework/Viewport/CairoFont.h	Mon Nov 05 10:04:56 2018 +0100
+++ b/Framework/Viewport/CairoFont.h	Mon Nov 05 10:06:18 2018 +0100
@@ -21,6 +21,14 @@
 
 #pragma once
 
+#if !defined(ORTHANC_SANDBOXED)
+#  error The macro ORTHANC_SANDBOXED must be defined
+#endif
+
+#if ORTHANC_SANDBOXED == 1
+#  error The class CairoFont cannot be used in sandboxed environments
+#endif
+
 #include "CairoContext.h"
 
 namespace OrthancStone
--- a/Framework/Viewport/CairoSurface.cpp	Mon Nov 05 10:04:56 2018 +0100
+++ b/Framework/Viewport/CairoSurface.cpp	Mon Nov 05 10:06:18 2018 +0100
@@ -106,6 +106,7 @@
   void CairoSurface::Copy(const CairoSurface& other)
   {
     Orthanc::ImageAccessor source, target;
+
     other.GetReadOnlyAccessor(source);
     GetWriteableAccessor(target);
 
--- a/Framework/Viewport/IMouseTracker.h	Mon Nov 05 10:04:56 2018 +0100
+++ b/Framework/Viewport/IMouseTracker.h	Mon Nov 05 10:06:18 2018 +0100
@@ -25,6 +25,9 @@
 
 namespace OrthancStone
 {
+  // this is tracking a mouse in screen coordinates/pixels unlike
+  // the IWorldSceneMouseTracker that is tracking a mouse
+  // in scene coordinates/mm.
   class IMouseTracker : public boost::noncopyable
   {
   public:
--- a/Framework/Viewport/IViewport.h	Mon Nov 05 10:04:56 2018 +0100
+++ b/Framework/Viewport/IViewport.h	Mon Nov 05 10:06:18 2018 +0100
@@ -40,14 +40,14 @@
       {
       }
 
-      virtual void NotifyChange(const IViewport& scene) = 0;
+      virtual void OnViewportContentChanged(const IViewport& scene) = 0;
     };
 
     virtual ~IViewport()
     {
     }
 
-    virtual void SetDefaultView() = 0;
+    virtual void FitContent() = 0;
 
     virtual void Register(IObserver& observer) = 0;
 
@@ -78,7 +78,8 @@
                             int y,
                             KeyboardModifiers modifiers) = 0;
 
-    virtual void KeyPressed(char key,
+    virtual void KeyPressed(KeyboardKeys key,
+                            char keyChar,
                             KeyboardModifiers modifiers) = 0;
 
     virtual bool HasUpdateContent() = 0;
@@ -86,6 +87,6 @@
     virtual void UpdateContent() = 0;
 
     // Should only be called from IWidget
-    virtual void NotifyChange(const IWidget& widget) = 0;
+    virtual void NotifyContentChanged(const IWidget& widget) = 0;
   };
 }
--- a/Framework/Viewport/WidgetViewport.cpp	Mon Nov 05 10:04:56 2018 +0100
+++ b/Framework/Viewport/WidgetViewport.cpp	Mon Nov 05 10:06:18 2018 +0100
@@ -36,11 +36,11 @@
   }
 
 
-  void WidgetViewport::SetDefaultView()
+  void WidgetViewport::FitContent()
   {
     if (centralWidget_.get() != NULL)
     {
-      centralWidget_->SetDefaultView();
+      centralWidget_->FitContent();
     }
   }
 
@@ -64,7 +64,7 @@
     }
 
     mouseTracker_.reset(NULL);
-      
+
     centralWidget_.reset(widget);
     centralWidget_->SetViewport(*this);
 
@@ -74,16 +74,16 @@
     }
 
     backgroundChanged_ = true;
-    observers_.Apply(*this, &IObserver::NotifyChange);
+    observers_.Apply(*this, &IObserver::OnViewportContentChanged);
 
     return *widget;
   }
 
 
-  void WidgetViewport::NotifyChange(const IWidget& widget)
+  void WidgetViewport::NotifyContentChanged(const IWidget& widget)
   {
     backgroundChanged_ = true;
-    observers_.Apply(*this, &IObserver::NotifyChange);
+    observers_.Apply(*this, &IObserver::OnViewportContentChanged);
   }
 
 
@@ -97,7 +97,7 @@
       centralWidget_->SetSize(width, height);
     }
 
-    observers_.Apply(*this, &IObserver::NotifyChange);
+    observers_.Apply(*this, &IObserver::OnViewportContentChanged);
   }
 
 
@@ -155,7 +155,7 @@
       mouseTracker_.reset(NULL);
     }      
 
-    observers_.Apply(*this, &IObserver::NotifyChange);
+    observers_.Apply(*this, &IObserver::OnViewportContentChanged);
   }
 
 
@@ -165,7 +165,7 @@
     {
       mouseTracker_->MouseUp();
       mouseTracker_.reset(NULL);
-      observers_.Apply(*this, &IObserver::NotifyChange);
+      observers_.Apply(*this, &IObserver::OnViewportContentChanged);
     }
   }
 
@@ -196,7 +196,7 @@
     if (repaint)
     {
       // The scene must be repainted, notify the observers
-      observers_.Apply(*this, &IObserver::NotifyChange);
+      observers_.Apply(*this, &IObserver::OnViewportContentChanged);
     }
   }
 
@@ -204,7 +204,7 @@
   void WidgetViewport::MouseEnter()
   {
     isMouseOver_ = true;
-    observers_.Apply(*this, &IObserver::NotifyChange);
+    observers_.Apply(*this, &IObserver::OnViewportContentChanged);
   }
 
 
@@ -218,7 +218,7 @@
       mouseTracker_.reset(NULL);
     }
 
-    observers_.Apply(*this, &IObserver::NotifyChange);
+    observers_.Apply(*this, &IObserver::OnViewportContentChanged);
   }
 
 
@@ -235,13 +235,14 @@
   }
 
 
-  void WidgetViewport::KeyPressed(char key,
+  void WidgetViewport::KeyPressed(KeyboardKeys key,
+                                  char keyChar,
                                   KeyboardModifiers modifiers)
   {
     if (centralWidget_.get() != NULL &&
         mouseTracker_.get() == NULL)
     {
-      centralWidget_->KeyPressed(key, modifiers);
+      centralWidget_->KeyPressed(key, keyChar, modifiers);
     }
   }
 
--- a/Framework/Viewport/WidgetViewport.h	Mon Nov 05 10:04:56 2018 +0100
+++ b/Framework/Viewport/WidgetViewport.h	Mon Nov 05 10:06:18 2018 +0100
@@ -45,13 +45,13 @@
   public:
     WidgetViewport();
 
-    virtual void SetDefaultView();
+    virtual void FitContent();
 
     virtual void SetStatusBar(IStatusBar& statusBar);
 
     IWidget& SetCentralWidget(IWidget* widget);  // Takes ownership
 
-    virtual void NotifyChange(const IWidget& widget);
+    virtual void NotifyContentChanged(const IWidget& widget);
 
     virtual void Register(IObserver& observer)
     {
@@ -82,7 +82,8 @@
                             int y,
                             KeyboardModifiers modifiers);
 
-    virtual void KeyPressed(char key,
+    virtual void KeyPressed(KeyboardKeys key,
+                            char keyChar,
                             KeyboardModifiers modifiers);
 
     virtual bool HasUpdateContent();
--- a/Framework/Volumes/ImageBuffer3D.cpp	Mon Nov 05 10:04:56 2018 +0100
+++ b/Framework/Volumes/ImageBuffer3D.cpp	Mon Nov 05 10:06:18 2018 +0100
@@ -13,7 +13,7 @@
  * 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/>.
  **/
@@ -91,7 +91,7 @@
 
       for (unsigned int y = 0; y < height_; y++)
       {
-        const void* source = (reinterpret_cast<const uint8_t*>(image_.GetConstRow(y + z * height_)) + 
+        const void* source = (reinterpret_cast<const uint8_t*>(image_.GetConstRow(y + z * height_)) +
                               bytesPerPixel * slice);
 
         memcpy(target, source, bytesPerPixel);
@@ -157,20 +157,20 @@
     Vector result;
     switch (projection)
     {
-      case VolumeProjection_Axial:
-        result = voxelDimensions_;
-        break;
+    case VolumeProjection_Axial:
+      result = voxelDimensions_;
+      break;
 
-      case VolumeProjection_Coronal:
-        LinearAlgebra::AssignVector(result, voxelDimensions_[0], voxelDimensions_[2], voxelDimensions_[1]);
-        break;
+    case VolumeProjection_Coronal:
+      LinearAlgebra::AssignVector(result, voxelDimensions_[0], voxelDimensions_[2], voxelDimensions_[1]);
+      break;
 
-      case VolumeProjection_Sagittal:
-        LinearAlgebra::AssignVector(result, voxelDimensions_[1], voxelDimensions_[2], voxelDimensions_[0]);
-        break;
+    case VolumeProjection_Sagittal:
+      LinearAlgebra::AssignVector(result, voxelDimensions_[1], voxelDimensions_[2], voxelDimensions_[0]);
+      break;
 
-      default:
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    default:
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
     }
 
     return result;
@@ -183,23 +183,23 @@
   {
     switch (projection)
     {
-      case VolumeProjection_Axial:
-        width = width_;
-        height = height_;
-        break;
+    case VolumeProjection_Axial:
+      width = width_;
+      height = height_;
+      break;
 
-      case VolumeProjection_Coronal:
-        width = width_;
-        height = depth_;
-        break;
+    case VolumeProjection_Coronal:
+      width = width_;
+      height = depth_;
+      break;
 
-      case VolumeProjection_Sagittal:
-        width = height_;
-        height = depth_;
-        break;
+    case VolumeProjection_Sagittal:
+      width = height_;
+      height = depth_;
+      break;
 
-      default:
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    default:
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
     }
   }
 
@@ -210,51 +210,51 @@
 
     switch (projection)
     {
-      case VolumeProjection_Axial:
-        for (unsigned int z = 0; z < depth_; z++)
-        {
-          Vector origin = axialGeometry_.GetOrigin();
-          origin += static_cast<double>(z) * voxelDimensions_[2] * axialGeometry_.GetNormal();
+    case VolumeProjection_Axial:
+      for (unsigned int z = 0; z < depth_; z++)
+      {
+        Vector origin = axialGeometry_.GetOrigin();
+        origin += static_cast<double>(z) * voxelDimensions_[2] * axialGeometry_.GetNormal();
 
-          result->AddSlice(origin, 
-                           axialGeometry_.GetAxisX(), 
-                           axialGeometry_.GetAxisY());
-        }
-        break;
+        result->AddSlice(origin,
+                         axialGeometry_.GetAxisX(),
+                         axialGeometry_.GetAxisY());
+      }
+      break;
 
-      case VolumeProjection_Coronal:
-        for (unsigned int y = 0; y < height_; y++)
-        {
-          Vector origin = axialGeometry_.GetOrigin();
-          origin += static_cast<double>(y) * voxelDimensions_[1] * axialGeometry_.GetAxisY();
-          origin += static_cast<double>(depth_ - 1) * voxelDimensions_[2] * axialGeometry_.GetNormal();
+    case VolumeProjection_Coronal:
+      for (unsigned int y = 0; y < height_; y++)
+      {
+        Vector origin = axialGeometry_.GetOrigin();
+        origin += static_cast<double>(y) * voxelDimensions_[1] * axialGeometry_.GetAxisY();
+        origin += static_cast<double>(depth_ - 1) * voxelDimensions_[2] * axialGeometry_.GetNormal();
 
-          result->AddSlice(origin, 
-                           axialGeometry_.GetAxisX(), 
-                           -axialGeometry_.GetNormal());
-        }
-        break;
+        result->AddSlice(origin,
+                         axialGeometry_.GetAxisX(),
+                         -axialGeometry_.GetNormal());
+      }
+      break;
 
-      case VolumeProjection_Sagittal:
-        for (unsigned int x = 0; x < width_; x++)
-        {
-          Vector origin = axialGeometry_.GetOrigin();
-          origin += static_cast<double>(x) * voxelDimensions_[0] * axialGeometry_.GetAxisX();
-          origin += static_cast<double>(depth_ - 1) * voxelDimensions_[2] * axialGeometry_.GetNormal();
+    case VolumeProjection_Sagittal:
+      for (unsigned int x = 0; x < width_; x++)
+      {
+        Vector origin = axialGeometry_.GetOrigin();
+        origin += static_cast<double>(x) * voxelDimensions_[0] * axialGeometry_.GetAxisX();
+        origin += static_cast<double>(depth_ - 1) * voxelDimensions_[2] * axialGeometry_.GetNormal();
 
-          result->AddSlice(origin, 
-                           axialGeometry_.GetAxisY(), 
-                           -axialGeometry_.GetNormal());
-        }
-        break;
+        result->AddSlice(origin,
+                         axialGeometry_.GetAxisY(),
+                         -axialGeometry_.GetNormal());
+      }
+      break;
 
-      default:
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);          
+    default:
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
     }
 
     return result.release();
   }
-    
+
 
   uint64_t ImageBuffer3D::GetEstimatedMemorySize() const
   {
@@ -272,27 +272,27 @@
     }
 
     float sliceMin, sliceMax;
-      
+
     switch (slice.GetFormat())
     {
-      case Orthanc::PixelFormat_Grayscale8:
-      case Orthanc::PixelFormat_Grayscale16:
-      case Orthanc::PixelFormat_Grayscale32:
-      case Orthanc::PixelFormat_SignedGrayscale16:
-      {
-        int64_t a, b;
-        Orthanc::ImageProcessing::GetMinMaxIntegerValue(a, b, slice);
-        sliceMin = static_cast<float>(a);
-        sliceMax = static_cast<float>(b);
-        break;
-      }
+    case Orthanc::PixelFormat_Grayscale8:
+    case Orthanc::PixelFormat_Grayscale16:
+    case Orthanc::PixelFormat_Grayscale32:
+    case Orthanc::PixelFormat_SignedGrayscale16:
+    {
+      int64_t a, b;
+      Orthanc::ImageProcessing::GetMinMaxIntegerValue(a, b, slice);
+      sliceMin = static_cast<float>(a);
+      sliceMax = static_cast<float>(b);
+      break;
+    }
 
-      case Orthanc::PixelFormat_Float32:
-        Orthanc::ImageProcessing::GetMinMaxFloatValue(sliceMin, sliceMax, slice);
-        break;
+    case Orthanc::PixelFormat_Float32:
+      Orthanc::ImageProcessing::GetMinMaxFloatValue(sliceMin, sliceMax, slice);
+      break;
 
-      default:
-        return;
+    default:
+      return;
     }
 
     if (hasRange_)
@@ -366,8 +366,8 @@
         sagittal_->GetReadOnlyAccessor(accessor_);
         break;
 
-      default:
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);          
+    default:
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
     }
   }
 
@@ -379,7 +379,7 @@
       if (sagittal_.get() != NULL)
       {
         // TODO
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);          
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
       }
 
       // Update the dynamic range of the underlying image, if
@@ -410,8 +410,8 @@
         sagittal_->GetWriteableAccessor(accessor_);
         break;
 
-      default:
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);          
+    default:
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
     }
   }
 
@@ -467,11 +467,11 @@
     const CoordinateSystem3D& axial = GetAxialGeometry();
     
     Vector origin = (axial.MapSliceToWorldCoordinates(-0.5 * ps[0], -0.5 * ps[1]) -
-                     0.5 * ps[2] * axial.GetNormal());
+        0.5 * ps[2] * axial.GetNormal());
 
     return (origin +
             axial.GetAxisX() * ps[0] * x * static_cast<double>(GetWidth()) +
-            axial.GetAxisY() * ps[1] * y * static_cast<double>(GetHeight()) +
-            axial.GetNormal() * ps[2] * z * static_cast<double>(GetDepth()));
+        axial.GetAxisY() * ps[1] * y * static_cast<double>(GetHeight()) +
+        axial.GetNormal() * ps[2] * z * static_cast<double>(GetDepth()));
   }
 }
--- a/Framework/Volumes/ImageBuffer3D.h	Mon Nov 05 10:04:56 2018 +0100
+++ b/Framework/Volumes/ImageBuffer3D.h	Mon Nov 05 10:06:18 2018 +0100
@@ -13,7 +13,7 @@
  * 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/>.
  **/
--- a/Framework/Volumes/StructureSetLoader.cpp	Mon Nov 05 10:04:56 2018 +0100
+++ b/Framework/Volumes/StructureSetLoader.cpp	Mon Nov 05 10:06:18 2018 +0100
@@ -27,130 +27,63 @@
 
 namespace OrthancStone
 {
-  class StructureSetLoader::Operation : public Orthanc::IDynamicObject
-  {
-  public:
-    enum Type
-    {
-      Type_LoadStructureSet,
-      Type_LookupSopInstanceUid,
-      Type_LoadReferencedSlice
-    };
-    
-  private:
-    Type         type_;
-    std::string  value_;
-
-  public:
-    Operation(Type type,
-              const std::string& value) :
-      type_(type),
-      value_(value)
-    {
-    }
-
-    Type GetType() const
-    {
-      return type_;
-    }
-
-    const std::string& GetIdentifier() const
-    {
-      return value_;
-    }
-  };
-
-
-  void StructureSetLoader::NotifyError(const std::string& uri,
-                                       Orthanc::IDynamicObject* payload)
-  {
-    // TODO
-  }
-
   
-  void StructureSetLoader::NotifySuccess(const std::string& uri,
-                                         const void* answer,
-                                         size_t answerSize,
-                                         Orthanc::IDynamicObject* payload)
-  {
-    std::auto_ptr<Operation> op(dynamic_cast<Operation*>(payload));
-
-    switch (op->GetType())
-    {
-      case Operation::Type_LoadStructureSet:
-      {
-        OrthancPlugins::FullOrthancDataset dataset(answer, answerSize);
-        structureSet_.reset(new DicomStructureSet(dataset));
-
-        std::set<std::string> instances;
-        structureSet_->GetReferencedInstances(instances);
-
-        for (std::set<std::string>::const_iterator it = instances.begin();
-             it != instances.end(); ++it)
-        {
-          orthanc_.SchedulePostRequest(*this, "/tools/lookup", *it,
-                                       new Operation(Operation::Type_LookupSopInstanceUid, *it));
-        }
-        
-        VolumeLoaderBase::NotifyGeometryReady();
-
-        break;
-      }
-        
-      case Operation::Type_LookupSopInstanceUid:
-      {
-        Json::Value lookup;
-        
-        if (MessagingToolbox::ParseJson(lookup, answer, answerSize))
-        {
-          if (lookup.type() != Json::arrayValue ||
-              lookup.size() != 1 ||
-              !lookup[0].isMember("Type") ||
-              !lookup[0].isMember("Path") ||
-              lookup[0]["Type"].type() != Json::stringValue ||
-              lookup[0]["ID"].type() != Json::stringValue ||
-              lookup[0]["Type"].asString() != "Instance")
-          {
-            throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol);          
-          }
-
-          const std::string& instance = lookup[0]["ID"].asString();
-          orthanc_.ScheduleGetRequest(*this, "/instances/" + instance + "/tags",
-                                      new Operation(Operation::Type_LoadReferencedSlice, instance));
-        }
-        else
-        {
-          // TODO
-        }
-        
-        break;
-      }
-
-      case Operation::Type_LoadReferencedSlice:
-      {
-        OrthancPlugins::FullOrthancDataset dataset(answer, answerSize);
-
-        Orthanc::DicomMap slice;
-        MessagingToolbox::ConvertDataset(slice, dataset);
-        structureSet_->AddReferencedSlice(slice);
-
-        VolumeLoaderBase::NotifyContentChange();
-
-        break;
-      }
-      
-      default:
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
-    }
-  } 
-
-  
-  StructureSetLoader::StructureSetLoader(IWebService& orthanc) :
+  StructureSetLoader::StructureSetLoader(MessageBroker& broker, OrthancApiClient& orthanc) :
+    OrthancStone::IObserver(broker),
     orthanc_(orthanc)
   {
   }
   
 
+  void StructureSetLoader::OnReferencedSliceLoaded(const OrthancApiClient::JsonResponseReadyMessage& message)
+  {
+    OrthancPlugins::FullOrthancDataset dataset(message.Response);
+
+    Orthanc::DicomMap slice;
+    MessagingToolbox::ConvertDataset(slice, dataset);
+    structureSet_->AddReferencedSlice(slice);
+
+    VolumeLoaderBase::NotifyContentChange();
+  }
+
+  void StructureSetLoader::OnStructureSetLoaded(const OrthancApiClient::JsonResponseReadyMessage& message)
+  {
+    OrthancPlugins::FullOrthancDataset dataset(message.Response);
+    structureSet_.reset(new DicomStructureSet(dataset));
+
+    std::set<std::string> instances;
+    structureSet_->GetReferencedInstances(instances);
+
+    for (std::set<std::string>::const_iterator it = instances.begin();
+         it != instances.end(); ++it)
+    {
+      orthanc_.PostBinaryAsyncExpectJson("/tools/lookup", *it,
+                            new Callable<StructureSetLoader, OrthancApiClient::JsonResponseReadyMessage>(*this, &StructureSetLoader::OnLookupCompleted));
+    }
+
+    VolumeLoaderBase::NotifyGeometryReady();
+  }
+
+  void StructureSetLoader::OnLookupCompleted(const OrthancApiClient::JsonResponseReadyMessage& message)
+  {
+    Json::Value lookup = message.Response;
+
+    if (lookup.type() != Json::arrayValue ||
+        lookup.size() != 1 ||
+        !lookup[0].isMember("Type") ||
+        !lookup[0].isMember("Path") ||
+        lookup[0]["Type"].type() != Json::stringValue ||
+        lookup[0]["ID"].type() != Json::stringValue ||
+        lookup[0]["Type"].asString() != "Instance")
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol);
+    }
+
+    const std::string& instance = lookup[0]["ID"].asString();
+    orthanc_.GetJsonAsync("/instances/" + instance + "/tags",
+                          new Callable<StructureSetLoader, OrthancApiClient::JsonResponseReadyMessage>(*this, &StructureSetLoader::OnReferencedSliceLoaded));
+  }
+
   void StructureSetLoader::ScheduleLoadInstance(const std::string& instance)
   {
     if (structureSet_.get() != NULL)
@@ -159,8 +92,8 @@
     }
     else
     {
-      const std::string uri = "/instances/" + instance + "/tags?ignore-length=3006-0050";
-      orthanc_.ScheduleGetRequest(*this, uri, new Operation(Operation::Type_LoadStructureSet, instance));
+      orthanc_.GetJsonAsync("/instances/" + instance + "/tags?ignore-length=3006-0050",
+                            new Callable<StructureSetLoader, OrthancApiClient::JsonResponseReadyMessage>(*this, &StructureSetLoader::OnStructureSetLoaded));
     }
   }
 
--- a/Framework/Volumes/StructureSetLoader.h	Mon Nov 05 10:04:56 2018 +0100
+++ b/Framework/Volumes/StructureSetLoader.h	Mon Nov 05 10:06:18 2018 +0100
@@ -22,31 +22,22 @@
 #pragma once
 
 #include "../Toolbox/DicomStructureSet.h"
-#include "../Toolbox/IWebService.h"
+#include "../Toolbox/OrthancApiClient.h"
 #include "VolumeLoaderBase.h"
 
 namespace OrthancStone
 {
   class StructureSetLoader :
     public VolumeLoaderBase,
-    private IWebService::ICallback
+    public OrthancStone::IObserver
   {
   private:
-    class Operation;
-    
-    virtual void NotifyError(const std::string& uri,
-                             Orthanc::IDynamicObject* payload);
 
-    virtual void NotifySuccess(const std::string& uri,
-                               const void* answer,
-                               size_t answerSize,
-                               Orthanc::IDynamicObject* payload);
-
-    IWebService&                      orthanc_;
+    OrthancApiClient&                      orthanc_;
     std::auto_ptr<DicomStructureSet>  structureSet_;
 
   public:
-    StructureSetLoader(IWebService& orthanc);
+    StructureSetLoader(MessageBroker& broker, OrthancApiClient& orthanc);
 
     void ScheduleLoadInstance(const std::string& instance);
 
@@ -56,5 +47,12 @@
     }
 
     DicomStructureSet& GetStructureSet();
+
+  protected:
+    void OnReferencedSliceLoaded(const OrthancApiClient::JsonResponseReadyMessage& message);
+
+    void OnStructureSetLoaded(const OrthancApiClient::JsonResponseReadyMessage& message);
+
+    void OnLookupCompleted(const OrthancApiClient::JsonResponseReadyMessage& message);
   };
 }
--- a/Framework/Widgets/CairoWidget.cpp	Mon Nov 05 10:04:56 2018 +0100
+++ b/Framework/Widgets/CairoWidget.cpp	Mon Nov 05 10:06:18 2018 +0100
@@ -32,6 +32,10 @@
     return true;
   }
 
+  CairoWidget::CairoWidget(const std::string& name) :
+    WidgetBase(name)
+  {
+  }
 
   void CairoWidget::SetSize(unsigned int width,
                             unsigned int height)
--- a/Framework/Widgets/CairoWidget.h	Mon Nov 05 10:04:56 2018 +0100
+++ b/Framework/Widgets/CairoWidget.h	Mon Nov 05 10:06:18 2018 +0100
@@ -38,6 +38,8 @@
                                       int y) = 0;
     
   public:
+    CairoWidget(const std::string& name);
+
     virtual void SetSize(unsigned int width,
                          unsigned int height);
 
--- a/Framework/Widgets/EmptyWidget.cpp	Mon Nov 05 10:04:56 2018 +0100
+++ b/Framework/Widgets/EmptyWidget.cpp	Mon Nov 05 10:06:18 2018 +0100
@@ -13,7 +13,7 @@
  * 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/>.
  **/
@@ -26,19 +26,16 @@
 
 namespace OrthancStone
 {
-  namespace Samples
+  bool EmptyWidget::Render(Orthanc::ImageAccessor& surface)
   {
-    bool EmptyWidget::Render(Orthanc::ImageAccessor& surface)
-    {
-      // Note: This call is slow
-      Orthanc::ImageProcessing::Set(surface, red_, green_, blue_, 255);
-      return true;
-    }
+    // Note: This call is slow
+    Orthanc::ImageProcessing::Set(surface, red_, green_, blue_, 255);
+    return true;
+  }
 
-  
-    void EmptyWidget::UpdateContent()
-    {
-      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
-    }
+
+  void EmptyWidget::UpdateContent()
+  {
+    throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
   }
 }
--- a/Framework/Widgets/EmptyWidget.h	Mon Nov 05 10:04:56 2018 +0100
+++ b/Framework/Widgets/EmptyWidget.h	Mon Nov 05 10:06:18 2018 +0100
@@ -13,7 +13,7 @@
  * 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/>.
  **/
@@ -25,93 +25,91 @@
 
 namespace OrthancStone
 {
-  namespace Samples
-  {
-    /**
+  /**
      * This is a test widget that simply fills its surface with an
      * uniform color.
      **/
-    class EmptyWidget : public IWidget
-    {
-    private:
-      uint8_t  red_;
-      uint8_t  green_;
-      uint8_t  blue_;
+  class EmptyWidget : public IWidget
+  {
+  private:
+    uint8_t  red_;
+    uint8_t  green_;
+    uint8_t  blue_;
 
-    public:
-      EmptyWidget(uint8_t red,
-                  uint8_t green,
-                  uint8_t blue) :
-        red_(red),
-        green_(green),
-        blue_(blue)
-      {
-      }
+  public:
+    EmptyWidget(uint8_t red,
+                uint8_t green,
+                uint8_t blue) :
+      red_(red),
+      green_(green),
+      blue_(blue)
+    {
+    }
+
+    virtual void FitContent()
+    {
+    }
 
-      virtual void SetDefaultView()
-      {
-      }
-  
-      virtual void SetParent(OrthancStone::IWidget& widget)
-      {
-      }
-    
-      virtual void SetViewport(IViewport& viewport)
-      {
-      }
+    virtual void SetParent(IWidget& widget)
+    {
+    }
+
+    virtual void SetViewport(IViewport& viewport)
+    {
+    }
 
-      virtual void NotifyChange()
-      {
-      }
+    virtual void NotifyContentChanged()
+    {
+    }
 
-      virtual void SetStatusBar(IStatusBar& statusBar)
-      {
-      }
+    virtual void SetStatusBar(IStatusBar& statusBar)
+    {
+    }
+
+    virtual void SetSize(unsigned int width,
+                         unsigned int height)
+    {
+    }
 
-      virtual void SetSize(unsigned int width, 
-                           unsigned int height)
-      {
-      }
- 
-      virtual bool Render(Orthanc::ImageAccessor& surface);
+    virtual bool Render(Orthanc::ImageAccessor& surface);
 
-      virtual IMouseTracker* CreateMouseTracker(MouseButton button,
-                                                int x,
-                                                int y,
-                                                KeyboardModifiers modifiers)
-      {
-        return NULL;
-      }
+    virtual IMouseTracker* CreateMouseTracker(MouseButton button,
+                                              int x,
+                                              int y,
+                                              KeyboardModifiers modifiers)
+    {
+      return NULL;
+    }
 
-      virtual void RenderMouseOver(Orthanc::ImageAccessor& target,
-                                   int x,
-                                   int y)
-      {
-      }
+    virtual void RenderMouseOver(Orthanc::ImageAccessor& target,
+                                 int x,
+                                 int y)
+    {
+    }
 
-      virtual void MouseWheel(MouseWheelDirection direction,
-                              int x,
-                              int y,
-                              KeyboardModifiers modifiers)
-      {
-      }
+    virtual void MouseWheel(MouseWheelDirection direction,
+                            int x,
+                            int y,
+                            KeyboardModifiers modifiers)
+    {
+    }
 
-      virtual void KeyPressed(char key,
-                              KeyboardModifiers modifiers)
-      {
-      }
+    virtual void KeyPressed(KeyboardKeys key,
+                            char keyChar,
+                            KeyboardModifiers modifiers)
+    {
+    }
 
-      virtual bool HasUpdateContent() const
-      {
-        return false;
-      }
+    virtual bool HasUpdateContent() const
+    {
+      return false;
+    }
 
-      virtual void UpdateContent();
+    virtual void UpdateContent();
 
-      virtual bool HasRenderMouseOver()
-      {
-        return false;
-      }
-    };
-  }
+    virtual bool HasRenderMouseOver()
+    {
+      return false;
+    }
+  };
 }
--- a/Framework/Widgets/IWidget.h	Mon Nov 05 10:04:56 2018 +0100
+++ b/Framework/Widgets/IWidget.h	Mon Nov 05 10:06:18 2018 +0100
@@ -35,7 +35,7 @@
     {
     }
 
-    virtual void SetDefaultView() = 0;
+    virtual void FitContent() = 0;
 
     virtual void SetParent(IWidget& parent) = 0;
     
@@ -64,7 +64,8 @@
                             int y,
                             KeyboardModifiers modifiers) = 0;
 
-    virtual void KeyPressed(char key,
+    virtual void KeyPressed(KeyboardKeys key,
+                            char keyChar,
                             KeyboardModifiers modifiers) = 0;
 
     virtual bool HasUpdateContent() const = 0;
@@ -73,6 +74,6 @@
 
     // Subclasses can call this method to signal the display of the
     // widget must be refreshed
-    virtual void NotifyChange() = 0;
+    virtual void NotifyContentChanged() = 0;
   };
 }
--- a/Framework/Widgets/IWorldSceneInteractor.h	Mon Nov 05 10:04:56 2018 +0100
+++ b/Framework/Widgets/IWorldSceneInteractor.h	Mon Nov 05 10:06:18 2018 +0100
@@ -13,7 +13,7 @@
  * 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/>.
  **/
@@ -29,37 +29,41 @@
 
 namespace OrthancStone
 {
-  class WorldSceneWidget;
+    class WorldSceneWidget;
 
-  class IWorldSceneInteractor : public boost::noncopyable
-  {
-  public:
-    virtual ~IWorldSceneInteractor()
+    class IWorldSceneInteractor : public boost::noncopyable
     {
-    }
-    
-    virtual IWorldSceneMouseTracker* CreateMouseTracker(WorldSceneWidget& widget,
-                                                        const ViewportGeometry& view,
-                                                        MouseButton button,
-                                                        double x,
-                                                        double y,
-                                                        IStatusBar* statusBar) = 0;
+    public:
+        virtual ~IWorldSceneInteractor()
+        {
+        }
+
+        virtual IWorldSceneMouseTracker* CreateMouseTracker(WorldSceneWidget& widget,
+                                                            const ViewportGeometry& view,
+                                                            MouseButton button,
+                                                            KeyboardModifiers modifiers,
+                                                            int viewportX,
+                                                            int viewportY,
+                                                            double x,
+                                                            double y,
+                                                            IStatusBar* statusBar) = 0;
 
-    virtual void MouseOver(CairoContext& context,
-                           WorldSceneWidget& widget,
-                           const ViewportGeometry& view,
-                           double x,
-                           double y,
-                           IStatusBar* statusBar) = 0;
+        virtual void MouseOver(CairoContext& context,
+                               WorldSceneWidget& widget,
+                               const ViewportGeometry& view,
+                               double x,
+                               double y,
+                               IStatusBar* statusBar) = 0;
 
-    virtual void MouseWheel(WorldSceneWidget& widget,
-                            MouseWheelDirection direction,
-                            KeyboardModifiers modifiers,
-                            IStatusBar* statusBar) = 0;
+        virtual void MouseWheel(WorldSceneWidget& widget,
+                                MouseWheelDirection direction,
+                                KeyboardModifiers modifiers,
+                                IStatusBar* statusBar) = 0;
 
-    virtual void KeyPressed(WorldSceneWidget& widget,
-                            char key,
-                            KeyboardModifiers modifiers,
-                            IStatusBar* statusBar) = 0;
-  };
+        virtual void KeyPressed(WorldSceneWidget& widget,
+                                KeyboardKeys key,
+                                char keyChar,
+                                KeyboardModifiers modifiers,
+                                IStatusBar* statusBar) = 0;
+    };
 }
--- a/Framework/Widgets/IWorldSceneMouseTracker.h	Mon Nov 05 10:04:56 2018 +0100
+++ b/Framework/Widgets/IWorldSceneMouseTracker.h	Mon Nov 05 10:06:18 2018 +0100
@@ -13,7 +13,7 @@
  * 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/>.
  **/
@@ -25,19 +25,27 @@
 
 namespace OrthancStone
 {
+
+  // this is tracking a mouse in scene coordinates/mm unlike
+  // the IMouseTracker that is tracking a mouse
+  // in screen coordinates/pixels.
   class IWorldSceneMouseTracker : public boost::noncopyable
   {
   public:
     virtual ~IWorldSceneMouseTracker()
     {
     }
-    
+
+    virtual bool HasRender() const = 0;
+
     virtual void Render(CairoContext& context,
                         double zoom) = 0;
-    
+
     virtual void MouseUp() = 0;
 
-    virtual void MouseMove(double x,
-                           double y) = 0;
+    virtual void MouseMove(int displayX,
+                           int displayY,
+                           double sceneX,
+                           double sceneY) = 0;
   };
 }
--- a/Framework/Widgets/LayerWidget.cpp	Mon Nov 05 10:04:56 2018 +0100
+++ b/Framework/Widgets/LayerWidget.cpp	Mon Nov 05 10:06:18 2018 +0100
@@ -13,7 +13,7 @@
  * 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/>.
  **/
@@ -23,6 +23,7 @@
 
 #include "../Layers/SliceOutlineRenderer.h"
 #include "../Toolbox/GeometryToolbox.h"
+#include "Framework/Layers/FrameRenderer.h"
 
 #include <Core/Logging.h>
 
@@ -37,7 +38,7 @@
     double                        thickness_;
     size_t                        countMissing_;
     std::vector<ILayerRenderer*>  renderers_;
-
+public:
     void DeleteLayer(size_t index)
     {
       if (index >= renderers_.size())
@@ -55,8 +56,7 @@
         countMissing_++;
       }
     }
-      
-  public:
+
     Scene(const CoordinateSystem3D& slice,
           double thickness,
           size_t countLayers) :
@@ -184,7 +184,7 @@
 #endif
         
         cairo_set_line_width(cr, 2.0 / view.GetZoom());
-        cairo_set_source_rgb(cr, 1, 1, 1); 
+        cairo_set_source_rgb(cr, 1, 1, 1);
         cairo_stroke_preserve(cr);
         cairo_set_source_rgb(cr, 1, 0, 0);
         cairo_fill(cr);
@@ -215,7 +215,7 @@
       {
         double z = (slice_.ProjectAlongNormal(slice.GetOrigin()) -
                     slice_.ProjectAlongNormal(slice_.GetOrigin()));
-      
+
         if (z < 0)
         {
           z = -z;
@@ -249,13 +249,13 @@
       return true;
     }
   }
-    
+
 
   void LayerWidget::GetLayerExtent(Extent2D& extent,
                                    ILayerSource& source) const
   {
     extent.Reset();
-    
+
     std::vector<Vector> points;
     if (source.GetExtent(points, slice_))
     {
@@ -268,7 +268,7 @@
     }
   }
 
-        
+
   Extent2D LayerWidget::GetSceneExtent()
   {
     Extent2D sceneExtent;
@@ -341,7 +341,7 @@
         currentScene_->ContainsPlane(slice))
     {
       currentScene_->SetLayer(index, tmp.release());
-      NotifyChange();
+      NotifyContentChanged();
     }
     else if (pendingScene_.get() != NULL &&
              pendingScene_->ContainsPlane(slice))
@@ -353,13 +353,16 @@
           pendingScene_->IsComplete())
       {
         currentScene_ = pendingScene_;
-        NotifyChange();
+        NotifyContentChanged();
       }
     }
   }
 
   
-  LayerWidget::LayerWidget() :
+  LayerWidget::LayerWidget(MessageBroker& broker, const std::string& name) :
+    WorldSceneWidget(name),
+    IObserver(broker),
+    IObservable(broker),
     started_(false)
   {
     SetBackgroundCleared(true);
@@ -374,6 +377,15 @@
     }
   }
   
+  void LayerWidget::ObserveLayer(ILayerSource& layer)
+  {
+    layer.RegisterObserverCallback(new Callable<LayerWidget, ILayerSource::GeometryReadyMessage>(*this, &LayerWidget::OnGeometryReady));
+    // currently ignore errors layer->RegisterObserverCallback(new Callable<LayerWidget, ILayerSource::GeometryErrorMessage>(*this, &LayerWidget::...));
+    layer.RegisterObserverCallback(new Callable<LayerWidget, ILayerSource::SliceChangedMessage>(*this, &LayerWidget::OnSliceChanged));
+    layer.RegisterObserverCallback(new Callable<LayerWidget, ILayerSource::ContentChangedMessage>(*this, &LayerWidget::OnContentChanged));
+    layer.RegisterObserverCallback(new Callable<LayerWidget, ILayerSource::LayerReadyMessage>(*this, &LayerWidget::OnLayerReady));
+  }
+
 
   size_t LayerWidget::AddLayer(ILayerSource* layer)  // Takes ownership
   {
@@ -388,14 +400,58 @@
     layersIndex_[layer] = index;
 
     ResetPendingScene();
-    layer->Register(*this);
+
+    ObserveLayer(*layer);
 
     ResetChangedLayers();
 
     return index;
   }
 
-  
+  void LayerWidget::ReplaceLayer(size_t index, ILayerSource* layer)  // Takes ownership
+  {
+    if (layer == NULL)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
+    }
+
+    if (index >= layers_.size())
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+
+    delete layers_[index];
+    layers_[index] = layer;
+    layersIndex_[layer] = index;
+
+    ResetPendingScene();
+
+    ObserveLayer(*layer);
+
+    InvalidateLayer(index);
+  }
+
+  void LayerWidget::RemoveLayer(size_t index)
+  {
+    if (index >= layers_.size())
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+
+    ILayerSource* previousLayer = layers_[index];
+    layersIndex_.erase(layersIndex_.find(previousLayer));
+    layers_.erase(layers_.begin() + index);
+    changedLayers_.erase(changedLayers_.begin() + index);
+    styles_.erase(styles_.begin() + index);
+
+    delete layers_[index];
+
+    currentScene_->DeleteLayer(index);
+    ResetPendingScene();
+
+    NotifyContentChanged();
+  }
+
   const RenderStyle& LayerWidget::GetLayerStyle(size_t layer) const
   {
     if (layer >= layers_.size())
@@ -429,7 +485,7 @@
       pendingScene_->SetLayerStyle(layer, style);
     }
 
-    NotifyChange();
+    NotifyContentChanged();
   }
   
 
@@ -457,26 +513,19 @@
     }
   }
 
-
-  void LayerWidget::NotifyGeometryReady(const ILayerSource& source)
+  void LayerWidget::OnGeometryReady(const ILayerSource::GeometryReadyMessage& message)
   {
     size_t i;
-    if (LookupLayer(i, source))
+    if (LookupLayer(i, message.origin_))
     {
-      LOG(INFO) << "Geometry ready for layer " << i;
+      LOG(INFO) << ": Geometry ready for layer " << i << " in " << GetName();
 
       changedLayers_[i] = true;
       //layers_[i]->ScheduleLayerCreation(slice_);
     }
+    EmitMessage(GeometryChangedMessage(*this));
   }
   
-
-  void LayerWidget::NotifyGeometryError(const ILayerSource& source)
-  {
-    LOG(ERROR) << "Cannot get geometry";
-  }
-  
-
   void LayerWidget::InvalidateAllLayers()
   {
     for (size_t i = 0; i < layers_.size(); i++)
@@ -503,39 +552,37 @@
   }
 
 
-  void LayerWidget::NotifyContentChange(const ILayerSource& source)
+  void LayerWidget::OnContentChanged(const ILayerSource::ContentChangedMessage& message)
   {
     size_t index;
-    if (LookupLayer(index, source))
+    if (LookupLayer(index, message.origin_))
     {
       InvalidateLayer(index);
     }
+    EmitMessage(LayerWidget::ContentChangedMessage(*this));
   }
   
 
-  void LayerWidget::NotifySliceChange(const ILayerSource& source,
-                                      const Slice& slice)
+  void LayerWidget::OnSliceChanged(const ILayerSource::SliceChangedMessage& message)
   {
-    if (slice.ContainsPlane(slice_))
+    if (message.slice_.ContainsPlane(slice_))
     {
       size_t index;
-      if (LookupLayer(index, source))
+      if (LookupLayer(index, message.origin_))
       {
         InvalidateLayer(index);
       }
     }
+    EmitMessage(LayerWidget::ContentChangedMessage(*this));
   }
   
   
-  void LayerWidget::NotifyLayerReady(std::auto_ptr<ILayerRenderer>& renderer,
-                                     const ILayerSource& source,
-                                     const CoordinateSystem3D& slice,
-                                     bool isError)
+  void LayerWidget::OnLayerReady(const ILayerSource::LayerReadyMessage& message)
   {
     size_t index;
-    if (LookupLayer(index, source))
+    if (LookupLayer(index, message.origin_))
     {
-      if (isError)
+      if (message.isError_)
       {
         LOG(ERROR) << "Using error renderer on layer " << index;
       }
@@ -543,17 +590,18 @@
       {
         LOG(INFO) << "Renderer ready for layer " << index;
       }
-      
-      if (renderer.get() != NULL)
+
+      if (message.renderer_.get() != NULL)
       {
-        UpdateLayer(index, renderer.release(), slice);
+        UpdateLayer(index, message.renderer_.release(), message.slice_);
       }
-      else if (isError)
+      else if (message.isError_)
       {
         // TODO
         //UpdateLayer(index, new SliceOutlineRenderer(slice), slice);
       }
     }
+    EmitMessage(LayerWidget::ContentChangedMessage(*this));
   }
 
 
--- a/Framework/Widgets/LayerWidget.h	Mon Nov 05 10:04:56 2018 +0100
+++ b/Framework/Widgets/LayerWidget.h	Mon Nov 05 10:06:18 2018 +0100
@@ -13,7 +13,7 @@
  * 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/>.
  **/
@@ -24,15 +24,21 @@
 #include "WorldSceneWidget.h"
 #include "../Layers/ILayerSource.h"
 #include "../Toolbox/Extent2D.h"
+#include "../../Framework/Messages/IObserver.h"
 
 #include <map>
 
 namespace OrthancStone
 {
   class LayerWidget :
-    public WorldSceneWidget,
-    private ILayerSource::IObserver
+      public WorldSceneWidget,
+      public IObserver,
+      public IObservable
   {
+  public:
+    typedef OriginMessage<MessageType_Widget_GeometryChanged, LayerWidget> GeometryChangedMessage;
+    typedef OriginMessage<MessageType_Widget_ContentChanged, LayerWidget> ContentChangedMessage;
+
   private:
     class Scene;
     
@@ -53,25 +59,23 @@
     void GetLayerExtent(Extent2D& extent,
                         ILayerSource& source) const;
 
-    virtual void NotifyGeometryReady(const ILayerSource& source);
+    void OnGeometryReady(const ILayerSource::GeometryReadyMessage& message);
 
-    virtual void NotifyGeometryError(const ILayerSource& source);
-
-    virtual void NotifyContentChange(const ILayerSource& source);
+    virtual void OnContentChanged(const ILayerSource::ContentChangedMessage& message);
 
-    virtual void NotifySliceChange(const ILayerSource& source,
-                                   const Slice& slice);
+    virtual void OnSliceChanged(const ILayerSource::SliceChangedMessage& message);
 
-    virtual void NotifyLayerReady(std::auto_ptr<ILayerRenderer>& renderer,
-                                  const ILayerSource& source,
-                                  const CoordinateSystem3D& slice,
-                                  bool isError);
+    virtual void OnLayerReady(const ILayerSource::LayerReadyMessage& message);
+
+    void ObserveLayer(ILayerSource& source);
 
     void ResetChangedLayers();
 
   public:
+    LayerWidget(MessageBroker& broker, const std::string& name);
+
     virtual Extent2D GetSceneExtent();
- 
+
   protected:
     virtual bool RenderScene(CairoContext& context,
                              const ViewportGeometry& view);
@@ -87,12 +91,14 @@
     void InvalidateLayer(size_t layer);
     
   public:
-    LayerWidget();
-
     virtual ~LayerWidget();
 
     size_t AddLayer(ILayerSource* layer);  // Takes ownership
 
+    void ReplaceLayer(size_t layerIndex, ILayerSource* layer); // Takes ownership
+
+    void RemoveLayer(size_t layerIndex);
+
     size_t GetLayerCount() const
     {
       return layers_.size();
--- a/Framework/Widgets/LayoutWidget.cpp	Mon Nov 05 10:04:56 2018 +0100
+++ b/Framework/Widgets/LayoutWidget.cpp	Mon Nov 05 10:06:18 2018 +0100
@@ -83,12 +83,10 @@
     int                     top_;
     unsigned int            width_;
     unsigned int            height_;
-    bool                    hasUpdate_;
 
   public:
     ChildWidget(IWidget* widget) :
-      widget_(widget),
-      hasUpdate_(widget->HasUpdateContent())
+      widget_(widget)
     {
       assert(widget != NULL);
       SetEmpty();
@@ -96,7 +94,7 @@
 
     void UpdateContent()
     {
-      if (hasUpdate_)
+      if (widget_->HasUpdateContent())
       {
         widget_->UpdateContent();
       }
@@ -266,11 +264,12 @@
       }
     }
 
-    NotifyChange(*this);
+    NotifyContentChanged(*this);
   }
 
 
-  LayoutWidget::LayoutWidget() :
+  LayoutWidget::LayoutWidget(const std::string& name) :
+    WidgetBase(name),
     isHorizontal_(true),
     width_(0),
     height_(0),
@@ -292,19 +291,19 @@
   }
 
 
-  void LayoutWidget::SetDefaultView()
+  void LayoutWidget::FitContent()
   {
     for (size_t i = 0; i < children_.size(); i++)
     {
-      children_[i]->GetWidget().SetDefaultView();
+      children_[i]->GetWidget().FitContent();
     }
   }
   
 
-  void LayoutWidget::NotifyChange(const IWidget& widget)
+  void LayoutWidget::NotifyContentChanged(const IWidget& widget)
   {
     // One of the children has changed
-    WidgetBase::NotifyChange();
+    WidgetBase::NotifyContentChanged();
   }
 
 
@@ -452,12 +451,13 @@
   }
 
 
-  void LayoutWidget::KeyPressed(char key,
+  void LayoutWidget::KeyPressed(KeyboardKeys key,
+                                char keyChar,
                                 KeyboardModifiers modifiers)
   {
     for (size_t i = 0; i < children_.size(); i++)
     {
-      children_[i]->GetWidget().KeyPressed(key, modifiers);
+      children_[i]->GetWidget().KeyPressed(key, keyChar, modifiers);
     }
   }
 
--- a/Framework/Widgets/LayoutWidget.h	Mon Nov 05 10:04:56 2018 +0100
+++ b/Framework/Widgets/LayoutWidget.h	Mon Nov 05 10:06:18 2018 +0100
@@ -49,13 +49,13 @@
     void ComputeChildrenExtents();
 
   public:
-    LayoutWidget();
+    LayoutWidget(const std::string& name);
 
     virtual ~LayoutWidget();
 
-    virtual void SetDefaultView();
+    virtual void FitContent();
 
-    virtual void NotifyChange(const IWidget& widget);
+    virtual void NotifyContentChanged(const IWidget& widget);
 
     void SetHorizontal();
 
@@ -117,7 +117,8 @@
                             int y,
                             KeyboardModifiers modifiers);
 
-    virtual void KeyPressed(char key,
+    virtual void KeyPressed(KeyboardKeys key,
+                            char keyChar,
                             KeyboardModifiers modifiers);
 
     virtual bool HasUpdateContent() const
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Widgets/PanMouseTracker.cpp	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,55 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 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 "PanMouseTracker.h"
+
+#include <Core/Logging.h>
+
+namespace OrthancStone
+{
+  PanMouseTracker::PanMouseTracker(WorldSceneWidget& that,
+                                   int x,
+                                   int y) :
+    that_(that)
+  {
+    that.GetView().GetPan(originalPanX_, originalPanY_);
+    that.GetView().MapPixelCenterToScene(downX_, downY_, x, y);
+  }
+    
+
+  void PanMouseTracker::Render(CairoContext& context,
+                               double zoom)
+  {
+    throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+  }
+
+
+  void PanMouseTracker::MouseMove(int displayX,
+                                  int displayY,
+                                  double x,
+                                  double y)
+  {
+    ViewportGeometry view = that_.GetView();
+    view.SetPan(originalPanX_ + (x - downX_) * view.GetZoom(),
+                originalPanY_ + (y - downY_) * view.GetZoom());
+    that_.SetView(view);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Widgets/PanMouseTracker.h	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,59 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 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
+
+#include "WorldSceneWidget.h"
+
+namespace OrthancStone
+{
+  class PanMouseTracker : public IWorldSceneMouseTracker
+  {
+  private:
+    WorldSceneWidget&  that_;
+    double             originalPanX_;
+    double             originalPanY_;
+    double             downX_;
+    double             downY_;
+    
+  public:
+    PanMouseTracker(WorldSceneWidget& that,
+                    int x,
+                    int y);
+    
+    virtual bool HasRender() const
+    {
+      return false;
+    }
+
+    virtual void MouseUp()
+    {
+    }
+
+    virtual void Render(CairoContext& context,
+                        double zoom);
+
+    virtual void MouseMove(int displayX,
+                           int displayY,
+                           double x,
+                           double y);
+  };
+}
--- a/Framework/Widgets/TestCairoWidget.cpp	Mon Nov 05 10:04:56 2018 +0100
+++ b/Framework/Widgets/TestCairoWidget.cpp	Mon Nov 05 10:06:18 2018 +0100
@@ -36,7 +36,7 @@
         value_ = 1;
       }
 
-      NotifyChange();
+      NotifyContentChanged();
     }
 
 
@@ -77,7 +77,8 @@
     }
 
 
-    TestCairoWidget::TestCairoWidget(bool animate) :
+    TestCairoWidget::TestCairoWidget(const std::string& name, bool animate) :
+      CairoWidget(name),
       width_(0),
       height_(0),
       value_(1),
@@ -114,10 +115,11 @@
     }
 
     
-    void TestCairoWidget::KeyPressed(char key,
+    void TestCairoWidget::KeyPressed(KeyboardKeys key,
+                                     char keyChar,
                                      KeyboardModifiers modifiers)
     {
-      UpdateStatusBar("Key pressed: \"" + std::string(1, key) + "\"");
+      UpdateStatusBar("Key pressed: \"" + std::string(1, keyChar) + "\"");
     }
   }
 }
--- a/Framework/Widgets/TestCairoWidget.h	Mon Nov 05 10:04:56 2018 +0100
+++ b/Framework/Widgets/TestCairoWidget.h	Mon Nov 05 10:06:18 2018 +0100
@@ -43,7 +43,7 @@
                                         int y);
 
     public:
-      TestCairoWidget(bool animate);
+      TestCairoWidget(const std::string& name, bool animate);
 
       virtual void SetSize(unsigned int width, 
                            unsigned int height);
@@ -58,7 +58,8 @@
                               int y,
                               KeyboardModifiers modifiers);
     
-      virtual void KeyPressed(char key,
+      virtual void KeyPressed(KeyboardKeys key,
+                              char keyChar,
                               KeyboardModifiers modifiers);
 
       virtual bool HasUpdateContent() const
--- a/Framework/Widgets/TestWorldSceneWidget.cpp	Mon Nov 05 10:04:56 2018 +0100
+++ b/Framework/Widgets/TestWorldSceneWidget.cpp	Mon Nov 05 10:06:18 2018 +0100
@@ -13,7 +13,7 @@
  * 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/>.
  **/
@@ -34,6 +34,9 @@
       virtual IWorldSceneMouseTracker* CreateMouseTracker(WorldSceneWidget& widget,
                                                           const ViewportGeometry& view,
                                                           MouseButton button,
+                                                          KeyboardModifiers modifiers,
+                                                          int viewportX,
+                                                          int viewportY,
                                                           double x,
                                                           double y,
                                                           IStatusBar* statusBar)
@@ -80,20 +83,21 @@
       }
 
       virtual void KeyPressed(WorldSceneWidget& widget,
-                              char key,
+                              KeyboardKeys key,
+                              char keyChar,
                               KeyboardModifiers modifiers,
                               IStatusBar* statusBar)
       {
         if (statusBar)
         {
-          statusBar->SetMessage("Key pressed: \"" + std::string(1, key) + "\"");
+          statusBar->SetMessage("Key pressed: \"" + std::string(1, keyChar) + "\"");
         }
       }
     };
 
 
     bool TestWorldSceneWidget::RenderScene(CairoContext& context,
-                                           const ViewportGeometry& view) 
+                                           const ViewportGeometry& view)
     {
       cairo_t* cr = context.GetObject();
 
@@ -110,7 +114,8 @@
     }
 
 
-    TestWorldSceneWidget::TestWorldSceneWidget(bool animate) :
+    TestWorldSceneWidget::TestWorldSceneWidget(const std::string& name, bool animate) :
+      WorldSceneWidget(name),
       interactor_(new Interactor),
       animate_(animate),
       count_(0)
@@ -130,7 +135,7 @@
       if (animate_)
       {
         count_++;
-        NotifyChange();
+        NotifyContentChanged();
       }
       else
       {
--- a/Framework/Widgets/TestWorldSceneWidget.h	Mon Nov 05 10:04:56 2018 +0100
+++ b/Framework/Widgets/TestWorldSceneWidget.h	Mon Nov 05 10:06:18 2018 +0100
@@ -43,7 +43,7 @@
                                const ViewportGeometry& view);
 
     public:
-      TestWorldSceneWidget(bool animate);
+      TestWorldSceneWidget(const std::string& name, bool animate);
 
       virtual Extent2D GetSceneExtent();
 
--- a/Framework/Widgets/WidgetBase.cpp	Mon Nov 05 10:04:56 2018 +0100
+++ b/Framework/Widgets/WidgetBase.cpp	Mon Nov 05 10:06:18 2018 +0100
@@ -27,16 +27,16 @@
 
 namespace OrthancStone
 {
-  void WidgetBase::NotifyChange()
+  void WidgetBase::NotifyContentChanged()
   {
     if (parent_ != NULL)
     {
-      parent_->NotifyChange();
+      parent_->NotifyContentChanged();
     }
 
     if (viewport_ != NULL)
     {
-      viewport_->NotifyChange(*this);
+      viewport_->NotifyContentChanged(*this);
     }
   }
 
@@ -101,12 +101,13 @@
   }
 
 
-  WidgetBase::WidgetBase() :
+  WidgetBase::WidgetBase(const std::string& name) :
     parent_(NULL),
     viewport_(NULL),
     statusBar_(NULL),
     backgroundCleared_(false),
-    transmitMouseOver_(false)
+    transmitMouseOver_(false),
+    name_(name)
   {
     backgroundColor_[0] = 0;
     backgroundColor_[1] = 0;
--- a/Framework/Widgets/WidgetBase.h	Mon Nov 05 10:04:56 2018 +0100
+++ b/Framework/Widgets/WidgetBase.h	Mon Nov 05 10:06:18 2018 +0100
@@ -36,6 +36,7 @@
     bool         backgroundCleared_;
     uint8_t      backgroundColor_[3];
     bool         transmitMouseOver_;
+    std::string  name_;
 
   protected:
     void ClearBackgroundOrthanc(Orthanc::ImageAccessor& target) const;
@@ -52,9 +53,9 @@
     }
 
   public:
-    WidgetBase();
+    WidgetBase(const std::string& name);
 
-    virtual void SetDefaultView()
+    virtual void FitContent()
     {
     }
   
@@ -104,6 +105,12 @@
       return transmitMouseOver_;
     }
 
-    virtual void NotifyChange();
+    virtual void NotifyContentChanged();
+
+    const std::string& GetName() const
+    {
+      return name_;
+    }
+
   };
 }
--- a/Framework/Widgets/WorldSceneWidget.cpp	Mon Nov 05 10:04:56 2018 +0100
+++ b/Framework/Widgets/WorldSceneWidget.cpp	Mon Nov 05 10:06:18 2018 +0100
@@ -13,7 +13,7 @@
  * 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/>.
  **/
@@ -21,195 +21,61 @@
 
 #include "WorldSceneWidget.h"
 
+#include "PanMouseTracker.h"
+#include "ZoomMouseTracker.h"
+
+#include <Core/Logging.h>
+
 #include <math.h>
 #include <memory>
 #include <cassert>
 
 namespace OrthancStone
 {
-  static void MapMouseToScene(double& sceneX,
-                              double& sceneY,
-                              const ViewportGeometry& view,
-                              int mouseX,
-                              int mouseY)
-  {
-    // Take the center of the pixel
-    double x, y;
-    x = static_cast<double>(mouseX) + 0.5;
-    y = static_cast<double>(mouseY) + 0.5;
-
-    view.MapDisplayToScene(sceneX, sceneY, x, y);
-  }
-
-
-  struct WorldSceneWidget::SizeChangeFunctor
-  {
-    ViewportGeometry& view_;
-
-    SizeChangeFunctor(ViewportGeometry& view) :
-      view_(view)
-    {
-    }
-
-    void operator() (IWorldObserver& observer,
-                     const WorldSceneWidget& source)
-    {
-      observer.NotifySizeChange(source, view_);
-    }
-  };
-
-
+  // this is an adapter between a IWorldSceneMouseTracker
+  // that is tracking a mouse in scene coordinates/mm and
+  // an IMouseTracker that is tracking a mouse
+  // in screen coordinates/pixels.
   class WorldSceneWidget::SceneMouseTracker : public IMouseTracker
   {
   private:
-    ViewportGeometry                       view_;
+    ViewportGeometry                        view_;
     std::auto_ptr<IWorldSceneMouseTracker>  tracker_;
-      
+
   public:
     SceneMouseTracker(const ViewportGeometry& view,
                       IWorldSceneMouseTracker* tracker) :
       view_(view),
       tracker_(tracker)
     {
-      assert(tracker != NULL);
+      if (tracker == NULL)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
+      }
     }
 
     virtual void Render(Orthanc::ImageAccessor& target)
     {
-      CairoSurface surface(target);
-      CairoContext context(surface); 
-      view_.ApplyTransform(context);
-      tracker_->Render(context, view_.GetZoom());
+      if (tracker_->HasRender())
+      {
+        CairoSurface surface(target);
+        CairoContext context(surface);
+        view_.ApplyTransform(context);
+        tracker_->Render(context, view_.GetZoom());
+      }
     }
 
-    virtual void MouseUp() 
+    virtual void MouseUp()
     {
       tracker_->MouseUp();
     }
 
-    virtual void MouseMove(int x, 
+    virtual void MouseMove(int x,
                            int y)
     {
       double sceneX, sceneY;
-      MapMouseToScene(sceneX, sceneY, view_, x, y);
-      tracker_->MouseMove(sceneX, sceneY);
-    }
-  };
-
-
-  class WorldSceneWidget::PanMouseTracker : public IMouseTracker
-  {
-  private:
-    WorldSceneWidget&  that_;  
-    double             previousPanX_;
-    double             previousPanY_;
-    double             downX_;
-    double             downY_;
-
-  public:
-    PanMouseTracker(WorldSceneWidget& that,
-                    int x,
-                    int y) :
-      that_(that),
-      downX_(x),
-      downY_(y)
-    {
-      that_.view_.GetPan(previousPanX_, previousPanY_);
-    }
-
-    virtual void Render(Orthanc::ImageAccessor& surface)
-    {
-    }
-
-    virtual void MouseUp() 
-    {
-    }
-
-    virtual void MouseMove(int x, 
-                           int y)
-    {
-      that_.view_.SetPan(previousPanX_ + x - downX_,
-                         previousPanY_ + y - downY_);
-
-      that_.observers_.Apply(that_, &IWorldObserver::NotifyViewChange, that_.view_);
-    }
-  };
-
-
-  class WorldSceneWidget::ZoomMouseTracker : public IMouseTracker
-  {
-  private:
-    WorldSceneWidget&  that_;  
-    int                downX_;
-    int                downY_;
-    double             centerX_;
-    double             centerY_;
-    double             oldZoom_;
-
-  public:
-    ZoomMouseTracker(WorldSceneWidget&  that,
-                     int x,
-                     int y) :
-      that_(that),
-      downX_(x),
-      downY_(y)
-    {
-      oldZoom_ = that_.view_.GetZoom();
-      MapMouseToScene(centerX_, centerY_, that_.view_, downX_, downY_);
-    }
-
-    virtual void Render(Orthanc::ImageAccessor& surface)
-    {
-    }
-
-    virtual void MouseUp() 
-    {
-    }
-
-    virtual void MouseMove(int x, 
-                           int y)
-    {
-      static const double MIN_ZOOM = -4;
-      static const double MAX_ZOOM = 4;
-
-      if (that_.view_.GetDisplayHeight() <= 3)
-      {
-        return;   // Cannot zoom on such a small image
-      }
-
-      double dy = (static_cast<double>(y - downY_) / 
-                   static_cast<double>(that_.view_.GetDisplayHeight() - 1)); // In the range [-1,1]
-      double z;
-
-      // Linear interpolation from [-1, 1] to [MIN_ZOOM, MAX_ZOOM]
-      if (dy < -1.0)
-      {
-        z = MIN_ZOOM;
-      }
-      else if (dy > 1.0)
-      {
-        z = MAX_ZOOM;
-      }
-      else
-      {
-        z = MIN_ZOOM + (MAX_ZOOM - MIN_ZOOM) * (dy + 1.0) / 2.0;
-      }
-
-      z = pow(2.0, z);
-
-      that_.view_.SetZoom(oldZoom_ * z);
-
-      // Correct the pan so that the original click point is kept at
-      // the same location on the display
-      double panX, panY;
-      that_.view_.GetPan(panX, panY);
-
-      int tx, ty;
-      that_.view_.MapSceneToDisplay(tx, ty, centerX_, centerY_);
-      that_.view_.SetPan(panX + static_cast<double>(downX_ - tx),
-                               panY + static_cast<double>(downY_ - ty));
-
-      that_.observers_.Apply(that_, &IWorldObserver::NotifyViewChange, that_.view_);
+      view_.MapPixelCenterToScene(sceneX, sceneY, x, y);
+      tracker_->MouseMove(x, y, sceneX, sceneY);
     }
   };
 
@@ -229,8 +95,12 @@
     view.ApplyTransform(context);
 
     double sceneX, sceneY;
-    MapMouseToScene(sceneX, sceneY, view, x, y);
-    RenderSceneMouseOver(context, view, sceneX, sceneY);
+    view.MapPixelCenterToScene(sceneX, sceneY, x, y);
+
+    if (interactor_)
+    {
+      interactor_->MouseOver(context, *this, view, sceneX, sceneY, GetStatusBar());
+    }
   }
 
 
@@ -244,20 +114,7 @@
                                  unsigned int height)
   {
     CairoWidget::SetSize(width, height);
-
     view_.SetDisplaySize(width, height);
-
-    if (observers_.IsEmpty())
-    {
-      // Without a size observer, reset to the default view
-      // view_.SetDefaultView();
-    }
-    else
-    {
-      // With a size observer, let it decide which view to use
-      SizeChangeFunctor functor(view_);
-      observers_.Notify(*this, functor);
-    }
   }
 
 
@@ -267,14 +124,12 @@
   }
 
 
-  void WorldSceneWidget::SetDefaultView()
+  void WorldSceneWidget::FitContent()
   {
     SetSceneExtent(view_);
-    view_.SetDefaultView();
+    view_.FitContent();
 
-    NotifyChange();
-
-    observers_.Apply(*this, &IWorldObserver::NotifyViewChange, view_);
+    NotifyContentChanged();
   }
 
 
@@ -282,15 +137,7 @@
   {
     view_ = view;
 
-    NotifyChange();
-
-    observers_.Apply(*this, &IWorldObserver::NotifyViewChange, view_);
-  }
-
-
-  ViewportGeometry WorldSceneWidget::GetView()
-  {
-    return view_;
+    NotifyContentChanged();
   }
 
 
@@ -300,50 +147,33 @@
                                                       KeyboardModifiers modifiers)
   {
     double sceneX, sceneY;
-    MapMouseToScene(sceneX, sceneY, view_, x, y);
+    view_.MapPixelCenterToScene(sceneX, sceneY, x, y);
+
+    // asks the Widget Interactor to provide a mouse tracker
+    std::auto_ptr<IWorldSceneMouseTracker> tracker;
 
-    std::auto_ptr<IWorldSceneMouseTracker> tracker
-      (CreateMouseSceneTracker(view_, button, sceneX, sceneY, modifiers));
-
+    if (interactor_)
+    {
+      tracker.reset(interactor_->CreateMouseTracker(*this, view_, button, modifiers, x, y, sceneX, sceneY, GetStatusBar()));
+    }
+    
     if (tracker.get() != NULL)
     {
       return new SceneMouseTracker(view_, tracker.release());
     }
-
-    switch (button)
+    else if (hasDefaultMouseEvents_)
     {
-      case MouseButton_Middle:
-        return new PanMouseTracker(*this, x, y);
-
-      case MouseButton_Right:
-        return new ZoomMouseTracker(*this, x, y);
-
-      default:
-        return NULL;
-    }
-  }
-
+      switch (button)
+      {
+        case MouseButton_Middle:
+          return new SceneMouseTracker(view_, new PanMouseTracker(*this, x, y));
 
-  void WorldSceneWidget::RenderSceneMouseOver(CairoContext& context,
-                                              const ViewportGeometry& view,
-                                              double x,
-                                              double y)
-  {
-    if (interactor_)
-    {
-      interactor_->MouseOver(context, *this, view, x, y, GetStatusBar());
-    }
-  }
+        case MouseButton_Right:
+          return new SceneMouseTracker(view_, new ZoomMouseTracker(*this, x, y));
 
-  IWorldSceneMouseTracker* WorldSceneWidget::CreateMouseSceneTracker(const ViewportGeometry& view,
-                                                                     MouseButton button,
-                                                                     double x,
-                                                                     double y,
-                                                                     KeyboardModifiers modifiers)
-  {
-    if (interactor_)
-    {
-      return interactor_->CreateMouseTracker(*this, view, button, x, y, GetStatusBar());
+        default:
+          return NULL;
+      }      
     }
     else
     {
@@ -355,7 +185,7 @@
   void WorldSceneWidget::MouseWheel(MouseWheelDirection direction,
                                     int x,
                                     int y,
-                                    KeyboardModifiers modifiers) 
+                                    KeyboardModifiers modifiers)
   {
     if (interactor_)
     {
@@ -364,12 +194,13 @@
   }
 
 
-  void WorldSceneWidget::KeyPressed(char key,
+  void WorldSceneWidget::KeyPressed(KeyboardKeys key,
+                                    char keyChar,
                                     KeyboardModifiers modifiers)
   {
     if (interactor_)
     {
-      interactor_->KeyPressed(*this, key, modifiers, GetStatusBar());
+      interactor_->KeyPressed(*this, key, keyChar, modifiers, GetStatusBar());
     }
   }
 }
--- a/Framework/Widgets/WorldSceneWidget.h	Mon Nov 05 10:04:56 2018 +0100
+++ b/Framework/Widgets/WorldSceneWidget.h	Mon Nov 05 10:06:18 2018 +0100
@@ -13,7 +13,7 @@
  * 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/>.
  **/
@@ -31,43 +31,23 @@
 {
   class WorldSceneWidget : public CairoWidget
   {
-  public:
-    class IWorldObserver : public boost::noncopyable
-    {
-    public:
-      virtual ~IWorldObserver()
-      {
-      }
-
-      virtual void NotifySizeChange(const WorldSceneWidget& source,
-                                    ViewportGeometry& view) = 0;  // Can be tuned by the observer
-
-      virtual void NotifyViewChange(const WorldSceneWidget& source,
-                                    const ViewportGeometry& view) = 0;
-    };
-
   private:
-    struct SizeChangeFunctor;
-
     class SceneMouseTracker;
-    class PanMouseTracker;
-    class ZoomMouseTracker;
-
-    typedef ObserversRegistry<WorldSceneWidget, IWorldObserver>  Observers;
 
     ViewportGeometry       view_;
-    Observers              observers_;
     IWorldSceneInteractor* interactor_;
+    bool                   hasDefaultMouseEvents_;
 
-  public:
+  protected:
     virtual Extent2D GetSceneExtent() = 0;
 
-  protected:
     virtual bool RenderScene(CairoContext& context,
                              const ViewportGeometry& view) = 0;
 
+    // From CairoWidget
     virtual bool RenderCairo(CairoContext& context);
 
+    // From CairoWidget
     virtual void RenderMouseOverCairo(CairoContext& context,
                                       int x,
                                       int y);
@@ -75,54 +55,49 @@
     void SetSceneExtent(ViewportGeometry& geometry);
 
   public:
-    WorldSceneWidget() :
-      interactor_(NULL)
+    WorldSceneWidget(const std::string& name) :
+      CairoWidget(name),
+      interactor_(NULL),
+      hasDefaultMouseEvents_(true)
     {
     }
 
-    void Register(IWorldObserver& observer)
+    void SetDefaultMouseEvents(bool value)
     {
-      observers_.Register(observer);
+      hasDefaultMouseEvents_ = value;
     }
 
-    void Unregister(IWorldObserver& observer)
+    bool HasDefaultMouseEvents() const
     {
-      observers_.Unregister(observer);
+      return hasDefaultMouseEvents_;
+    }
+
+    void SetInteractor(IWorldSceneInteractor& interactor);
+
+    void SetView(const ViewportGeometry& view);
+
+    const ViewportGeometry& GetView() const
+    {
+      return view_;
     }
 
     virtual void SetSize(unsigned int width,
                          unsigned int height);
 
-    void SetInteractor(IWorldSceneInteractor& interactor);
-
-    virtual void SetDefaultView();
-
-    void SetView(const ViewportGeometry& view);
-
-    ViewportGeometry GetView();
+    virtual void FitContent();
 
     virtual IMouseTracker* CreateMouseTracker(MouseButton button,
                                               int x,
                                               int y,
                                               KeyboardModifiers modifiers);
 
-    virtual void RenderSceneMouseOver(CairoContext& context,
-                                      const ViewportGeometry& view,
-                                      double x,
-                                      double y);
-
-    virtual IWorldSceneMouseTracker* CreateMouseSceneTracker(const ViewportGeometry& view,
-                                                             MouseButton button,
-                                                             double x,
-                                                             double y,
-                                                             KeyboardModifiers modifiers);
-
     virtual void MouseWheel(MouseWheelDirection direction,
                             int x,
                             int y,
                             KeyboardModifiers modifiers);
 
-    virtual void KeyPressed(char key,
+    virtual void KeyPressed(KeyboardKeys key,
+                            char keyChar,
                             KeyboardModifiers modifiers);
   };
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Widgets/ZoomMouseTracker.cpp	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,107 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 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 "ZoomMouseTracker.h"
+
+#include <Core/Logging.h>
+
+namespace OrthancStone
+{
+  ZoomMouseTracker::ZoomMouseTracker(WorldSceneWidget& that,
+                                     int x,
+                                     int y) :
+    that_(that),
+    originalZoom_(that.GetView().GetZoom()),
+    downX_(x),
+    downY_(y)
+  {
+    that.GetView().MapPixelCenterToScene(centerX_, centerY_, x, y);
+
+    unsigned int height = that.GetView().GetDisplayHeight();
+      
+    if (height <= 3)
+    {
+      idle_ = true;
+      LOG(WARNING) << "image is too small to zoom (current height = " << height << ")";
+    }
+    else
+    {
+      idle_ = false;
+      normalization_ = 1.0 / static_cast<double>(height - 1);
+    }
+  }
+    
+
+  void ZoomMouseTracker::Render(CairoContext& context,
+                                double zoom)
+  {
+    throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+  }
+
+
+  void ZoomMouseTracker::MouseMove(int displayX,
+                                   int displayY,
+                                   double x,
+                                   double y)
+  {
+    static const double MIN_ZOOM = -4;
+    static const double MAX_ZOOM = 4;
+
+      
+    if (!idle_)
+    {
+      double dy = static_cast<double>(displayY - downY_) * normalization_;  // In the range [-1,1]
+      double z;
+
+      // Linear interpolation from [-1, 1] to [MIN_ZOOM, MAX_ZOOM]
+      if (dy < -1.0)
+      {
+        z = MIN_ZOOM;
+      }
+      else if (dy > 1.0)
+      {
+        z = MAX_ZOOM;
+      }
+      else
+      {
+        z = MIN_ZOOM + (MAX_ZOOM - MIN_ZOOM) * (dy + 1.0) / 2.0;
+      }
+
+      z = pow(2.0, z);
+
+      ViewportGeometry view = that_.GetView();
+        
+      view.SetZoom(z * originalZoom_);
+        
+      // Correct the pan so that the original click point is kept at
+      // the same location on the display
+      double panX, panY;
+      view.GetPan(panX, panY);
+
+      int tx, ty;
+      view.MapSceneToDisplay(tx, ty, centerX_, centerY_);
+      view.SetPan(panX + static_cast<double>(downX_ - tx),
+                  panY + static_cast<double>(downY_ - ty));
+        
+      that_.SetView(view);
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Widgets/ZoomMouseTracker.h	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,62 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 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
+
+#include "WorldSceneWidget.h"
+
+namespace OrthancStone
+{
+  class ZoomMouseTracker : public IWorldSceneMouseTracker
+  {
+  private:
+    WorldSceneWidget&  that_;
+    double             originalZoom_;
+    int                downX_;
+    int                downY_;
+    double             centerX_;
+    double             centerY_;
+    bool               idle_;
+    double             normalization_;
+    
+  public:
+    ZoomMouseTracker(WorldSceneWidget& that,
+                     int x,
+                     int y);
+    
+    virtual bool HasRender() const
+    {
+      return false;
+    }
+
+    virtual void MouseUp()
+    {
+    }
+
+    virtual void Render(CairoContext& context,
+                        double zoom);
+
+    virtual void MouseMove(int displayX,
+                           int displayY,
+                           double x,
+                           double y);
+  };
+}
--- a/Framework/dev.h	Mon Nov 05 10:04:56 2018 +0100
+++ b/Framework/dev.h	Mon Nov 05 10:06:18 2018 +0100
@@ -13,7 +13,7 @@
  * 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/>.
  **/
@@ -41,10 +41,10 @@
 namespace OrthancStone
 {
   // TODO: Handle errors while loading
-  class OrthancVolumeImage : 
-    public SlicedVolumeBase,
-    private OrthancSlicesLoader::ICallback
-  { 
+  class OrthancVolumeImage :
+      public SlicedVolumeBase,
+      public OrthancStone::IObserver
+  {
   private:
     OrthancSlicesLoader           loader_;
     std::auto_ptr<ImageBuffer3D>  image_;
@@ -64,7 +64,7 @@
     }
 
 
-    static bool IsCompatible(const Slice& a, 
+    static bool IsCompatible(const Slice& a,
                              const Slice& b)
     {
       if (!GeometryToolbox::IsParallel(a.GetGeometry().GetNormal(),
@@ -98,15 +98,15 @@
     }
 
 
-    static double GetDistance(const Slice& a, 
+    static double GetDistance(const Slice& a,
                               const Slice& b)
     {
-      return fabs(a.GetGeometry().ProjectAlongNormal(a.GetGeometry().GetOrigin()) - 
+      return fabs(a.GetGeometry().ProjectAlongNormal(a.GetGeometry().GetOrigin()) -
                   a.GetGeometry().ProjectAlongNormal(b.GetGeometry().GetOrigin()));
     }
 
 
-    virtual void NotifyGeometryReady(const OrthancSlicesLoader& loader)
+    void OnSliceGeometryReady(const OrthancSlicesLoader& loader)
     {
       if (loader.GetSliceCount() == 0)
       {
@@ -151,15 +151,15 @@
       unsigned int width = loader.GetSlice(0).GetWidth();
       unsigned int height = loader.GetSlice(0).GetHeight();
       Orthanc::PixelFormat format = loader.GetSlice(0).GetConverter().GetExpectedPixelFormat();
-      LOG(INFO) << "Creating a volume image of size " << width << "x" << height 
+      LOG(INFO) << "Creating a volume image of size " << width << "x" << height
                 << "x" << loader.GetSliceCount() << " in " << Orthanc::EnumerationToString(format);
 
       image_.reset(new ImageBuffer3D(format, width, height, loader.GetSliceCount(), computeRange_));
       image_->SetAxialGeometry(loader.GetSlice(0).GetGeometry());
-      image_->SetVoxelDimensions(loader.GetSlice(0).GetPixelSpacingX(), 
+      image_->SetVoxelDimensions(loader.GetSlice(0).GetPixelSpacingX(),
                                  loader.GetSlice(0).GetPixelSpacingY(), spacingZ);
       image_->Clear();
-      
+
       downloadStack_.reset(new DownloadStack(loader.GetSliceCount()));
       pendingSlices_ = loader.GetSliceCount();
 
@@ -173,24 +173,18 @@
       SlicedVolumeBase::NotifyGeometryReady();
     }
 
-    virtual void NotifyGeometryError(const OrthancSlicesLoader& loader)
-    {
-      LOG(ERROR) << "Unable to download a volume image";
-      SlicedVolumeBase::NotifyGeometryError();
-    }
-
-    virtual void NotifySliceImageReady(const OrthancSlicesLoader& loader,
-                                       unsigned int sliceIndex,
-                                       const Slice& slice,
-                                       std::auto_ptr<Orthanc::ImageAccessor>& image,
-                                       SliceImageQuality quality)
+    virtual void OnSliceImageReady(const OrthancSlicesLoader& loader,
+                                   unsigned int sliceIndex,
+                                   const Slice& slice,
+                                   const boost::shared_ptr<Orthanc::ImageAccessor>& image,
+                                   SliceImageQuality quality)
     {
       {
         ImageBuffer3D::SliceWriter writer(*image_, VolumeProjection_Axial, sliceIndex);
         Orthanc::ImageProcessing::Copy(writer.GetAccessor(), *image);
       }
 
-      SlicedVolumeBase::NotifySliceChange(sliceIndex, slice);     
+      SlicedVolumeBase::NotifySliceChange(sliceIndex, slice);
 
       if (pendingSlices_ == 1)
       {
@@ -205,22 +199,47 @@
       ScheduleSliceDownload();
     }
 
-    virtual void NotifySliceImageError(const OrthancSlicesLoader& loader,
-                                       unsigned int sliceIndex,
-                                       const Slice& slice,
-                                       SliceImageQuality quality)
+    virtual void HandleMessage(const IObservable& from, const IMessage& message)
     {
-      LOG(ERROR) << "Cannot download slice " << sliceIndex << " in a volume image";
-      ScheduleSliceDownload();
+      switch (message.GetType())
+      {
+      case MessageType_SliceLoader_GeometryReady:
+        OnSliceGeometryReady(dynamic_cast<const OrthancSlicesLoader&>(from));
+      case MessageType_SliceLoader_GeometryError:
+      {
+        LOG(ERROR) << "Unable to download a volume image";
+        SlicedVolumeBase::NotifyGeometryError();
+      }; break;
+      case MessageType_SliceLoader_ImageReady:
+      {
+        const OrthancSlicesLoader::SliceImageReadyMessage& msg = dynamic_cast<const OrthancSlicesLoader::SliceImageReadyMessage&>(message);
+        OnSliceImageReady(dynamic_cast<const OrthancSlicesLoader&>(from),
+                          msg.sliceIndex_,
+                          msg.slice_,
+                          msg.image_,
+                          msg.effectiveQuality_);
+      }; break;
+      case MessageType_SliceLoader_ImageError:
+      {
+        const OrthancSlicesLoader::SliceImageErrorMessage& msg = dynamic_cast<const OrthancSlicesLoader::SliceImageErrorMessage&>(message);
+        LOG(ERROR) << "Cannot download slice " << msg.sliceIndex_ << " in a volume image";
+        ScheduleSliceDownload();
+      }; break;
+      default:
+        VLOG("unhandled message type" << message.GetType());
+      }
     }
 
   public:
-    OrthancVolumeImage(IWebService& orthanc,
-                       bool computeRange) : 
-      loader_(*this, orthanc),
+    OrthancVolumeImage(MessageBroker& broker,
+                       OrthancApiClient& orthanc,
+                       bool computeRange) :
+      OrthancStone::IObserver(broker),
+      loader_(broker, orthanc),
       computeRange_(computeRange),
       pendingSlices_(0)
     {
+      // TODO: replace with new callables loader_.RegisterObserver(*this);
     }
 
     void ScheduleLoadSeries(const std::string& seriesId)
@@ -352,7 +371,7 @@
                  axialThickness * axial.GetGeometry().GetNormal());
       
       reference_ = CoordinateSystem3D(origin,
-                                      axial.GetGeometry().GetAxisX(), 
+                                      axial.GetGeometry().GetAxisX(),
                                       -axial.GetGeometry().GetNormal());
     }
 
@@ -374,7 +393,7 @@
                  axialThickness * axial.GetGeometry().GetNormal());
       
       reference_ = CoordinateSystem3D(origin,
-                                      axial.GetGeometry().GetAxisY(), 
+                                      axial.GetGeometry().GetAxisY(),
                                       axial.GetGeometry().GetNormal());
     }
 
@@ -391,20 +410,20 @@
 
       switch (projection)
       {
-        case VolumeProjection_Axial:
-          SetupAxial(volume);
-          break;
+      case VolumeProjection_Axial:
+        SetupAxial(volume);
+        break;
 
-        case VolumeProjection_Coronal:
-          SetupCoronal(volume);
-          break;
+      case VolumeProjection_Coronal:
+        SetupCoronal(volume);
+        break;
 
-        case VolumeProjection_Sagittal:
-          SetupSagittal(volume);
-          break;
+      case VolumeProjection_Sagittal:
+        SetupSagittal(volume);
+        break;
 
-        default:
-          throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+      default:
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
       }
     }
 
@@ -468,8 +487,8 @@
 
 
   class VolumeImageSource :
-    public LayerSourceBase,
-    private ISlicedVolume::IObserver
+      public LayerSourceBase,
+      private ISlicedVolume::IObserver
   {
   private:
     OrthancVolumeImage&                 volume_;
@@ -493,12 +512,12 @@
       
       LayerSourceBase::NotifyGeometryReady();
     }
-      
+
     virtual void NotifyGeometryError(const ISlicedVolume& volume)
     {
       LayerSourceBase::NotifyGeometryError();
     }
-      
+
     virtual void NotifyContentChange(const ISlicedVolume& volume)
     {
       LayerSourceBase::NotifyContentChange();
@@ -527,17 +546,17 @@
 
       switch (projection)
       {
-        case VolumeProjection_Axial:
-          return *axialGeometry_;
+      case VolumeProjection_Axial:
+        return *axialGeometry_;
 
-        case VolumeProjection_Sagittal:
-          return *sagittalGeometry_;
+      case VolumeProjection_Sagittal:
+        return *sagittalGeometry_;
 
-        case VolumeProjection_Coronal:
-          return *coronalGeometry_;
+      case VolumeProjection_Coronal:
+        return *coronalGeometry_;
 
-        default:
-          throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+      default:
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
       }
     }
 
@@ -576,7 +595,8 @@
 
 
   public:
-    VolumeImageSource(OrthancVolumeImage&  volume) :
+    VolumeImageSource(MessageBroker& broker, OrthancVolumeImage&  volume) :
+      LayerSourceBase(broker),
       volume_(volume)
     {
       volume_.Register(*this);
@@ -593,7 +613,7 @@
         return false;
       }
       else
-      {       
+      {
         // As the slices of the volumic image are arranged in a box,
         // we only consider one single reference slice (the one with index 0).
         std::auto_ptr<Slice> slice(GetProjectionGeometry(projection).GetSlice(0));
@@ -630,9 +650,9 @@
 
           std::auto_ptr<Slice> slice(geometry.GetSlice(closest));
           LayerSourceBase::NotifyLayerReady(
-            FrameRenderer::CreateRenderer(frame.release(), *slice, isFullQuality),
-            //new SliceOutlineRenderer(slice),
-            slice->GetGeometry(), false);
+                FrameRenderer::CreateRenderer(frame.release(), *slice, isFullQuality),
+                //new SliceOutlineRenderer(slice),
+                slice->GetGeometry(), false);
           return;
         }
       }
@@ -645,8 +665,8 @@
 
 
   class VolumeImageInteractor :
-    public IWorldSceneInteractor,
-    protected ISlicedVolume::IObserver
+      public IWorldSceneInteractor,
+      protected ISlicedVolume::IObserver
   {
   private:
     LayerWidget&                        widget_;
@@ -664,14 +684,14 @@
         slices_.reset(new VolumeImageGeometry(image, projection_));
         SetSlice(slices_->GetSliceCount() / 2);
 
-        widget_.SetDefaultView();
+        widget_.FitContent();
       }
     }
-      
+
     virtual void NotifyGeometryError(const ISlicedVolume& volume)
     {
     }
-      
+
     virtual void NotifyContentChange(const ISlicedVolume& volume)
     {
     }
@@ -711,38 +731,39 @@
                             IStatusBar* statusBar)
     {
       int scale = (modifiers & KeyboardModifiers_Control ? 10 : 1);
-          
+
       switch (direction)
       {
-        case MouseWheelDirection_Up:
-          OffsetSlice(-scale);
-          break;
+      case MouseWheelDirection_Up:
+        OffsetSlice(-scale);
+        break;
 
-        case MouseWheelDirection_Down:
-          OffsetSlice(scale);
-          break;
+      case MouseWheelDirection_Down:
+        OffsetSlice(scale);
+        break;
 
-        default:
-          break;
+      default:
+        break;
       }
     }
 
     virtual void KeyPressed(WorldSceneWidget& widget,
-                            char key,
+                            KeyboardKeys key,
+                            char keyChar,
                             KeyboardModifiers modifiers,
                             IStatusBar* statusBar)
     {
-      switch (key)
+      switch (keyChar)
       {
-        case 's':
-          widget.SetDefaultView();
-          break;
+      case 's':
+        widget.FitContent();
+        break;
 
-        default:
-          break;
+      default:
+        break;
       }
     }
-      
+
   public:
     VolumeImageInteractor(OrthancVolumeImage& volume,
                           LayerWidget& widget,
@@ -787,13 +808,13 @@
           slice = slices_->GetSliceCount() - 1;
         }
 
-        if (slice != static_cast<int>(slice_)) 
+        if (slice != static_cast<int>(slice_))
         {
           SetSlice(slice);
-        }   
+        }
       }
     }
-      
+
     void SetSlice(size_t slice)
     {
       if (slices_.get() != NULL)
@@ -814,7 +835,8 @@
     LayerWidget&  otherPlane_;
 
   public:
-    SliceLocationSource(LayerWidget&  otherPlane) :
+    SliceLocationSource(MessageBroker& broker, LayerWidget&  otherPlane) :
+      LayerSourceBase(broker),
       otherPlane_(otherPlane)
     {
       NotifyGeometryReady();
@@ -835,7 +857,7 @@
       const CoordinateSystem3D& slice = otherPlane_.GetSlice();
 
       // Compute the line of intersection between the two slices
-      if (!GeometryToolbox::IntersectTwoPlanes(p, d, 
+      if (!GeometryToolbox::IntersectTwoPlanes(p, d,
                                                slice.GetOrigin(), slice.GetNormal(),
                                                viewportSlice.GetOrigin(), viewportSlice.GetNormal()))
       {
@@ -850,7 +872,7 @@
 
         const Extent2D extent = otherPlane_.GetSceneExtent();
         
-        if (GeometryToolbox::ClipLineToRectangle(x1, y1, x2, y2, 
+        if (GeometryToolbox::ClipLineToRectangle(x1, y1, x2, y2,
                                                  x1, y1, x2, y2,
                                                  extent.GetX1(), extent.GetY1(),
                                                  extent.GetX2(), extent.GetY2()))
@@ -863,6 +885,6 @@
           NotifyLayerReady(NULL, reference.GetGeometry(), false);
         }
       }
-    }      
+    }
   };
 }
--- a/Platforms/Generic/CMakeLists.txt	Mon Nov 05 10:04:56 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,98 +0,0 @@
-cmake_minimum_required(VERSION 2.8)
-project(OrthancStone)
-
-
-#####################################################################
-## Configuration for Orthanc
-#####################################################################
-
-include(../../Resources/CMake/Version.cmake)
-
-if (ORTHANC_STONE_VERSION STREQUAL "mainline")
-  set(ORTHANC_FRAMEWORK_VERSION "mainline")
-  set(ORTHANC_FRAMEWORK_DEFAULT_SOURCE "hg")
-else()
-  set(ORTHANC_FRAMEWORK_VERSION "1.3.2")
-  set(ORTHANC_FRAMEWORK_DEFAULT_SOURCE "web")
-endif()
-
-set(ORTHANC_FRAMEWORK_SOURCE "${ORTHANC_FRAMEWORK_DEFAULT_SOURCE}" CACHE STRING "Source of the Orthanc source code (can be \"hg\", \"archive\", \"web\" or \"path\")")
-set(ORTHANC_FRAMEWORK_ARCHIVE "" CACHE STRING "Path to the Orthanc archive, if ORTHANC_FRAMEWORK_SOURCE is \"archive\"")
-set(ORTHANC_FRAMEWORK_ROOT "" CACHE STRING "Path to the Orthanc source directory, if ORTHANC_FRAMEWORK_SOURCE is \"path\"")
-
-
-#####################################################################
-## Build a static library containing the Orthanc Stone framework
-#####################################################################
-
-include(../../Resources/CMake/OrthancStoneParameters.cmake)
-
-LIST(APPEND ORTHANC_BOOST_COMPONENTS program_options)
-
-SET(ORTHANC_SANDBOXED OFF)
-SET(ENABLE_CRYPTO_OPTIONS ON)
-SET(ENABLE_GOOGLE_TEST ON)
-SET(ENABLE_WEB_CLIENT ON)
-
-include(../../Resources/CMake/OrthancStoneConfiguration.cmake)
-
-add_library(OrthancStone STATIC
-  ${ORTHANC_STONE_SOURCES}
-  )
-
-
-#####################################################################
-## Build all the sample applications
-#####################################################################
-
-macro(BuildSample Target Sample)
-  add_executable(${Target}
-    ${ORTHANC_STONE_ROOT}/Applications/Samples/SampleMainSdl.cpp
-    ${APPLICATIONS_SOURCES}
-    )
-  set_target_properties(${Target} PROPERTIES COMPILE_DEFINITIONS ORTHANC_STONE_SAMPLE=${Sample})
-  target_link_libraries(${Target} OrthancStone)
-endmacro()
-
-
-# TODO - Re-enable all these samples!
-
-BuildSample(OrthancStoneEmpty 1)
-BuildSample(OrthancStoneTestPattern 2)
-BuildSample(OrthancStoneSingleFrame 3)
-BuildSample(OrthancStoneSingleVolume 4)
-#BuildSample(OrthancStoneBasicPetCtFusion 5)
-#BuildSample(OrthancStoneSynchronizedSeries 6)
-#BuildSample(OrthancStoneLayoutPetCtFusion 7)
-
-
-#####################################################################
-## Build the unit tests
-#####################################################################
-
-add_executable(UnitTests
-  ${GOOGLE_TEST_SOURCES}
-  ${ORTHANC_STONE_ROOT}/UnitTestsSources/UnitTestsMain.cpp
-  )
-
-target_link_libraries(UnitTests OrthancStone)
-
-
-#####################################################################
-## Generate the documentation if Doxygen is present
-#####################################################################
-
-find_package(Doxygen)
-if (DOXYGEN_FOUND)
-  configure_file(
-    ${ORTHANC_STONE_ROOT}/Resources/OrthancStone.doxygen
-    ${CMAKE_CURRENT_BINARY_DIR}/OrthancStone.doxygen
-    @ONLY)
-
-  add_custom_target(doc
-    ${DOXYGEN_EXECUTABLE} ${CMAKE_CURRENT_BINARY_DIR}/OrthancStone.doxygen
-    COMMENT "Generating documentation with Doxygen" VERBATIM
-    )
-else()
-  message("Doxygen not found. The documentation will not be built.")
-endif()
--- a/Platforms/Generic/Oracle.cpp	Mon Nov 05 10:04:56 2018 +0100
+++ b/Platforms/Generic/Oracle.cpp	Mon Nov 05 10:06:18 2018 +0100
@@ -27,6 +27,7 @@
 
 #include <vector>
 #include <stdio.h>
+#include <boost/thread/mutex.hpp>
 
 namespace OrthancStone
 {
@@ -40,7 +41,6 @@
       State_Stopped
     };
 
-    boost::mutex*                  globalMutex_;
     boost::mutex                   oracleMutex_;
     State                          state_;
     std::vector<boost::thread*>    threads_;
@@ -66,28 +66,26 @@
         if (item.get() != NULL)
         {
           IOracleCommand& command = dynamic_cast<IOracleCommand&>(*item);
-          command.Execute();
+          try
+          {
+            command.Execute();
+          }
+          catch (Orthanc::OrthancException& ex)
+          {
+            // this is probably a curl error that has been triggered.  We may just ignore it.
+            // The command.success_ will stay at false and this will be handled in the command.Commit
+          }
 
           // Random sleeping to test
           //boost::this_thread::sleep(boost::posix_time::milliseconds(50 * (1 + rand() % 10)));
 
-          if (that->globalMutex_ != NULL)
-          {
-            boost::mutex::scoped_lock lock(*that->globalMutex_);
-            command.Commit();
-          }
-          else
-          {
-            command.Commit();
-          }
+          command.Commit();
         }
       }
     }
     
   public:
-    PImpl(boost::mutex* globalMutex,
-          unsigned int threadCount) :
-      globalMutex_(globalMutex),
+    PImpl(unsigned int threadCount) :
       state_(State_Init),
       threads_(threadCount)
     {
@@ -182,19 +180,11 @@
   };
   
 
-  Oracle::Oracle(boost::mutex& globalMutex,
-                 unsigned int threadCount) :
-    pimpl_(new PImpl(&globalMutex, threadCount))
+  Oracle::Oracle(unsigned int threadCount) :
+    pimpl_(new PImpl(threadCount))
   {
   }
 
-
-  Oracle::Oracle(unsigned int threadCount) :
-    pimpl_(new PImpl(NULL, threadCount))
-  {
-  }
-
-
   void Oracle::Start()
   {
     pimpl_->Start();
--- a/Platforms/Generic/Oracle.h	Mon Nov 05 10:04:56 2018 +0100
+++ b/Platforms/Generic/Oracle.h	Mon Nov 05 10:06:18 2018 +0100
@@ -24,7 +24,6 @@
 #include "IOracleCommand.h"
 
 #include <boost/shared_ptr.hpp>
-#include <boost/thread/mutex.hpp>
 
 namespace OrthancStone
 {
@@ -36,9 +35,6 @@
     boost::shared_ptr<PImpl>  pimpl_;
 
   public:
-    Oracle(boost::mutex& globalMutex,
-           unsigned int threadCount);
-
     Oracle(unsigned int threadCount);
 
     void Start();
--- a/Platforms/Generic/OracleWebService.h	Mon Nov 05 10:04:56 2018 +0100
+++ b/Platforms/Generic/OracleWebService.h	Mon Nov 05 10:06:18 2018 +0100
@@ -13,7 +13,7 @@
  * 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/>.
  **/
@@ -25,36 +25,73 @@
 #include "Oracle.h"
 #include "WebServiceGetCommand.h"
 #include "WebServicePostCommand.h"
+#include "WebServiceDeleteCommand.h"
+#include "../../Applications/Generic/NativeStoneApplicationContext.h"
 
 namespace OrthancStone
 {
+  // The OracleWebService performs HTTP requests in a native environment.
+  // It uses a thread pool to handle multiple HTTP requests in a same time.
+  // It works asynchronously to mimick the behaviour of the WebService running in a WASM environment.
   class OracleWebService : public IWebService
   {
   private:
     Oracle&                        oracle_;
+    NativeStoneApplicationContext& context_;
     Orthanc::WebServiceParameters  parameters_;
 
   public:
-    OracleWebService(Oracle& oracle,
-                     const Orthanc::WebServiceParameters& parameters) : 
+    OracleWebService(MessageBroker& broker,
+                     Oracle& oracle,
+                     const Orthanc::WebServiceParameters& parameters,
+                     NativeStoneApplicationContext& context) :
+      IWebService(broker),
       oracle_(oracle),
+      context_(context),
       parameters_(parameters)
     {
     }
 
-    virtual void ScheduleGetRequest(ICallback& callback,
-                                    const std::string& uri,
-                                    Orthanc::IDynamicObject* payload)
+    virtual void GetAsync(const std::string& uri,
+                          const Headers& headers,
+                          Orthanc::IDynamicObject* payload, // takes ownership
+                          MessageHandler<IWebService::HttpRequestSuccessMessage>* successCallback,   // takes ownership
+                          MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback = NULL,// takes ownership
+                          unsigned int timeoutInSeconds = 60)
     {
-      oracle_.Submit(new WebServiceGetCommand(callback, parameters_, uri, payload));
+      oracle_.Submit(new WebServiceGetCommand(broker_, successCallback, failureCallback, parameters_, uri, headers, timeoutInSeconds, payload, context_));
     }
 
-    virtual void SchedulePostRequest(ICallback& callback,
-                                     const std::string& uri,
-                                     const std::string& body,
-                                     Orthanc::IDynamicObject* payload)
+    virtual void PostAsync(const std::string& uri,
+                           const Headers& headers,
+                           const std::string& body,
+                           Orthanc::IDynamicObject* payload, // takes ownership
+                           MessageHandler<IWebService::HttpRequestSuccessMessage>* successCallback, // takes ownership
+                           MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback = NULL, // takes ownership
+                           unsigned int timeoutInSeconds = 60)
     {
-      oracle_.Submit(new WebServicePostCommand(callback, parameters_, uri, body, payload));
+      oracle_.Submit(new WebServicePostCommand(broker_, successCallback, failureCallback, parameters_, uri, headers, timeoutInSeconds, body, payload, context_));
+    }
+
+    virtual void DeleteAsync(const std::string& uri,
+                             const Headers& headers,
+                             Orthanc::IDynamicObject* payload,
+                             MessageHandler<IWebService::HttpRequestSuccessMessage>* successCallback,
+                             MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback = NULL,
+                             unsigned int timeoutInSeconds = 60)
+    {
+      oracle_.Submit(new WebServiceDeleteCommand(broker_, successCallback, failureCallback, parameters_, uri, headers, timeoutInSeconds, payload, context_));
+    }
+
+
+    void Start()
+    {
+      oracle_.Start();
+    }
+
+    void Stop()
+    {
+      oracle_.Stop();
     }
   };
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Platforms/Generic/WebServiceCommandBase.cpp	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,65 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 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 "WebServiceCommandBase.h"
+
+#include <Core/HttpClient.h>
+
+namespace OrthancStone
+{
+  WebServiceCommandBase::WebServiceCommandBase(MessageBroker& broker,
+                                               MessageHandler<IWebService::HttpRequestSuccessMessage>* successCallback,
+                                               MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback,
+                                               const Orthanc::WebServiceParameters& parameters,
+                                               const std::string& uri,
+                                               const IWebService::Headers& headers,
+                                               unsigned int timeoutInSeconds,
+                                               Orthanc::IDynamicObject* payload /* takes ownership */,
+                                               NativeStoneApplicationContext& context) :
+    IObservable(broker),
+    successCallback_(successCallback),
+    failureCallback_(failureCallback),
+    parameters_(parameters),
+    uri_(uri),
+    headers_(headers),
+    payload_(payload),
+    context_(context),
+    timeoutInSeconds_(timeoutInSeconds)
+  {
+  }
+
+
+  void WebServiceCommandBase::Commit()
+  {
+    NativeStoneApplicationContext::GlobalMutexLocker lock(context_);  // we want to make sure that, i.e, the UpdateThread is not triggered while we are updating the "model" with the result of a WebServiceCommand
+
+    if (success_ && successCallback_.get() != NULL)
+    {
+      successCallback_->Apply(IWebService::HttpRequestSuccessMessage(uri_, answer_.c_str(), answer_.size(), payload_.release()));
+    }
+    else if (!success_ && failureCallback_.get() != NULL)
+    {
+      failureCallback_->Apply(IWebService::HttpRequestErrorMessage(uri_, payload_.release()));
+    }
+
+  }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Platforms/Generic/WebServiceCommandBase.h	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,68 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 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
+
+#include "IOracleCommand.h"
+
+#include "../../Framework/Toolbox/IWebService.h"
+#include "../../Framework/Messages/IObservable.h"
+#include "../../Framework/Messages/ICallable.h"
+#include "../../Applications/Generic/NativeStoneApplicationContext.h"
+
+#include <Core/WebServiceParameters.h>
+
+#include <memory>
+
+namespace OrthancStone
+{
+  class WebServiceCommandBase : public IOracleCommand, IObservable
+  {
+  protected:
+    std::auto_ptr<MessageHandler<IWebService::HttpRequestSuccessMessage> >  successCallback_;
+    std::auto_ptr<MessageHandler<IWebService::HttpRequestErrorMessage> >    failureCallback_;
+    Orthanc::WebServiceParameters           parameters_;
+    std::string                             uri_;
+    std::map<std::string, std::string>      headers_;
+    std::auto_ptr<Orthanc::IDynamicObject>  payload_;
+    bool                                    success_;
+    std::string                             answer_;
+    NativeStoneApplicationContext&          context_;
+    unsigned int                            timeoutInSeconds_;
+
+  public:
+    WebServiceCommandBase(MessageBroker& broker,
+                          MessageHandler<IWebService::HttpRequestSuccessMessage>* successCallback,  // takes ownership
+                          MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback,  // takes ownership
+                          const Orthanc::WebServiceParameters& parameters,
+                          const std::string& uri,
+                          const std::map<std::string, std::string>& headers,
+                          unsigned int timeoutInSeconds,
+                          Orthanc::IDynamicObject* payload /* takes ownership */,
+                          NativeStoneApplicationContext& context
+                          );
+
+    virtual void Execute() = 0;
+
+    virtual void Commit();
+  };
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Platforms/Generic/WebServiceDeleteCommand.cpp	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,55 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 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 "WebServiceDeleteCommand.h"
+
+#include <Core/HttpClient.h>
+
+namespace OrthancStone
+{
+  WebServiceDeleteCommand::WebServiceDeleteCommand(MessageBroker& broker,
+                                                   MessageHandler<IWebService::HttpRequestSuccessMessage>* successCallback,  // takes ownership
+                                                   MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback,  // takes ownership
+                                                   const Orthanc::WebServiceParameters& parameters,
+                                                   const std::string& uri,
+                                                   const IWebService::Headers& headers,
+                                                   unsigned int timeoutInSeconds,
+                                                   Orthanc::IDynamicObject* payload /* takes ownership */,
+                                                   NativeStoneApplicationContext& context) :
+    WebServiceCommandBase(broker, successCallback, failureCallback, parameters, uri, headers, timeoutInSeconds, payload, context)
+  {
+  }
+
+  void WebServiceDeleteCommand::Execute()
+  {
+    Orthanc::HttpClient client(parameters_, uri_);
+    client.SetTimeout(timeoutInSeconds_);
+    client.SetMethod(Orthanc::HttpMethod_Delete);
+
+    for (IWebService::Headers::const_iterator it = headers_.begin(); it != headers_.end(); it++ )
+    {
+      client.AddHeader(it->first, it->second);
+    }
+
+    success_ = client.Apply(answer_);
+  }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Platforms/Generic/WebServiceDeleteCommand.h	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,43 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 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
+
+#include "WebServiceCommandBase.h"
+
+namespace OrthancStone
+{
+  class WebServiceDeleteCommand : public WebServiceCommandBase
+  {
+  public:
+    WebServiceDeleteCommand(MessageBroker& broker,
+                            MessageHandler<IWebService::HttpRequestSuccessMessage>* successCallback,  // takes ownership
+                            MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback,  // takes ownership
+                            const Orthanc::WebServiceParameters& parameters,
+                            const std::string& uri,
+                            const IWebService::Headers& headers,
+                            unsigned int timeoutInSeconds,
+                            Orthanc::IDynamicObject* payload /* takes ownership */,
+                            NativeStoneApplicationContext& context);
+
+    virtual void Execute();
+  };
+}
--- a/Platforms/Generic/WebServiceGetCommand.cpp	Mon Nov 05 10:04:56 2018 +0100
+++ b/Platforms/Generic/WebServiceGetCommand.cpp	Mon Nov 05 10:06:18 2018 +0100
@@ -13,7 +13,7 @@
  * 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/>.
  **/
@@ -25,14 +25,17 @@
 
 namespace OrthancStone
 {
-  WebServiceGetCommand::WebServiceGetCommand(IWebService::ICallback& callback,
+
+  WebServiceGetCommand::WebServiceGetCommand(MessageBroker& broker,
+                                             MessageHandler<IWebService::HttpRequestSuccessMessage>* successCallback,  // takes ownership
+                                             MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback,  // takes ownership
                                              const Orthanc::WebServiceParameters& parameters,
                                              const std::string& uri,
-                                             Orthanc::IDynamicObject* payload /* takes ownership */) :
-    callback_(callback),
-    parameters_(parameters),
-    uri_(uri),
-    payload_(payload)
+                                             const IWebService::Headers& headers,
+                                             unsigned int timeoutInSeconds,
+                                             Orthanc::IDynamicObject* payload /* takes ownership */,
+                                             NativeStoneApplicationContext& context) :
+    WebServiceCommandBase(broker, successCallback, failureCallback, parameters, uri, headers, timeoutInSeconds, payload, context)
   {
   }
 
@@ -40,21 +43,15 @@
   void WebServiceGetCommand::Execute()
   {
     Orthanc::HttpClient client(parameters_, uri_);
-    client.SetTimeout(60);
+    client.SetTimeout(timeoutInSeconds_);
     client.SetMethod(Orthanc::HttpMethod_Get);
+
+    for (IWebService::Headers::const_iterator it = headers_.begin(); it != headers_.end(); it++ )
+    {
+      client.AddHeader(it->first, it->second);
+    }
+
     success_ = client.Apply(answer_);
   }
 
-
-  void WebServiceGetCommand::Commit()
-  {
-    if (success_)
-    {
-      callback_.NotifySuccess(uri_, answer_.c_str(), answer_.size(), payload_.release());
-    }
-    else
-    {
-      callback_.NotifyError(uri_, payload_.release());
-    }
-  }
 }
--- a/Platforms/Generic/WebServiceGetCommand.h	Mon Nov 05 10:04:56 2018 +0100
+++ b/Platforms/Generic/WebServiceGetCommand.h	Mon Nov 05 10:06:18 2018 +0100
@@ -13,7 +13,7 @@
  * 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/>.
  **/
@@ -21,34 +21,24 @@
 
 #pragma once
 
-#include "IOracleCommand.h"
-
-#include "../../Framework/Toolbox/IWebService.h"
-
-#include <Core/WebServiceParameters.h>
-
-#include <memory>
+#include "WebServiceCommandBase.h"
 
 namespace OrthancStone
 {
-  class WebServiceGetCommand : public IOracleCommand
+  class WebServiceGetCommand : public WebServiceCommandBase
   {
-  private:
-    IWebService::ICallback&                 callback_;
-    Orthanc::WebServiceParameters           parameters_;
-    std::string                             uri_;
-    std::auto_ptr<Orthanc::IDynamicObject>  payload_;
-    bool                                    success_;
-    std::string                             answer_;
-
   public:
-    WebServiceGetCommand(IWebService::ICallback& callback,
+    WebServiceGetCommand(MessageBroker& broker,
+                         MessageHandler<IWebService::HttpRequestSuccessMessage>* successCallback,  // takes ownership
+                         MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback,  // takes ownership
                          const Orthanc::WebServiceParameters& parameters,
                          const std::string& uri,
-                         Orthanc::IDynamicObject* payload /* takes ownership */);
+                         const IWebService::Headers& headers,
+                         unsigned int timeoutInSeconds,
+                         Orthanc::IDynamicObject* payload /* takes ownership */,
+                         NativeStoneApplicationContext& context);
 
     virtual void Execute();
+  };
 
-    virtual void Commit();
-  };
 }
--- a/Platforms/Generic/WebServicePostCommand.cpp	Mon Nov 05 10:04:56 2018 +0100
+++ b/Platforms/Generic/WebServicePostCommand.cpp	Mon Nov 05 10:06:18 2018 +0100
@@ -25,37 +25,34 @@
 
 namespace OrthancStone
 {
-  WebServicePostCommand::WebServicePostCommand(IWebService::ICallback& callback,
+  WebServicePostCommand::WebServicePostCommand(MessageBroker& broker,
+                                               MessageHandler<IWebService::HttpRequestSuccessMessage>* successCallback,  // takes ownership
+                                               MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback,  // takes ownership
                                                const Orthanc::WebServiceParameters& parameters,
                                                const std::string& uri,
+                                               const IWebService::Headers& headers,
+                                               unsigned int timeoutInSeconds,
                                                const std::string& body,
-                                               Orthanc::IDynamicObject* payload /* takes ownership */) :
-    callback_(callback),
-    parameters_(parameters),
-    uri_(uri),
-    body_(body),
-    payload_(payload)
+                                               Orthanc::IDynamicObject* payload /* takes ownership */,
+                                               NativeStoneApplicationContext& context) :
+    WebServiceCommandBase(broker, successCallback, failureCallback, parameters, uri, headers, timeoutInSeconds, payload, context),
+    body_(body)
   {
   }
 
   void WebServicePostCommand::Execute()
   {
     Orthanc::HttpClient client(parameters_, uri_);
-    client.SetTimeout(60);
+    client.SetTimeout(timeoutInSeconds_);
     client.SetMethod(Orthanc::HttpMethod_Post);
     client.GetBody().swap(body_);
+
+    for (IWebService::Headers::const_iterator it = headers_.begin(); it != headers_.end(); it++ )
+    {
+      client.AddHeader(it->first, it->second);
+    }
+
     success_ = client.Apply(answer_);
   }
 
-  void WebServicePostCommand::Commit()
-  {
-    if (success_)
-    {
-      callback_.NotifySuccess(uri_, answer_.c_str(), answer_.size(), payload_.release());
-    }
-    else
-    {
-      callback_.NotifyError(uri_, payload_.release());
-    }
-  }
 }
--- a/Platforms/Generic/WebServicePostCommand.h	Mon Nov 05 10:04:56 2018 +0100
+++ b/Platforms/Generic/WebServicePostCommand.h	Mon Nov 05 10:06:18 2018 +0100
@@ -21,36 +21,27 @@
 
 #pragma once
 
-#include "IOracleCommand.h"
-
-#include "../../Framework/Toolbox/IWebService.h"
-
-#include <Core/WebServiceParameters.h>
-
-#include <memory>
+#include "WebServiceCommandBase.h"
 
 namespace OrthancStone
 {
-  class WebServicePostCommand : public IOracleCommand
+  class WebServicePostCommand : public WebServiceCommandBase
   {
-  private:
-    IWebService::ICallback&                 callback_;
-    Orthanc::WebServiceParameters           parameters_;
-    std::string                             uri_;
+  protected:
     std::string                             body_;
-    std::auto_ptr<Orthanc::IDynamicObject>  payload_;
-    bool                                    success_;
-    std::string                             answer_;
 
   public:
-    WebServicePostCommand(IWebService::ICallback& callback,
+    WebServicePostCommand(MessageBroker& broker,
+                          MessageHandler<IWebService::HttpRequestSuccessMessage>* successCallback,  // takes ownership
+                          MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback,  // takes ownership
                           const Orthanc::WebServiceParameters& parameters,
                           const std::string& uri,
+                          const IWebService::Headers& headers,
+                          unsigned int timeoutInSeconds,
                           const std::string& body,
-                          Orthanc::IDynamicObject* payload /* takes ownership */);
+                          Orthanc::IDynamicObject* payload /* takes ownership */,
+                          NativeStoneApplicationContext& context);
 
     virtual void Execute();
-
-    virtual void Commit();
   };
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Platforms/Wasm/Defaults.cpp	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,284 @@
+#include "Defaults.h"
+
+#include "WasmWebService.h"
+#include <Framework/dev.h>
+#include "Framework/Widgets/TestCairoWidget.h"
+#include <Framework/Viewport/WidgetViewport.h>
+#include <Framework/Widgets/LayerWidget.h>
+#include <algorithm>
+#include "Applications/Wasm/StartupParametersBuilder.h"
+#include "Platforms/Wasm/WasmPlatformApplicationAdapter.h"
+
+static unsigned int width_ = 0;
+static unsigned int height_ = 0;
+
+/**********************************/
+
+static std::unique_ptr<OrthancStone::IStoneApplication> application;
+static std::unique_ptr<OrthancStone::WasmPlatformApplicationAdapter> applicationWasmAdapter = NULL;
+static std::unique_ptr<OrthancStone::StoneApplicationContext> context;
+static OrthancStone::StartupParametersBuilder startupParametersBuilder;
+static OrthancStone::MessageBroker broker;
+
+static OrthancStone::ViewportContentChangedObserver viewportContentChangedObserver_;
+static OrthancStone::StatusBar statusBar_;
+
+static std::list<std::shared_ptr<OrthancStone::WidgetViewport>> viewports_;
+
+std::shared_ptr<OrthancStone::WidgetViewport> FindViewportSharedPtr(ViewportHandle viewport) {
+  for (const auto& v : viewports_) {
+    if (v.get() == viewport) {
+      return v;
+    }
+  }
+  assert(false);
+  return std::shared_ptr<OrthancStone::WidgetViewport>();
+}
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+  using namespace OrthancStone;
+
+  // when WASM needs a C++ viewport
+  ViewportHandle EMSCRIPTEN_KEEPALIVE CreateCppViewport() {
+    
+    std::shared_ptr<OrthancStone::WidgetViewport> viewport(new OrthancStone::WidgetViewport);
+    printf("viewport %x\n", (int)viewport.get());
+
+    viewports_.push_back(viewport);
+
+    printf("There are now %d viewports in C++\n", viewports_.size());
+
+    viewport->SetStatusBar(statusBar_);
+    viewport->Register(viewportContentChangedObserver_);
+
+    return viewport.get();
+  }
+
+  // when WASM does not need a viewport anymore, it should release it 
+  void EMSCRIPTEN_KEEPALIVE ReleaseCppViewport(ViewportHandle viewport) {
+    viewports_.remove_if([viewport](const std::shared_ptr<OrthancStone::WidgetViewport>& v) { return v.get() == viewport;});
+
+    printf("There are now %d viewports in C++\n", viewports_.size());
+  }
+
+  void EMSCRIPTEN_KEEPALIVE CreateWasmApplication(ViewportHandle viewport) {
+
+    printf("CreateWasmApplication\n");
+
+    application.reset(CreateUserApplication(broker));
+    applicationWasmAdapter.reset(CreateWasmApplicationAdapter(broker, application.get())); 
+    WasmWebService::SetBroker(broker);
+
+    startupParametersBuilder.Clear();
+  }
+
+  void EMSCRIPTEN_KEEPALIVE SetStartupParameter(const char* keyc,
+                                                  const char* value) {
+    startupParametersBuilder.SetStartupParameter(keyc, value);
+  }
+
+  void EMSCRIPTEN_KEEPALIVE StartWasmApplication() {
+
+    printf("StartWasmApplication\n");
+
+    // recreate a command line from uri arguments and parse it
+    boost::program_options::variables_map parameters;
+    boost::program_options::options_description options;
+    application->DeclareStartupOptions(options);
+    startupParametersBuilder.GetStartupParameters(parameters, options);
+
+    context.reset(new OrthancStone::StoneApplicationContext());
+    context->SetWebService(OrthancStone::WasmWebService::GetInstance());
+    application->Initialize(context.get(), statusBar_, parameters);
+    application->InitializeWasm();
+
+//    viewport->SetSize(width_, height_);
+    printf("StartWasmApplication - completed\n");
+  }
+  
+  void EMSCRIPTEN_KEEPALIVE NotifyUpdateContent()
+  {
+    for (auto viewport : viewports_) {
+      // TODO Only launch the JavaScript timer if "HasUpdateContent()"
+      if (viewport->HasUpdateContent())
+      {
+        viewport->UpdateContent();
+      }
+
+    }
+
+  }
+  
+
+  void EMSCRIPTEN_KEEPALIVE ViewportSetSize(ViewportHandle viewport, unsigned int width, unsigned int height)
+  {
+    width_ = width;
+    height_ = height;
+    
+    viewport->SetSize(width, height);
+  }
+
+  int EMSCRIPTEN_KEEPALIVE ViewportRender(ViewportHandle viewport,
+                                          unsigned int width,
+                                          unsigned int height,
+                                          uint8_t* data)
+  {
+    viewportContentChangedObserver_.Reset();
+
+    //printf("ViewportRender called %dx%d\n", width, height);
+    if (width == 0 ||
+        height == 0)
+    {
+      return 1;
+    }
+
+    Orthanc::ImageAccessor surface;
+    surface.AssignWritable(Orthanc::PixelFormat_BGRA32, width, height, 4 * width, data);
+
+    viewport->Render(surface);
+
+    // Convert from BGRA32 memory layout (only color mode supported by
+    // Cairo, which corresponds to CAIRO_FORMAT_ARGB32) to RGBA32 (as
+    // expected by HTML5 canvas). This simply amounts to swapping the
+    // B and R channels.
+    uint8_t* p = data;
+    for (unsigned int y = 0; y < height; y++) {
+      for (unsigned int x = 0; x < width; x++) {
+        uint8_t tmp = p[0];
+        p[0] = p[2];
+        p[2] = tmp;
+        
+        p += 4;
+      }
+    }
+
+    return 1;
+  }
+
+
+  void EMSCRIPTEN_KEEPALIVE ViewportMouseDown(ViewportHandle viewport,
+                                              unsigned int rawButton,
+                                              int x,
+                                              int y,
+                                              unsigned int rawModifiers)
+  {
+    OrthancStone::MouseButton button;
+    switch (rawButton)
+    {
+      case 0:
+        button = OrthancStone::MouseButton_Left;
+        break;
+
+      case 1:
+        button = OrthancStone::MouseButton_Middle;
+        break;
+
+      case 2:
+        button = OrthancStone::MouseButton_Right;
+        break;
+
+      default:
+        return;  // Unknown button
+    }
+
+    viewport->MouseDown(button, x, y, OrthancStone::KeyboardModifiers_None /* TODO */);
+  }
+  
+
+  void EMSCRIPTEN_KEEPALIVE ViewportMouseWheel(ViewportHandle viewport,
+                                               int deltaY,
+                                               int x,
+                                               int y,
+                                               int isControl)
+  {
+    if (deltaY != 0)
+    {
+      OrthancStone::MouseWheelDirection direction = (deltaY < 0 ?
+                                                     OrthancStone::MouseWheelDirection_Up :
+                                                     OrthancStone::MouseWheelDirection_Down);
+      OrthancStone::KeyboardModifiers modifiers = OrthancStone::KeyboardModifiers_None;
+
+      if (isControl != 0)
+      {
+        modifiers = OrthancStone::KeyboardModifiers_Control;
+      }
+
+      viewport->MouseWheel(direction, x, y, modifiers);
+    }
+  }
+  
+
+  void EMSCRIPTEN_KEEPALIVE ViewportMouseMove(ViewportHandle viewport,
+                                              int x,
+                                              int y)
+  {
+    viewport->MouseMove(x, y);
+  }
+  
+  void EMSCRIPTEN_KEEPALIVE ViewportKeyPressed(ViewportHandle viewport,
+                                               int key,
+                                               const char* keyChar, 
+                                               bool isShiftPressed, 
+                                               bool isControlPressed,
+                                               bool isAltPressed)
+                                               
+  {
+    OrthancStone::KeyboardModifiers modifiers = OrthancStone::KeyboardModifiers_None;
+    if (isShiftPressed) {
+      modifiers = static_cast<OrthancStone::KeyboardModifiers>(modifiers + OrthancStone::KeyboardModifiers_Shift);
+    }
+    if (isControlPressed) {
+      modifiers = static_cast<OrthancStone::KeyboardModifiers>(modifiers + OrthancStone::KeyboardModifiers_Control);
+    }
+    if (isAltPressed) {
+      modifiers = static_cast<OrthancStone::KeyboardModifiers>(modifiers + OrthancStone::KeyboardModifiers_Alt);
+    }
+
+    char c = 0;
+    if (keyChar != NULL && key == OrthancStone::KeyboardKeys_Generic) {
+      c = keyChar[0];
+    }
+    viewport->KeyPressed(static_cast<OrthancStone::KeyboardKeys>(key), c, modifiers);
+  }
+  
+
+  void EMSCRIPTEN_KEEPALIVE ViewportMouseUp(ViewportHandle viewport)
+  {
+    viewport->MouseUp();
+  }
+  
+
+  void EMSCRIPTEN_KEEPALIVE ViewportMouseEnter(ViewportHandle viewport)
+  {
+    viewport->MouseEnter();
+  }
+  
+
+  void EMSCRIPTEN_KEEPALIVE ViewportMouseLeave(ViewportHandle viewport)
+  {
+    viewport->MouseLeave();
+  }
+
+  const char* EMSCRIPTEN_KEEPALIVE SendMessageToStoneApplication(const char* message) 
+  {
+    static std::string output; // we don't want the string to be deallocated when we return to JS code so we always use the same string (this is fine since JS is single-thread)
+
+    printf("SendMessageToStoneApplication\n");
+    printf("%s", message);
+
+    if (applicationWasmAdapter.get() != NULL) {
+      printf("sending message to C++\n");
+      applicationWasmAdapter->HandleMessageFromWeb(output, std::string(message));
+      return output.c_str();
+    }
+    printf("This stone application does not have a Web Adapter");
+    return NULL;
+  }
+
+
+#ifdef __cplusplus
+}
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Platforms/Wasm/Defaults.h	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,79 @@
+#pragma once
+
+#include <emscripten/emscripten.h>
+
+#include <Framework/dev.h>
+#include <Framework/Viewport/WidgetViewport.h>
+#include <Framework/Widgets/LayerWidget.h>
+#include <Framework/Widgets/LayoutWidget.h>
+#include <Applications/IStoneApplication.h>
+#include <Platforms/Wasm/WasmPlatformApplicationAdapter.h>
+
+typedef OrthancStone::WidgetViewport* ViewportHandle; // the objects exchanged between JS and C++
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+  
+  // JS methods accessible from C++
+  extern void ScheduleWebViewportRedrawFromCpp(ViewportHandle cppViewportHandle);
+  extern void UpdateStoneApplicationStatusFromCpp(const char* statusUpdateMessage);
+  
+  // C++ methods accessible from JS
+  extern void EMSCRIPTEN_KEEPALIVE CreateWasmApplication(ViewportHandle cppViewportHandle);
+  extern void EMSCRIPTEN_KEEPALIVE SetStartupParameter(const char* keyc, const char* value);
+  
+
+#ifdef __cplusplus
+}
+#endif
+
+// these methods must be implemented in the custom app "mainWasm.cpp"
+extern OrthancStone::IStoneApplication* CreateUserApplication(OrthancStone::MessageBroker& broker);
+extern OrthancStone::WasmPlatformApplicationAdapter* CreateWasmApplicationAdapter(OrthancStone::MessageBroker& broker, OrthancStone::IStoneApplication* application);
+
+namespace OrthancStone {
+
+  // default Observer to trigger Viewport redraw when something changes in the Viewport
+  class ViewportContentChangedObserver :
+    public OrthancStone::IViewport::IObserver
+  {
+  private:
+    // Flag to avoid flooding JavaScript with redundant Redraw requests
+    bool isScheduled_; 
+
+  public:
+    ViewportContentChangedObserver() :
+      isScheduled_(false)
+    {
+    }
+
+    void Reset()
+    {
+      isScheduled_ = false;
+    }
+
+    virtual void OnViewportContentChanged(const OrthancStone::IViewport &viewport)
+    {
+      if (!isScheduled_)
+      {
+        ScheduleWebViewportRedrawFromCpp((ViewportHandle)&viewport);  // loosing constness when transmitted to Web
+        isScheduled_ = true;
+      }
+    }
+  };
+
+  // default status bar to log messages on the console/stdout
+  class StatusBar : public OrthancStone::IStatusBar
+  {
+  public:
+    virtual void ClearMessage()
+    {
+    }
+
+    virtual void SetMessage(const std::string& message)
+    {
+      printf("%s\n", message.c_str());
+    }
+  };
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Platforms/Wasm/WasmPlatformApplicationAdapter.cpp	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,51 @@
+#include "WasmPlatformApplicationAdapter.h"
+
+#include "Framework/Toolbox/MessagingToolbox.h"
+#include "Framework/StoneException.h"
+#include <Applications/Commands/BaseCommandBuilder.h>
+#include <stdio.h>
+#include "Platforms/Wasm/Defaults.h"
+
+namespace OrthancStone
+{
+    WasmPlatformApplicationAdapter::WasmPlatformApplicationAdapter(MessageBroker& broker, IStoneApplication& application)
+    : IObserver(broker),
+      application_(application)
+    {
+    }
+
+    void WasmPlatformApplicationAdapter::HandleMessageFromWeb(std::string& output, const std::string& input)
+    {
+      try
+      {
+        Json::Value inputJson;
+        // if the message is a command, build it and execute it
+        if (MessagingToolbox::ParseJson(inputJson, input.c_str(), input.size()))
+        {
+            std::unique_ptr<ICommand> command(application_.GetCommandBuilder().CreateFromJson(inputJson));
+            if (command.get() == NULL) 
+              printf("Could not parse command: '%s'\n", input.c_str());
+            else
+              application_.ExecuteCommand(*command);
+        }
+      }
+      catch (StoneException& exc)
+      {
+        printf("Error while handling message from web (error code = %d):\n", exc.GetErrorCode());
+        printf("While interpreting input: '%s'\n", input.c_str());
+      }
+    }
+
+    void WasmPlatformApplicationAdapter::NotifyStatusUpdateFromCppToWeb(const std::string& statusUpdateMessage)
+    {
+      try
+      {
+        UpdateStoneApplicationStatusFromCpp(statusUpdateMessage.c_str());
+      }
+      catch (...)
+      {
+        printf("Error while handling message to web\n");
+      }
+    }
+
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Platforms/Wasm/WasmPlatformApplicationAdapter.h	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,18 @@
+#pragma once
+
+#include <string>
+#include <Framework/Messages/IObserver.h>
+#include <Applications/IStoneApplication.h>
+
+namespace OrthancStone
+{
+  class WasmPlatformApplicationAdapter : public IObserver
+  {
+      IStoneApplication&  application_;
+    public:
+      WasmPlatformApplicationAdapter(MessageBroker& broker, IStoneApplication& application);
+
+      virtual void HandleMessageFromWeb(std::string& output, const std::string& input);
+      virtual void NotifyStatusUpdateFromCppToWeb(const std::string& statusUpdateMessage);
+  };
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Platforms/Wasm/WasmViewport.cpp	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,13 @@
+#include "WasmViewport.h"
+
+#include <vector>
+#include <memory>
+
+std::vector<std::shared_ptr<OrthancStone::WidgetViewport>> wasmViewports;
+
+void AttachWidgetToWasmViewport(const char* htmlCanvasId, OrthancStone::IWidget* centralWidget) {
+    std::shared_ptr<OrthancStone::WidgetViewport> viewport(CreateWasmViewportFromCpp(htmlCanvasId));
+    viewport->SetCentralWidget(centralWidget);
+
+    wasmViewports.push_back(viewport);
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Platforms/Wasm/WasmViewport.h	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,18 @@
+#pragma once
+
+#include <Framework/Viewport/WidgetViewport.h>
+
+#include <emscripten/emscripten.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+  // JS methods accessible from C++
+  extern OrthancStone::WidgetViewport* CreateWasmViewportFromCpp(const char* htmlCanvasId);
+
+#ifdef __cplusplus
+}
+#endif
+
+extern void AttachWidgetToWasmViewport(const char* htmlCanvasId, OrthancStone::IWidget* centralWidget);
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Platforms/Wasm/WasmWebService.cpp	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,152 @@
+#include "WasmWebService.h"
+#include "json/value.h"
+#include "json/writer.h"
+#include <emscripten/emscripten.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+  extern void WasmWebService_GetAsync(void* callableSuccess,
+                                      void* callableFailure,
+                                      const char* uri,
+                                      const char* headersInJsonString,
+                                      void* payload,
+                                      unsigned int timeoutInSeconds);
+
+  extern void WasmWebService_PostAsync(void* callableSuccess,
+                                       void* callableFailure,
+                                       const char* uri,
+                                       const char* headersInJsonString,
+                                       const void* body,
+                                       size_t bodySize,
+                                       void* payload,
+                                       unsigned int timeoutInSeconds);
+
+  extern void WasmWebService_DeleteAsync(void* callableSuccess,
+                                         void* callableFailure,
+                                         const char* uri,
+                                         const char* headersInJsonString,
+                                         void* payload,
+                                         unsigned int timeoutInSeconds);
+
+  void EMSCRIPTEN_KEEPALIVE WasmWebService_NotifyError(void* failureCallable,
+                                                       const char* uri,
+                                                       void* payload)
+  {
+    if (failureCallable == NULL)
+    {
+      throw;
+    }
+    else
+    {
+      reinterpret_cast<OrthancStone::MessageHandler<OrthancStone::IWebService::HttpRequestErrorMessage>*>(failureCallable)->
+        Apply(OrthancStone::IWebService::HttpRequestErrorMessage(uri, reinterpret_cast<Orthanc::IDynamicObject*>(payload)));
+    }
+  }
+
+  void EMSCRIPTEN_KEEPALIVE WasmWebService_NotifySuccess(void* successCallable,
+                                                         const char* uri,
+                                                         const void* body,
+                                                         size_t bodySize,
+                                                         void* payload)
+  {
+    if (successCallable == NULL)
+    {
+      throw;
+    }
+    else
+    {
+      reinterpret_cast<OrthancStone::MessageHandler<OrthancStone::IWebService::HttpRequestSuccessMessage>*>(successCallable)->
+        Apply(OrthancStone::IWebService::HttpRequestSuccessMessage(uri, body, bodySize, reinterpret_cast<Orthanc::IDynamicObject*>(payload)));
+   }
+  }
+
+  void EMSCRIPTEN_KEEPALIVE WasmWebService_SetBaseUri(const char* baseUri)
+  {
+    OrthancStone::WasmWebService::GetInstance().SetBaseUri(baseUri);
+  }
+
+#ifdef __cplusplus
+}
+#endif
+
+
+
+namespace OrthancStone
+{
+  MessageBroker* WasmWebService::broker_ = NULL;
+
+  void WasmWebService::SetBaseUri(const std::string baseUri)
+  {
+    // Make sure the base url ends with "/"
+    if (baseUri.empty() ||
+        baseUri[baseUri.size() - 1] != '/')
+    {
+      baseUri_ = baseUri + "/";
+    }
+    else
+    {
+      baseUri_ = baseUri;
+    }
+  }
+
+  void ToJsonString(std::string& output, const IWebService::Headers& headers)
+  {
+    Json::Value jsonHeaders;
+    for (IWebService::Headers::const_iterator it = headers.begin(); it != headers.end(); it++ )
+    {
+      jsonHeaders[it->first] = it->second;
+    }
+
+    Json::StreamWriterBuilder builder;
+    std::unique_ptr<Json::StreamWriter> writer(builder.newStreamWriter());
+    std::ostringstream outputStr;
+
+    writer->write(jsonHeaders, &outputStr);
+    output = outputStr.str();
+  }
+
+  void WasmWebService::PostAsync(const std::string& relativeUri,
+                                 const Headers& headers,
+                                 const std::string& body,
+                                 Orthanc::IDynamicObject* payload,
+                                 MessageHandler<IWebService::HttpRequestSuccessMessage>* successCallable,
+                                 MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallable,
+                                 unsigned int timeoutInSeconds)
+  {
+    std::string uri = baseUri_ + relativeUri;
+    std::string headersInJsonString;
+    ToJsonString(headersInJsonString, headers);
+    WasmWebService_PostAsync(successCallable, failureCallable, uri.c_str(), headersInJsonString.c_str(),
+                                       body.c_str(), body.size(), payload, timeoutInSeconds);
+  }
+
+  void WasmWebService::DeleteAsync(const std::string& relativeUri,
+                                   const Headers& headers,
+                                   Orthanc::IDynamicObject* payload,
+                                   MessageHandler<IWebService::HttpRequestSuccessMessage>* successCallable,
+                                   MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallable,
+                                   unsigned int timeoutInSeconds)
+  {
+    std::string uri = baseUri_ + relativeUri;
+    std::string headersInJsonString;
+    ToJsonString(headersInJsonString, headers);
+    WasmWebService_DeleteAsync(successCallable, failureCallable, uri.c_str(), headersInJsonString.c_str(),
+                               payload, timeoutInSeconds);
+  }
+
+  void WasmWebService::GetAsync(const std::string& relativeUri,
+                                 const Headers& headers,
+                                 Orthanc::IDynamicObject* payload,
+                                 MessageHandler<IWebService::HttpRequestSuccessMessage>* successCallable,
+                                 MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallable,
+                                 unsigned int timeoutInSeconds)
+  {
+    std::string uri = baseUri_ + relativeUri;
+    std::string headersInJsonString;
+    ToJsonString(headersInJsonString, headers);
+    WasmWebService_GetAsync(successCallable, failureCallable, uri.c_str(), headersInJsonString.c_str(), payload, timeoutInSeconds);
+  }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Platforms/Wasm/WasmWebService.h	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,70 @@
+#pragma once
+
+#include <Framework/Toolbox/IWebService.h>
+#include <Core/OrthancException.h>
+
+namespace OrthancStone
+{
+  class WasmWebService : public IWebService
+  {
+  private:
+    std::string  baseUri_;
+    static MessageBroker* broker_;
+
+    // Private constructor => Singleton design pattern
+    WasmWebService(MessageBroker& broker) :
+      IWebService(broker),
+      baseUri_("../../")   // note: this is configurable from the JS code by calling WasmWebService_SetBaseUri
+    {
+    }
+
+  public:
+    static WasmWebService& GetInstance()
+    {
+      if (broker_ == NULL)
+      {
+        printf("WasmWebService::GetInstance(): broker not initialized\n");
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+      }
+      static WasmWebService instance(*broker_);
+      return instance;
+    }
+
+    static void SetBroker(MessageBroker& broker)
+    {
+      broker_ = &broker;
+    }
+
+    void SetBaseUri(const std::string baseUri);
+
+    virtual void GetAsync(const std::string& uri,
+                          const Headers& headers,
+                          Orthanc::IDynamicObject* payload,
+                          MessageHandler<IWebService::HttpRequestSuccessMessage>* successCallable,
+                          MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallable = NULL,
+                          unsigned int timeoutInSeconds = 60);
+
+    virtual void PostAsync(const std::string& uri,
+                           const Headers& headers,
+                           const std::string& body,
+                           Orthanc::IDynamicObject* payload,
+                           MessageHandler<IWebService::HttpRequestSuccessMessage>* successCallable,
+                           MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallable = NULL,
+                           unsigned int timeoutInSeconds = 60);
+
+    virtual void DeleteAsync(const std::string& uri,
+                             const Headers& headers,
+                             Orthanc::IDynamicObject* payload,
+                             MessageHandler<IWebService::HttpRequestSuccessMessage>* successCallable,
+                             MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallable = NULL,
+                             unsigned int timeoutInSeconds = 60);
+
+    virtual void Start()
+    {
+    }
+    
+    virtual void Stop()
+    {
+    }
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Platforms/Wasm/WasmWebService.js	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,90 @@
+mergeInto(LibraryManager.library, {
+  WasmWebService_GetAsync: function(callableSuccess, callableFailure, url, headersInJsonString, payload, timeoutInSeconds) {
+    // Directly use XMLHttpRequest (no jQuery) to retrieve the raw binary data
+    // http://www.henryalgus.com/reading-binary-files-using-jquery-ajax/
+    var xhr = new XMLHttpRequest();
+    var url_ = UTF8ToString(url);
+    var headersInJsonString_ = UTF8ToString(headersInJsonString);
+
+    xhr.open('GET', url_, true);
+    xhr.responseType = 'arraybuffer';
+    xhr.timeout = timeoutInSeconds * 1000;
+    var headers = JSON.parse(headersInJsonString_);
+    for (var key in headers) {
+      xhr.setRequestHeader(key, headers[key]);
+    }
+    //console.log(xhr); 
+    xhr.onreadystatechange = function() {
+      if (this.readyState == XMLHttpRequest.DONE) {
+        if (xhr.status === 200) {
+          // TODO - Is "new Uint8Array()" necessary? This copies the
+          // answer to the WebAssembly stack, hence necessitating
+          // increasing the TOTAL_STACK parameter of Emscripten
+          WasmWebService_NotifySuccess(callableSuccess, url_, new Uint8Array(this.response),
+                                       this.response.byteLength, payload);
+        } else {
+          WasmWebService_NotifyError(callableFailure, url_, payload);
+        }
+      }
+    }
+    
+    xhr.send();
+  },
+
+  WasmWebService_PostAsync: function(callableSuccess, callableFailure, url, headersInJsonString, body, bodySize, payload, timeoutInSeconds) {
+    var xhr = new XMLHttpRequest();
+    var url_ = UTF8ToString(url);
+    var headersInJsonString_ = UTF8ToString(headersInJsonString);
+    xhr.open('POST', url_, true);
+    xhr.timeout = timeoutInSeconds * 1000;
+    xhr.responseType = 'arraybuffer';
+    xhr.setRequestHeader('Content-type', 'application/octet-stream');
+
+    var headers = JSON.parse(headersInJsonString_);
+    for (var key in headers) {
+      xhr.setRequestHeader(key, headers[key]);
+    }
+    
+    xhr.onreadystatechange = function() {
+      if (this.readyState == XMLHttpRequest.DONE) {
+        if (xhr.status === 200) {
+          WasmWebService_NotifySuccess(callableSuccess, url_, new Uint8Array(this.response),
+                                       this.response.byteLength, payload);
+        } else {
+          WasmWebService_NotifyError(callableFailure, url_, payload);
+        }
+      }
+    }
+
+    xhr.send(new Uint8ClampedArray(HEAPU8.buffer, body, bodySize));
+  },
+
+  WasmWebService_DeleteAsync: function(callableSuccess, callableFailure, url, headersInJsonString, payload, timeoutInSeconds) {
+    var xhr = new XMLHttpRequest();
+    var url_ = UTF8ToString(url);
+    var headersInJsonString_ = UTF8ToString(headersInJsonString);
+    xhr.open('DELETE', url_, true);
+    xhr.timeout = timeoutInSeconds * 1000;
+    xhr.responseType = 'arraybuffer';
+    xhr.setRequestHeader('Content-type', 'application/octet-stream');
+  
+    var headers = JSON.parse(headersInJsonString_);
+    for (var key in headers) {
+      xhr.setRequestHeader(key, headers[key]);
+    }
+    
+    xhr.onreadystatechange = function() {
+      if (this.readyState == XMLHttpRequest.DONE) {
+        if (xhr.status === 200) {
+          WasmWebService_NotifySuccess(callableSuccess, url_, new Uint8Array(this.response),
+                                       this.response.byteLength, payload);
+        } else {
+          WasmWebService_NotifyError(callableFailure, url_, payload);
+        }
+      }
+    }
+  
+    xhr.send();
+  }
+  
+});
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Platforms/Wasm/default-library.js	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,16 @@
+// this file contains the JS method you want to expose to C++ code
+
+mergeInto(LibraryManager.library, {
+  ScheduleWebViewportRedrawFromCpp: function(cppViewportHandle) {
+    ScheduleWebViewportRedraw(cppViewportHandle);
+  },
+  CreateWasmViewportFromCpp: function(htmlCanvasId) {
+    return CreateWasmViewport(htmlCanvasId);
+  },
+  // each time the StoneApplication updates its status, it may signal it through this method. i.e, to change the status of a button in the web interface
+  UpdateStoneApplicationStatusFromCpp: function(statusUpdateMessage) {
+    var statusUpdateMessage_ = UTF8ToString(statusUpdateMessage);
+    UpdateWebApplication(statusUpdateMessage_);
+  }
+});
+  
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Platforms/Wasm/stone-framework-loader.ts	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,96 @@
+module Stone {
+    /**
+     * This file contains primitives to interface with WebAssembly and
+     * with the Stone framework.
+     **/
+    
+    export declare type InitializationCallback = () => void;
+    
+    export declare var StoneFrameworkModule : any;
+    
+    //const ASSETS_FOLDER : string = "assets/lib";
+    //const WASM_FILENAME : string = "orthanc-framework";
+    
+    
+    export class Framework
+    {
+      private static singleton_ : Framework = null;
+      private static wasmModuleName_ : string = null;
+
+      public static Configure(wasmModuleName: string) {
+        this.wasmModuleName_ = wasmModuleName;
+      }
+
+      private constructor(verbose : boolean) 
+      {
+        //this.ccall('Initialize', null, [ 'number' ], [ verbose ]);
+      }
+    
+      
+      public ccall(name: string,
+                   returnType: string,
+                   argTypes: Array<string>,
+                   argValues: Array<any>) : any
+      {
+        return StoneFrameworkModule.ccall(name, returnType, argTypes, argValues);
+      }
+    
+      
+      public cwrap(name: string,
+                   returnType: string,
+                   argTypes: Array<string>) : any
+      {
+        return StoneFrameworkModule.cwrap(name, returnType, argTypes);
+      }
+    
+      
+      public static GetInstance() : Framework
+      {
+        if (Framework.singleton_ == null) {
+          throw new Error('The WebAssembly module is not loaded yet');
+        } else {
+          return Framework.singleton_;
+        }
+      }
+      
+    
+      public static Initialize(verbose: boolean,
+                               callback: InitializationCallback)
+      {
+        console.log('Initializing WebAssembly Module');
+    
+        (<any> window).StoneFrameworkModule = {
+          preRun: [ 
+            function() {
+              console.log('Loading the Stone Framework using WebAssembly');
+            }
+          ],
+          postRun: [ 
+            function()  {
+              // This function is called by ".js" wrapper once the ".wasm"
+              // WebAssembly module has been loaded and compiled by the
+              // browser
+              console.log('WebAssembly is ready');
+              Framework.singleton_ = new Framework(verbose);
+              callback();
+            }
+          ],
+          print: function(text : string) {
+            console.log(text);
+          },
+          printErr: function(text : string) {
+            console.error(text);
+          },
+          totalDependencies: 0
+        };
+    
+        // Dynamic loading of the JavaScript wrapper around WebAssembly
+        var script = document.createElement('script');
+        script.type = 'application/javascript';
+        //script.src = "orthanc-stone.js"; // ASSETS_FOLDER + '/' + WASM_FILENAME + '.js';
+        script.src = this.wasmModuleName_ + ".js";//  "OrthancStoneSimpleViewer.js"; // ASSETS_FOLDER + '/' + WASM_FILENAME + '.js';
+        script.async = true;
+        document.head.appendChild(script);
+      }
+    }
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Platforms/Wasm/tsconfig-stone.json	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,7 @@
+{
+    "include" : [
+        "stone-framework-loader.ts",
+        "wasm-application-runner.ts",
+        "wasm-viewport.ts"
+    ]
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Platforms/Wasm/wasm-application-runner.ts	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,113 @@
+///<reference path='stone-framework-loader.ts'/>
+///<reference path='wasm-viewport.ts'/>
+
+if (!('WebAssembly' in window)) {
+  alert('Sorry, your browser does not support WebAssembly :(');
+}
+
+declare var StoneFrameworkModule : Stone.Framework;
+
+// global functions
+var WasmWebService_NotifyError: Function = null;
+var WasmWebService_NotifySuccess: Function = null;
+var WasmWebService_SetBaseUri: Function = null;
+var NotifyUpdateContent: Function = null;
+var SetStartupParameter: Function = null;
+var CreateWasmApplication: Function = null;
+var CreateCppViewport: Function = null;
+var ReleaseCppViewport: Function = null;
+var StartWasmApplication: Function = null;
+var SendMessageToStoneApplication: Function = null;
+
+
+function UpdateContentThread() {
+  if (NotifyUpdateContent != null) {
+    NotifyUpdateContent();
+  }
+
+  setTimeout(UpdateContentThread, 100);  // Update the viewport content every 100ms if need be
+}
+
+
+function GetUriParameters(): Map<string, string> {
+  var parameters = window.location.search.substr(1);
+
+  if (parameters != null &&
+    parameters != '') {
+    var result = new Map<string, string>();
+    var tokens = parameters.split('&');
+
+    for (var i = 0; i < tokens.length; i++) {
+      var tmp = tokens[i].split('=');
+      if (tmp.length == 2) {
+        result[tmp[0]] = decodeURIComponent(tmp[1]);
+      }
+    }
+
+    return result;
+  }
+  else {
+    return new Map<string, string>();
+  }
+}
+
+// function UpdateWebApplication(statusUpdateMessage: string) {
+//   console.log(statusUpdateMessage);
+// }
+
+function _InitializeWasmApplication(orthancBaseUrl: string): void {
+
+  CreateWasmApplication();
+  WasmWebService_SetBaseUri(orthancBaseUrl);
+
+
+  // parse uri and transmit the parameters to the app before initializing it
+  let parameters = GetUriParameters();
+
+  for (let key in parameters) {
+    if (parameters.hasOwnProperty(key)) {
+      SetStartupParameter(key, parameters[key]);
+    }
+  }
+
+  StartWasmApplication();
+
+  // trigger a first resize of the canvas that have just been initialized
+  Stone.WasmViewport.ResizeAll();
+
+  UpdateContentThread();
+}
+
+function InitializeWasmApplication(wasmModuleName: string, orthancBaseUrl: string) {
+  
+  Stone.Framework.Configure(wasmModuleName);
+
+  // Wait for the Orthanc Framework to be initialized (this initializes
+  // the WebAssembly environment) and then, create and initialize the Wasm application
+  Stone.Framework.Initialize(true, function () {
+
+    console.log("Connecting C++ methods to JS methods");
+    
+    SetStartupParameter = StoneFrameworkModule.cwrap('SetStartupParameter', null, ['string', 'string']);
+    CreateWasmApplication = StoneFrameworkModule.cwrap('CreateWasmApplication', null, ['number']);
+    CreateCppViewport = StoneFrameworkModule.cwrap('CreateCppViewport', 'number', []);
+    ReleaseCppViewport = StoneFrameworkModule.cwrap('ReleaseCppViewport', null, ['number']);
+    StartWasmApplication = StoneFrameworkModule.cwrap('StartWasmApplication', null, ['number']);
+
+    WasmWebService_NotifySuccess = StoneFrameworkModule.cwrap('WasmWebService_NotifySuccess', null, ['number', 'string', 'array', 'number', 'number']);
+    WasmWebService_NotifyError = StoneFrameworkModule.cwrap('WasmWebService_NotifyError', null, ['number', 'string', 'number']);
+    WasmWebService_SetBaseUri = StoneFrameworkModule.cwrap('WasmWebService_SetBaseUri', null, ['string']);
+    NotifyUpdateContent = StoneFrameworkModule.cwrap('NotifyUpdateContent', null, []);
+
+    SendMessageToStoneApplication = StoneFrameworkModule.cwrap('SendMessageToStoneApplication', 'string', ['string']);
+
+    console.log("Connecting C++ methods to JS methods - done");
+
+    // Prevent scrolling
+    document.body.addEventListener('touchmove', function (event) {
+      event.preventDefault();
+    }, false);
+
+    _InitializeWasmApplication(orthancBaseUrl);
+  });
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Platforms/Wasm/wasm-viewport.ts	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,298 @@
+var isPendingRedraw = false;
+
+function ScheduleWebViewportRedraw(cppViewportHandle: any) : void
+{
+  if (!isPendingRedraw) {
+    isPendingRedraw = true;
+    console.log('Scheduling a refresh of the viewport, as its content changed');
+    window.requestAnimationFrame(function() {
+      isPendingRedraw = false;
+      Stone.WasmViewport.GetFromCppViewport(cppViewportHandle).Redraw();
+    });
+  }
+}
+
+declare function UTF8ToString(any): string;
+
+function CreateWasmViewport(htmlCanvasId: string) : any {
+  var cppViewportHandle = CreateCppViewport();
+  var canvasId = UTF8ToString(htmlCanvasId);
+  var webViewport = new Stone.WasmViewport(StoneFrameworkModule, canvasId, cppViewportHandle);  // viewports are stored in a static map in WasmViewport -> won't be deleted
+  webViewport.Initialize();
+
+  return cppViewportHandle;
+}
+
+module Stone {
+  
+//  export declare type InitializationCallback = () => void;
+  
+//  export declare var StoneFrameworkModule : any;
+  
+  //const ASSETS_FOLDER : string = "assets/lib";
+  //const WASM_FILENAME : string = "orthanc-framework";
+
+  export class WasmViewport {
+
+    private static viewportsMapByCppHandle_ : Map<number, WasmViewport> = new Map<number, WasmViewport>(); // key = the C++ handle
+    private static viewportsMapByCanvasId_ : Map<string, WasmViewport> = new Map<string, WasmViewport>(); // key = the canvasId
+
+    private module_ : any;
+    private canvasId_ : string;
+    private htmlCanvas_ : HTMLCanvasElement;
+    private context_ : CanvasRenderingContext2D;
+    private imageData_ : any = null;
+    private renderingBuffer_ : any = null;
+    private touchZoom_ : any = false;
+    private touchTranslation_ : any = false;
+
+    private ViewportSetSize : Function;
+    private ViewportRender : Function;
+    private ViewportMouseDown : Function;
+    private ViewportMouseMove : Function;
+    private ViewportMouseUp : Function;
+    private ViewportMouseEnter : Function;
+    private ViewportMouseLeave : Function;
+    private ViewportMouseWheel : Function;
+    private ViewportKeyPressed : Function;
+
+    private pimpl_ : any; // Private pointer to the underlying WebAssembly C++ object
+
+    public constructor(module: any, canvasId: string, cppViewport: any) {
+      
+      this.pimpl_ = cppViewport;
+      WasmViewport.viewportsMapByCppHandle_[this.pimpl_] = this;
+      WasmViewport.viewportsMapByCanvasId_[canvasId] = this;
+
+      this.module_ = module;
+      this.canvasId_ = canvasId;
+      this.htmlCanvas_ = document.getElementById(this.canvasId_) as HTMLCanvasElement;
+      if (this.htmlCanvas_ == null) {
+        console.log("Can not create WasmViewport, did not find the canvas whose id is '", this.canvasId_, "'");
+      }
+      this.context_ = this.htmlCanvas_.getContext('2d');
+
+      this.ViewportSetSize = this.module_.cwrap('ViewportSetSize', null, [ 'number', 'number', 'number' ]);
+      this.ViewportRender = this.module_.cwrap('ViewportRender', null, [ 'number', 'number', 'number', 'number' ]);
+      this.ViewportMouseDown = this.module_.cwrap('ViewportMouseDown', null, [ 'number', 'number', 'number', 'number', 'number' ]);
+      this.ViewportMouseMove = this.module_.cwrap('ViewportMouseMove', null, [ 'number', 'number', 'number' ]);
+      this.ViewportMouseUp = this.module_.cwrap('ViewportMouseUp', null, [ 'number' ]);
+      this.ViewportMouseEnter = this.module_.cwrap('ViewportMouseEnter', null, [ 'number' ]);
+      this.ViewportMouseLeave = this.module_.cwrap('ViewportMouseLeave', null, [ 'number' ]);
+      this.ViewportMouseWheel = this.module_.cwrap('ViewportMouseWheel', null, [ 'number', 'number', 'number', 'number', 'number' ]);
+      this.ViewportKeyPressed = this.module_.cwrap('ViewportKeyPressed', null, [ 'number', 'number', 'string', 'number', 'number' ]);
+    }
+
+    public GetCppViewport() : number {
+      return this.pimpl_;
+    }
+
+    public static GetFromCppViewport(cppViewportHandle: number) : WasmViewport {
+      if (WasmViewport.viewportsMapByCppHandle_[cppViewportHandle] !== undefined) {
+        return WasmViewport.viewportsMapByCppHandle_[cppViewportHandle];
+      }
+      console.log("WasmViewport not found !");
+      return undefined;
+    }
+
+    public static GetFromCanvasId(canvasId: string) : WasmViewport {
+      if (WasmViewport.viewportsMapByCanvasId_[canvasId] !== undefined) {
+        return WasmViewport.viewportsMapByCanvasId_[canvasId];
+      }
+      console.log("WasmViewport not found !");
+      return undefined;
+    }
+
+    public static ResizeAll() {
+      for (let canvasId in WasmViewport.viewportsMapByCanvasId_) {
+        WasmViewport.viewportsMapByCanvasId_[canvasId].Resize();
+      }
+    }
+
+    public Redraw() {
+      if (this.imageData_ === null ||
+          this.renderingBuffer_ === null ||
+          this.ViewportRender(this.pimpl_,
+                         this.imageData_.width,
+                         this.imageData_.height,
+                         this.renderingBuffer_) == 0) {
+        console.log('The rendering has failed');
+      } else {
+        // Create an accessor to the rendering buffer (i.e. create a
+        // "window" above the heap of the WASM module), then copy it to
+        // the ImageData object
+        this.imageData_.data.set(new Uint8ClampedArray(
+          this.module_.buffer,
+          this.renderingBuffer_,
+          this.imageData_.width * this.imageData_.height * 4));
+        
+        this.context_.putImageData(this.imageData_, 0, 0);
+      }
+    }
+  
+    public Resize() {
+      if (this.imageData_ != null &&
+          (this.imageData_.width != window.innerWidth ||
+           this.imageData_.height != window.innerHeight)) {
+        this.imageData_ = null;
+      }
+      
+      // width/height can be defined in percent of window width/height through html attributes like data-width-ratio="50" and data-height-ratio="20"
+      var widthRatio = Number(this.htmlCanvas_.dataset["widthRatio"]) || 100;
+      var heightRatio = Number(this.htmlCanvas_.dataset["heightRatio"]) || 100;
+
+      this.htmlCanvas_.width = window.innerWidth * (widthRatio / 100);  
+      this.htmlCanvas_.height = window.innerHeight * (heightRatio / 100);
+
+      console.log("resizing WasmViewport: ", this.htmlCanvas_.width, "x", this.htmlCanvas_.height);
+
+      if (this.imageData_ === null) {
+        this.imageData_ = this.context_.getImageData(0, 0, this.htmlCanvas_.width, this.htmlCanvas_.height);
+        this.ViewportSetSize(this.pimpl_, this.htmlCanvas_.width, this.htmlCanvas_.height);
+  
+        if (this.renderingBuffer_ != null) {
+          this.module_._free(this.renderingBuffer_);
+        }
+        
+        this.renderingBuffer_ = this.module_._malloc(this.imageData_.width * this.imageData_.height * 4);
+      } else {
+        this.ViewportSetSize(this.pimpl_, this.htmlCanvas_.width, this.htmlCanvas_.height);
+      }
+      
+      this.Redraw();
+    }
+
+    public Initialize() {
+      
+      // Force the rendering of the viewport for the first time
+      this.Resize();
+    
+      var that : WasmViewport = this;
+      // Register an event listener to call the Resize() function 
+      // each time the window is resized.
+      window.addEventListener('resize', function(event) {
+        that.Resize();
+      }, false);
+  
+      this.htmlCanvas_.addEventListener('contextmenu', function(event) {
+        // Prevent right click on the canvas
+        event.preventDefault();
+      }, false);
+      
+      this.htmlCanvas_.addEventListener('mouseleave', function(event) {
+        that.ViewportMouseLeave(that.pimpl_);
+      });
+      
+      this.htmlCanvas_.addEventListener('mouseenter', function(event) {
+        that.ViewportMouseEnter(that.pimpl_);
+      });
+    
+      this.htmlCanvas_.addEventListener('mousedown', function(event) {
+        var x = event.pageX - this.offsetLeft;
+        var y = event.pageY - this.offsetTop;
+        that.ViewportMouseDown(that.pimpl_, event.button, x, y, 0 /* TODO */);    
+      });
+    
+      this.htmlCanvas_.addEventListener('mousemove', function(event) {
+        var x = event.pageX - this.offsetLeft;
+        var y = event.pageY - this.offsetTop;
+        that.ViewportMouseMove(that.pimpl_, x, y);
+      });
+    
+      this.htmlCanvas_.addEventListener('mouseup', function(event) {
+        that.ViewportMouseUp(that.pimpl_);
+      });
+    
+      window.addEventListener('keydown', function(event) {
+        var keyChar = event.key;
+        var keyCode = event.keyCode
+        if (keyChar.length == 1) {
+          keyCode = 0; // maps to OrthancStone::KeyboardKeys_Generic
+        } else {
+          keyChar = null;
+        }
+//        console.log("key: ", keyCode, keyChar);
+        that.ViewportKeyPressed(that.pimpl_, keyCode, keyChar, event.shiftKey, event.ctrlKey, event.altKey);
+      });
+    
+      this.htmlCanvas_.addEventListener('wheel', function(event) {
+        var x = event.pageX - this.offsetLeft;
+        var y = event.pageY - this.offsetTop;
+        that.ViewportMouseWheel(that.pimpl_, event.deltaY, x, y, event.ctrlKey);
+        event.preventDefault();
+      });
+
+      this.htmlCanvas_.addEventListener('touchstart', function(event) {
+        that.ResetTouch();
+      });
+    
+      this.htmlCanvas_.addEventListener('touchend', function(event) {
+        that.ResetTouch();
+      });
+    
+      this.htmlCanvas_.addEventListener('touchmove', function(event) {
+        if (that.touchTranslation_.length == 2) {
+          var t = that.GetTouchTranslation(event);
+          that.ViewportMouseMove(that.pimpl_, t[0], t[1]);
+        }
+        else if (that.touchZoom_.length == 3) {
+          var z0 = that.touchZoom_;
+          var z1 = that.GetTouchZoom(event);
+          that.ViewportMouseMove(that.pimpl_, z0[0], z0[1] - z0[2] + z1[2]);
+        }
+        else {
+          // Realize the gesture event
+          if (event.targetTouches.length == 1) {
+            // Exactly one finger inside the canvas => Setup a translation
+            that.touchTranslation_ = that.GetTouchTranslation(event);
+            that.ViewportMouseDown(that.pimpl_, 
+                                  1 /* middle button */,
+                                  that.touchTranslation_[0],
+                                  that.touchTranslation_[1], 0);
+          } else if (event.targetTouches.length == 2) {
+            // Exactly 2 fingers inside the canvas => Setup a pinch/zoom
+            that.touchZoom_ = that.GetTouchZoom(event);
+            var z0 = that.touchZoom_;
+            that.ViewportMouseDown(that.pimpl_, 
+                                  2 /* right button */,
+                                  z0[0],
+                                  z0[1], 0);
+          }        
+        }
+      });
+    }  
+
+  public ResetTouch() {
+    if (this.touchTranslation_ ||
+        this.touchZoom_) {
+      this.ViewportMouseUp(this.pimpl_);
+    }
+
+    this.touchTranslation_ = false;
+    this.touchZoom_ = false;
+  }
+  
+  public GetTouchTranslation(event) {
+    var touch = event.targetTouches[0];
+    return [
+      touch.pageX,
+      touch.pageY
+    ];
+  }
+    
+  public GetTouchZoom(event) {
+    var touch1 = event.targetTouches[0];
+    var touch2 = event.targetTouches[1];
+    var dx = (touch1.pageX - touch2.pageX);
+    var dy = (touch1.pageY - touch2.pageY);
+    var d = Math.sqrt(dx * dx + dy * dy);
+    return [
+      (touch1.pageX + touch2.pageX) / 2.0,
+      (touch1.pageY + touch2.pageY) / 2.0,
+      d
+    ];
+  }
+    
+}
+}
+  
\ No newline at end of file
--- a/Platforms/WebAssembly/CMakeLists.txt	Mon Nov 05 10:04:56 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,49 +0,0 @@
-# Usage (Linux):
-# source ~/Downloads/emsdk/emsdk_env.sh && cmake -DCMAKE_TOOLCHAIN_FILE=${EMSCRIPTEN}/cmake/Modules/Platform/Emscripten.cmake ..
-
-cmake_minimum_required(VERSION 2.8.3)
-
-
-#####################################################################
-## Configuration of the Emscripten compiler for WebAssembly target
-#####################################################################
-
-set(WASM_FLAGS "-s WASM=1")
-set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${WASM_FLAGS}")
-set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${WASM_FLAGS}")
-set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} --js-library ${CMAKE_SOURCE_DIR}/library.js")
-
-# Handling of memory
-#set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s ALLOW_MEMORY_GROWTH=1")  # Resize
-#set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s TOTAL_MEMORY=536870912")  # 512MB
-set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s ALLOW_MEMORY_GROWTH=1 -s TOTAL_MEMORY=536870912")  # 512MB + resize
-#set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s ALLOW_MEMORY_GROWTH=1 -s TOTAL_MEMORY=1073741824")  # 1GB + resize
-
-# To debug exceptions
-#set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s DEMANGLE_SUPPORT=1 -s ASSERTIONS=2")
-
-
-#####################################################################
-## Build a static library containing the Orthanc Stone framework
-#####################################################################
-
-include(../../Resources/CMake/OrthancStoneParameters.cmake)
-
-SET(ORTHANC_SANDBOXED ON)
-SET(ENABLE_SDL OFF)
-
-include(../../Resources/CMake/OrthancStoneConfiguration.cmake)
-
-add_library(OrthancStone STATIC ${ORTHANC_STONE_SOURCES})
-
-
-
-
-
-
-# Regenerate a dummy "library.c" file each time the "library.js" file
-# is modified, so as to force a new execution of the linking
-add_custom_command(
-    OUTPUT "${AUTOGENERATED_DIR}/library.c"
-    COMMAND ${CMAKE_COMMAND} -E touch "${AUTOGENERATED_DIR}/library.c" ""
-    DEPENDS "${CMAKE_SOURCE_DIR}/library.js")
--- a/Platforms/WebAssembly/library.js	Mon Nov 05 10:04:56 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,2 +0,0 @@
-mergeInto(LibraryManager.library, {
-});
--- a/README	Mon Nov 05 10:04:56 2018 +0100
+++ b/README	Mon Nov 05 10:06:18 2018 +0100
@@ -72,6 +72,10 @@
 * Optionally, SDL, a cross-platform multimedia library:
   https://www.libsdl.org/
 
+Prerequisites to compile on Ubuntu: 
+```
+sudo apt-get install -y libcairo-dev libpixman-1-dev libsdl2-dev
+```
 
 Installation and usage
 ----------------------
@@ -83,7 +87,43 @@
 http://book.orthanc-server.com/developers/stone.html
 
 Stone of Orthanc comes with several sample applications in the
-"Samples" folder. These samples use SDL.
+"Samples" folder. These samples can be compiled into Web Assembly
+or into native SDL applications.
+
+to build the WASM samples:
+-------------------------
+```
+cd ~/orthanc-stone/Applications/Samples
+./build-wasm.sh
+```
+
+to serve the WASM samples:
+```
+# launch an Orthanc listening on 8042 port:
+Orthanc
+
+# launch an nginx that will serve the WASM static files and reverse proxy Orthanc
+sudo nginx -p $(pwd) -c nginx.local.conf
+```
+Now, you can open the samples in http://localhost:9977
+
+to build the SDL native samples (SimpleViewer only):
+-------------------------------
+```
+mkdir -p ~/builds/orthanc-stone-build
+cd ~/builds/orthanc-stone-build
+cmake -DALLOW_DOWNLOADS=ON -DENABLE_SDL=ON ~/orthanc-stone/Applications/Samples/
+cmake --build . --target OrthancStoneSimpleViewer -- -j 5
+```
+
+to execute the native samples:
+```
+# launch an Orthanc listening on 8042 port:
+Orthanc
+
+# launch the sample
+./OrthancStoneSimpleViewer --studyId=XX
+``` 
 
 
 Licensing
--- a/Resources/CMake/OrthancStoneConfiguration.cmake	Mon Nov 05 10:04:56 2018 +0100
+++ b/Resources/CMake/OrthancStoneConfiguration.cmake	Mon Nov 05 10:06:18 2018 +0100
@@ -22,6 +22,12 @@
 ## Configure the Orthanc Framework
 #####################################################################
 
+if (ENABLE_DCMTK)
+  set(ENABLE_LOCALE ON)
+else()
+  set(ENABLE_LOCALE OFF)  # Disable support for locales (notably in Boost)
+endif()
+
 include(${ORTHANC_ROOT}/Resources/CMake/OrthancFrameworkConfiguration.cmake)
 include_directories(${ORTHANC_ROOT})
 
@@ -39,6 +45,10 @@
     message(FATAL_ERROR "Cannot enable SDL in sandboxed environments")
   endif()
 
+  if (ENABLE_QT)
+    message(FATAL_ERROR "Cannot enable QT in sandboxed environments")
+  endif()
+
   if (ENABLE_SSL)
     message(FATAL_ERROR "Cannot enable SSL in sandboxed environments")
   endif()
@@ -69,12 +79,26 @@
 endif()
 
 
-if (ENABLE_SDL)
-  include(${CMAKE_CURRENT_LIST_DIR}/SdlConfiguration.cmake)  
+if (ENABLE_SDL AND ENABLE_QT)
+  message("SDL and QT may not be defined together")
+elseif(ENABLE_SDL)
+  message("SDL is enabled")
+  include(${CMAKE_CURRENT_LIST_DIR}/SdlConfiguration.cmake)
+  add_definitions(-DORTHANC_ENABLE_NATIVE=1)
+  add_definitions(-DORTHANC_ENABLE_QT=0)
   add_definitions(-DORTHANC_ENABLE_SDL=1)
+elseif(ENABLE_QT)
+  message("QT is enabled")
+  include(${CMAKE_CURRENT_LIST_DIR}/QtConfiguration.cmake)
+  add_definitions(-DORTHANC_ENABLE_NATIVE=1)
+  add_definitions(-DORTHANC_ENABLE_QT=1)
+  add_definitions(-DORTHANC_ENABLE_SDL=0)
 else()
+  message("SDL and QT are both disabled")
   unset(USE_SYSTEM_SDL CACHE)
   add_definitions(-DORTHANC_ENABLE_SDL=0)
+  add_definitions(-DORTHANC_ENABLE_QT=0)
+  add_definitions(-DORTHANC_ENABLE_NATIVE=0)
 endif()
 
 
@@ -93,7 +117,9 @@
   -DORTHANC_ENABLE_LOGGING_PLUGIN=0
   )
 
-
+if (CMAKE_BUILD_TYPE STREQUAL "Debug")
+  add_definitions(-DCHECK_OBSERVERS_MESSAGES)
+endif()
 
 #####################################################################
 ## Embed the colormaps into the binaries
@@ -105,6 +131,9 @@
   # "OrthancStoneParameters.cmake"
   ${DCMTK_DICTIONARIES}
 
+  FONT_UBUNTU_MONO_BOLD_16   ${ORTHANC_ROOT}/Resources/Fonts/UbuntuMonoBold-16.json
+  #FONT_UBUNTU_MONO_BOLD_64   ${ORTHANC_ROOT}/Resources/Fonts/UbuntuMonoBold-64.json
+
   # Resources specific to the Stone of Orthanc
   COLORMAP_HOT    ${ORTHANC_STONE_ROOT}/Resources/Colormaps/hot.lut
   COLORMAP_JET    ${ORTHANC_STONE_ROOT}/Resources/Colormaps/jet.lut
@@ -141,39 +170,87 @@
 ## All the source files required to build Stone of Orthanc
 #####################################################################
 
+set(APPLICATIONS_SOURCES
+    ${ORTHANC_STONE_ROOT}/Applications/IStoneApplication.h
+    ${ORTHANC_STONE_ROOT}/Applications/StoneApplicationContext.cpp
+    ${ORTHANC_STONE_ROOT}/Applications/Commands/BaseCommandBuilder.cpp
+    ${ORTHANC_STONE_ROOT}/Applications/Commands/ICommand.h
+    ${ORTHANC_STONE_ROOT}/Applications/Commands/ICommandExecutor.h
+    ${ORTHANC_STONE_ROOT}/Applications/Commands/ICommandBuilder.h
+    )
+
 if (NOT ORTHANC_SANDBOXED)
   set(PLATFORM_SOURCES
+    ${ORTHANC_STONE_ROOT}/Framework/Viewport/CairoFont.cpp
+    ${ORTHANC_STONE_ROOT}/Platforms/Generic/WebServiceCommandBase.cpp
     ${ORTHANC_STONE_ROOT}/Platforms/Generic/WebServiceGetCommand.cpp
     ${ORTHANC_STONE_ROOT}/Platforms/Generic/WebServicePostCommand.cpp
+    ${ORTHANC_STONE_ROOT}/Platforms/Generic/WebServiceDeleteCommand.cpp
     ${ORTHANC_STONE_ROOT}/Platforms/Generic/Oracle.cpp
+    ${ORTHANC_STONE_ROOT}/Platforms/Generic/OracleWebService.h
     )
 
-  set(APPLICATIONS_SOURCES
-    ${ORTHANC_STONE_ROOT}/Applications/BasicApplicationContext.cpp
-    ${ORTHANC_STONE_ROOT}/Applications/IBasicApplication.cpp
-    ${ORTHANC_STONE_ROOT}/Applications/Sdl/SdlEngine.cpp
-    ${ORTHANC_STONE_ROOT}/Applications/Sdl/SdlCairoSurface.cpp
-    ${ORTHANC_STONE_ROOT}/Applications/Sdl/SdlOrthancSurface.cpp
-    ${ORTHANC_STONE_ROOT}/Applications/Sdl/SdlWindow.cpp
+  if (ENABLE_SDL OR ENABLE_QT)
+    list(APPEND APPLICATIONS_SOURCES
+      ${ORTHANC_STONE_ROOT}/Applications/Generic/NativeStoneApplicationRunner.cpp
+      ${ORTHANC_STONE_ROOT}/Applications/Generic/NativeStoneApplicationContext.cpp
+      )
+    if (ENABLE_SDL)
+      list(APPEND APPLICATIONS_SOURCES
+        ${ORTHANC_STONE_ROOT}/Applications/Sdl/SdlStoneApplicationRunner.cpp
+        ${ORTHANC_STONE_ROOT}/Applications/Sdl/SdlEngine.cpp
+        ${ORTHANC_STONE_ROOT}/Applications/Sdl/SdlCairoSurface.cpp
+        ${ORTHANC_STONE_ROOT}/Applications/Sdl/SdlOrthancSurface.cpp
+        ${ORTHANC_STONE_ROOT}/Applications/Sdl/SdlWindow.cpp
+        )
+    endif()
+  endif()
+elseif (ENABLE_WASM)
+  list(APPEND APPLICATIONS_SOURCES
+    ${ORTHANC_STONE_ROOT}/Applications/Wasm/StartupParametersBuilder.cpp
     )
+
+  set(STONE_WASM_SOURCES
+    ${ORTHANC_STONE_ROOT}/Platforms/Wasm/Defaults.cpp
+    ${ORTHANC_STONE_ROOT}/Platforms/Wasm/WasmWebService.cpp
+    ${ORTHANC_STONE_ROOT}/Platforms/Wasm/WasmViewport.cpp
+    ${ORTHANC_STONE_ROOT}/Platforms/Wasm/WasmPlatformApplicationAdapter.cpp
+    ${AUTOGENERATED_DIR}/WasmWebService.c
+    ${AUTOGENERATED_DIR}/default-library.c
+  )
+
+  # Regenerate a dummy "WasmWebService.c" file each time the "WasmWebService.js" file
+  # is modified, so as to force a new execution of the linking
+  add_custom_command(
+    OUTPUT "${AUTOGENERATED_DIR}/WasmWebService.c"
+    COMMAND ${CMAKE_COMMAND} -E touch "${AUTOGENERATED_DIR}/WasmWebService.c" ""
+    DEPENDS "${ORTHANC_STONE_ROOT}/Platforms/Wasm/WasmWebService.js")
+  add_custom_command(
+    OUTPUT "${AUTOGENERATED_DIR}/default-library.c"
+    COMMAND ${CMAKE_COMMAND} -E touch "${AUTOGENERATED_DIR}/default-library.c" ""
+    DEPENDS "${ORTHANC_STONE_ROOT}/Platforms/Wasm/default-library.js")
 endif()
 
 list(APPEND ORTHANC_STONE_SOURCES
   #${ORTHANC_STONE_ROOT}/Framework/Layers/SeriesFrameRendererFactory.cpp
   #${ORTHANC_STONE_ROOT}/Framework/Layers/SiblingSliceLocationFactory.cpp
   #${ORTHANC_STONE_ROOT}/Framework/Layers/SingleFrameRendererFactory.cpp
-  ${ORTHANC_STONE_ROOT}/Framework/StoneEnumerations.cpp
+
   ${ORTHANC_STONE_ROOT}/Framework/Layers/CircleMeasureTracker.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Layers/ColorFrameRenderer.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Layers/DicomStructureSetRendererFactory.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Layers/FrameRenderer.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Layers/GrayscaleFrameRenderer.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Layers/ILayerSource.h
   ${ORTHANC_STONE_ROOT}/Framework/Layers/LayerSourceBase.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Layers/LineLayerRenderer.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Layers/LineMeasureTracker.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Layers/OrthancFrameLayerSource.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Layers/RenderStyle.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Layers/SliceOutlineRenderer.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/SmartLoader.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/StoneEnumerations.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/StoneException.h
   ${ORTHANC_STONE_ROOT}/Framework/Toolbox/CoordinateSystem3D.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Toolbox/DicomFrameConverter.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Toolbox/DicomStructureSet.cpp
@@ -181,10 +258,12 @@
   ${ORTHANC_STONE_ROOT}/Framework/Toolbox/Extent2D.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Toolbox/FiniteProjectiveCamera.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Toolbox/GeometryToolbox.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Toolbox/IWebService.h
   ${ORTHANC_STONE_ROOT}/Framework/Toolbox/ImageGeometry.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Toolbox/LinearAlgebra.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Toolbox/MessagingToolbox.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Toolbox/OrientedBoundingBox.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Toolbox/OrthancApiClient.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Toolbox/OrthancSlicesLoader.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Toolbox/ParallelSlices.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Toolbox/ParallelSlicesCursor.cpp
@@ -193,8 +272,9 @@
   ${ORTHANC_STONE_ROOT}/Framework/Toolbox/SlicesSorter.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Toolbox/ViewportGeometry.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Viewport/CairoContext.cpp
-  ${ORTHANC_STONE_ROOT}/Framework/Viewport/CairoFont.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Viewport/CairoSurface.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Viewport/IStatusBar.h
+  ${ORTHANC_STONE_ROOT}/Framework/Viewport/IViewport.h
   ${ORTHANC_STONE_ROOT}/Framework/Viewport/WidgetViewport.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Volumes/ImageBuffer3D.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Volumes/SlicedVolumeBase.cpp
@@ -203,17 +283,32 @@
   ${ORTHANC_STONE_ROOT}/Framework/Volumes/VolumeReslicer.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Widgets/CairoWidget.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Widgets/EmptyWidget.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Widgets/IWidget.h
+  ${ORTHANC_STONE_ROOT}/Framework/Widgets/IWorldSceneInteractor.h
+  ${ORTHANC_STONE_ROOT}/Framework/Widgets/IWorldSceneMouseTracker.h
   ${ORTHANC_STONE_ROOT}/Framework/Widgets/LayerWidget.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Widgets/LayoutWidget.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Widgets/PanMouseTracker.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Widgets/TestCairoWidget.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Widgets/TestWorldSceneWidget.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Widgets/WidgetBase.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Widgets/WorldSceneWidget.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Widgets/ZoomMouseTracker.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/dev.h
 
+  ${ORTHANC_STONE_ROOT}/Framework/Messages/ICallable.h
+  ${ORTHANC_STONE_ROOT}/Framework/Messages/IMessage.h
+  ${ORTHANC_STONE_ROOT}/Framework/Messages/IObservable.h
+  ${ORTHANC_STONE_ROOT}/Framework/Messages/IObserver.h
+  ${ORTHANC_STONE_ROOT}/Framework/Messages/MessageBroker.h
+  ${ORTHANC_STONE_ROOT}/Framework/Messages/MessageForwarder.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Messages/MessageType.h
+  ${ORTHANC_STONE_ROOT}/Framework/Messages/Promise.h
+
+  ${ORTHANC_ROOT}/Plugins/Samples/Common/DicomDatasetReader.cpp
   ${ORTHANC_ROOT}/Plugins/Samples/Common/DicomPath.cpp
+  ${ORTHANC_ROOT}/Plugins/Samples/Common/FullOrthancDataset.cpp
   ${ORTHANC_ROOT}/Plugins/Samples/Common/IOrthancConnection.cpp
-  ${ORTHANC_ROOT}/Plugins/Samples/Common/DicomDatasetReader.cpp
-  ${ORTHANC_ROOT}/Plugins/Samples/Common/FullOrthancDataset.cpp
   
   ${PLATFORM_SOURCES}
   ${APPLICATIONS_SOURCES}
@@ -227,5 +322,7 @@
 
   # Optional components
   ${SDL_SOURCES}
+  ${QT_SOURCES}
   ${BOOST_EXTENDED_SOURCES}
   )
+
--- a/Resources/CMake/OrthancStoneParameters.cmake	Mon Nov 05 10:04:56 2018 +0100
+++ b/Resources/CMake/OrthancStoneParameters.cmake	Mon Nov 05 10:06:18 2018 +0100
@@ -25,7 +25,7 @@
 include(${CMAKE_CURRENT_LIST_DIR}/../../Resources/Orthanc/DownloadOrthancFramework.cmake)
 include(${ORTHANC_ROOT}/Resources/CMake/OrthancFrameworkParameters.cmake)
 
-set(ENABLE_LOCALE OFF)         # Disable support for locales (notably in Boost)
+set(ENABLE_DCMTK OFF)
 set(ENABLE_GOOGLE_TEST ON)
 set(ENABLE_SQLITE OFF)
 set(ENABLE_JPEG ON)
@@ -49,4 +49,3 @@
 ## the Stone of Orthanc
 #####################################################################
 
-set(ENABLE_SDL ON CACHE INTERNAL "Include support for SDL")
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/CMake/QtConfiguration.cmake	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,77 @@
+# Stone of Orthanc
+# Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+# Department, University Hospital of Liege, Belgium
+# Copyright (C) 2017-2018 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/>.
+
+
+set(CMAKE_AUTOMOC OFF)
+set(CMAKE_AUTOUIC OFF)
+
+# Find the QtWidgets library
+find_package(Qt5Widgets QUIET)
+
+if (Qt5Widgets_FOUND)
+  message("Qt5 has been detected")
+  find_package(Qt5Core REQUIRED)
+  link_libraries(
+    Qt5::Widgets
+    Qt5::Core
+    )
+
+  # Create aliases for the CMake commands
+  macro(ORTHANC_QT_WRAP_UI)
+    QT5_WRAP_UI(${ARGN})
+  endmacro()
+  
+  macro(ORTHANC_QT_WRAP_CPP)
+    QT5_WRAP_CPP(${ARGN})
+  endmacro()
+    
+else()
+  message("Qt5 has not been found, trying with Qt4")
+  find_package(Qt4 REQUIRED QtGui)
+  link_libraries(
+    Qt4::QtGui
+    )
+
+  # Create aliases for the CMake commands
+  macro(ORTHANC_QT_WRAP_UI)
+    QT4_WRAP_UI(${ARGN})
+  endmacro()
+  
+  macro(ORTHANC_QT_WRAP_CPP)
+    QT4_WRAP_CPP(${ARGN})
+  endmacro()
+  
+endif()
+
+list(APPEND QT_SOURCES
+  ${ORTHANC_STONE_ROOT}/Applications/Qt/QCairoWidget.cpp
+  ${ORTHANC_STONE_ROOT}/Applications/Qt/QtStoneApplicationRunner.cpp
+  ${ORTHANC_STONE_ROOT}/Applications/Qt/QStoneMainWindow.cpp
+  )
+
+
+# NB: Including CMAKE_CURRENT_BINARY_DIR is mandatory, as the CMake
+# macros for Qt will put their result in that directory, which cannot
+# be changed.
+# https://stackoverflow.com/a/4016784/881731
+
+include_directories(
+  ${ORTHANC_STONE_ROOT}/Applications/Qt/
+  ${CMAKE_CURRENT_BINARY_DIR}
+  )
+
--- a/Resources/Orthanc/DownloadOrthancFramework.cmake	Mon Nov 05 10:04:56 2018 +0100
+++ b/Resources/Orthanc/DownloadOrthancFramework.cmake	Mon Nov 05 10:06:18 2018 +0100
@@ -87,6 +87,8 @@
         set(ORTHANC_FRAMEWORK_MD5 "d0ccdf68e855d8224331f13774992750")
       elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.4.0")
         set(ORTHANC_FRAMEWORK_MD5 "81e15f34d97ac32bbd7d26e85698835a")
+      elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.4.1")
+        set(ORTHANC_FRAMEWORK_MD5 "9b6f6114264b17ed421b574cd6476127")
       elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.4.2")
         set(ORTHANC_FRAMEWORK_MD5 "d1ee84927dcf668e60eb5868d24b9394")
       endif()
--- a/TODO	Mon Nov 05 10:04:56 2018 +0100
+++ b/TODO	Mon Nov 05 10:06:18 2018 +0100
@@ -26,7 +26,6 @@
 -------------
 
 * Tune number of loading threads in LayeredSceneWidget
-* Add cache over IOrthancServices (for SDL/Qt/...)
 * LayoutWidget: Do not update full background if only 1 widget has changed
 * LayoutWidget: Threads to refresh each child
 * Implement binary search to speed up search for closest slice
@@ -38,7 +37,6 @@
 Platform-specific
 -----------------
 
-* Qt widget example
 * Add precompiled headers for Microsoft Visual Studio
 * Investigate crash in CurlOrthancConnection if using MinGW32 in Release mode
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/UnitTestsSources/TestCommands.cpp	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,108 @@
+///**
+// * Stone of Orthanc
+// * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+// * Department, University Hospital of Liege, Belgium
+// * Copyright (C) 2017-2018 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 "gtest/gtest.h"
+
+//#include "../Applications/Commands/BaseCommandFactory.h"
+//#include "Core/OrthancException.h"
+
+//class CommandIncrement: public OrthancStone::BaseCommand<CommandIncrement>
+//{
+//public:
+//  static int counter;
+//  int increment_;
+//public:
+//  CommandIncrement()
+//    : OrthancStone::BaseCommand<CommandIncrement>("increment"),
+//      increment_(0)
+//  {}
+
+//  virtual void Execute()
+//  {
+//    counter += increment_;
+//  }
+//  virtual void Configure(const Json::Value& arguments)
+//  {
+//    increment_ = arguments["increment"].asInt();
+//  }
+//};
+
+//// COMMAND("name", "arg1", "int", "arg2", "string")
+//// COMMAND(name, arg1, arg2)
+
+
+//int CommandIncrement::counter = 0;
+
+//TEST(Commands, CreateNoop)
+//{
+//  OrthancStone::BaseCommandFactory factory;
+
+//  factory.RegisterCommandClass<OrthancStone::NoopCommand>();
+
+//  Json::Value cmdJson;
+//  cmdJson["command"] = "noop";
+
+//  std::auto_ptr<OrthancStone::ICommand> command(factory.CreateFromJson(cmdJson));
+
+//  ASSERT_TRUE(command.get() != NULL);
+//  ASSERT_EQ("noop", command->GetName());
+//}
+
+//TEST(Commands, Execute)
+//{
+//  OrthancStone::BaseCommandFactory factory;
+
+//  factory.RegisterCommandClass<OrthancStone::NoopCommand>();
+//  factory.RegisterCommandClass<CommandIncrement>();
+
+//  Json::Value cmdJson;
+//  cmdJson["command"] = "increment";
+//  cmdJson["args"]["increment"] = 2;
+
+//  std::auto_ptr<OrthancStone::ICommand> command(factory.CreateFromJson(cmdJson));
+
+//  ASSERT_TRUE(command.get() != NULL);
+//  CommandIncrement::counter = 0;
+//  command->Execute();
+//  ASSERT_EQ(2, CommandIncrement::counter);
+//}
+
+//TEST(Commands, TryCreateUnknowCommand)
+//{
+//  OrthancStone::BaseCommandFactory factory;
+//  factory.RegisterCommandClass<OrthancStone::NoopCommand>();
+
+//  Json::Value cmdJson;
+//  cmdJson["command"] = "unknown";
+
+//  ASSERT_THROW(std::auto_ptr<OrthancStone::ICommand> command(factory.CreateFromJson(cmdJson)), Orthanc::OrthancException);
+//}
+
+//TEST(Commands, TryCreateCommandFromInvalidJson)
+//{
+//  OrthancStone::BaseCommandFactory factory;
+//  factory.RegisterCommandClass<OrthancStone::NoopCommand>();
+
+//  Json::Value cmdJson;
+//  cmdJson["command-name"] = "noop";
+
+//  ASSERT_THROW(std::auto_ptr<OrthancStone::ICommand> command(factory.CreateFromJson(cmdJson)), Orthanc::OrthancException);
+//}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/UnitTestsSources/TestExceptions.cpp	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,43 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 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 "gtest/gtest.h"
+
+#include "../Framework/StoneException.h"
+
+
+
+TEST(StoneExceptions, OrthancToStoneConversion)
+{
+  bool hasBeenCatched = false;
+  try {
+    throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+  }
+  catch (Orthanc::OrthancException& orthancException)
+  {
+    hasBeenCatched = true;
+    OrthancStone::StoneOrthancException stoneException(orthancException);
+    ASSERT_EQ(OrthancStone::ErrorCode_OrthancError, stoneException.GetErrorCode());
+    ASSERT_EQ(Orthanc::ErrorCode_InternalError, stoneException.GetOrthancErrorCode());
+  }
+
+  ASSERT_TRUE(hasBeenCatched);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/UnitTestsSources/TestMessageBroker.cpp	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,158 @@
+///**
+// * Stone of Orthanc
+// * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+// * Department, University Hospital of Liege, Belgium
+// * Copyright (C) 2017-2018 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 "gtest/gtest.h"
+
+//#include "../Framework/Messages/MessageBroker.h"
+//#include "../Framework/Messages/IMessage.h"
+//#include "../Framework/Messages/IObservable.h"
+//#include "../Framework/Messages/IObserver.h"
+//#include "../Framework/StoneEnumerations.h"
+
+
+//static int test1Counter = 0;
+//static int test2Counter = 0;
+//class MyFullObserver : public OrthancStone::IObserver
+//{
+
+//public:
+//  MyFullObserver(OrthancStone::MessageBroker& broker)
+//    : OrthancStone::IObserver(broker)
+//  {
+////    DeclareHandledMessage(OrthancStone::MessageType_Test1);
+////    DeclareIgnoredMessage(OrthancStone::MessageType_Test2);
+//  }
+
+
+//  void HandleMessage(OrthancStone::IObservable& from, const OrthancStone::IMessage& message) {
+//    switch (message.GetType())
+//    {
+//    case OrthancStone::MessageType_Test1:
+//      test1Counter++;
+//      break;
+//    case OrthancStone::MessageType_Test2:
+//      test2Counter++;
+//      break;
+//    default:
+//      throw OrthancStone::MessageNotDeclaredException(message.GetType());
+//    }
+//  }
+
+//};
+
+//class MyPartialObserver : public OrthancStone::IObserver
+//{
+
+//public:
+//  MyPartialObserver(OrthancStone::MessageBroker& broker)
+//    : OrthancStone::IObserver(broker)
+//  {
+////    DeclareHandledMessage(OrthancStone::MessageType_Test1);
+//    // don't declare Test2 on purpose
+//  }
+
+
+//  void HandleMessage(OrthancStone::IObservable& from, const OrthancStone::IMessage& message) {
+//    switch (message.GetType())
+//    {
+//    case OrthancStone::MessageType_Test1:
+//      test1Counter++;
+//      break;
+//    case OrthancStone::MessageType_Test2:
+//      test2Counter++;
+//      break;
+//    default:
+//      throw OrthancStone::MessageNotDeclaredException(message.GetType());
+//    }
+//  }
+
+//};
+
+
+//class MyObservable : public OrthancStone::IObservable
+//{
+
+//public:
+//  MyObservable(OrthancStone::MessageBroker& broker)
+//    : OrthancStone::IObservable(broker)
+//  {
+//    DeclareEmittableMessage(OrthancStone::MessageType_Test1);
+//    DeclareEmittableMessage(OrthancStone::MessageType_Test2);
+//  }
+
+//};
+
+
+//TEST(MessageBroker, NormalUsage)
+//{
+//  OrthancStone::MessageBroker broker;
+//  MyObservable observable(broker);
+
+//  test1Counter = 0;
+
+//  // no observers have been registered -> nothing shall happen
+//  observable.EmitMessage(OrthancStone::IMessage(OrthancStone::MessageType_Test1));
+
+//  ASSERT_EQ(0, test1Counter);
+
+//  // register an observer, check it is called
+//  MyFullObserver fullObserver(broker);
+//  ASSERT_NO_THROW(observable.RegisterObserver(fullObserver));
+
+//  observable.EmitMessage(OrthancStone::IMessage(OrthancStone::MessageType_Test1));
+
+//  ASSERT_EQ(1, test1Counter);
+
+//  // register an invalid observer, check it raises an exception
+//  MyPartialObserver partialObserver(broker);
+//  ASSERT_THROW(observable.RegisterObserver(partialObserver), OrthancStone::MessageNotDeclaredException);
+
+//  // check an exception is thrown when the observable emits an undeclared message
+//  ASSERT_THROW(observable.EmitMessage(OrthancStone::IMessage(OrthancStone::MessageType_LayerSource_GeometryReady)), OrthancStone::MessageNotDeclaredException);
+
+//  // unregister the observer, make sure nothing happens afterwards
+//  observable.UnregisterObserver(fullObserver);
+//  observable.EmitMessage(OrthancStone::IMessage(OrthancStone::MessageType_Test1));
+//  ASSERT_EQ(1, test1Counter);
+//}
+
+//TEST(MessageBroker, DeleteObserverWhileRegistered)
+//{
+//  OrthancStone::MessageBroker broker;
+//  MyObservable observable(broker);
+
+//  test1Counter = 0;
+
+//  {
+//    // register an observer, check it is called
+//    MyFullObserver observer(broker);
+//    observable.RegisterObserver(observer);
+
+//    observable.EmitMessage(OrthancStone::IMessage(OrthancStone::MessageType_Test1));
+
+//    ASSERT_EQ(1, test1Counter);
+//  }
+
+//  // at this point, the observer has been deleted, the handle shall not be called again (and it shall not crash !)
+//  observable.EmitMessage(OrthancStone::IMessage(OrthancStone::MessageType_Test1));
+
+//  ASSERT_EQ(1, test1Counter);
+//}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/UnitTestsSources/TestMessageBroker2.cpp	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,410 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 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 "gtest/gtest.h"
+
+#include "Framework/Messages/MessageBroker.h"
+#include "Framework/Messages/Promise.h"
+#include "Framework/Messages/IObservable.h"
+#include "Framework/Messages/IObserver.h"
+#include "Framework/Messages/MessageForwarder.h"
+
+
+int testCounter = 0;
+namespace {
+
+  using namespace OrthancStone;
+
+
+  enum CustomMessageType
+  {
+    CustomMessageType_First = MessageType_CustomMessage + 1,
+
+    CustomMessageType_Completed,
+    CustomMessageType_Increment
+  };
+
+
+  class MyObservable : public IObservable
+  {
+  public:
+    struct MyCustomMessage: public BaseMessage<CustomMessageType_Completed>
+    {
+      int payload_;
+
+      MyCustomMessage(int payload)
+        : BaseMessage(),
+          payload_(payload)
+      {}
+    };
+
+    MyObservable(MessageBroker& broker)
+      : IObservable(broker)
+    {}
+
+  };
+
+  class MyObserver : public IObserver
+  {
+  public:
+    MyObserver(MessageBroker& broker)
+      : IObserver(broker)
+    {}
+
+    void HandleCompletedMessage(const MyObservable::MyCustomMessage& message)
+    {
+      testCounter += message.payload_;
+    }
+
+  };
+
+
+  class MyIntermediate : public IObserver, public IObservable
+  {
+    IObservable& observedObject_;
+  public:
+    MyIntermediate(MessageBroker& broker, IObservable& observedObject)
+      : IObserver(broker),
+        IObservable(broker),
+        observedObject_(observedObject)
+    {
+      observedObject_.RegisterObserverCallback(new MessageForwarder<MyObservable::MyCustomMessage>(broker, *this));
+    }
+  };
+
+
+  class MyPromiseSource : public IObservable
+  {
+    Promise* currentPromise_;
+  public:
+    struct MyPromiseMessage: public BaseMessage<MessageType_Test1>
+    {
+      int increment;
+
+      MyPromiseMessage(int increment)
+        : BaseMessage(),
+          increment(increment)
+      {}
+    };
+
+    MyPromiseSource(MessageBroker& broker)
+      : IObservable(broker),
+        currentPromise_(NULL)
+    {}
+
+    Promise& StartSomethingAsync()
+    {
+      currentPromise_ = new Promise(broker_);
+      return *currentPromise_;
+    }
+
+    void CompleteSomethingAsyncWithSuccess(int payload)
+    {
+      currentPromise_->Success(MyPromiseMessage(payload));
+      delete currentPromise_;
+    }
+
+    void CompleteSomethingAsyncWithFailure(int payload)
+    {
+      currentPromise_->Failure(MyPromiseMessage(payload));
+      delete currentPromise_;
+    }
+  };
+
+
+  class MyPromiseTarget : public IObserver
+  {
+  public:
+    MyPromiseTarget(MessageBroker& broker)
+      : IObserver(broker)
+    {}
+
+    void IncrementCounter(const MyPromiseSource::MyPromiseMessage& args)
+    {
+      testCounter += args.increment;
+    }
+
+    void DecrementCounter(const MyPromiseSource::MyPromiseMessage& args)
+    {
+      testCounter -= args.increment;
+    }
+  };
+}
+
+
+TEST(MessageBroker2, TestPermanentConnectionSimpleUseCase)
+{
+  MessageBroker broker;
+  MyObservable  observable(broker);
+  MyObserver    observer(broker);
+
+  // create a permanent connection between an observable and an observer
+  observable.RegisterObserverCallback(new Callable<MyObserver, MyObservable::MyCustomMessage>(observer, &MyObserver::HandleCompletedMessage));
+
+  testCounter = 0;
+  observable.EmitMessage(MyObservable::MyCustomMessage(12));
+  ASSERT_EQ(12, testCounter);
+
+  // the connection is permanent; if we emit the same message again, the observer will be notified again
+  testCounter = 0;
+  observable.EmitMessage(MyObservable::MyCustomMessage(20));
+  ASSERT_EQ(20, testCounter);
+}
+
+TEST(MessageBroker2, TestMessageForwarderSimpleUseCase)
+{
+  MessageBroker broker;
+  MyObservable  observable(broker);
+  MyIntermediate intermediate(broker, observable);
+  MyObserver    observer(broker);
+
+  // let the observer observers the intermediate that is actually forwarding the messages from the observable
+  intermediate.RegisterObserverCallback(new Callable<MyObserver, MyObservable::MyCustomMessage>(observer, &MyObserver::HandleCompletedMessage));
+
+  testCounter = 0;
+  observable.EmitMessage(MyObservable::MyCustomMessage(12));
+  ASSERT_EQ(12, testCounter);
+
+  // the connection is permanent; if we emit the same message again, the observer will be notified again
+  testCounter = 0;
+  observable.EmitMessage(MyObservable::MyCustomMessage(20));
+  ASSERT_EQ(20, testCounter);
+}
+
+TEST(MessageBroker2, TestPermanentConnectionDeleteObserver)
+{
+  MessageBroker broker;
+  MyObservable  observable(broker);
+  MyObserver*   observer = new MyObserver(broker);
+
+  // create a permanent connection between an observable and an observer
+  observable.RegisterObserverCallback(new Callable<MyObserver, MyObservable::MyCustomMessage>(*observer, &MyObserver::HandleCompletedMessage));
+
+  testCounter = 0;
+  observable.EmitMessage(MyObservable::MyCustomMessage(12));
+  ASSERT_EQ(12, testCounter);
+
+  // delete the observer and check that the callback is not called anymore
+  delete observer;
+
+  // the connection is permanent; if we emit the same message again, the observer will be notified again
+  testCounter = 0;
+  observable.EmitMessage(MyObservable::MyCustomMessage(20));
+  ASSERT_EQ(0, testCounter);
+}
+
+TEST(MessageBroker2, TestMessageForwarderDeleteIntermediate)
+{
+  MessageBroker broker;
+  MyObservable  observable(broker);
+  MyIntermediate* intermediate = new MyIntermediate(broker, observable);
+  MyObserver    observer(broker);
+
+  // let the observer observers the intermediate that is actually forwarding the messages from the observable
+  intermediate->RegisterObserverCallback(new Callable<MyObserver, MyObservable::MyCustomMessage>(observer, &MyObserver::HandleCompletedMessage));
+
+  testCounter = 0;
+  observable.EmitMessage(MyObservable::MyCustomMessage(12));
+  ASSERT_EQ(12, testCounter);
+
+  delete intermediate;
+
+  observable.EmitMessage(MyObservable::MyCustomMessage(20));
+  ASSERT_EQ(12, testCounter);
+}
+
+TEST(MessageBroker2, TestCustomMessage)
+{
+  MessageBroker broker;
+  MyObservable  observable(broker);
+  MyIntermediate intermediate(broker, observable);
+  MyObserver    observer(broker);
+
+  // let the observer observers the intermediate that is actually forwarding the messages from the observable
+  intermediate.RegisterObserverCallback(new Callable<MyObserver, MyObservable::MyCustomMessage>(observer, &MyObserver::HandleCompletedMessage));
+
+  testCounter = 0;
+  observable.EmitMessage(MyObservable::MyCustomMessage(12));
+  ASSERT_EQ(12, testCounter);
+
+  // the connection is permanent; if we emit the same message again, the observer will be notified again
+  testCounter = 0;
+  observable.EmitMessage(MyObservable::MyCustomMessage(20));
+  ASSERT_EQ(20, testCounter);
+}
+
+
+TEST(MessageBroker2, TestPromiseSuccessFailure)
+{
+  MessageBroker broker;
+  MyPromiseSource  source(broker);
+  MyPromiseTarget target(broker);
+
+  // test a successful promise
+  source.StartSomethingAsync()
+      .Then(new Callable<MyPromiseTarget, MyPromiseSource::MyPromiseMessage>(target, &MyPromiseTarget::IncrementCounter))
+      .Else(new Callable<MyPromiseTarget, MyPromiseSource::MyPromiseMessage>(target, &MyPromiseTarget::DecrementCounter));
+
+  testCounter = 0;
+  source.CompleteSomethingAsyncWithSuccess(10);
+  ASSERT_EQ(10, testCounter);
+
+  // test a failing promise
+  source.StartSomethingAsync()
+      .Then(new Callable<MyPromiseTarget, MyPromiseSource::MyPromiseMessage>(target, &MyPromiseTarget::IncrementCounter))
+      .Else(new Callable<MyPromiseTarget, MyPromiseSource::MyPromiseMessage>(target, &MyPromiseTarget::DecrementCounter));
+
+  testCounter = 0;
+  source.CompleteSomethingAsyncWithFailure(15);
+  ASSERT_EQ(-15, testCounter);
+}
+
+TEST(MessageBroker2, TestPromiseDeleteTarget)
+{
+  MessageBroker broker;
+  MyPromiseSource source(broker);
+  MyPromiseTarget* target = new MyPromiseTarget(broker);
+
+  // create the promise
+  source.StartSomethingAsync()
+      .Then(new Callable<MyPromiseTarget, MyPromiseSource::MyPromiseMessage>(*target, &MyPromiseTarget::IncrementCounter))
+      .Else(new Callable<MyPromiseTarget, MyPromiseSource::MyPromiseMessage>(*target, &MyPromiseTarget::DecrementCounter));
+
+  // delete the promise target
+  delete target;
+
+  // trigger the promise, make sure it does not throw and does not call the callback
+  testCounter = 0;
+  source.CompleteSomethingAsyncWithSuccess(10);
+  ASSERT_EQ(0, testCounter);
+
+  // test a failing promise
+  source.StartSomethingAsync()
+      .Then(new Callable<MyPromiseTarget, MyPromiseSource::MyPromiseMessage>(*target, &MyPromiseTarget::IncrementCounter))
+      .Else(new Callable<MyPromiseTarget, MyPromiseSource::MyPromiseMessage>(*target, &MyPromiseTarget::DecrementCounter));
+
+  testCounter = 0;
+  source.CompleteSomethingAsyncWithFailure(15);
+  ASSERT_EQ(0, testCounter);
+}
+
+#if __cplusplus >= 201103L
+
+#include <functional>
+
+namespace OrthancStone {
+
+  template <typename TMessage>
+  class LambdaCallable : public MessageHandler<TMessage>
+  {
+  private:
+
+    IObserver&      observer_;
+    std::function<void (const TMessage&)> lambda_;
+
+  public:
+    LambdaCallable(IObserver& observer,
+                    std::function<void (const TMessage&)> lambdaFunction) :
+             observer_(observer),
+             lambda_(lambdaFunction)
+    {
+    }
+
+    virtual void Apply(const IMessage& message)
+    {
+      lambda_(dynamic_cast<const TMessage&>(message));
+    }
+
+    virtual MessageType GetMessageType() const
+    {
+      return static_cast<MessageType>(TMessage::Type);
+    }
+
+    virtual IObserver* GetObserver() const
+    {
+      return &observer_;
+    }
+  };
+
+
+}
+
+TEST(MessageBroker2, TestLambdaSimpleUseCase)
+{
+  MessageBroker broker;
+  MyObservable  observable(broker);
+  MyObserver*   observer = new MyObserver(broker);
+
+  // create a permanent connection between an observable and an observer
+  observable.RegisterObserverCallback(new LambdaCallable<MyObservable::MyCustomMessage>(*observer, [&](const MyObservable::MyCustomMessage& message) {testCounter += 2 * message.payload_;}));
+
+  testCounter = 0;
+  observable.EmitMessage(MyObservable::MyCustomMessage(12));
+  ASSERT_EQ(24, testCounter);
+
+  // delete the observer and check that the callback is not called anymore
+  delete observer;
+
+  // the connection is permanent; if we emit the same message again, the observer will be notified again
+  testCounter = 0;
+  observable.EmitMessage(MyObservable::MyCustomMessage(20));
+  ASSERT_EQ(0, testCounter);
+}
+
+namespace {
+  class MyObserverWithLambda : public IObserver {
+  private:
+    int multiplier_;  // this is a private variable we want to access in a lambda
+
+  public:
+    MyObserverWithLambda(MessageBroker& broker, int multiplier, MyObservable& observable)
+      : IObserver(broker),
+        multiplier_(multiplier)
+    {
+      // register a callable to a lambda that access private members
+      observable.RegisterObserverCallback(new LambdaCallable<MyObservable::MyCustomMessage>(*this, [this](const MyObservable::MyCustomMessage& message) {
+        testCounter += multiplier_ * message.payload_;
+      }));
+
+    }
+  };
+}
+
+TEST(MessageBroker2, TestLambdaCaptureThisAndAccessPrivateMembers)
+{
+  MessageBroker broker;
+  MyObservable  observable(broker);
+  MyObserverWithLambda*   observer = new MyObserverWithLambda(broker, 3, observable);
+
+  testCounter = 0;
+  observable.EmitMessage(MyObservable::MyCustomMessage(12));
+  ASSERT_EQ(36, testCounter);
+
+  // delete the observer and check that the callback is not called anymore
+  delete observer;
+
+  // the connection is permanent; if we emit the same message again, the observer will be notified again
+  testCounter = 0;
+  observable.EmitMessage(MyObservable::MyCustomMessage(20));
+  ASSERT_EQ(0, testCounter);
+}
+
+#endif // C++ 11
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/UnitTestsSources/TestMessageBroker2_connect_ok.cpp	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,226 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 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 "gtest/gtest.h"
+
+#include <boost/noncopyable.hpp>
+#include <boost/function.hpp>
+#include <boost/bind.hpp>
+
+#include <string>
+#include <map>
+#include <set>
+
+int testCounter = 0;
+namespace {
+
+  enum MessageType
+  {
+    // used in unit tests only
+    MessageType_Test1,
+    MessageType_Test2,
+
+    MessageType_LastGenericStoneMessage
+  };
+
+  struct IMessage  : public boost::noncopyable
+  {
+    MessageType messageType_;
+  public:
+    IMessage(const MessageType& messageType)
+      : messageType_(messageType)
+    {}
+    virtual ~IMessage() {}
+
+    MessageType GetType() const {return messageType_;}
+  };
+
+
+  class IObserver;
+  class IObservable;
+
+  /*
+   * This is a central message broker.  It keeps track of all observers and knows
+   * when an observer is deleted.
+   * This way, it can prevent an observable to send a message to a dead observer.
+   */
+  class MessageBroker : public boost::noncopyable
+  {
+
+    std::set<IObserver*> activeObservers_;  // the list of observers that are currently alive (that have not been deleted)
+
+  public:
+
+    void Register(IObserver& observer)
+    {
+      activeObservers_.insert(&observer);
+    }
+
+    void Unregister(IObserver& observer)
+    {
+      activeObservers_.erase(&observer);
+    }
+
+    void EmitMessage(IObservable& from, std::set<IObserver*> observers, const IMessage& message);
+  };
+
+
+  class IObserver : public boost::noncopyable
+  {
+  protected:
+    MessageBroker&                    broker_;
+
+  public:
+    IObserver(MessageBroker& broker)
+      : broker_(broker)
+    {
+      broker_.Register(*this);
+    }
+
+    virtual ~IObserver()
+    {
+      broker_.Unregister(*this);
+    }
+
+    void HandleMessage_(IObservable &from, const IMessage &message)
+    {
+
+      HandleMessage(from, message);
+    }
+
+    virtual void HandleMessage(IObservable& from, const IMessage& message) = 0;
+
+
+  protected:
+
+
+  };
+
+//  struct ICallableObserver
+//  {
+//    IObserver* observer;
+//  };
+
+//  typedef void (IObserver::*ObserverSingleMesssageHandler)(IObservable& from, const IMessage& message);
+
+//  template <typename TObserver>
+//  struct CallableObserver : public ICallableObserver
+//  {
+//    void (TObserver::*ptrToMemberHandler)(IObservable& from, const IMessage& message);
+//  };
+
+  struct CallableObserver
+  {
+    IObserver* observer;
+    boost::function<void (IObservable& from, const IMessage& message)> f;
+  };
+
+  class IObservable : public boost::noncopyable
+  {
+  protected:
+    MessageBroker&                     broker_;
+
+    std::set<IObserver*>              observers_;
+
+    std::map<MessageType, std::set<CallableObserver*> > callables_;
+  public:
+
+    IObservable(MessageBroker& broker)
+      : broker_(broker)
+    {
+    }
+    virtual ~IObservable()
+    {
+    }
+
+    void EmitMessage(const IMessage& message)
+    {
+      //broker_.EmitMessage(*this, observers_, message);
+
+      // TODO check if observer is still alive and call !
+      CallableObserver* callable = *(callables_[message.GetType()].begin());
+      callable->f(*this, message);
+    }
+
+    void RegisterObserver(IObserver& observer)
+    {
+      observers_.insert(&observer);
+    }
+
+    void UnregisterObserver(IObserver& observer)
+    {
+      observers_.erase(&observer);
+    }
+
+
+    //template<typename TObserver> void Connect(MessageType messageType, IObserver& observer, void (TObserver::*ptrToMemberHandler)(IObservable& from, const IMessage& message))
+    void Connect(MessageType messageType, IObserver& observer, boost::function<void (IObservable& from, const IMessage& message)> f)
+    {
+      callables_[messageType] = std::set<CallableObserver*>();
+      CallableObserver* callable = new CallableObserver();
+      callable->observer = &observer;
+      callable->f = f;
+      callables_[messageType].insert(callable);
+    }
+  };
+
+
+  class MyObservable : public IObservable
+  {
+  public:
+    MyObservable(MessageBroker& broker)
+      : IObservable(broker)
+    {}
+  };
+
+  class MyObserver : public IObserver
+  {
+  public:
+    MyObserver(MessageBroker& broker)
+      : IObserver(broker)
+    {}
+    virtual void HandleMessage(IObservable& from, const IMessage& message) {}
+    void HandleSpecificMessage(IObservable& from, const IMessage& message)
+    {
+      testCounter++;
+    }
+
+  };
+
+}
+
+//#define STONE_CONNECT(observabe, messageType, observerPtr, observerMemberFnPtr)
+
+TEST(MessageBroker2, Test1)
+{
+  MessageBroker broker;
+  MyObservable  observable(broker);
+  MyObserver    observer(broker);
+
+
+  observable.Connect(MessageType_Test1, observer, boost::bind(&MyObserver::HandleSpecificMessage, &observer, _1, _2));
+  //STONE_CONNECT(observable, MessageType_Test1, observer, &MyObserver::HandleSpecificMessage)
+  observable.EmitMessage(IMessage(MessageType_Test1));
+
+  ASSERT_EQ(1, testCounter);
+}
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/UnitTestsSources/TestMessageBroker2_promise_and_connect_ok.cpp	Mon Nov 05 10:06:18 2018 +0100
@@ -0,0 +1,520 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 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 "gtest/gtest.h"
+
+#include <boost/noncopyable.hpp>
+#include <boost/function.hpp>
+#include <boost/bind.hpp>
+
+#include <string>
+#include <map>
+#include <set>
+
+int testCounter = 0;
+namespace {
+
+  enum MessageType
+  {
+    MessageType_Test1,
+    MessageType_Test2,
+
+    MessageType_CustomMessage,
+    MessageType_LastGenericStoneMessage
+  };
+
+  struct IMessage  : public boost::noncopyable
+  {
+    MessageType messageType_;
+  public:
+    IMessage(const MessageType& messageType)
+      : messageType_(messageType)
+    {}
+    virtual ~IMessage() {}
+
+    virtual int GetType() const {return messageType_;}
+  };
+
+
+  struct ICustomMessage  : public IMessage
+  {
+    int customMessageType_;
+  public:
+    ICustomMessage(int customMessageType)
+      : IMessage(MessageType_CustomMessage),
+        customMessageType_(customMessageType)
+    {}
+    virtual ~ICustomMessage() {}
+
+    virtual int GetType() const {return customMessageType_;}
+  };
+
+
+  class IObserver;
+  class IObservable;
+  class IPromiseTarget;
+  class IPromiseSource;
+  class Promise;
+
+  /*
+   * This is a central message broker.  It keeps track of all observers and knows
+   * when an observer is deleted.
+   * This way, it can prevent an observable to send a message to a delete observer.
+   * It does the same book-keeping for the IPromiseTarget and IPromiseSource
+   */
+  class MessageBroker : public boost::noncopyable
+  {
+
+    std::set<IObserver*> activeObservers_;  // the list of observers that are currently alive (that have not been deleted)
+    std::set<IPromiseTarget*> activePromiseTargets_;
+    std::set<IPromiseSource*> activePromiseSources_;
+
+  public:
+
+    void Register(IObserver& observer)
+    {
+      activeObservers_.insert(&observer);
+    }
+
+    void Unregister(IObserver& observer)
+    {
+      activeObservers_.erase(&observer);
+    }
+
+    void Register(IPromiseTarget& target)
+    {
+      activePromiseTargets_.insert(&target);
+    }
+
+    void Unregister(IPromiseTarget& target)
+    {
+      activePromiseTargets_.erase(&target);
+    }
+
+    void Register(IPromiseSource& source)
+    {
+      activePromiseSources_.insert(&source);
+    }
+
+    void Unregister(IPromiseSource& source)
+    {
+      activePromiseSources_.erase(&source);
+    }
+
+    void EmitMessage(IObservable& from, std::set<IObserver*> observers, const IMessage& message);
+
+    bool IsActive(IPromiseTarget* target)
+    {
+      return activePromiseTargets_.find(target) != activePromiseTargets_.end();
+    }
+
+    bool IsActive(IPromiseSource* source)
+    {
+      return activePromiseSources_.find(source) != activePromiseSources_.end();
+    }
+
+    bool IsActive(IObserver* observer)
+    {
+      return activeObservers_.find(observer) != activeObservers_.end();
+    }
+  };
+
+  struct IPromiseArgs
+  {
+public:
+    virtual ~IPromiseArgs() {}
+  };
+
+  class EmptyPromiseArguments : public IPromiseArgs
+  {
+
+  };
+
+  class Promise : public boost::noncopyable
+  {
+  protected:
+    MessageBroker&                    broker_;
+
+    IPromiseTarget*                                           successTarget_;
+    boost::function<void (const IPromiseArgs& message)>       successCallable_;
+
+    IPromiseTarget*                                           failureTarget_;
+    boost::function<void (const IPromiseArgs& message)>       failureCallable_;
+
+  public:
+    Promise(MessageBroker& broker)
+      : broker_(broker),
+        successTarget_(NULL),
+        failureTarget_(NULL)
+    {
+    }
+
+    void Success(const IPromiseArgs& message)
+    {
+      // check the target is still alive in the broker
+      if (broker_.IsActive(successTarget_))
+      {
+        successCallable_(message);
+      }
+    }
+
+    void Failure(const IPromiseArgs& message)
+    {
+      // check the target is still alive in the broker
+      if (broker_.IsActive(failureTarget_))
+      {
+        failureCallable_(message);
+      }
+    }
+
+    Promise& Then(IPromiseTarget* target, boost::function<void (const IPromiseArgs& message)> f)
+    {
+      if (successTarget_ != NULL)
+      {
+        // TODO: throw throw new "Promise may only have a single success target"
+      }
+      successTarget_ = target;
+      successCallable_ = f;
+      return *this;
+    }
+
+    Promise& Else(IPromiseTarget* target, boost::function<void (const IPromiseArgs& message)> f)
+    {
+      if (failureTarget_ != NULL)
+      {
+        // TODO: throw throw new "Promise may only have a single failure target"
+      }
+      failureTarget_ = target;
+      failureCallable_ = f;
+      return *this;
+    }
+
+  };
+
+  class IObserver : public boost::noncopyable
+  {
+  protected:
+    MessageBroker&                    broker_;
+
+  public:
+    IObserver(MessageBroker& broker)
+      : broker_(broker)
+    {
+      broker_.Register(*this);
+    }
+
+    virtual ~IObserver()
+    {
+      broker_.Unregister(*this);
+    }
+
+  };
+
+  class IPromiseTarget : public boost::noncopyable
+  {
+  protected:
+    MessageBroker&                    broker_;
+
+  public:
+    IPromiseTarget(MessageBroker& broker)
+      : broker_(broker)
+    {
+      broker_.Register(*this);
+    }
+
+    virtual ~IPromiseTarget()
+    {
+      broker_.Unregister(*this);
+    }
+  };
+
+  class IPromiseSource : public boost::noncopyable
+  {
+  protected:
+    MessageBroker&                    broker_;
+
+  public:
+    IPromiseSource(MessageBroker& broker)
+      : broker_(broker)
+    {
+      broker_.Register(*this);
+    }
+
+    virtual ~IPromiseSource()
+    {
+      broker_.Unregister(*this);
+    }
+  };
+
+
+  struct CallableObserver
+  {
+    IObserver* observer;
+    boost::function<void (IObservable& from, const IMessage& message)> f;
+  };
+
+  class IObservable : public boost::noncopyable
+  {
+  protected:
+    MessageBroker&                     broker_;
+
+    std::set<IObserver*>              observers_;
+
+    std::map<int, std::set<CallableObserver*> > callables_;
+  public:
+
+    IObservable(MessageBroker& broker)
+      : broker_(broker)
+    {
+    }
+    virtual ~IObservable()
+    {
+    }
+
+    void EmitMessage(const IMessage& message)
+    {
+      //broker_.EmitMessage(*this, observers_, message);
+      int messageType = message.GetType();
+      if (callables_.find(messageType) != callables_.end())
+      {
+        for (std::set<CallableObserver*>::iterator observer = callables_[messageType].begin(); observer != callables_[messageType].end(); observer++)
+        {
+          CallableObserver* callable = *observer;
+          if (broker_.IsActive(callable->observer))
+          {
+            callable->f(*this, message);
+          }
+        }
+      }
+
+    }
+
+    void RegisterObserver(IObserver& observer)
+    {
+      observers_.insert(&observer);
+    }
+
+    void UnregisterObserver(IObserver& observer)
+    {
+      observers_.erase(&observer);
+    }
+
+    //template<typename TObserver> void Connect(MessageType messageType, IObserver& observer, void (TObserver::*ptrToMemberHandler)(IObservable& from, const IMessage& message))
+    void Connect(int messageType, IObserver& observer, boost::function<void (IObservable& from, const IMessage& message)> f)
+    {
+      callables_[messageType] = std::set<CallableObserver*>();
+      CallableObserver* callable = new CallableObserver();
+      callable->observer = &observer;
+      callable->f = f;
+      callables_[messageType].insert(callable);
+    }
+  };
+
+
+  enum CustomMessageType
+  {
+    CustomMessageType_First = MessageType_LastGenericStoneMessage + 1,
+
+    CustomMessageType_Completed
+  };
+
+  class MyObservable : public IObservable
+  {
+  public:
+    struct MyCustomMessage: public ICustomMessage
+    {
+      int payload_;
+      MyCustomMessage(int payload)
+        : ICustomMessage(CustomMessageType_Completed),
+          payload_(payload)
+      {}
+    };
+
+    MyObservable(MessageBroker& broker)
+      : IObservable(broker)
+    {}
+
+  };
+
+  class MyObserver : public IObserver
+  {
+  public:
+    MyObserver(MessageBroker& broker)
+      : IObserver(broker)
+    {}
+    void HandleCompletedMessage(IObservable& from, const IMessage& message)
+    {
+      const MyObservable::MyCustomMessage& msg = dynamic_cast<const MyObservable::MyCustomMessage&>(message);
+      testCounter += msg.payload_;
+    }
+
+  };
+
+
+  class MyPromiseSource : public IPromiseSource
+  {
+    Promise* currentPromise_;
+  public:
+    struct MyPromiseArgs : public IPromiseArgs
+    {
+      int increment;
+    };
+
+    MyPromiseSource(MessageBroker& broker)
+      : IPromiseSource(broker),
+        currentPromise_(NULL)
+    {}
+
+    Promise& StartSomethingAsync()
+    {
+      currentPromise_ = new Promise(broker_);
+      return *currentPromise_;
+    }
+
+    void CompleteSomethingAsyncWithSuccess()
+    {
+      currentPromise_->Success(EmptyPromiseArguments());
+      delete currentPromise_;
+    }
+
+    void CompleteSomethingAsyncWithFailure()
+    {
+      currentPromise_->Failure(EmptyPromiseArguments());
+      delete currentPromise_;
+    }
+  };
+
+
+  class MyPromiseTarget : public IPromiseTarget
+  {
+  public:
+    MyPromiseTarget(MessageBroker& broker)
+      : IPromiseTarget(broker)
+    {}
+
+    void IncrementCounter(const IPromiseArgs& args)
+    {
+      testCounter++;
+    }
+
+    void DecrementCounter(const IPromiseArgs& args)
+    {
+      testCounter--;
+    }
+  };
+}
+
+#define CONNECT_MESSAGES(observablePtr, messageType, observerPtr, observerFnPtr) (observablePtr)->Connect(messageType, *(observerPtr), boost::bind(observerFnPtr, observerPtr, _1, _2))
+#define PTHEN(targetPtr, targetFnPtr) Then(targetPtr, boost::bind(targetFnPtr, targetPtr, _1))
+#define PELSE(targetPtr, targetFnPtr) Else(targetPtr, boost::bind(targetFnPtr, targetPtr, _1))
+
+
+TEST(MessageBroker2, TestPermanentConnectionSimpleUseCase)
+{
+  MessageBroker broker;
+  MyObservable  observable(broker);
+  MyObserver    observer(broker);
+
+  // create a permanent connection between an observable and an observer
+  CONNECT_MESSAGES(&observable, CustomMessageType_Completed, &observer, &MyObserver::HandleCompletedMessage);
+
+  testCounter = 0;
+  observable.EmitMessage(MyObservable::MyCustomMessage(12));
+  ASSERT_EQ(12, testCounter);
+
+  // the connection is permanent; if we emit the same message again, the observer will be notified again
+  testCounter = 0;
+  observable.EmitMessage(MyObservable::MyCustomMessage(20));
+  ASSERT_EQ(20, testCounter);
+}
+
+TEST(MessageBroker2, TestPermanentConnectionDeleteObserver)
+{
+  MessageBroker broker;
+  MyObservable  observable(broker);
+  MyObserver*   observer = new MyObserver(broker);
+
+  // create a permanent connection between an observable and an observer
+  CONNECT_MESSAGES(&observable, CustomMessageType_Completed, observer, &MyObserver::HandleCompletedMessage);
+
+  testCounter = 0;
+  observable.EmitMessage(MyObservable::MyCustomMessage(12));
+  ASSERT_EQ(12, testCounter);
+
+  // delete the observer and check that the callback is not called anymore
+  delete observer;
+
+  // the connection is permanent; if we emit the same message again, the observer will be notified again
+  testCounter = 0;
+  observable.EmitMessage(MyObservable::MyCustomMessage(20));
+  ASSERT_EQ(0, testCounter);
+}
+
+
+TEST(MessageBroker2, TestPromiseSuccessFailure)
+{
+  MessageBroker broker;
+  MyPromiseSource  source(broker);
+  MyPromiseTarget target(broker);
+
+  // test a successful promise
+  source.StartSomethingAsync()
+      .PTHEN(&target, &MyPromiseTarget::IncrementCounter)
+      .PELSE(&target, &MyPromiseTarget::DecrementCounter);
+
+  testCounter = 0;
+  source.CompleteSomethingAsyncWithSuccess();
+  ASSERT_EQ(1, testCounter);
+
+  // test a failing promise
+  source.StartSomethingAsync()
+      .PTHEN(&target, &MyPromiseTarget::IncrementCounter)
+      .PELSE(&target, &MyPromiseTarget::DecrementCounter);
+
+  testCounter = 0;
+  source.CompleteSomethingAsyncWithFailure();
+  ASSERT_EQ(-1, testCounter);
+}
+
+//TEST(MessageBroker2, TestPromiseDeleteTarget)
+//{
+//  MessageBroker broker;
+//  MyPromiseSource  source(broker);
+//  MyPromiseTarget target(broker);
+
+//  // test a successful promise
+//  source.StartSomethingAsync()
+//      .PTHEN(&target, &MyPromiseTarget::IncrementCounter)
+//      .PELSE(&target, &MyPromiseTarget::DecrementCounter);
+
+//  testCounter = 0;
+//  source.CompleteSomethingAsyncWithSuccess();
+//  ASSERT_EQ(1, testCounter);
+
+//  // test a failing promise
+//  source.StartSomethingAsync()
+//      .PTHEN(&target, &MyPromiseTarget::IncrementCounter)
+//      .PELSE(&target, &MyPromiseTarget::DecrementCounter);
+
+//  testCounter = 0;
+//  source.CompleteSomethingAsyncWithFailure();
+//  ASSERT_EQ(-1, testCounter);
+//}
--- a/UnitTestsSources/UnitTestsMain.cpp	Mon Nov 05 10:04:56 2018 +0100
+++ b/UnitTestsSources/UnitTestsMain.cpp	Mon Nov 05 10:06:18 2018 +0100
@@ -26,6 +26,7 @@
 #include "../Framework/Layers/LayerSourceBase.h"
 #include "../Framework/Toolbox/DownloadStack.h"
 #include "../Framework/Toolbox/FiniteProjectiveCamera.h"
+#include "../Framework/Toolbox/MessagingToolbox.h"
 #include "../Framework/Toolbox/OrthancSlicesLoader.h"
 #include "../Framework/Volumes/ImageBuffer3D.h"
 #include "../Framework/Volumes/SlicedVolumeBase.h"
@@ -55,7 +56,7 @@
 
       for (size_t i = 0; i < loader.GetSliceCount(); i++)
       {
-        const_cast<OrthancSlicesLoader&>(loader).ScheduleLoadSliceImage(i, SliceImageQuality_Full);
+        const_cast<OrthancSlicesLoader&>(loader).ScheduleLoadSliceImage(i, SliceImageQuality_FullPng);
       }
     }
 
@@ -724,6 +725,12 @@
   */
 }
 
+TEST(MessagingToolbox, ParseJson)
+{
+  Json::Value response;
+  std::string source = "{\"command\":\"panel:takeDarkImage\",\"commandType\":\"simple\",\"args\":{}}";
+  ASSERT_TRUE(OrthancStone::MessagingToolbox::ParseJson(response, source.c_str(), source.size()));
+}
 
 int main(int argc, char **argv)
 {