changeset 540:7428c5dfa5df ct-pet-dose-struct

Initial commit non working app
author Benjamin Golinvaux <bgo@osimis.io>
date Tue, 19 Mar 2019 16:29:07 +0100
parents 9d124a81c34a
children fb7f4a5bdfc0
files Applications/Samples/CtPetDoseStructFusion/AppStatus.h Applications/Samples/CtPetDoseStructFusion/CMakeLists.txt Applications/Samples/CtPetDoseStructFusion/CtPetDoseStructFusionApplication.cpp Applications/Samples/CtPetDoseStructFusion/CtPetDoseStructFusionApplication.h Applications/Samples/CtPetDoseStructFusion/MainWidgetInteractor.cpp Applications/Samples/CtPetDoseStructFusion/MainWidgetInteractor.h Applications/Samples/CtPetDoseStructFusion/Messages.h Applications/Samples/CtPetDoseStructFusion/Qt/CtPetDoseStructFusionMainWindow.cpp Applications/Samples/CtPetDoseStructFusion/Qt/CtPetDoseStructFusionMainWindow.h Applications/Samples/CtPetDoseStructFusion/Qt/CtPetDoseStructFusionMainWindow.ui Applications/Samples/CtPetDoseStructFusion/Qt/mainQt.cpp Applications/Samples/CtPetDoseStructFusion/ThumbnailInteractor.cpp Applications/Samples/CtPetDoseStructFusion/ThumbnailInteractor.h Applications/Samples/CtPetDoseStructFusion/Wasm/CtPetDoseStructFusionWasmApplicationAdapter.cpp Applications/Samples/CtPetDoseStructFusion/Wasm/CtPetDoseStructFusionWasmApplicationAdapter.h Applications/Samples/CtPetDoseStructFusion/Wasm/ct-pet-dose-struct-fusion.html Applications/Samples/CtPetDoseStructFusion/Wasm/ct-pet-dose-struct-fusion.ts Applications/Samples/CtPetDoseStructFusion/Wasm/mainWasm.cpp Applications/Samples/CtPetDoseStructFusion/Wasm/styles.css Applications/Samples/CtPetDoseStructFusion/Wasm/tsconfig-ct-pet-dose-struct-fusion.json
diffstat 20 files changed, 1552 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/CtPetDoseStructFusion/AppStatus.h	Tue Mar 19 16:29:07 2019 +0100
@@ -0,0 +1,27 @@
+#pragma once
+
+#include <string>
+
+
+namespace CtPetDoseStructFusion
+{
+  struct AppStatus
+  {
+    std::string patientId;
+    std::string studyDescription;
+    std::string currentSeriesIdInMainViewport;
+    // note: if you add members here, update the serialization code below and deserialization in ct-dose-struct-fusion.ts -> onAppStatusUpdated()
+
+
+    AppStatus()
+    {
+    }
+
+    void ToJson(Json::Value &output) const
+    {
+      output["patientId"] = patientId;
+      output["studyDescription"] = studyDescription;
+      output["currentSeriesIdInMainViewport"] = currentSeriesIdInMainViewport;
+    }
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/CtPetDoseStructFusion/CMakeLists.txt	Tue Mar 19 16:29:07 2019 +0100
@@ -0,0 +1,145 @@
+cmake_minimum_required(VERSION 2.8.3)
+project(CtPetDoseStructFusion)
+
+include(../../../Resources/CMake/OrthancStoneParameters.cmake)
+
+if (OPENSSL_NO_CAPIENG)
+add_definitions(-DOPENSSL_NO_CAPIENG=1)
+endif()
+
+set(ENABLE_SDL OFF CACHE BOOL "Target SDL Native application")
+set(ENABLE_QT OFF CACHE BOOL "Target Qt Native application")
+set(ENABLE_WASM OFF CACHE BOOL "Target WASM application")
+
+if (ENABLE_WASM)
+  #####################################################################
+  ## Configuration of the Emscripten compiler for WebAssembly target
+  #####################################################################
+
+  set(WASM_FLAGS "-s WASM=1")
+  set(WASM_FLAGS "${WASM_FLAGS} -s STRICT=1") # drops support for all deprecated build options
+  set(WASM_FLAGS "${WASM_FLAGS} -s FILESYSTEM=1") # if we don't include it, gen_uuid.c fails to build because srand, getpid(), ... are not defined
+  set(WASM_FLAGS "${WASM_FLAGS} -s DISABLE_EXCEPTION_CATCHING=0") # actually enable exception catching 
+  set(WASM_FLAGS "${WASM_FLAGS} -s ERROR_ON_MISSING_LIBRARIES=1")
+
+  if (CMAKE_BUILD_TYPE MATCHES DEBUG)
+    set(WASM_FLAGS "${WASM_FLAGS} -g4") # generate debug information
+    set(WASM_FLAGS "${WASM_FLAGS} -s ASSERTIONS=2") # more runtime checks
+  else()
+    set(WASM_FLAGS "${WASM_FLAGS} -Os") # optimize for web (speed and size)
+  endif()
+
+  set(WASM_MODULE_NAME "StoneFrameworkModule" CACHE STRING "Name of the WebAssembly module")
+
+  set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${WASM_FLAGS}")
+  set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${WASM_FLAGS}")
+
+  set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} ${WASM_FLAGS}")  # not always clear which flags are for the compiler and which one are for the linker -> pass them all to the linker too
+
+  set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} --js-library ${STONE_SOURCES_DIR}/Platforms/Wasm/WasmWebService.js")
+  set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} --js-library ${STONE_SOURCES_DIR}/Platforms/Wasm/WasmDelayedCallExecutor.js")
+  set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} --js-library ${STONE_SOURCES_DIR}/Platforms/Wasm/default-library.js")
+  set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s EXTRA_EXPORTED_RUNTIME_METHODS='[\"ccall\", \"cwrap\"]'")
+  set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s EXPORT_NAME='\"${WASM_MODULE_NAME}\"'")
+
+# +-------------------------------+
+# | Commented for now!            |     
+# +-------------------------------+
+  # set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s ALLOW_MEMORY_GROWTH=1")
+  # set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s TOTAL_MEMORY=536870912")
+  # set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s TOTAL_STACK=128000000")
+
+  add_definitions(-DORTHANC_ENABLE_WASM=1)
+  set(ORTHANC_SANDBOXED ON)
+
+elseif (ENABLE_QT OR ENABLE_SDL)
+
+  set(ENABLE_NATIVE ON)
+  set(ORTHANC_SANDBOXED OFF)
+  set(ENABLE_CRYPTO_OPTIONS ON)
+  set(ENABLE_GOOGLE_TEST ON)
+  set(ENABLE_WEB_CLIENT ON)
+
+endif()
+
+#####################################################################
+## Configuration for Orthanc
+#####################################################################
+
+if (ORTHANC_STONE_VERSION STREQUAL "mainline")
+  set(ORTHANC_FRAMEWORK_VERSION "mainline")
+  set(ORTHANC_FRAMEWORK_DEFAULT_SOURCE "hg")
+else()
+  set(ORTHANC_FRAMEWORK_VERSION "1.4.1")
+  set(ORTHANC_FRAMEWORK_DEFAULT_SOURCE "web")
+endif()
+
+set(ORTHANC_FRAMEWORK_SOURCE "${ORTHANC_FRAMEWORK_DEFAULT_SOURCE}" CACHE STRING "Source of the Orthanc source code (can be \"hg\", \"archive\", \"web\" or \"path\")")
+set(ORTHANC_FRAMEWORK_ARCHIVE "" CACHE STRING "Path to the Orthanc archive, if ORTHANC_FRAMEWORK_SOURCE is \"archive\"")
+set(ORTHANC_FRAMEWORK_ROOT "" CACHE STRING "Path to the Orthanc source directory, if ORTHANC_FRAMEWORK_SOURCE is \"path\"")
+
+#####################################################################
+## Build a static library containing the Orthanc Stone framework
+#####################################################################
+
+
+LIST(APPEND ORTHANC_BOOST_COMPONENTS program_options)
+
+include(../../Resources/CMake/OrthancStoneConfiguration.cmake)
+
+add_library(OrthancStone STATIC
+  ${ORTHANC_STONE_SOURCES}
+  )
+
+#####################################################################
+## Build the CDSF applications
+#####################################################################
+
+if (ENABLE_QT OR ENABLE_WASM)
+  if (ENABLE_QT)
+    list(APPEND CDSF_APPLICATION_SOURCES
+      ${ORTHANC_STONE_ROOT}/Applications/Samples/CtPetDoseStructFusion/Qt/CtPetDoseStructFusionMainWindow.cpp
+      ${ORTHANC_STONE_ROOT}/Applications/Samples/CtPetDoseStructFusion/Qt/CtPetDoseStructFusionMainWindow.ui
+      ${ORTHANC_STONE_ROOT}/Applications/Samples/CtPetDoseStructFusion/Qt/mainQt.cpp
+    )
+
+    ORTHANC_QT_WRAP_UI(CDSF_APPLICATION_SOURCES
+      ${ORTHANC_STONE_ROOT}/Applications/Samples/CtPetDoseStructFusion/Qt/CtPetDoseStructFusionMainWindow.ui
+    )
+
+    ORTHANC_QT_WRAP_CPP(CDSF_APPLICATION_SOURCES
+      ${ORTHANC_STONE_ROOT}/Applications/Samples/CtPetDoseStructFusion/Qt/CtPetDoseStructFusionMainWindow.h
+    )
+
+  elseif (ENABLE_WASM)
+    list(APPEND CDSF_APPLICATION_SOURCES
+      ${ORTHANC_STONE_ROOT}/Applications/Samples/CtPetDoseStructFusion/Wasm/mainWasm.cpp
+      ${ORTHANC_STONE_ROOT}/Applications/Samples/CtPetDoseStructFusion/Wasm/CtPetDoseStructFusionWasmApplicationAdapter.cpp
+      ${ORTHANC_STONE_ROOT}/Applications/Samples/CtPetDoseStructFusion/Wasm/CtPetDoseStructFusionWasmApplicationAdapter.h
+      ${STONE_WASM_SOURCES}
+    )
+  endif()
+
+  add_executable(CtPetDoseStructFusionApplication
+    ${ORTHANC_STONE_ROOT}/Applications/Samples/CtPetDoseStructFusion/AppStatus.h
+    ${ORTHANC_STONE_ROOT}/Applications/Samples/CtPetDoseStructFusion/MainWidgetInteractor.cpp
+    ${ORTHANC_STONE_ROOT}/Applications/Samples/CtPetDoseStructFusion/MainWidgetInteractor.h
+    ${ORTHANC_STONE_ROOT}/Applications/Samples/CtPetDoseStructFusion/Messages.h
+    ${ORTHANC_STONE_ROOT}/Applications/Samples/CtPetDoseStructFusion/CtPetDoseStructFusionApplication.cpp
+    ${ORTHANC_STONE_ROOT}/Applications/Samples/CtPetDoseStructFusion/CtPetDoseStructFusionApplication.h
+    ${ORTHANC_STONE_ROOT}/Applications/Samples/CtPetDoseStructFusion/ThumbnailInteractor.cpp
+    ${ORTHANC_STONE_ROOT}/Applications/Samples/CtPetDoseStructFusion/ThumbnailInteractor.h
+    ${CDSF_APPLICATION_SOURCES}
+    )
+  target_link_libraries(CtPetDoseStructFusionApplication OrthancStone)
+
+endif()
+
+
+
+
+
+
+
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/CtPetDoseStructFusion/CtPetDoseStructFusionApplication.cpp	Tue Mar 19 16:29:07 2019 +0100
@@ -0,0 +1,239 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "CtPetDoseStructFusionApplication.h"
+
+#if ORTHANC_ENABLE_QT == 1
+#  include "Qt/CtPetDoseStructFusionMainWindow.h"
+#endif
+
+#if ORTHANC_ENABLE_WASM == 1
+#  include <Platforms/Wasm/WasmViewport.h>
+#endif
+
+namespace CtPetDoseStructFusion
+{
+
+  void CtPetDoseStructFusionApplication::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 SliceViewerWidget(IObserver::GetBroker(), "main-viewport");
+      //mainWidget_->RegisterObserver(*this);
+
+      // hierarchy
+      mainLayout_->AddWidget(thumbnailsLayout_);
+      mainLayout_->AddWidget(mainWidget_);
+
+      // sources
+      smartLoader_.reset(new SmartLoader(IObserver::GetBroker(), context->GetOrthancApiClient()));
+      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 TODO_BGO_CDSF
+    if (parameters.count("studyId") < 1)
+    {
+      LOG(WARNING) << "The study ID is missing, will take the first studyId found in Orthanc";
+      context->GetOrthancApiClient().GetJsonAsync("/studies", new Callable<CtPetDoseStructFusionApplication, OrthancApiClient::JsonResponseReadyMessage>(*this, &CtPetDoseStructFusionApplication::OnStudyListReceived));
+    }
+    else
+    {
+      SelectStudy(parameters["studyId"].as<std::string>());
+    }
+#endif
+// TODO_BGO_CDSF
+  }
+
+
+  void CtPetDoseStructFusionApplication::DeclareStartupOptions(boost::program_options::options_description& options)
+  {
+    boost::program_options::options_description generic("Sample options");
+#if TODO_BGO_CDSF
+    generic.add_options()
+        ("studyId", boost::program_options::value<std::string>(),
+         "Orthanc ID of the study")
+        ;
+
+    options.add(generic);
+#endif
+  }
+
+  void CtPetDoseStructFusionApplication::OnStudyListReceived(const OrthancApiClient::JsonResponseReadyMessage& message)
+  {
+    const Json::Value& response = message.GetJson();
+
+    if (response.isArray() &&
+        response.size() >= 1)
+    {
+      SelectStudy(response[0].asString());
+    }
+  }
+  void CtPetDoseStructFusionApplication::OnStudyReceived(const OrthancApiClient::JsonResponseReadyMessage& message)
+  {
+    const Json::Value& response = message.GetJson();
+
+    if (response.isObject() && response["Series"].isArray())
+    {
+      for (size_t i=0; i < response["Series"].size(); i++)
+      {
+        context_->GetOrthancApiClient().GetJsonAsync(
+          "/series/" + response["Series"][(int)i].asString(), 
+          new Callable<
+              CtPetDoseStructFusionApplication
+            , OrthancApiClient::JsonResponseReadyMessage>(
+              *this, 
+              &CtPetDoseStructFusionApplication::OnSeriesReceived
+          )
+        );
+      }
+    }
+  }
+
+  void CtPetDoseStructFusionApplication::OnSeriesReceived(const OrthancApiClient::JsonResponseReadyMessage& message)
+  {
+    const Json::Value& response = message.GetJson();
+
+    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 CtPetDoseStructFusionApplication::LoadThumbnailForSeries(const std::string& seriesId, const std::string& instanceId)
+  {
+    LOG(INFO) << "Loading thumbnail for series " << seriesId;
+    
+    SliceViewerWidget* thumbnailWidget = 
+      new SliceViewerWidget(IObserver::GetBroker(), "thumbnail-series-" + seriesId);
+    thumbnails_.push_back(thumbnailWidget);
+    thumbnailsLayout_->AddWidget(thumbnailWidget);
+    
+    thumbnailWidget->RegisterObserverCallback(
+      new Callable<CtPetDoseStructFusionApplication, SliceViewerWidget::GeometryChangedMessage>
+      (*this, &CtPetDoseStructFusionApplication::OnWidgetGeometryChanged));
+    
+    smartLoader_->SetFrameInWidget(*thumbnailWidget, 0, instanceId, 0);
+    thumbnailWidget->SetInteractor(*thumbnailInteractor_);
+  }
+
+  void CtPetDoseStructFusionApplication::SelectStudy(const std::string& studyId)
+  {
+    context_->GetOrthancApiClient().GetJsonAsync("/studies/" + studyId, new Callable<CtPetDoseStructFusionApplication, OrthancApiClient::JsonResponseReadyMessage>(*this, &CtPetDoseStructFusionApplication::OnStudyReceived));
+  }
+
+  void CtPetDoseStructFusionApplication::OnWidgetGeometryChanged(const SliceViewerWidget::GeometryChangedMessage& message)
+  {
+    // TODO: The "const_cast" could probably be replaced by "mainWidget_"
+    const_cast<SliceViewerWidget&>(message.GetOrigin()).FitContent();
+  }
+
+  void CtPetDoseStructFusionApplication::SelectSeriesInMainViewport(const std::string& seriesId)
+  {
+    smartLoader_->SetFrameInWidget(*mainWidget_, 0, instancesIdsPerSeriesId_[seriesId][0], 0);
+  }
+
+  bool CtPetDoseStructFusionApplication::Handle(const StoneSampleCommands::SelectTool& value) ORTHANC_OVERRIDE
+  {
+    currentTool_ = value.tool;
+    return true;
+  }
+
+  bool CtPetDoseStructFusionApplication::Handle(const StoneSampleCommands::Action& value) ORTHANC_OVERRIDE
+  {
+    switch (value.type)
+    {
+    case ActionType_Invert:
+      // TODO
+      break;
+    case ActionType_UndoCrop:
+      // TODO
+      break;
+    case ActionType_Rotate:
+      // TODO
+      break;
+    default:
+      throw std::runtime_error("Action type not supported");
+    }
+    return true;
+  }
+
+#if ORTHANC_ENABLE_QT==1
+  QStoneMainWindow* CtPetDoseStructFusionApplication::CreateQtMainWindow()
+  {
+    return new CtPetDoseStructFusionMainWindow(dynamic_cast<OrthancStone::NativeStoneApplicationContext&>(*context_), *this);
+  }
+#endif
+
+#if ORTHANC_ENABLE_WASM==1
+  void CtPetDoseStructFusionApplication::InitializeWasm() {
+
+    AttachWidgetToWasmViewport("canvasThumbnails", thumbnailsLayout_);
+    AttachWidgetToWasmViewport("canvasMain", mainWidget_);
+  }
+#endif
+
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/CtPetDoseStructFusion/CtPetDoseStructFusionApplication.h	Tue Mar 19 16:29:07 2019 +0100
@@ -0,0 +1,170 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+ /*
+ This header contains the command definitions for the sample applications
+ */
+#include "Applications/Samples/StoneSampleCommands_generated.hpp"
+using namespace StoneSampleCommands;
+
+#include "Applications/IStoneApplication.h"
+
+#include "Framework/Layers/CircleMeasureTracker.h"
+#include "Framework/Layers/LineMeasureTracker.h"
+#include "Framework/Widgets/SliceViewerWidget.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/CtPetDoseStructFusionMainWindow.h"
+#endif
+
+#include <Core/Images/Font.h>
+#include <Core/Logging.h>
+
+#include "ThumbnailInteractor.h"
+#include "MainWidgetInteractor.h"
+#include "AppStatus.h"
+#include "Messages.h"
+
+using namespace OrthancStone;
+
+
+namespace CtPetDoseStructFusion
+{
+
+  class CtPetDoseStructFusionApplication
+    : public IStoneApplication
+    , public IObserver
+    , public IObservable
+    , public StoneSampleCommands::IHandler
+  {
+  public:
+
+    struct StatusUpdatedMessage : public BaseMessage<CtPetDoseStructFusionMessageType_AppStatusUpdated>
+    {
+      const AppStatus& status_;
+
+      StatusUpdatedMessage(const AppStatus& status)
+        : BaseMessage(),
+          status_(status)
+      {
+      }
+    };
+
+  private:
+    Tool                                currentTool_;
+
+    std::auto_ptr<MainWidgetInteractor> mainWidgetInteractor_;
+    std::auto_ptr<ThumbnailInteractor>  thumbnailInteractor_;
+    LayoutWidget*                       mainLayout_;
+    LayoutWidget*                       thumbnailsLayout_;
+    SliceViewerWidget*                  mainWidget_;
+    std::vector<SliceViewerWidget*>     thumbnails_;
+    std::map<std::string, std::vector<std::string> > instancesIdsPerSeriesId_;
+    std::map<std::string, Json::Value>  seriesTags_;
+    unsigned int                        currentInstanceIndex_;
+    OrthancStone::WidgetViewport*       wasmViewport1_;
+    OrthancStone::WidgetViewport*       wasmViewport2_;
+
+    IStatusBar*                         statusBar_;
+    std::auto_ptr<SmartLoader>          smartLoader_;
+
+    Orthanc::Font                       font_;
+
+  public:
+    CtPetDoseStructFusionApplication(MessageBroker& broker) :
+      IObserver(broker),
+      IObservable(broker),
+      currentTool_(StoneSampleCommands::Tool_LineMeasure),
+      mainLayout_(NULL),
+      currentInstanceIndex_(0),
+      wasmViewport1_(NULL),
+      wasmViewport2_(NULL)
+    {
+      font_.LoadFromResource(Orthanc::EmbeddedResources::FONT_UBUNTU_MONO_BOLD_16);
+    }
+
+    virtual void Finalize() {}
+    virtual IWidget* GetCentralWidget() {return mainLayout_;}
+
+    virtual void DeclareStartupOptions(boost::program_options::options_description& options);
+    virtual void Initialize(StoneApplicationContext* context,
+                            IStatusBar& statusBar,
+                            const boost::program_options::variables_map& parameters);
+
+    void OnStudyListReceived(const OrthancApiClient::JsonResponseReadyMessage& message);
+
+    void OnStudyReceived(const OrthancApiClient::JsonResponseReadyMessage& message);
+
+    void OnSeriesReceived(const OrthancApiClient::JsonResponseReadyMessage& message);
+
+    void LoadThumbnailForSeries(const std::string& seriesId, const std::string& instanceId);
+
+    void SelectStudy(const std::string& studyId);
+
+    void OnWidgetGeometryChanged(const SliceViewerWidget::GeometryChangedMessage& message);
+
+    void SelectSeriesInMainViewport(const std::string& seriesId);
+
+
+    Tool GetCurrentTool() const
+    {
+      return currentTool_;
+    }
+
+    const Orthanc::Font& GetFont() const
+    {
+      return font_;
+    }
+
+    // ExecuteAction method was empty (its body was a single "TODO" comment)
+    virtual bool Handle(const SelectTool& value) ORTHANC_OVERRIDE;
+    virtual bool Handle(const Action& value) ORTHANC_OVERRIDE;
+
+    template<typename T>
+    bool ExecuteCommand(const T& cmd)
+    {
+      std::string cmdStr = StoneSampleCommands::StoneSerialize(cmd);
+      return StoneSampleCommands::StoneDispatchToHandler(cmdStr, this);
+    }
+
+    virtual std::string GetTitle() const {return "CtPetDoseStructFusion";}
+
+#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/CtPetDoseStructFusion/MainWidgetInteractor.cpp	Tue Mar 19 16:29:07 2019 +0100
@@ -0,0 +1,111 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+#include "MainWidgetInteractor.h"
+
+#include "CtPetDoseStructFusionApplication.h"
+
+namespace CtPetDoseStructFusion {
+
+  IWorldSceneMouseTracker* MainWidgetInteractor::CreateMouseTracker(WorldSceneWidget& widget,
+                                                                    const ViewportGeometry& view,
+                                                                    MouseButton button,
+                                                                    KeyboardModifiers modifiers,
+                                                                    int viewportX,
+                                                                    int viewportY,
+                                                                    double x,
+                                                                    double y,
+                                                                    IStatusBar* statusBar,
+                                                                    const std::vector<Touch>& displayTouches)
+  {
+    if (button == MouseButton_Left)
+    {
+      if (application_.GetCurrentTool() == Tool_LineMeasure)
+      {
+        return new LineMeasureTracker(statusBar, dynamic_cast<SliceViewerWidget&>(widget).GetSlice(),
+                                      x, y, 255, 0, 0, application_.GetFont());
+      }
+      else if (application_.GetCurrentTool() == Tool_CircleMeasure)
+      {
+        return new CircleMeasureTracker(statusBar, dynamic_cast<SliceViewerWidget&>(widget).GetSlice(),
+                                        x, y, 255, 0, 0, application_.GetFont());
+      }
+      else if (application_.GetCurrentTool() == Tool_Crop)
+      {
+        // TODO
+      }
+      else if (application_.GetCurrentTool() == Tool_Windowing)
+      {
+        // TODO
+      }
+      else if (application_.GetCurrentTool() == Tool_Zoom)
+      {
+        // TODO
+      }
+      else if (application_.GetCurrentTool() == Tool_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<SliceViewerWidget&>(widget).GetSlice().MapSliceToWorldCoordinates(x, y);
+
+      char buf[64];
+      sprintf(buf, "X = %.02f Y = %.02f Z = %.02f (in cm)",
+              p[0] / 10.0, p[1] / 10.0, p[2] / 10.0);
+      statusBar->SetMessage(buf);
+    }
+  }
+
+  void MainWidgetInteractor::MouseWheel(WorldSceneWidget& widget,
+                                        MouseWheelDirection direction,
+                                        KeyboardModifiers modifiers,
+                                        IStatusBar* statusBar)
+  {
+  }
+
+  void MainWidgetInteractor::KeyPressed(WorldSceneWidget& widget,
+                                        KeyboardKeys key,
+                                        char keyChar,
+                                        KeyboardModifiers modifiers,
+                                        IStatusBar* statusBar)
+  {
+    switch (keyChar)
+    {
+    case 's':
+      widget.FitContent();
+      break;
+
+    default:
+      break;
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/CtPetDoseStructFusion/MainWidgetInteractor.h	Tue Mar 19 16:29:07 2019 +0100
@@ -0,0 +1,76 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+#pragma once
+
+#include "Framework/Widgets/IWorldSceneInteractor.h"
+
+using namespace OrthancStone;
+
+namespace CtPetDoseStructFusion {
+
+  class CtPetDoseStructFusionApplication;
+
+  class MainWidgetInteractor : public IWorldSceneInteractor
+  {
+  private:
+    CtPetDoseStructFusionApplication&  application_;
+
+  public:
+    MainWidgetInteractor(CtPetDoseStructFusionApplication&  application) :
+      application_(application)
+    {
+    }
+
+    /**
+        WorldSceneWidget: 
+    */
+    virtual IWorldSceneMouseTracker* CreateMouseTracker(WorldSceneWidget& widget,
+                                                        const ViewportGeometry& view,
+                                                        MouseButton button,
+                                                        KeyboardModifiers modifiers,
+                                                        int viewportX,
+                                                        int viewportY,
+                                                        double x,
+                                                        double y,
+                                                        IStatusBar* statusBar,
+                                                        const std::vector<Touch>& displayTouches);
+
+    virtual void MouseOver(CairoContext& context,
+                           WorldSceneWidget& widget,
+                           const ViewportGeometry& view,
+                           double x,
+                           double y,
+                           IStatusBar* statusBar);
+
+    virtual void MouseWheel(WorldSceneWidget& widget,
+                            MouseWheelDirection direction,
+                            KeyboardModifiers modifiers,
+                            IStatusBar* statusBar);
+
+    virtual void KeyPressed(WorldSceneWidget& widget,
+                            KeyboardKeys key,
+                            char keyChar,
+                            KeyboardModifiers modifiers,
+                            IStatusBar* statusBar);
+  };
+
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/CtPetDoseStructFusion/Messages.h	Tue Mar 19 16:29:07 2019 +0100
@@ -0,0 +1,10 @@
+#pragma once
+
+namespace CtPetDoseStructFusion
+{
+  enum CtPetDoseStructFusionMessageType
+  {
+    CtPetDoseStructFusionMessageType_First = OrthancStone::MessageType_CustomMessage,
+    CtPetDoseStructFusionMessageType_AppStatusUpdated
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/CtPetDoseStructFusion/Qt/CtPetDoseStructFusionMainWindow.cpp	Tue Mar 19 16:29:07 2019 +0100
@@ -0,0 +1,110 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+#include "CtPetDoseStructFusionMainWindow.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_CtPetDoseStructFusionMainWindow.h>
+#include "../CtPetDoseStructFusionApplication.h"
+
+
+namespace CtPetDoseStructFusion
+{
+
+  template<typename T, typename U>
+  bool ExecuteCommand(U* handler, const T& command)
+  {
+    std::string serializedCommand = StoneSerialize(command);
+    StoneDispatchToHandler(serializedCommand, handler);
+  }
+
+  CtPetDoseStructFusionMainWindow::CtPetDoseStructFusionMainWindow(
+    OrthancStone::NativeStoneApplicationContext& context,
+    CtPetDoseStructFusionApplication& stoneApplication,
+    QWidget *parent) :
+    QStoneMainWindow(context, parent),
+    ui_(new Ui::CtPetDoseStructFusionMainWindow),
+    stoneApplication_(stoneApplication)
+  {
+    ui_->setupUi(this);
+    SetCentralStoneWidget(*ui_->cairoCentralWidget);
+
+#if QT_VERSION >= 0x050000
+    connect(ui_->toolButtonCrop, &QToolButton::clicked, this, &CtPetDoseStructFusionMainWindow::cropClicked);
+    connect(ui_->pushButtonUndoCrop, &QToolButton::clicked, this, &CtPetDoseStructFusionMainWindow::undoCropClicked);
+    connect(ui_->toolButtonLine, &QToolButton::clicked, this, &CtPetDoseStructFusionMainWindow::lineClicked);
+    connect(ui_->toolButtonCircle, &QToolButton::clicked, this, &CtPetDoseStructFusionMainWindow::circleClicked);
+    connect(ui_->toolButtonWindowing, &QToolButton::clicked, this, &CtPetDoseStructFusionMainWindow::windowingClicked);
+    connect(ui_->pushButtonRotate, &QPushButton::clicked, this, &CtPetDoseStructFusionMainWindow::rotateClicked);
+    connect(ui_->pushButtonInvert, &QPushButton::clicked, this, &CtPetDoseStructFusionMainWindow::invertClicked);
+#else
+    connect(ui_->toolButtonCrop, SIGNAL(clicked()), this, SLOT(cropClicked()));
+    connect(ui_->toolButtonLine, SIGNAL(clicked()), this, SLOT(lineClicked()));
+    connect(ui_->toolButtonCircle, SIGNAL(clicked()), this, SLOT(circleClicked()));
+    connect(ui_->toolButtonWindowing, SIGNAL(clicked()), this, SLOT(windowingClicked()));
+    connect(ui_->pushButtonUndoCrop, SIGNAL(clicked()), this, SLOT(undoCropClicked()));
+    connect(ui_->pushButtonRotate, SIGNAL(clicked()), this, SLOT(rotateClicked()));
+    connect(ui_->pushButtonInvert, SIGNAL(clicked()), this, SLOT(invertClicked()));
+#endif
+  }
+
+  CtPetDoseStructFusionMainWindow::~CtPetDoseStructFusionMainWindow()
+  {
+    delete ui_;
+  }
+
+  void CtPetDoseStructFusionMainWindow::cropClicked()
+  {
+    stoneApplication_.ExecuteCommand(SelectTool(Tool_Crop));
+  }
+
+  void CtPetDoseStructFusionMainWindow::undoCropClicked()
+  {
+    stoneApplication_.ExecuteCommand(Action(ActionType_UndoCrop));
+  }
+
+  void CtPetDoseStructFusionMainWindow::lineClicked()
+  {
+    stoneApplication_.ExecuteCommand(SelectTool(Tool_LineMeasure));
+  }
+
+  void CtPetDoseStructFusionMainWindow::circleClicked()
+  {
+    stoneApplication_.ExecuteCommand(SelectTool(Tool_CircleMeasure));
+  }
+
+  void CtPetDoseStructFusionMainWindow::windowingClicked()
+  {
+    stoneApplication_.ExecuteCommand(SelectTool(Tool_Windowing));
+  }
+
+  void CtPetDoseStructFusionMainWindow::rotateClicked()
+  {
+    stoneApplication_.ExecuteCommand(Action(ActionType_Rotate));
+  }
+
+  void CtPetDoseStructFusionMainWindow::invertClicked()
+  {
+    stoneApplication_.ExecuteCommand(Action(ActionType_Invert));
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/CtPetDoseStructFusion/Qt/CtPetDoseStructFusionMainWindow.h	Tue Mar 19 16:29:07 2019 +0100
@@ -0,0 +1,57 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+#pragma once
+
+#include <Applications/Qt/QCairoWidget.h>
+#include <Applications/Qt/QStoneMainWindow.h>
+
+namespace Ui 
+{
+  class CtPetDoseStructFusionMainWindow;
+}
+
+using namespace OrthancStone;
+
+namespace CtPetDoseStructFusion
+{
+  class CtPetDoseStructFusionApplication;
+
+  class CtPetDoseStructFusionMainWindow : public QStoneMainWindow
+  {
+    Q_OBJECT
+
+  private:
+    Ui::CtPetDoseStructFusionMainWindow*   ui_;
+    CtPetDoseStructFusionApplication&      stoneApplication_;
+
+  public:
+    explicit CtPetDoseStructFusionMainWindow(OrthancStone::NativeStoneApplicationContext& context, CtPetDoseStructFusionApplication& stoneApplication, QWidget *parent = 0);
+    ~CtPetDoseStructFusionMainWindow();
+
+  private slots:
+    void cropClicked();
+    void undoCropClicked();
+    void rotateClicked();
+    void windowingClicked();
+    void lineClicked();
+    void circleClicked();
+    void invertClicked();
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/CtPetDoseStructFusion/Qt/CtPetDoseStructFusionMainWindow.ui	Tue Mar 19 16:29:07 2019 +0100
@@ -0,0 +1,151 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>CtPetDoseStructFusionMainWindow</class>
+ <widget class="QMainWindow" name="CtPetDoseStructFusionMainWindow">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>903</width>
+    <height>634</height>
+   </rect>
+  </property>
+  <property name="minimumSize">
+   <size>
+    <width>500</width>
+    <height>300</height>
+   </size>
+  </property>
+  <property name="baseSize">
+   <size>
+    <width>500</width>
+    <height>300</height>
+   </size>
+  </property>
+  <property name="windowTitle">
+   <string>Stone of Orthanc</string>
+  </property>
+  <property name="layoutDirection">
+   <enum>Qt::LeftToRight</enum>
+  </property>
+  <widget class="QWidget" name="centralwidget">
+   <property name="sizePolicy">
+    <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
+     <horstretch>0</horstretch>
+     <verstretch>0</verstretch>
+    </sizepolicy>
+   </property>
+   <property name="layoutDirection">
+    <enum>Qt::LeftToRight</enum>
+   </property>
+   <layout class="QVBoxLayout" name="verticalLayout_2" stretch="0,0">
+    <property name="sizeConstraint">
+     <enum>QLayout::SetDefaultConstraint</enum>
+    </property>
+    <item>
+     <widget class="QCairoWidget" name="cairoCentralWidget">
+      <property name="minimumSize">
+       <size>
+        <width>0</width>
+        <height>500</height>
+       </size>
+      </property>
+     </widget>
+    </item>
+    <item>
+     <widget class="QGroupBox" name="horizontalGroupBox">
+      <property name="minimumSize">
+       <size>
+        <width>0</width>
+        <height>100</height>
+       </size>
+      </property>
+      <property name="maximumSize">
+       <size>
+        <width>16777215</width>
+        <height>100</height>
+       </size>
+      </property>
+      <layout class="QHBoxLayout" name="horizontalLayout">
+       <item>
+        <widget class="QToolButton" name="toolButtonWindowing">
+         <property name="text">
+          <string>windowing</string>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="QToolButton" name="toolButtonCrop">
+         <property name="text">
+          <string>crop</string>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="QPushButton" name="pushButtonUndoCrop">
+         <property name="text">
+          <string>undo crop</string>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="QToolButton" name="toolButtonLine">
+         <property name="text">
+          <string>line</string>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="QToolButton" name="toolButtonCircle">
+         <property name="text">
+          <string>circle</string>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="QPushButton" name="pushButtonRotate">
+         <property name="text">
+          <string>rotate</string>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="QPushButton" name="pushButtonInvert">
+         <property name="text">
+          <string>invert</string>
+         </property>
+        </widget>
+       </item>
+      </layout>
+     </widget>
+    </item>
+   </layout>
+  </widget>
+  <widget class="QMenuBar" name="menubar">
+   <property name="geometry">
+    <rect>
+     <x>0</x>
+     <y>0</y>
+     <width>903</width>
+     <height>22</height>
+    </rect>
+   </property>
+   <widget class="QMenu" name="menuTest">
+    <property name="title">
+     <string>Test</string>
+    </property>
+   </widget>
+   <addaction name="menuTest"/>
+  </widget>
+  <widget class="QStatusBar" name="statusbar"/>
+ </widget>
+ <customwidgets>
+  <customwidget>
+   <class>QCairoWidget</class>
+   <extends>QGraphicsView</extends>
+   <header location="global">QCairoWidget.h</header>
+  </customwidget>
+ </customwidgets>
+ <resources/>
+ <connections/>
+</ui>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/CtPetDoseStructFusion/Qt/mainQt.cpp	Tue Mar 19 16:29:07 2019 +0100
@@ -0,0 +1,14 @@
+#include "Applications/Qt/QtStoneApplicationRunner.h"
+
+#include "../CtPetDoseStructFusionApplication.h"
+#include "Framework/Messages/MessageBroker.h"
+
+
+int main(int argc, char* argv[]) 
+{
+  OrthancStone::MessageBroker broker;
+  CtPetDoseStructFusion::CtPetDoseStructFusionApplication 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/CtPetDoseStructFusion/ThumbnailInteractor.cpp	Tue Mar 19 16:29:07 2019 +0100
@@ -0,0 +1,46 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+#include "ThumbnailInteractor.h"
+
+#include "CtPetDoseStructFusionApplication.h"
+
+namespace CtPetDoseStructFusion {
+
+  IWorldSceneMouseTracker* ThumbnailInteractor::CreateMouseTracker(WorldSceneWidget& widget,
+                                                                   const ViewportGeometry& view,
+                                                                   MouseButton button,
+                                                                   KeyboardModifiers modifiers,
+                                                                   int viewportX,
+                                                                   int viewportY,
+                                                                   double x,
+                                                                   double y,
+                                                                   IStatusBar* statusBar,
+                                                                   const std::vector<Touch>& displayTouches)
+  {
+    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/CtPetDoseStructFusion/ThumbnailInteractor.h	Tue Mar 19 16:29:07 2019 +0100
@@ -0,0 +1,77 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "Framework/Widgets/IWorldSceneInteractor.h"
+
+using namespace OrthancStone;
+
+namespace CtPetDoseStructFusion {
+
+  class CtPetDoseStructFusionApplication;
+
+  class ThumbnailInteractor : public IWorldSceneInteractor
+  {
+  private:
+    CtPetDoseStructFusionApplication&  application_;
+  public:
+    ThumbnailInteractor(CtPetDoseStructFusionApplication&  application) :
+      application_(application)
+    {
+    }
+
+    virtual IWorldSceneMouseTracker* CreateMouseTracker(WorldSceneWidget& widget,
+                                                        const ViewportGeometry& view,
+                                                        MouseButton button,
+                                                        KeyboardModifiers modifiers,
+                                                        int viewportX,
+                                                        int viewportY,
+                                                        double x,
+                                                        double y,
+                                                        IStatusBar* statusBar,
+                                                        const std::vector<Touch>& displayTouches);
+
+    virtual void MouseOver(CairoContext& context,
+                           WorldSceneWidget& widget,
+                           const ViewportGeometry& view,
+                           double x,
+                           double y,
+                           IStatusBar* statusBar)
+    {}
+
+    virtual void MouseWheel(WorldSceneWidget& widget,
+                            MouseWheelDirection direction,
+                            KeyboardModifiers modifiers,
+                            IStatusBar* statusBar)
+    {}
+
+    virtual void KeyPressed(WorldSceneWidget& widget,
+                            KeyboardKeys key,
+                            char keyChar,
+                            KeyboardModifiers modifiers,
+                            IStatusBar* statusBar)
+    {}
+
+  };
+
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/CtPetDoseStructFusion/Wasm/CtPetDoseStructFusionWasmApplicationAdapter.cpp	Tue Mar 19 16:29:07 2019 +0100
@@ -0,0 +1,51 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+#include "CtPetDoseStructFusionWasmApplicationAdapter.h"
+
+namespace CtPetDoseStructFusion
+{
+
+  CtPetDoseStructFusionWasmApplicationAdapter::CtPetDoseStructFusionWasmApplicationAdapter(MessageBroker &broker, CtPetDoseStructFusionApplication &application)
+      : WasmPlatformApplicationAdapter(broker, application),
+        viewerApplication_(application)
+  {
+    application.RegisterObserverCallback(new Callable<CtPetDoseStructFusionWasmApplicationAdapter, CtPetDoseStructFusionApplication::StatusUpdatedMessage>(*this, &CtPetDoseStructFusionWasmApplicationAdapter::OnStatusUpdated));
+  }
+
+  void CtPetDoseStructFusionWasmApplicationAdapter::OnStatusUpdated(const CtPetDoseStructFusionApplication::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);
+
+    NotifyStatusUpdateFromCppToWebWithString(outputStr.str());
+  }
+
+} // namespace CtPetDoseStructFusion
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/CtPetDoseStructFusion/Wasm/CtPetDoseStructFusionWasmApplicationAdapter.h	Tue Mar 19 16:29:07 2019 +0100
@@ -0,0 +1,43 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+#pragma once
+
+#include <string>
+#include <Framework/Messages/IObserver.h>
+#include <Platforms/Wasm/WasmPlatformApplicationAdapter.h>
+
+#include "../CtPetDoseStructFusionApplication.h"
+
+namespace CtPetDoseStructFusion {
+
+  class CtPetDoseStructFusionWasmApplicationAdapter : public WasmPlatformApplicationAdapter
+    {
+      CtPetDoseStructFusionApplication&  viewerApplication_;
+
+    public:
+      CtPetDoseStructFusionWasmApplicationAdapter(MessageBroker& broker, CtPetDoseStructFusionApplication& application);
+
+    private:
+      void OnStatusUpdated(const CtPetDoseStructFusionApplication::StatusUpdatedMessage& message);
+
+    };
+
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/CtPetDoseStructFusion/Wasm/ct-pet-dose-struct-fusion.html	Tue Mar 19 16:29:07 2019 +0100
@@ -0,0 +1,43 @@
+<!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 style="height: calc(100% - 50px)">
+    <div style="width: 20%; height: 100%; display: inline-block">
+      <canvas id="canvasThumbnails"></canvas>
+    </div>
+    <div style="width: 70%; height: 100%; display: inline-block">
+      <canvas id="canvasMain"></canvas>
+    </div>
+  </div>
+  <div id="toolbox" style="height: 50px">
+    <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-ct-dose-struct-fusion.js"></script>
+</body>
+
+</html>
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/CtPetDoseStructFusion/Wasm/ct-pet-dose-struct-fusion.ts	Tue Mar 19 16:29:07 2019 +0100
@@ -0,0 +1,81 @@
+import wasmApplicationRunner = require('../../../../Platforms/Wasm/wasm-application-runner');
+
+wasmApplicationRunner.InitializeWasmApplication("OrthancStoneCtPetDoseStructFusion", "/orthanc");
+
+function SelectTool(toolName: string) {
+  var command = {
+    command: "selectTool:" + toolName,
+    commandType: "generic-no-arg-command",
+    args: {
+    }                                                                                                                       
+  };
+  wasmApplicationRunner.SendCommandToStoneApplication(JSON.stringify(command));
+}
+
+function PerformAction(actionName: string) {
+  var command = {
+    command: "action:" + actionName,
+    commandType: "generic-no-arg-command",
+    args: {
+    }
+  };
+  wasmApplicationRunner.SendCommandToStoneApplication(JSON.stringify(command));
+}
+
+class CtPetDoseStructFusionUI {
+
+  private _labelPatientId: HTMLSpanElement;
+  private _labelStudyDescription: HTMLSpanElement;
+
+  public constructor() {
+    // install "SelectTool" handlers
+    document.querySelectorAll("[tool-selector]").forEach((e) => {
+      (e as HTMLButtonElement).addEventListener("click", () => {
+        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 CtPetDoseStructFusionUI();
+
+// 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 UpdateWebApplicationWithString(statusUpdateMessageString: string) {
+  console.log("updating web application with string: ", statusUpdateMessageString);
+  let statusUpdateMessage = JSON.parse(statusUpdateMessageString);
+
+  if ("event" in statusUpdateMessage) {
+    let eventName = statusUpdateMessage["event"];
+    if (eventName == "appStatusUpdated") {
+      ui.onAppStatusUpdated(statusUpdateMessage["data"]);
+    }
+  }
+}
+
+function UpdateWebApplicationWithSerializedMessage(statusUpdateMessageString: string) {
+  console.log("updating web application with serialized message: ", statusUpdateMessageString);
+  console.log("<not supported in the simple viewer!>");
+}
+
+// make it available to other js scripts in the application
+(<any> window).UpdateWebApplicationWithString = UpdateWebApplicationWithString;
+(<any> window).UpdateWebApplicationWithSerializedMessage = UpdateWebApplicationWithSerializedMessage;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/CtPetDoseStructFusion/Wasm/mainWasm.cpp	Tue Mar 19 16:29:07 2019 +0100
@@ -0,0 +1,38 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+#include "Platforms/Wasm/WasmWebService.h"
+#include "Platforms/Wasm/WasmViewport.h"
+
+#include <emscripten/emscripten.h>
+
+#include "../CtPetDoseStructFusionApplication.h"
+#include "CtPetDoseStructFusionWasmApplicationAdapter.h"
+
+
+OrthancStone::IStoneApplication* CreateUserApplication(OrthancStone::MessageBroker& broker) {
+  
+  return new CtPetDoseStructFusion::CtPetDoseStructFusionApplication(broker);
+}
+
+OrthancStone::WasmPlatformApplicationAdapter* CreateWasmApplicationAdapter(OrthancStone::MessageBroker& broker, IStoneApplication* application)
+{
+  return new CtPetDoseStructFusion::CtPetDoseStructFusionWasmApplicationAdapter(broker, *(dynamic_cast<CtPetDoseStructFusion::CtPetDoseStructFusionApplication*>(application)));
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/CtPetDoseStructFusion/Wasm/styles.css	Tue Mar 19 16:29:07 2019 +0100
@@ -0,0 +1,54 @@
+html, body {
+    width: 100%;
+    height: 100%;
+    margin: 0px;
+    border: 0;
+    overflow: hidden; /*  Disable scrollbars */
+    display: block;  /* No floating content on sides */
+    background-color: black;
+    color: white;
+    font-family: Arial, Helvetica, sans-serif;
+}
+
+canvas {
+    left:0px;
+    top:0px;
+}
+
+#canvas-group {
+    padding:5px;
+    background-color: grey;
+}
+
+#status-group {
+    padding:5px;
+}
+
+#worklist-group {
+    padding:5px;
+}
+
+.vsol-button {
+    height: 40px;
+}
+
+#thumbnails-group ul li {
+    display: inline;
+    list-style: none;
+}
+
+.thumbnail {
+    width: 100px;
+    height: 100px;
+    padding: 3px;
+}
+
+.thumbnail-selected {
+    border-width: 1px;
+    border-color: red;
+    border-style: solid;
+}
+
+#template-thumbnail-li {
+    display: none !important;
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/CtPetDoseStructFusion/Wasm/tsconfig-ct-pet-dose-struct-fusion.json	Tue Mar 19 16:29:07 2019 +0100
@@ -0,0 +1,9 @@
+{
+    "extends" : "../../Web/tsconfig-samples",
+    "compilerOptions": {
+    },
+    "include" : [
+        "ct-dose-struct-fusion.ts",
+        "../../build-wasm/ApplicationCommands_generated.ts"
+    ]
+}