changeset 319:daa04d15192c am-2

new SimpleViewer sample that has been split in multiple files to be able to scale it
author am@osimis.io
date Thu, 11 Oct 2018 13:16:54 +0200
parents 3a4ca166fafa
children 10d188d6e5cc
files .hgignore Applications/Commands/BaseCommandBuilder.cpp Applications/Commands/ICommand.h Applications/IStoneApplication.h Applications/Qt/QtStoneApplicationRunner.cpp Applications/Qt/QtStoneApplicationRunner.h Applications/Samples/CMakeLists.txt Applications/Samples/SampleList.h Applications/Samples/SimpleViewer/AppStatus.h Applications/Samples/SimpleViewer/MainWidgetInteractor.cpp Applications/Samples/SimpleViewer/MainWidgetInteractor.h Applications/Samples/SimpleViewer/Messages.h Applications/Samples/SimpleViewer/Qt/SimpleViewerMainWindow.cpp Applications/Samples/SimpleViewer/Qt/SimpleViewerMainWindow.h Applications/Samples/SimpleViewer/Qt/SimpleViewerMainWindow.ui Applications/Samples/SimpleViewer/Qt/mainQt.cpp Applications/Samples/SimpleViewer/SimpleViewerApplication.cpp Applications/Samples/SimpleViewer/SimpleViewerApplication.h Applications/Samples/SimpleViewer/ThumbnailInteractor.cpp Applications/Samples/SimpleViewer/ThumbnailInteractor.h Applications/Samples/SimpleViewer/Wasm/SimpleViewerWasmApplicationAdapter.cpp Applications/Samples/SimpleViewer/Wasm/SimpleViewerWasmApplicationAdapter.h Applications/Samples/SimpleViewer/Wasm/mainWasm.cpp Applications/Samples/SimpleViewer/Wasm/simple-viewer.html Applications/Samples/SimpleViewer/Wasm/simple-viewer.ts Applications/Samples/SimpleViewer/Wasm/styles.css Applications/Samples/SimpleViewer/Wasm/tsconfig-simple-viewer.json Applications/Samples/SimpleViewerApplication.h Applications/Samples/SimpleViewerApplicationSingleFile.h Applications/Samples/Web/index.html Applications/Samples/Web/simple-viewer-single-file.html Applications/Samples/Web/simple-viewer-single-file.ts Applications/Samples/Web/simple-viewer.html Applications/Samples/Web/simple-viewer.ts Applications/Samples/Web/tsconfig-simple-viewer-single-file.json Applications/Samples/Web/tsconfig-simple-viewer.json Applications/Samples/build-web.sh Platforms/Wasm/WasmPlatformApplicationAdapter.cpp
diffstat 38 files changed, 1951 insertions(+), 550 deletions(-) [+]
line wrap: on
line diff
--- a/.hgignore	Mon Oct 08 17:10:08 2018 +0200
+++ b/.hgignore	Thu Oct 11 13:16:54 2018 +0200
@@ -4,3 +4,4 @@
 Applications/Samples/ThirdPartyDownloads/
 Applications/Samples/build-wasm/
 Applications/Samples/build-web/
+.vscode/
--- a/Applications/Commands/BaseCommandBuilder.cpp	Mon Oct 08 17:10:08 2018 +0200
+++ b/Applications/Commands/BaseCommandBuilder.cpp	Thu Oct 11 13:16:54 2018 +0200
@@ -32,32 +32,18 @@
       throw StoneException(ErrorCode_CommandJsonInvalidFormat);
     }
 
-    if (commandJson["commandType"].isString() && commandJson["commandType"].asString() == "simple")
+    if (commandJson["commandType"].isString() && commandJson["commandType"].asString() == "generic-no-arg-command")
     {
         printf("creating a simple command\n");
-        return new SimpleCommand(commandJson["command"].asString().c_str());
+        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;
-    // std::string commandName = commandJson["command"].asString();
-
-
-
-
-    // CommandCreationFunctions::const_iterator it = commands_.find(commandName);
-    // if (it == commands_.end())
-    // {
-    //   throw Orthanc::OrthancException(Orthanc::ErrorCode_UnknownResource);  // TODO: use StoneException ?
-    // }
-
-    // // call the CreateCommandFn to build the command
-    // ICommand* command = it->second();
-    // if (commandJson["args"].isObject())
-    // {
-    //   command->Configure(commandJson["args"]);
-    // }
-
-    // return command;
   }
 
 }
--- a/Applications/Commands/ICommand.h	Mon Oct 08 17:10:08 2018 +0200
+++ b/Applications/Commands/ICommand.h	Thu Oct 11 13:16:54 2018 +0200
@@ -70,13 +70,25 @@
     virtual void Execute() {}
   };
 
-  class SimpleCommand : public BaseCommand<SimpleCommand>
+  class GenericNoArgCommand : public BaseCommand<GenericNoArgCommand>
   {
   public:
-    SimpleCommand(const std::string& name)
+    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 !
+  };
+
 }
--- a/Applications/IStoneApplication.h	Mon Oct 08 17:10:08 2018 +0200
+++ b/Applications/IStoneApplication.h	Thu Oct 11 13:16:54 2018 +0200
@@ -28,8 +28,13 @@
 #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.
@@ -50,6 +55,9 @@
 #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;
--- a/Applications/Qt/QtStoneApplicationRunner.cpp	Mon Oct 08 17:10:08 2018 +0200
+++ b/Applications/Qt/QtStoneApplicationRunner.cpp	Thu Oct 11 13:16:54 2018 +0200
@@ -50,11 +50,11 @@
   {
     context.Start();
 
-    QApplication app(argc, argv);
-    InitializeMainWindow(context);
+    QApplication qtApplication(argc, argv);
+    window_.reset(application_.CreateQtMainWindow());
 
     window_->show();
-    app.exec();
+    qtApplication.exec();
 
     context.Stop();
   }
--- a/Applications/Qt/QtStoneApplicationRunner.h	Mon Oct 08 17:10:08 2018 +0200
+++ b/Applications/Qt/QtStoneApplicationRunner.h	Thu Oct 11 13:16:54 2018 +0200
@@ -35,7 +35,6 @@
   protected:
     std::auto_ptr<QStoneMainWindow> window_;
 
-    virtual void InitializeMainWindow(NativeStoneApplicationContext& context) = 0;
   public:
     QtStoneApplicationRunner(MessageBroker& broker,
                              IStoneApplication& application)
--- a/Applications/Samples/CMakeLists.txt	Mon Oct 08 17:10:08 2018 +0200
+++ b/Applications/Samples/CMakeLists.txt	Thu Oct 11 13:16:54 2018 +0200
@@ -85,13 +85,13 @@
 include_directories(${ORTHANC_STONE_ROOT})
 
 # files common to all samples
-list(APPEND APPLICATIONS_SOURCES
+list(APPEND SAMPLE_APPLICATIONS_SOURCES
   ${ORTHANC_STONE_ROOT}/Applications/Samples/SampleInteractor.h
   ${ORTHANC_STONE_ROOT}/Applications/Samples/SampleApplicationBase.h
   )
 
 if (ENABLE_QT)
-  list(APPEND APPLICATIONS_SOURCES
+  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/SampleMainWindow.ui
@@ -99,13 +99,13 @@
 endif()
 
 if (ENABLE_NATIVE)
-  list(APPEND APPLICATIONS_SOURCES
+  list(APPEND SAMPLE_APPLICATIONS_SOURCES
     ${ORTHANC_STONE_ROOT}/Applications/Samples/SampleMainNative.cpp
     )
 
 elseif (ENABLE_WASM)
 
-  list(APPEND APPLICATIONS_SOURCES
+  list(APPEND SAMPLE_APPLICATIONS_SOURCES
     ${ORTHANC_STONE_ROOT}/Applications/Samples/SampleMainWasm.cpp
     ${STONE_WASM_SOURCES}
     )
@@ -115,7 +115,7 @@
 macro(BuildSingleFileSample Target Header Sample)
   add_executable(${Target}
     ${ORTHANC_STONE_ROOT}/Applications/Samples/${Header}
-    ${APPLICATIONS_SOURCES}
+    ${SAMPLE_APPLICATIONS_SOURCES}
     )
   set_target_properties(${Target} PROPERTIES COMPILE_DEFINITIONS ORTHANC_STONE_SAMPLE=${Sample})
   target_link_libraries(${Target} OrthancStone)
@@ -128,7 +128,34 @@
 #BuildSingleFileSample(OrthancStoneBasicPetCtFusion 5)
 #BuildSingleFileSample(OrthancStoneSynchronizedSeries 6)
 #BuildSingleFileSample(OrthancStoneLayoutPetCtFusion 7)
-BuildSingleFileSample(OrthancStoneSimpleViewer SimpleViewerApplication.h 8)
+BuildSingleFileSample(OrthancStoneSimpleViewerSingleFile SimpleViewerApplicationSingleFile.h 8)  # we keep that one just as a sample before we convert another sample to this pattern
+
+##### SimpleViewer sample #######
+
+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
+    )
+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)
+
 
 #####################################################################
 ## Build the unit tests
--- a/Applications/Samples/SampleList.h	Mon Oct 08 17:10:08 2018 +0200
+++ b/Applications/Samples/SampleList.h	Thu Oct 11 13:16:54 2018 +0200
@@ -29,7 +29,7 @@
 typedef OrthancStone::Samples::LayoutPetCtFusionApplication SampleApplication;
 
 #elif ORTHANC_STONE_SAMPLE == 8
-#include "SimpleViewerApplication.h"
+#include "SimpleViewerApplicationSingleFile.h"
 typedef OrthancStone::Samples::SimpleViewerApplication SampleApplication;
 
 #else
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/SimpleViewer/AppStatus.h	Thu Oct 11 13:16:54 2018 +0200
@@ -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	Thu Oct 11 13:16:54 2018 +0200
@@ -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 "MainWidgetInteractor.h"
+
+#include "SimpleViewerApplication.h"
+
+namespace SimpleViewer {
+
+  IWorldSceneMouseTracker* MainWidgetInteractor::CreateMouseTracker(WorldSceneWidget& widget,
+                                                                    const ViewportGeometry& view,
+                                                                    MouseButton button,
+                                                                    KeyboardModifiers modifiers,
+                                                                    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, 10);
+      }
+      else if (application_.GetCurrentTool() == SimpleViewerApplication::Tools_CircleMeasure)
+      {
+        return new CircleMeasureTracker(statusBar, dynamic_cast<LayerWidget&>(widget).GetSlice(), x, y, 255, 0, 0, 10);
+      }
+      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,
+                                        char key,
+                                        KeyboardModifiers modifiers,
+                                        IStatusBar* statusBar)
+  {
+    switch (key)
+    {
+    case 's':
+      widget.SetDefaultView();
+      break;
+
+    default:
+      break;
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/SimpleViewer/MainWidgetInteractor.h	Thu Oct 11 13:16:54 2018 +0200
@@ -0,0 +1,70 @@
+/**
+ * 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,
+                                                        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,
+                            char key,
+                            KeyboardModifiers modifiers,
+                            IStatusBar* statusBar);
+  };
+
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/SimpleViewer/Messages.h	Thu Oct 11 13:16:54 2018 +0200
@@ -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	Thu Oct 11 13:16:54 2018 +0200
@@ -0,0 +1,89 @@
+/**
+ * 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);
+
+    connect(ui_->toolButtonCrop, &QToolButton::clicked, this, &SimpleViewerMainWindow::cropClicked);
+    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);
+  }
+
+  SimpleViewerMainWindow::~SimpleViewerMainWindow()
+  {
+    delete ui_;
+  }
+
+  void SimpleViewerMainWindow::cropClicked()
+  {
+    GenericNoArgCommand command("selectTool: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	Thu Oct 11 13:16:54 2018 +0200
@@ -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 <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 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	Thu Oct 11 13:16:54 2018 +0200
@@ -0,0 +1,144 @@
+<?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="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	Thu Oct 11 13:16:54 2018 +0200
@@ -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	Thu Oct 11 13:16:54 2018 +0200
@@ -0,0 +1,238 @@
+/**
+ * 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_.SetDefaultView();
+  }
+
+  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: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	Thu Oct 11 13:16:54 2018 +0200
@@ -0,0 +1,162 @@
+/**
+ * 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/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
+    };
+
+  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_;
+
+  public:
+    SimpleViewerApplication(MessageBroker& broker) :
+      IObserver(broker),
+      IObservable(broker),
+      currentTool_(Tools_LineMeasure),
+      mainLayout_(NULL),
+      currentInstanceIndex_(0),
+      wasmViewport1_(NULL),
+      wasmViewport2_(NULL)
+    {
+    }
+
+    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_;}
+
+    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	Thu Oct 11 13:16:54 2018 +0200
@@ -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 "ThumbnailInteractor.h"
+
+#include "SimpleViewerApplication.h"
+
+namespace SimpleViewer {
+
+  IWorldSceneMouseTracker* ThumbnailInteractor::CreateMouseTracker(WorldSceneWidget& widget,
+                                                                   const ViewportGeometry& view,
+                                                                   MouseButton button,
+                                                                   KeyboardModifiers modifiers,
+                                                                   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	Thu Oct 11 13:16:54 2018 +0200
@@ -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 ThumbnailInteractor : public IWorldSceneInteractor
+  {
+  private:
+    SimpleViewerApplication&  application_;
+  public:
+    ThumbnailInteractor(SimpleViewerApplication&  application) :
+      application_(application)
+    {
+    }
+
+    virtual IWorldSceneMouseTracker* CreateMouseTracker(WorldSceneWidget& widget,
+                                                        const ViewportGeometry& view,
+                                                        MouseButton button,
+                                                        KeyboardModifiers modifiers,
+                                                        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,
+                            char key,
+                            KeyboardModifiers modifiers,
+                            IStatusBar* statusBar)
+    {}
+
+  };
+
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/SimpleViewer/Wasm/SimpleViewerWasmApplicationAdapter.cpp	Thu Oct 11 13:16:54 2018 +0200
@@ -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	Thu Oct 11 13:16:54 2018 +0200
@@ -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	Thu Oct 11 13:16:54 2018 +0200
@@ -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	Thu Oct 11 13:16:54 2018 +0200
@@ -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	Thu Oct 11 13:16:54 2018 +0200
@@ -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	Thu Oct 11 13:16:54 2018 +0200
@@ -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	Thu Oct 11 13:16:54 2018 +0200
@@ -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
--- a/Applications/Samples/SimpleViewerApplication.h	Mon Oct 08 17:10:08 2018 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,414 +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 "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/Logging.h>
-
-namespace OrthancStone
-{
-  namespace Samples
-  {
-    class SimpleViewerApplication :
-        public SampleApplicationBase,
-        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,
-                                                            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,
-                                char key,
-                                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,
-                                                            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, 10);
-            }
-            else if (application_.currentTool_ == Tools_CircleMeasure)
-            {
-              return new CircleMeasureTracker(statusBar, dynamic_cast<LayerWidget&>(widget).GetSlice(), x, y, 255, 0, 0, 10);
-            }
-          }
-          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,
-                                char key,
-                                KeyboardModifiers modifiers,
-                                IStatusBar* statusBar)
-        {
-          switch (key)
-          {
-          case 's':
-            widget.SetDefaultView();
-            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::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_;
-
-      unsigned int                    currentInstanceIndex_;
-      OrthancStone::WidgetViewport*   wasmViewport1_;
-      OrthancStone::WidgetViewport*   wasmViewport2_;
-
-      IStatusBar*                     statusBar_;
-      std::unique_ptr<SmartLoader>    smartLoader_;
-      std::unique_ptr<OrthancApiClient>      orthancApiClient_;
-
-    public:
-      SimpleViewerApplication(MessageBroker& broker) :
-        IObserver(broker),
-        currentTool_(Tools_LineMeasure),
-        mainLayout_(NULL),
-        currentInstanceIndex_(0),
-        wasmViewport1_(NULL),
-        wasmViewport2_(NULL)
-      {
-//        DeclareIgnoredMessage(MessageType_Widget_ContentChanged);
-      }
-
-      virtual void Finalize() {}
-      virtual IWidget* GetCentralWidget() {return mainLayout_;}
-
-      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
-          if (mainWidget_->GetLayerCount() == 0)
-          {
-            smartLoader_->SetFrameInWidget(*mainWidget_, 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_.SetDefaultView();
-      }
-
-      void SelectSeriesInMainViewport(const std::string& seriesId)
-      {
-        smartLoader_->SetFrameInWidget(*mainWidget_, 0, instancesIdsPerSeriesId_[seriesId][0], 0);
-      }
-
-      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
-    };
-
-
-  }
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/SimpleViewerApplicationSingleFile.h	Thu Oct 11 13:16:54 2018 +0200
@@ -0,0 +1,424 @@
+/**
+ * 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
+
+#if ORTHANC_ENABLE_QT==1
+#include "Qt/SampleMainWindow.h"
+#endif
+#include <Core/Logging.h>
+
+namespace OrthancStone
+{
+  namespace Samples
+  {
+    class SimpleViewerApplication :
+        public SampleApplicationBase,
+        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,
+                                                            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,
+                                char key,
+                                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,
+                                                            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, 10);
+            }
+            else if (application_.currentTool_ == Tools_CircleMeasure)
+            {
+              return new CircleMeasureTracker(statusBar, dynamic_cast<LayerWidget&>(widget).GetSlice(), x, y, 255, 0, 0, 10);
+            }
+          }
+          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,
+                                char key,
+                                KeyboardModifiers modifiers,
+                                IStatusBar* statusBar)
+        {
+          switch (key)
+          {
+          case 's':
+            widget.SetDefaultView();
+            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::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_;
+
+      unsigned int                    currentInstanceIndex_;
+      OrthancStone::WidgetViewport*   wasmViewport1_;
+      OrthancStone::WidgetViewport*   wasmViewport2_;
+
+      IStatusBar*                     statusBar_;
+      std::unique_ptr<SmartLoader>    smartLoader_;
+      std::unique_ptr<OrthancApiClient>      orthancApiClient_;
+
+    public:
+      SimpleViewerApplication(MessageBroker& broker) :
+        IObserver(broker),
+        currentTool_(Tools_LineMeasure),
+        mainLayout_(NULL),
+        currentInstanceIndex_(0),
+        wasmViewport1_(NULL),
+        wasmViewport2_(NULL)
+      {
+//        DeclareIgnoredMessage(MessageType_Widget_ContentChanged);
+      }
+
+      virtual void Finalize() {}
+      virtual IWidget* GetCentralWidget() {return mainLayout_;}
+
+      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
+          if (mainWidget_->GetLayerCount() == 0)
+          {
+            smartLoader_->SetFrameInWidget(*mainWidget_, 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_.SetDefaultView();
+      }
+
+      void SelectSeriesInMainViewport(const std::string& seriesId)
+      {
+        smartLoader_->SetFrameInWidget(*mainWidget_, 0, instancesIdsPerSeriesId_[seriesId][0], 0);
+      }
+
+      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
+
+#if ORTHANC_ENABLE_QT==1
+      virtual QStoneMainWindow* CreateQtMainWindow() {
+        return new SampleMainWindow(dynamic_cast<OrthancStone::NativeStoneApplicationContext&>(*context_), *this);
+      }
+#endif
+    };
+
+
+  }
+}
--- a/Applications/Samples/Web/index.html	Mon Oct 08 17:10:08 2018 +0200
+++ b/Applications/Samples/Web/index.html	Thu Oct 11 13:16:54 2018 +0200
@@ -14,7 +14,8 @@
 
 <body>
     <ul>
-        <li><a href="simple-viewer.html">Simple Viewer</a></li>
+      <li><a href="simple-viewer/simple-viewer.html">Simple Viewer Project</a></li>
+      <li><a href="simple-viewer-single-file.html">Simple Viewer Single file (to be replaced by other samples)</a></li>
     </ul>
 </body>
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/Web/simple-viewer-single-file.html	Thu Oct 11 13:16:54 2018 +0200
@@ -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	Thu Oct 11 13:16:54 2018 +0200
@@ -0,0 +1,51 @@
+///<reference path='../../../Platforms/Wasm/wasm-application-runner.ts'/>
+
+InitializeWasmApplication("OrthancStoneSimpleViewer", "/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];
+  }
+}
--- a/Applications/Samples/Web/simple-viewer.html	Mon Oct 08 17:10:08 2018 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,34 +0,0 @@
-<!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.js"></script>
-</body>
-
-</html>
\ No newline at end of file
--- a/Applications/Samples/Web/simple-viewer.ts	Mon Oct 08 17:10:08 2018 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,51 +0,0 @@
-///<reference path='../../../Platforms/Wasm/wasm-application-runner.ts'/>
-
-InitializeWasmApplication("OrthancStoneSimpleViewer", "/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/tsconfig-simple-viewer-single-file.json	Thu Oct 11 13:16:54 2018 +0200
@@ -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
--- a/Applications/Samples/Web/tsconfig-simple-viewer.json	Mon Oct 08 17:10:08 2018 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,10 +0,0 @@
-{
-    "extends" : "./tsconfig-samples",
-    "compilerOptions": {
-        "outFile": "../build-web/app-simple-viewer.js"
-    },
-    "include" : [
-        "simple-viewer.ts",
-        "common-samples.ts"
-    ]
-}
\ No newline at end of file
--- a/Applications/Samples/build-web.sh	Mon Oct 08 17:10:08 2018 +0200
+++ b/Applications/Samples/build-web.sh	Thu Oct 11 13:16:54 2018 +0200
@@ -2,7 +2,7 @@
 
 set -e
 
-# this script currently assumes that the wasm code has been built on its side and is availabie in Wasm/build/
+# this script currently assumes that the wasm code has been built on its side and is availabie in build-wasm/
 
 currentDir=$(pwd)
 samplesRootDir=$(pwd)
@@ -10,12 +10,22 @@
 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
 
-cp $samplesRootDir/Web/simple-viewer.html $outputDir
-tsc --allowJs --project $samplesRootDir/Web/tsconfig-simple-viewer.json
-cp $currentDir/build-wasm/OrthancStoneSimpleViewer.js  $outputDir
-cp $currentDir/build-wasm/OrthancStoneSimpleViewer.wasm  $outputDir
+# build simple-viewer-single-file (obsolete project)
+cp $samplesRootDir/Web/simple-viewer-single-file.html $outputDir
+tsc --allowJs --project $samplesRootDir/Web/tsconfig-simple-viewer-single-file.json
+cp $currentDir/build-wasm/OrthancStoneSimpleViewerSingleFile.js  $outputDir
+cp $currentDir/build-wasm/OrthancStoneSimpleViewerSingleFile.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
--- a/Platforms/Wasm/WasmPlatformApplicationAdapter.cpp	Mon Oct 08 17:10:08 2018 +0200
+++ b/Platforms/Wasm/WasmPlatformApplicationAdapter.cpp	Thu Oct 11 13:16:54 2018 +0200
@@ -19,10 +19,14 @@
       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));
-            application_.ExecuteCommand(*command);
+            if (command.get() == NULL) 
+              printf("Could not parse command: '%s'\n", input.c_str());
+            else
+              application_.ExecuteCommand(*command);
         }
       }
       catch (StoneException& exc)