# HG changeset patch # User amazy # Date 1545990034 -3600 # Node ID a750f11892ec4b217c0b20d84bd7b04083929c2d # Parent 237160cbb44ffe4f618bfa780b2eecce37b7ffab# Parent b70e9be013e4ecd34185551394516c99813a2ff5 merge default->am-vsol-upgrade diff -r b70e9be013e4 -r a750f11892ec Applications/Generic/NativeStoneApplicationContext.cpp --- a/Applications/Generic/NativeStoneApplicationContext.cpp Mon Dec 24 13:41:12 2018 +0100 +++ b/Applications/Generic/NativeStoneApplicationContext.cpp Fri Dec 28 10:40:34 2018 +0100 @@ -57,7 +57,7 @@ void NativeStoneApplicationContext::Start() { - boost::mutex::scoped_lock lock(globalMutex_); + boost::recursive_mutex::scoped_lock lock(globalMutex_); if (stopped_ && centralViewport_.HasAnimation()) diff -r b70e9be013e4 -r a750f11892ec Applications/Generic/NativeStoneApplicationContext.h --- a/Applications/Generic/NativeStoneApplicationContext.h Mon Dec 24 13:41:12 2018 +0100 +++ b/Applications/Generic/NativeStoneApplicationContext.h Fri Dec 28 10:40:34 2018 +0100 @@ -36,7 +36,7 @@ private: static void UpdateThread(NativeStoneApplicationContext* that); - boost::mutex globalMutex_; + boost::recursive_mutex globalMutex_; WidgetViewport centralViewport_; boost::thread updateThread_; bool stopped_; @@ -46,8 +46,8 @@ class GlobalMutexLocker: public boost::noncopyable { private: - NativeStoneApplicationContext& that_; - boost::mutex::scoped_lock lock_; + NativeStoneApplicationContext& that_; + boost::recursive_mutex::scoped_lock lock_; public: GlobalMutexLocker(NativeStoneApplicationContext& that) : diff -r b70e9be013e4 -r a750f11892ec Applications/Generic/NativeStoneApplicationRunner.cpp --- a/Applications/Generic/NativeStoneApplicationRunner.cpp Mon Dec 24 13:41:12 2018 +0100 +++ b/Applications/Generic/NativeStoneApplicationRunner.cpp Fri Dec 28 10:40:34 2018 +0100 @@ -27,6 +27,7 @@ #include "../../Framework/Toolbox/MessagingToolbox.h" #include "../../Platforms/Generic/OracleWebService.h" +#include "../../Platforms/Generic/OracleDelayedCallExecutor.h" #include "NativeStoneApplicationContext.h" #include @@ -188,7 +189,7 @@ NativeStoneApplicationContext context(broker_); { - Oracle oracle(4); // use 4 threads to download content + Oracle oracle(6); // use multiple threads to execute asynchronous tasks like download content oracle.Start(); { @@ -196,6 +197,9 @@ context.SetWebService(webService); context.SetOrthancBaseUrl(webServiceParameters.GetUrl()); + OracleDelayedCallExecutor delayedExecutor(broker_, oracle, context); + context.SetDelayedCallExecutor(delayedExecutor); + application_.Initialize(&context, statusBar, parameters); { diff -r b70e9be013e4 -r a750f11892ec Applications/Samples/CMakeLists.txt --- a/Applications/Samples/CMakeLists.txt Mon Dec 24 13:41:12 2018 +0100 +++ b/Applications/Samples/CMakeLists.txt Fri Dec 28 10:40:34 2018 +0100 @@ -23,7 +23,7 @@ set(WASM_MODULE_NAME "StoneFrameworkModule" CACHE STRING "Name of the WebAssembly module") set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${WASM_FLAGS}") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${WASM_FLAGS}") - set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} --js-library ${STONE_SOURCES_DIR}/Applications/Samples/samples-library.js --js-library ${STONE_SOURCES_DIR}/Platforms/Wasm/WasmWebService.js --js-library ${STONE_SOURCES_DIR}/Platforms/Wasm/default-library.js -s EXTRA_EXPORTED_RUNTIME_METHODS='[\"ccall\", \"cwrap\"]'") + set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} --js-library ${STONE_SOURCES_DIR}/Applications/Samples/samples-library.js --js-library ${STONE_SOURCES_DIR}/Platforms/Wasm/WasmWebService.js --js-library ${STONE_SOURCES_DIR}/Platforms/Wasm/WasmDelayedCallExecutor.js --js-library ${STONE_SOURCES_DIR}/Platforms/Wasm/default-library.js -s EXTRA_EXPORTED_RUNTIME_METHODS='[\"ccall\", \"cwrap\"]'") # Handling of memory #set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s ALLOW_MEMORY_GROWTH=1") # Resize @@ -160,8 +160,6 @@ ) ORTHANC_QT_WRAP_CPP(SIMPLE_VIEWER_APPLICATION_SOURCES - ${ORTHANC_STONE_ROOT}/Applications/Qt/QCairoWidget.h - ${ORTHANC_STONE_ROOT}/Applications/Qt/QStoneMainWindow.h ${ORTHANC_STONE_ROOT}/Applications/Samples/SimpleViewer/Qt/SimpleViewerMainWindow.h ) @@ -195,7 +193,6 @@ ${ORTHANC_STONE_ROOT}/UnitTestsSources/TestCommands.cpp ${ORTHANC_STONE_ROOT}/UnitTestsSources/TestExceptions.cpp ${ORTHANC_STONE_ROOT}/UnitTestsSources/TestMessageBroker.cpp - ${ORTHANC_STONE_ROOT}/UnitTestsSources/TestMessageBroker2.cpp ${ORTHANC_STONE_ROOT}/UnitTestsSources/UnitTestsMain.cpp ) diff -r b70e9be013e4 -r a750f11892ec Applications/Samples/SingleFrameEditorApplication.h --- a/Applications/Samples/SingleFrameEditorApplication.h Mon Dec 24 13:41:12 2018 +0100 +++ b/Applications/Samples/SingleFrameEditorApplication.h Fri Dec 28 10:40:34 2018 +0100 @@ -31,6 +31,8 @@ #include "../../Framework/Radiography/RadiographySceneCommand.h" #include "../../Framework/Radiography/RadiographyWidget.h" #include "../../Framework/Radiography/RadiographyWindowingTracker.h" +#include "../../Framework/Radiography/RadiographySceneWriter.h" +#include "../../Framework/Radiography/RadiographySceneReader.h" #include #include @@ -48,8 +50,8 @@ namespace Samples { class RadiographyEditorInteractor : - public IWorldSceneInteractor, - public IObserver + public IWorldSceneInteractor, + public IObserver { private: enum Tool @@ -60,7 +62,7 @@ Tool_Resize, Tool_Windowing }; - + StoneApplicationContext* context_; UndoRedoStack undoRedoStack_; @@ -71,8 +73,8 @@ { return 10.0; } - - + + public: RadiographyEditorInteractor(MessageBroker& broker) : IObserver(broker), @@ -85,7 +87,7 @@ { context_ = &context; } - + virtual IWorldSceneMouseTracker* CreateMouseTracker(WorldSceneWidget& worldWidget, const ViewportGeometry& view, MouseButton button, @@ -101,16 +103,16 @@ if (button == MouseButton_Left) { size_t selected; - + if (tool_ == Tool_Windowing) { return new RadiographyWindowingTracker( - undoRedoStack_, widget.GetScene(), - viewportX, viewportY, - RadiographyWindowingTracker::Action_DecreaseWidth, - RadiographyWindowingTracker::Action_IncreaseWidth, - RadiographyWindowingTracker::Action_DecreaseCenter, - RadiographyWindowingTracker::Action_IncreaseCenter); + undoRedoStack_, widget.GetScene(), + viewportX, viewportY, + RadiographyWindowingTracker::Action_DecreaseWidth, + RadiographyWindowingTracker::Action_IncreaseWidth, + RadiographyWindowingTracker::Action_DecreaseCenter, + RadiographyWindowingTracker::Action_IncreaseCenter); } else if (!widget.LookupSelectedLayer(selected)) { @@ -133,23 +135,23 @@ { switch (tool_) { - case Tool_Crop: - return new RadiographyLayerCropTracker + case Tool_Crop: + return new RadiographyLayerCropTracker (undoRedoStack_, widget.GetScene(), view, selected, x, y, corner); - case Tool_Resize: - return new RadiographyLayerResizeTracker + case Tool_Resize: + return new RadiographyLayerResizeTracker (undoRedoStack_, widget.GetScene(), selected, x, y, corner, (modifiers & KeyboardModifiers_Shift)); - default: - throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); } } else { size_t layer; - + if (widget.GetScene().LookupLayer(layer, x, y)) { widget.Select(layer); @@ -158,7 +160,7 @@ { widget.Unselect(); } - + return NULL; } } @@ -172,18 +174,18 @@ { switch (tool_) { - case Tool_Move: - return new RadiographyLayerMoveTracker + case Tool_Move: + return new RadiographyLayerMoveTracker (undoRedoStack_, widget.GetScene(), layer, x, y, (modifiers & KeyboardModifiers_Shift)); - case Tool_Rotate: - return new RadiographyLayerRotateTracker + case Tool_Rotate: + return new RadiographyLayerRotateTracker (undoRedoStack_, widget.GetScene(), view, layer, x, y, (modifiers & KeyboardModifiers_Shift)); - - default: - break; + + default: + break; } return NULL; @@ -232,14 +234,14 @@ tool_ == Tool_Resize)) { RadiographyScene::LayerAccessor accessor(widget.GetScene(), selected); - + Corner corner; if (accessor.GetLayer().LookupCorner(corner, x, y, view.GetZoom(), GetHandleSize())) { accessor.GetLayer().GetCorner(x, y, corner); - + double z = 1.0 / view.GetZoom(); - + context.SetSourceColor(255, 0, 0); cairo_t* cr = context.GetObject(); cairo_set_line_width(cr, 2.0 * z); @@ -270,118 +272,141 @@ switch (keyChar) { - case 'a': - widget.FitContent(); - break; + case 'a': + widget.FitContent(); + break; + + case 'c': + tool_ = Tool_Crop; + break; - case 'c': - tool_ = Tool_Crop; - break; + case 'd': + { + // dump to json and reload + Json::Value snapshot; + RadiographySceneWriter writer; + writer.Write(snapshot, widget.GetScene()); - case 'e': - { - Orthanc::DicomMap tags; + LOG(INFO) << "JSON export was successful: " + << snapshot.toStyledString(); + + boost::shared_ptr scene(new RadiographyScene(GetBroker())); + RadiographySceneReader reader(*scene, context_->GetOrthancApiClient()); + + Orthanc::FontRegistry fontRegistry; + fontRegistry.AddFromResource(Orthanc::EmbeddedResources::FONT_UBUNTU_MONO_BOLD_16); + + reader.SetFontRegistry(fontRegistry); + reader.Read(snapshot); - // Minimal set of tags to generate a valid CR image - tags.SetValue(Orthanc::DICOM_TAG_ACCESSION_NUMBER, "NOPE", false); - tags.SetValue(Orthanc::DICOM_TAG_BODY_PART_EXAMINED, "PELVIS", false); - tags.SetValue(Orthanc::DICOM_TAG_INSTANCE_NUMBER, "1", false); - //tags.SetValue(Orthanc::DICOM_TAG_LATERALITY, "", false); - tags.SetValue(Orthanc::DICOM_TAG_MANUFACTURER, "OSIMIS", false); - tags.SetValue(Orthanc::DICOM_TAG_MODALITY, "CR", false); - tags.SetValue(Orthanc::DICOM_TAG_PATIENT_BIRTH_DATE, "20000101", false); - tags.SetValue(Orthanc::DICOM_TAG_PATIENT_ID, "hello", false); - tags.SetValue(Orthanc::DICOM_TAG_PATIENT_NAME, "HELLO^WORLD", false); - tags.SetValue(Orthanc::DICOM_TAG_PATIENT_ORIENTATION, "", false); - tags.SetValue(Orthanc::DICOM_TAG_PATIENT_SEX, "M", false); - tags.SetValue(Orthanc::DICOM_TAG_REFERRING_PHYSICIAN_NAME, "HOUSE^MD", false); - tags.SetValue(Orthanc::DICOM_TAG_SERIES_NUMBER, "1", false); - tags.SetValue(Orthanc::DICOM_TAG_SOP_CLASS_UID, "1.2.840.10008.5.1.4.1.1.1", false); - tags.SetValue(Orthanc::DICOM_TAG_STUDY_ID, "STUDY", false); - tags.SetValue(Orthanc::DICOM_TAG_VIEW_POSITION, "", false); + widget.SetScene(scene); + };break; + + case 'e': + { + Orthanc::DicomMap tags; - if (context_ != NULL) - { - widget.GetScene().ExportDicom(context_->GetOrthancApiClient(), - tags, 0.1, 0.1, widget.IsInverted(), - widget.GetInterpolation(), EXPORT_USING_PAM); - } - - break; + // Minimal set of tags to generate a valid CR image + tags.SetValue(Orthanc::DICOM_TAG_ACCESSION_NUMBER, "NOPE", false); + tags.SetValue(Orthanc::DICOM_TAG_BODY_PART_EXAMINED, "PELVIS", false); + tags.SetValue(Orthanc::DICOM_TAG_INSTANCE_NUMBER, "1", false); + //tags.SetValue(Orthanc::DICOM_TAG_LATERALITY, "", false); + tags.SetValue(Orthanc::DICOM_TAG_MANUFACTURER, "OSIMIS", false); + tags.SetValue(Orthanc::DICOM_TAG_MODALITY, "CR", false); + tags.SetValue(Orthanc::DICOM_TAG_PATIENT_BIRTH_DATE, "20000101", false); + tags.SetValue(Orthanc::DICOM_TAG_PATIENT_ID, "hello", false); + tags.SetValue(Orthanc::DICOM_TAG_PATIENT_NAME, "HELLO^WORLD", false); + tags.SetValue(Orthanc::DICOM_TAG_PATIENT_ORIENTATION, "", false); + tags.SetValue(Orthanc::DICOM_TAG_PATIENT_SEX, "M", false); + tags.SetValue(Orthanc::DICOM_TAG_REFERRING_PHYSICIAN_NAME, "HOUSE^MD", false); + tags.SetValue(Orthanc::DICOM_TAG_SERIES_NUMBER, "1", false); + tags.SetValue(Orthanc::DICOM_TAG_SOP_CLASS_UID, "1.2.840.10008.5.1.4.1.1.1", false); + tags.SetValue(Orthanc::DICOM_TAG_STUDY_ID, "STUDY", false); + tags.SetValue(Orthanc::DICOM_TAG_VIEW_POSITION, "", false); + + if (context_ != NULL) + { + widget.GetScene().ExportDicom(context_->GetOrthancApiClient(), + tags, std::string(), 0.1, 0.1, widget.IsInverted(), + widget.GetInterpolation(), EXPORT_USING_PAM); } - case 'i': - widget.SwitchInvert(); - break; - - case 'm': - tool_ = Tool_Move; + break; + } + + case 'i': + widget.SwitchInvert(); + break; + + case 'm': + tool_ = Tool_Move; + break; + + case 'n': + { + switch (widget.GetInterpolation()) + { + case ImageInterpolation_Nearest: + LOG(INFO) << "Switching to bilinear interpolation"; + widget.SetInterpolation(ImageInterpolation_Bilinear); break; - case 'n': - { - switch (widget.GetInterpolation()) - { - case ImageInterpolation_Nearest: - LOG(INFO) << "Switching to bilinear interpolation"; - widget.SetInterpolation(ImageInterpolation_Bilinear); - break; - - case ImageInterpolation_Bilinear: - LOG(INFO) << "Switching to nearest neighbor interpolation"; - widget.SetInterpolation(ImageInterpolation_Nearest); - break; - - default: - throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); - } - - break; - } - - case 'r': - tool_ = Tool_Rotate; + case ImageInterpolation_Bilinear: + LOG(INFO) << "Switching to nearest neighbor interpolation"; + widget.SetInterpolation(ImageInterpolation_Nearest); break; - case 's': - tool_ = Tool_Resize; - break; + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + + break; + } - case 'w': - tool_ = Tool_Windowing; - break; + case 'r': + tool_ = Tool_Rotate; + break; + + case 's': + tool_ = Tool_Resize; + break; + + case 'w': + tool_ = Tool_Windowing; + break; - case 'y': - if (modifiers & KeyboardModifiers_Control) - { - undoRedoStack_.Redo(); - widget.NotifyContentChanged(); - } - break; + case 'y': + if (modifiers & KeyboardModifiers_Control) + { + undoRedoStack_.Redo(); + widget.NotifyContentChanged(); + } + break; - case 'z': - if (modifiers & KeyboardModifiers_Control) - { - undoRedoStack_.Undo(); - widget.NotifyContentChanged(); - } - break; - - default: - break; + case 'z': + if (modifiers & KeyboardModifiers_Control) + { + undoRedoStack_.Undo(); + widget.NotifyContentChanged(); + } + break; + + default: + break; } } }; - - + + class SingleFrameEditorApplication : - public SampleSingleCanvasApplicationBase, - public IObserver + public SampleSingleCanvasApplicationBase, + public IObserver { private: - std::auto_ptr scene_; - RadiographyEditorInteractor interactor_; + boost::shared_ptr scene_; + RadiographyEditorInteractor interactor_; + Orthanc::FontRegistry fontRegistry_; public: SingleFrameEditorApplication(MessageBroker& broker) : @@ -399,11 +424,11 @@ { boost::program_options::options_description generic("Sample options"); generic.add_options() - ("instance", boost::program_options::value(), - "Orthanc ID of the instance") - ("frame", boost::program_options::value()->default_value(0), - "Number of the frame, for multi-frame DICOM instances") - ; + ("instance", boost::program_options::value(), + "Orthanc ID of the instance") + ("frame", boost::program_options::value()->default_value(0), + "Number of the frame, for multi-frame DICOM instances") + ; options.add(generic); } @@ -440,12 +465,11 @@ std::string instance = parameters["instance"].as(); int frame = parameters["frame"].as(); - Orthanc::FontRegistry fonts; - fonts.AddFromResource(Orthanc::EmbeddedResources::FONT_UBUNTU_MONO_BOLD_16); + fontRegistry_.AddFromResource(Orthanc::EmbeddedResources::FONT_UBUNTU_MONO_BOLD_16); scene_.reset(new RadiographyScene(GetBroker())); //scene_->LoadDicomFrame(instance, frame, false); //.SetPan(200, 0); - scene_->LoadDicomFrame(context->GetOrthancApiClient(), "61f3143e-96f34791-ad6bbb8d-62559e75-45943e1b", 0, false); + scene_->LoadDicomFrame(context->GetOrthancApiClient(), "61f3143e-96f34791-ad6bbb8d-62559e75-45943e1b", 0, false, NULL); #if !defined(ORTHANC_ENABLE_WASM) || ORTHANC_ENABLE_WASM != 1 Orthanc::HttpClient::ConfigureSsl(true, "/etc/ssl/certs/ca-certificates.crt"); @@ -454,18 +478,18 @@ //scene_->LoadDicomWebFrame(context->GetWebService()); { - RadiographyLayer& layer = scene_->LoadText(fonts.GetFont(0), "Hello\nworld"); + RadiographyLayer& layer = scene_->LoadText(fontRegistry_.GetFont(0), "Hello\nworld", NULL); layer.SetResizeable(true); } { - RadiographyLayer& layer = scene_->LoadTestBlock(100, 50); + RadiographyLayer& layer = scene_->LoadTestBlock(100, 50, NULL); layer.SetResizeable(true); layer.SetPan(0, 200); } - mainWidget_ = new RadiographyWidget(GetBroker(), *scene_, "main-widget"); + mainWidget_ = new RadiographyWidget(GetBroker(), scene_, "main-widget"); mainWidget_->SetTransmitMouseOver(true); mainWidget_->SetInteractor(interactor_); diff -r b70e9be013e4 -r a750f11892ec Applications/Samples/build-wasm.sh --- a/Applications/Samples/build-wasm.sh Mon Dec 24 13:41:12 2018 +0100 +++ b/Applications/Samples/build-wasm.sh Fri Dec 28 10:40:34 2018 +0100 @@ -1,7 +1,16 @@ #!/bin/bash +# +# usage: +# to build all targets: +# ./build-wasm.sh +# +# to build a single target: +# ./build-wasm.sh OrthancStoneSingleFrameEditor set -e +target=${1:-all} + currentDir=$(pwd) samplesRootDir=$(pwd) @@ -10,7 +19,7 @@ source ~/Downloads/emsdk/emsdk_env.sh cmake -DCMAKE_TOOLCHAIN_FILE=${EMSCRIPTEN}/cmake/Modules/Platform/Emscripten.cmake -DCMAKE_BUILD_TYPE=Release -DSTONE_SOURCES_DIR=$currentDir/../../../orthanc-stone -DORTHANC_FRAMEWORK_SOURCE=path -DORTHANC_FRAMEWORK_ROOT=$currentDir/../../../orthanc -DALLOW_DOWNLOADS=ON .. -DENABLE_WASM=ON -make -j 5 +make -j 5 $target echo "-- building the web application -- " cd $currentDir diff -r b70e9be013e4 -r a750f11892ec Applications/Samples/build-web.sh --- a/Applications/Samples/build-web.sh Mon Dec 24 13:41:12 2018 +0100 +++ b/Applications/Samples/build-web.sh Fri Dec 28 10:40:34 2018 +0100 @@ -2,6 +2,7 @@ set -e +target=${1:-all} # this script currently assumes that the wasm code has been built on its side and is availabie in build-wasm/ currentDir=$(pwd) @@ -15,29 +16,37 @@ cp $samplesRootDir/Web/samples-styles.css $outputDir # build simple-viewer-single-file (obsolete project) -cp $samplesRootDir/Web/simple-viewer-single-file.html $outputDir -tsc --allowJs --project $samplesRootDir/Web/simple-viewer-single-file.tsconfig.json -cp $currentDir/build-wasm/OrthancStoneSimpleViewerSingleFile.js $outputDir -cp $currentDir/build-wasm/OrthancStoneSimpleViewerSingleFile.wasm $outputDir +if [[ $target == "all" || $target == "OrthancStoneSimpleViewerSingleFile" ]]; then + cp $samplesRootDir/Web/simple-viewer-single-file.html $outputDir + tsc --allowJs --project $samplesRootDir/Web/simple-viewer-single-file.tsconfig.json + cp $currentDir/build-wasm/OrthancStoneSimpleViewerSingleFile.js $outputDir + cp $currentDir/build-wasm/OrthancStoneSimpleViewerSingleFile.wasm $outputDir +fi # build single-frame -cp $samplesRootDir/Web/single-frame.html $outputDir -tsc --allowJs --project $samplesRootDir/Web/single-frame.tsconfig.json -cp $currentDir/build-wasm/OrthancStoneSingleFrame.js $outputDir -cp $currentDir/build-wasm/OrthancStoneSingleFrame.wasm $outputDir +if [[ $target == "all" || $target == "OrthancStoneSingleFrame" ]]; then + cp $samplesRootDir/Web/single-frame.html $outputDir + tsc --allowJs --project $samplesRootDir/Web/single-frame.tsconfig.json + cp $currentDir/build-wasm/OrthancStoneSingleFrame.js $outputDir + cp $currentDir/build-wasm/OrthancStoneSingleFrame.wasm $outputDir +fi # build single-frame-editor -cp $samplesRootDir/Web/single-frame-editor.html $outputDir -tsc --allowJs --project $samplesRootDir/Web/single-frame-editor.tsconfig.json -cp $currentDir/build-wasm/OrthancStoneSingleFrameEditor.js $outputDir -cp $currentDir/build-wasm/OrthancStoneSingleFrameEditor.wasm $outputDir +if [[ $target == "all" || $target == "OrthancStoneSingleFrameEditor" ]]; then + cp $samplesRootDir/Web/single-frame-editor.html $outputDir + tsc --allowJs --project $samplesRootDir/Web/single-frame-editor.tsconfig.json + cp $currentDir/build-wasm/OrthancStoneSingleFrameEditor.js $outputDir + cp $currentDir/build-wasm/OrthancStoneSingleFrameEditor.wasm $outputDir +fi # 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/ +if [[ $target == "all" || $target == "OrthancStoneSimpleViewer" ]]; then + 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/ +fi cd $currentDir diff -r b70e9be013e4 -r a750f11892ec Applications/StoneApplicationContext.h --- a/Applications/StoneApplicationContext.h Mon Dec 24 13:41:12 2018 +0100 +++ b/Applications/StoneApplicationContext.h Fri Dec 28 10:40:34 2018 +0100 @@ -22,6 +22,7 @@ #pragma once #include "../Framework/Toolbox/IWebService.h" +#include "../Framework/Toolbox/IDelayedCallExecutor.h" #include "../Framework/Toolbox/OrthancApiClient.h" #include "../Framework/Viewport/WidgetViewport.h" @@ -41,6 +42,7 @@ private: MessageBroker& broker_; IWebService* webService_; + IDelayedCallExecutor* delayedCallExecutor_; std::auto_ptr orthanc_; std::string orthancBaseUrl_; @@ -49,7 +51,8 @@ public: StoneApplicationContext(MessageBroker& broker) : broker_(broker), - webService_(NULL) + webService_(NULL), + delayedCallExecutor_(NULL) { } @@ -74,5 +77,15 @@ void SetWebService(IWebService& webService); void SetOrthancBaseUrl(const std::string& baseUrl); + + void SetDelayedCallExecutor(IDelayedCallExecutor& delayedCallExecutor) + { + delayedCallExecutor_ = &delayedCallExecutor; + } + + IDelayedCallExecutor& GetDelayedCallExecutor() + { + return *delayedCallExecutor_; + } }; } diff -r b70e9be013e4 -r a750f11892ec Framework/Messages/IObservable.cpp --- a/Framework/Messages/IObservable.cpp Mon Dec 24 13:41:12 2018 +0100 +++ b/Framework/Messages/IObservable.cpp Fri Dec 28 10:40:34 2018 +0100 @@ -64,6 +64,25 @@ callables_[messageType].insert(callable); } + void IObservable::Unregister(IObserver *observer) + { + // delete all callables from this observer + for (Callables::iterator itCallableSet = callables_.begin(); + itCallableSet != callables_.end(); ++itCallableSet) + { + for (std::set::const_iterator + itCallable = itCallableSet->second.begin(); itCallable != itCallableSet->second.end(); ) + { + if ((*itCallable)->GetObserver() == observer) + { + delete *itCallable; + itCallableSet->second.erase(itCallable++); + } + else + ++itCallable; + } + } + } void IObservable::EmitMessage(const IMessage& message) { diff -r b70e9be013e4 -r a750f11892ec Framework/Messages/IObservable.h --- a/Framework/Messages/IObservable.h Mon Dec 24 13:41:12 2018 +0100 +++ b/Framework/Messages/IObservable.h Fri Dec 28 10:40:34 2018 +0100 @@ -58,6 +58,8 @@ // Takes ownsership void RegisterObserverCallback(ICallable* callable); + void Unregister(IObserver* observer); + void EmitMessage(const IMessage& message); // Takes ownsership diff -r b70e9be013e4 -r a750f11892ec Framework/Messages/MessageForwarder.h --- a/Framework/Messages/MessageForwarder.h Mon Dec 24 13:41:12 2018 +0100 +++ b/Framework/Messages/MessageForwarder.h Fri Dec 28 10:40:34 2018 +0100 @@ -59,10 +59,10 @@ * C is an observer of B and knows that B is re-emitting many messages from A * * instead of implementing a callback, B will create a MessageForwarder that will emit the messages in his name: - * A.RegisterObserverCallback(new MessageForwarder(broker, *this) // where this is B + * A.RegisterObserverCallback(new MessageForwarder(broker, *this) // where "this" is B * * in C: - * B.RegisterObserverCallback(new Callable(*this, &B::MyCallback)) // where this is C + * B.RegisterObserverCallback(new Callable(*this, &B::MyCallback)) // where "this" is C */ template class MessageForwarder : public IMessageForwarder, public Callable, TMessage> diff -r b70e9be013e4 -r a750f11892ec Framework/Radiography/RadiographyAlphaLayer.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Radiography/RadiographyAlphaLayer.cpp Fri Dec 28 10:40:34 2018 +0100 @@ -0,0 +1,132 @@ +/** + * 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 . + **/ + + +#include "RadiographyAlphaLayer.h" + +#include "RadiographyScene.h" + +#include +#include + +namespace OrthancStone +{ + + void RadiographyAlphaLayer::SetAlpha(Orthanc::ImageAccessor* image) + { + std::auto_ptr raii(image); + + if (image == NULL) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); + } + + if (image->GetFormat() != Orthanc::PixelFormat_Grayscale8) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageFormat); + } + + SetSize(image->GetWidth(), image->GetHeight()); + alpha_ = raii; + } + + void RadiographyAlphaLayer::Render(Orthanc::ImageAccessor& buffer, + const AffineTransform2D& viewTransform, + ImageInterpolation interpolation) const + { + if (alpha_.get() == NULL) + { + return; + } + + if (buffer.GetFormat() != Orthanc::PixelFormat_Float32) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageFormat); + } + + unsigned int cropX, cropY, cropWidth, cropHeight; + GetCrop(cropX, cropY, cropWidth, cropHeight); + + const AffineTransform2D t = AffineTransform2D::Combine( + viewTransform, GetTransform(), + AffineTransform2D::CreateOffset(cropX, cropY)); + + Orthanc::ImageAccessor cropped; + alpha_->GetRegion(cropped, cropX, cropY, cropWidth, cropHeight); + + Orthanc::Image tmp(Orthanc::PixelFormat_Grayscale8, buffer.GetWidth(), buffer.GetHeight(), false); + + t.Apply(tmp, cropped, interpolation, true /* clear */); + + // Blit + const unsigned int width = buffer.GetWidth(); + const unsigned int height = buffer.GetHeight(); + + float value = foreground_; + + if (useWindowing_) + { + float center, width; + if (scene_.GetWindowing(center, width)) + { + value = center + width / 2.0f; // set it to the maximum pixel value of the image + } + } + + for (unsigned int y = 0; y < height; y++) + { + float *q = reinterpret_cast(buffer.GetRow(y)); + const uint8_t *p = reinterpret_cast(tmp.GetRow(y)); + + for (unsigned int x = 0; x < width; x++, p++, q++) + { + float a = static_cast(*p) / 255.0f; + + *q = (a * value + (1.0f - a) * (*q)); + } + } + } + + bool RadiographyAlphaLayer::GetRange(float& minValue, + float& maxValue) const + { + if (useWindowing_) + { + return false; + } + else + { + minValue = 0; + maxValue = 0; + + if (foreground_ < 0) + { + minValue = foreground_; + } + + if (foreground_ > 0) + { + maxValue = foreground_; + } + + return true; + } + } +} diff -r b70e9be013e4 -r a750f11892ec Framework/Radiography/RadiographyAlphaLayer.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Radiography/RadiographyAlphaLayer.h Fri Dec 28 10:40:34 2018 +0100 @@ -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 . + **/ + + +#pragma once + +#include "RadiographyLayer.h" + +namespace OrthancStone +{ + class RadiographyScene; + + // creates a transparent layer whose alpha channel is provided as a UINT8 image to SetAlpha. + // The color of the "mask" is either defined by a ForegroundValue or by the center value of the + // windowing from the scene. + class RadiographyAlphaLayer : public RadiographyLayer + { + private: + const RadiographyScene& scene_; + std::auto_ptr alpha_; // Grayscale8 + bool useWindowing_; + float foreground_; + + public: + RadiographyAlphaLayer(const RadiographyScene& scene) : + scene_(scene), + useWindowing_(true), + foreground_(0) + { + } + + + void SetForegroundValue(float foreground) + { + useWindowing_ = false; + foreground_ = foreground; + } + + float GetForegroundValue() const + { + return foreground_; + } + + bool IsUsingWindowing() const + { + return useWindowing_; + } + + void SetAlpha(Orthanc::ImageAccessor* image); + + virtual bool GetDefaultWindowing(float& center, + float& width) const + { + return false; + } + + + virtual void Render(Orthanc::ImageAccessor& buffer, + const AffineTransform2D& viewTransform, + ImageInterpolation interpolation) const; + + virtual bool GetRange(float& minValue, + float& maxValue) const; + + const Orthanc::ImageAccessor& GetAlpha() const + { + return *(alpha_.get()); + } + + + }; +} diff -r b70e9be013e4 -r a750f11892ec Framework/Radiography/RadiographyDicomLayer.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Radiography/RadiographyDicomLayer.cpp Fri Dec 28 10:40:34 2018 +0100 @@ -0,0 +1,169 @@ +/** + * 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 . + **/ + + +#include "RadiographyDicomLayer.h" + +#include "RadiographyScene.h" +#include "../Toolbox/DicomFrameConverter.h" + +#include +#include +#include +#include + +static OrthancPlugins::DicomTag ConvertTag(const Orthanc::DicomTag& tag) +{ + return OrthancPlugins::DicomTag(tag.GetGroup(), tag.GetElement()); +} + +namespace OrthancStone +{ + + void RadiographyDicomLayer::ApplyConverter() + { + if (source_.get() != NULL && + converter_.get() != NULL) + { + converted_.reset(converter_->ConvertFrame(*source_)); + } + } + + + void RadiographyDicomLayer::SetDicomTags(const OrthancPlugins::FullOrthancDataset& dataset) + { + converter_.reset(new DicomFrameConverter); + converter_->ReadParameters(dataset); + ApplyConverter(); + + std::string tmp; + Vector pixelSpacing; + + if (dataset.GetStringValue(tmp, ConvertTag(Orthanc::DICOM_TAG_PIXEL_SPACING)) && + LinearAlgebra::ParseVector(pixelSpacing, tmp) && + pixelSpacing.size() == 2) + { + SetPixelSpacing(pixelSpacing[0], pixelSpacing[1]); + } + + OrthancPlugins::DicomDatasetReader reader(dataset); + + unsigned int width, height; + if (!reader.GetUnsignedIntegerValue(width, ConvertTag(Orthanc::DICOM_TAG_COLUMNS)) || + !reader.GetUnsignedIntegerValue(height, ConvertTag(Orthanc::DICOM_TAG_ROWS))) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat); + } + else + { + SetSize(width, height); + } + + if (dataset.GetStringValue(tmp, ConvertTag(Orthanc::DICOM_TAG_PHOTOMETRIC_INTERPRETATION))) + { + if (tmp == "MONOCHROME1") + { + SetPreferredPhotomotricDisplayMode(PhotometricDisplayMode_Monochrome1); + } + else if (tmp == "MONOCHROME2") + { + SetPreferredPhotomotricDisplayMode(PhotometricDisplayMode_Monochrome2); + } + } + } + + void RadiographyDicomLayer::SetSourceImage(Orthanc::ImageAccessor* image) // Takes ownership + { + std::auto_ptr raii(image); + + if (image == NULL) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); + } + + SetSize(image->GetWidth(), image->GetHeight()); + + source_ = raii; + ApplyConverter(); + } + + void RadiographyDicomLayer::Render(Orthanc::ImageAccessor& buffer, + const AffineTransform2D& viewTransform, + ImageInterpolation interpolation) const + { + if (converted_.get() != NULL) + { + if (converted_->GetFormat() != Orthanc::PixelFormat_Float32) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + + unsigned int cropX, cropY, cropWidth, cropHeight; + GetCrop(cropX, cropY, cropWidth, cropHeight); + + AffineTransform2D t = AffineTransform2D::Combine( + viewTransform, GetTransform(), + AffineTransform2D::CreateOffset(cropX, cropY)); + + Orthanc::ImageAccessor cropped; + converted_->GetRegion(cropped, cropX, cropY, cropWidth, cropHeight); + + t.Apply(buffer, cropped, interpolation, false); + } + } + + + bool RadiographyDicomLayer::GetDefaultWindowing(float& center, + float& width) const + { + if (converter_.get() != NULL && + converter_->HasDefaultWindow()) + { + center = static_cast(converter_->GetDefaultWindowCenter()); + width = static_cast(converter_->GetDefaultWindowWidth()); + return true; + } + else + { + return false; + } + } + + + bool RadiographyDicomLayer::GetRange(float& minValue, + float& maxValue) const + { + if (converted_.get() != NULL) + { + if (converted_->GetFormat() != Orthanc::PixelFormat_Float32) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + + Orthanc::ImageProcessing::GetMinMaxFloatValue(minValue, maxValue, *converted_); + return true; + } + else + { + return false; + } + } + +} diff -r b70e9be013e4 -r a750f11892ec Framework/Radiography/RadiographyDicomLayer.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Radiography/RadiographyDicomLayer.h Fri Dec 28 10:40:34 2018 +0100 @@ -0,0 +1,74 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "RadiographyLayer.h" +#include + +namespace OrthancStone +{ + class RadiographyScene; + class DicomFrameConverter; + + class RadiographyDicomLayer : public RadiographyLayer + { + private: + std::auto_ptr source_; // Content of PixelData + std::auto_ptr converter_; + std::auto_ptr converted_; // Float32 + std::string instanceId_; + unsigned int frame_; + + void ApplyConverter(); + + public: + void SetInstance(const std::string& instanceId, unsigned int frame) + { + instanceId_ = instanceId; + frame_ = frame; + } + + std::string GetInstanceId() const + { + return instanceId_; + } + + unsigned int GetFrame() const + { + return frame_; + } + + void SetDicomTags(const OrthancPlugins::FullOrthancDataset& dataset); + + void SetSourceImage(Orthanc::ImageAccessor* image); // Takes ownership + + virtual void Render(Orthanc::ImageAccessor& buffer, + const AffineTransform2D& viewTransform, + ImageInterpolation interpolation) const; + + virtual bool GetDefaultWindowing(float& center, + float& width) const; + + virtual bool GetRange(float& minValue, + float& maxValue) const; + }; +} diff -r b70e9be013e4 -r a750f11892ec Framework/Radiography/RadiographyLayer.cpp --- a/Framework/Radiography/RadiographyLayer.cpp Mon Dec 24 13:41:12 2018 +0100 +++ b/Framework/Radiography/RadiographyLayer.cpp Fri Dec 28 10:40:34 2018 +0100 @@ -32,18 +32,41 @@ } + RadiographyLayer::Geometry::Geometry() : + hasCrop_(false), + panX_(0), + panY_(0), + angle_(0), + resizeable_(false), + pixelSpacingX_(1), + pixelSpacingY_(1) + { + + } + + void RadiographyLayer::Geometry::GetCrop(unsigned int &x, unsigned int &y, unsigned int &width, unsigned int &height) const + { + if (!hasCrop_) + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); // you should probably use RadiographyLayer::GetCrop() or at least call HasCrop() before + + x = cropX_; + y = cropY_; + width = cropWidth_; + height = cropHeight_; + } + void RadiographyLayer::UpdateTransform() { - transform_ = AffineTransform2D::CreateScaling(pixelSpacingX_, pixelSpacingY_); + transform_ = AffineTransform2D::CreateScaling(geometry_.GetPixelSpacingX(), geometry_.GetPixelSpacingY()); double centerX, centerY; GetCenter(centerX, centerY); transform_ = AffineTransform2D::Combine( - AffineTransform2D::CreateOffset(panX_ + centerX, panY_ + centerY), - AffineTransform2D::CreateRotation(angle_), - AffineTransform2D::CreateOffset(-centerX, -centerY), - transform_); + AffineTransform2D::CreateOffset(geometry_.GetPanX() + centerX, geometry_.GetPanY() + centerY), + AffineTransform2D::CreateRotation(geometry_.GetAngle()), + AffineTransform2D::CreateOffset(-centerX, -centerY), + transform_); transformInverse_ = AffineTransform2D::Invert(transform_); } @@ -73,28 +96,28 @@ switch (corner) { - case Corner_TopLeft: - x = dx; - y = dy; - break; + case Corner_TopLeft: + x = dx; + y = dy; + break; - case Corner_TopRight: - x = dx + dwidth; - y = dy; - break; + case Corner_TopRight: + x = dx + dwidth; + y = dy; + break; - case Corner_BottomLeft: - x = dx; - y = dy + dheight; - break; + case Corner_BottomLeft: + x = dx; + y = dy + dheight; + break; - case Corner_BottomRight: - x = dx + dwidth; - y = dy + dheight; - break; + case Corner_BottomRight: + x = dx + dwidth; + y = dy + dheight; + break; - default: - throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); } transform_.Apply(x, y); @@ -105,7 +128,7 @@ double y) const { transformInverse_.Apply(x, y); - + unsigned int cropX, cropY, cropWidth, cropHeight; GetCrop(cropX, cropY, cropWidth, cropHeight); @@ -127,7 +150,7 @@ cairo_t* cr = context.GetObject(); cairo_set_line_width(cr, 2.0 / zoom); - + double x, y; x = dx; y = dy; @@ -163,17 +186,16 @@ hasSize_(false), width_(0), height_(0), - hasCrop_(false), - pixelSpacingX_(1), - pixelSpacingY_(1), - panX_(0), - panY_(0), - angle_(0), - resizeable_(false) + prefferedPhotometricDisplayMode_(PhotometricDisplayMode_Default) { UpdateTransform(); } + void RadiographyLayer::ResetCrop() + { + geometry_.ResetCrop(); + UpdateTransform(); + } void RadiographyLayer::SetCrop(unsigned int x, unsigned int y, @@ -184,34 +206,36 @@ { throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); } - + if (x + width > width_ || y + height > height_) { throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); } - - hasCrop_ = true; - cropX_ = x; - cropY_ = y; - cropWidth_ = width; - cropHeight_ = height; + geometry_.SetCrop(x, y, width, height); UpdateTransform(); } - + void RadiographyLayer::SetGeometry(const Geometry& geometry) + { + geometry_ = geometry; + + if (hasSize_) + { + UpdateTransform(); + } + } + + void RadiographyLayer::GetCrop(unsigned int& x, unsigned int& y, unsigned int& width, unsigned int& height) const { - if (hasCrop_) + if (GetGeometry().HasCrop()) { - x = cropX_; - y = cropY_; - width = cropWidth_; - height = cropHeight_; + GetGeometry().GetCrop(x, y, width, height); } else { @@ -222,10 +246,10 @@ } } - + void RadiographyLayer::SetAngle(double angle) { - angle_ = angle; + geometry_.SetAngle(angle); UpdateTransform(); } @@ -239,7 +263,7 @@ { throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageSize); } - + hasSize_ = true; width_ = width; height_ = height; @@ -251,7 +275,7 @@ Extent2D RadiographyLayer::GetExtent() const { Extent2D extent; - + unsigned int x, y, width, height; GetCrop(x, y, width, height); @@ -264,7 +288,7 @@ AddToExtent(extent, dx + dwidth, dy); AddToExtent(extent, dx, dy + dheight); AddToExtent(extent, dx + dwidth, dy + dheight); - + return extent; } @@ -282,7 +306,7 @@ else { transformInverse_.Apply(sceneX, sceneY); - + int x = static_cast(std::floor(sceneX)); int y = static_cast(std::floor(sceneY)); @@ -320,8 +344,7 @@ void RadiographyLayer::SetPan(double x, double y) { - panX_ = x; - panY_ = y; + geometry_.SetPan(x, y); UpdateTransform(); } @@ -329,8 +352,7 @@ void RadiographyLayer::SetPixelSpacing(double x, double y) { - pixelSpacingX_ = x; - pixelSpacingY_ = y; + geometry_.SetPixelSpacing(x, y); UpdateTransform(); } @@ -352,8 +374,8 @@ GetCrop(cropX, cropY, cropWidth, cropHeight); GetCornerInternal(x, y, corner, cropX, cropY, cropWidth, cropHeight); } - - + + bool RadiographyLayer::LookupCorner(Corner& corner /* out */, double x, double y, @@ -366,26 +388,26 @@ Corner_BottomLeft, Corner_BottomRight }; - + unsigned int cropX, cropY, cropWidth, cropHeight; GetCrop(cropX, cropY, cropWidth, cropHeight); double threshold = Square(viewportDistance / zoom); - + for (size_t i = 0; i < 4; i++) { double cx, cy; GetCornerInternal(cx, cy, CORNERS[i], cropX, cropY, cropWidth, cropHeight); double d = Square(cx - x) + Square(cy - y); - + if (d <= threshold) { corner = CORNERS[i]; return true; } } - + return false; } } diff -r b70e9be013e4 -r a750f11892ec Framework/Radiography/RadiographyLayer.h --- a/Framework/Radiography/RadiographyLayer.h Mon Dec 24 13:41:12 2018 +0100 +++ b/Framework/Radiography/RadiographyLayer.h Fri Dec 28 10:40:34 2018 +0100 @@ -30,25 +30,118 @@ class RadiographyLayer : public boost::noncopyable { friend class RadiographyScene; - + + public: + class Geometry + { + bool hasCrop_; + unsigned int cropX_; + unsigned int cropY_; + unsigned int cropWidth_; + unsigned int cropHeight_; + double panX_; + double panY_; + double angle_; + bool resizeable_; + double pixelSpacingX_; + double pixelSpacingY_; + + public: + Geometry(); + + void ResetCrop() + { + hasCrop_ = false; + } + + void SetCrop(unsigned int x, + unsigned int y, + unsigned int width, + unsigned int height) + { + hasCrop_ = true; + cropX_ = x; + cropY_ = y; + cropWidth_ = width; + cropHeight_ = height; + } + + bool HasCrop() const + { + return hasCrop_; + } + + void GetCrop(unsigned int& x, + unsigned int& y, + unsigned int& width, + unsigned int& height) const; + + void SetAngle(double angle) + { + angle_ = angle; + } + + double GetAngle() const + { + return angle_; + } + + void SetPan(double x, + double y) + { + panX_ = x; + panY_ = y; + } + + double GetPanX() const + { + return panX_; + } + + double GetPanY() const + { + return panY_; + } + + bool IsResizeable() const + { + return resizeable_; + } + + void SetResizeable(bool resizeable) + { + resizeable_ = resizeable; + } + + void SetPixelSpacing(double x, + double y) + { + pixelSpacingX_ = x; + pixelSpacingY_ = y; + } + + double GetPixelSpacingX() const + { + return pixelSpacingX_; + } + + double GetPixelSpacingY() const + { + return pixelSpacingY_; + } + + }; + private: size_t index_; bool hasSize_; unsigned int width_; unsigned int height_; - bool hasCrop_; - unsigned int cropX_; - unsigned int cropY_; - unsigned int cropWidth_; - unsigned int cropHeight_; AffineTransform2D transform_; AffineTransform2D transformInverse_; - double pixelSpacingX_; - double pixelSpacingY_; - double panX_; - double panY_; - double angle_; - bool resizeable_; + Geometry geometry_; + PhotometricDisplayMode prefferedPhotometricDisplayMode_; + protected: const AffineTransform2D& GetTransform() const @@ -56,6 +149,11 @@ return transform_; } + void SetPreferredPhotomotricDisplayMode(PhotometricDisplayMode prefferedPhotometricDisplayMode) + { + prefferedPhotometricDisplayMode_ = prefferedPhotometricDisplayMode; + } + private: void UpdateTransform(); @@ -94,11 +192,15 @@ return index_; } - void ResetCrop() + const Geometry& GetGeometry() const { - hasCrop_ = false; + return geometry_; } + void SetGeometry(const Geometry& geometry); + + void ResetCrop(); + void SetCrop(unsigned int x, unsigned int y, unsigned int width, @@ -111,14 +213,22 @@ void SetAngle(double angle); - double GetAngle() const + void SetPan(double x, + double y); + + void SetResizeable(bool resizeable) { - return angle_; + geometry_.SetResizeable(resizeable); } void SetSize(unsigned int width, unsigned int height); + bool HasSize() const + { + return hasSize_; + } + unsigned int GetWidth() const { return width_; @@ -136,32 +246,9 @@ double sceneX, double sceneY) const; - void SetPan(double x, - double y); - void SetPixelSpacing(double x, double y); - double GetPixelSpacingX() const - { - return pixelSpacingX_; - } - - double GetPixelSpacingY() const - { - return pixelSpacingY_; - } - - double GetPanX() const - { - return panX_; - } - - double GetPanY() const - { - return panY_; - } - void GetCenter(double& centerX, double& centerY) const; @@ -175,19 +262,14 @@ double zoom, double viewportDistance) const; - bool IsResizeable() const - { - return resizeable_; - } - - void SetResizeable(bool resizeable) - { - resizeable_ = resizeable; - } - virtual bool GetDefaultWindowing(float& center, float& width) const = 0; + PhotometricDisplayMode GetPreferredPhotomotricDisplayMode() const + { + return prefferedPhotometricDisplayMode_; + } + virtual void Render(Orthanc::ImageAccessor& buffer, const AffineTransform2D& viewTransform, ImageInterpolation interpolation) const = 0; diff -r b70e9be013e4 -r a750f11892ec Framework/Radiography/RadiographyLayerCropTracker.cpp --- a/Framework/Radiography/RadiographyLayerCropTracker.cpp Mon Dec 24 13:41:12 2018 +0100 +++ b/Framework/Radiography/RadiographyLayerCropTracker.cpp Fri Dec 28 10:40:34 2018 +0100 @@ -77,7 +77,7 @@ { if (accessor_.IsValid()) { - accessor_.GetLayer().GetCrop(cropX_, cropY_, cropWidth_, cropHeight_); + accessor_.GetLayer().GetCrop(cropX_, cropY_, cropWidth_, cropHeight_); } } diff -r b70e9be013e4 -r a750f11892ec Framework/Radiography/RadiographyLayerMoveTracker.cpp --- a/Framework/Radiography/RadiographyLayerMoveTracker.cpp Mon Dec 24 13:41:12 2018 +0100 +++ b/Framework/Radiography/RadiographyLayerMoveTracker.cpp Fri Dec 28 10:40:34 2018 +0100 @@ -51,8 +51,8 @@ RadiographySceneCommand(tracker.accessor_), sourceX_(tracker.panX_), sourceY_(tracker.panY_), - targetX_(tracker.accessor_.GetLayer().GetPanX()), - targetY_(tracker.accessor_.GetLayer().GetPanY()) + targetX_(tracker.accessor_.GetLayer().GetGeometry().GetPanX()), + targetY_(tracker.accessor_.GetLayer().GetGeometry().GetPanY()) { } }; @@ -72,8 +72,8 @@ { if (accessor_.IsValid()) { - panX_ = accessor_.GetLayer().GetPanX(); - panY_ = accessor_.GetLayer().GetPanY(); + panX_ = accessor_.GetLayer().GetGeometry().GetPanX(); + panY_ = accessor_.GetLayer().GetGeometry().GetPanY(); } } diff -r b70e9be013e4 -r a750f11892ec Framework/Radiography/RadiographyLayerResizeTracker.cpp --- a/Framework/Radiography/RadiographyLayerResizeTracker.cpp Mon Dec 24 13:41:12 2018 +0100 +++ b/Framework/Radiography/RadiographyLayerResizeTracker.cpp Fri Dec 28 10:40:34 2018 +0100 @@ -73,10 +73,10 @@ sourceSpacingY_(tracker.originalSpacingY_), sourcePanX_(tracker.originalPanX_), sourcePanY_(tracker.originalPanY_), - targetSpacingX_(tracker.accessor_.GetLayer().GetPixelSpacingX()), - targetSpacingY_(tracker.accessor_.GetLayer().GetPixelSpacingY()), - targetPanX_(tracker.accessor_.GetLayer().GetPanX()), - targetPanY_(tracker.accessor_.GetLayer().GetPanY()) + targetSpacingX_(tracker.accessor_.GetLayer().GetGeometry().GetPixelSpacingX()), + targetSpacingY_(tracker.accessor_.GetLayer().GetGeometry().GetPixelSpacingY()), + targetPanX_(tracker.accessor_.GetLayer().GetGeometry().GetPanX()), + targetPanY_(tracker.accessor_.GetLayer().GetGeometry().GetPanY()) { } }; @@ -94,12 +94,12 @@ roundScaling_(roundScaling) { if (accessor_.IsValid() && - accessor_.GetLayer().IsResizeable()) + accessor_.GetLayer().GetGeometry().IsResizeable()) { - originalSpacingX_ = accessor_.GetLayer().GetPixelSpacingX(); - originalSpacingY_ = accessor_.GetLayer().GetPixelSpacingY(); - originalPanX_ = accessor_.GetLayer().GetPanX(); - originalPanY_ = accessor_.GetLayer().GetPanY(); + originalSpacingX_ = accessor_.GetLayer().GetGeometry().GetPixelSpacingX(); + originalSpacingY_ = accessor_.GetLayer().GetGeometry().GetPixelSpacingY(); + originalPanX_ = accessor_.GetLayer().GetGeometry().GetPanX(); + originalPanY_ = accessor_.GetLayer().GetGeometry().GetPanY(); switch (corner) { @@ -149,7 +149,7 @@ void RadiographyLayerResizeTracker::MouseUp() { if (accessor_.IsValid() && - accessor_.GetLayer().IsResizeable()) + accessor_.GetLayer().GetGeometry().IsResizeable()) { undoRedoStack_.Add(new UndoRedoCommand(*this)); } @@ -164,7 +164,7 @@ static const double ROUND_SCALING = 0.1; if (accessor_.IsValid() && - accessor_.GetLayer().IsResizeable()) + accessor_.GetLayer().GetGeometry().IsResizeable()) { double scaling = ComputeDistance(oppositeX_, oppositeY_, sceneX, sceneY) * baseScaling_; @@ -180,8 +180,8 @@ // Keep the opposite corner at a fixed location double ox, oy; layer.GetCorner(ox, oy, oppositeCorner_); - layer.SetPan(layer.GetPanX() + oppositeX_ - ox, - layer.GetPanY() + oppositeY_ - oy); + layer.SetPan(layer.GetGeometry().GetPanX() + oppositeX_ - ox, + layer.GetGeometry().GetPanY() + oppositeY_ - oy); } } } diff -r b70e9be013e4 -r a750f11892ec Framework/Radiography/RadiographyLayerRotateTracker.cpp --- a/Framework/Radiography/RadiographyLayerRotateTracker.cpp Mon Dec 24 13:41:12 2018 +0100 +++ b/Framework/Radiography/RadiographyLayerRotateTracker.cpp Fri Dec 28 10:40:34 2018 +0100 @@ -58,7 +58,7 @@ UndoRedoCommand(const RadiographyLayerRotateTracker& tracker) : RadiographySceneCommand(tracker.accessor_), sourceAngle_(tracker.originalAngle_), - targetAngle_(tracker.accessor_.GetLayer().GetAngle()) + targetAngle_(tracker.accessor_.GetLayer().GetGeometry().GetAngle()) { } }; @@ -100,7 +100,7 @@ if (accessor_.IsValid()) { accessor_.GetLayer().GetCenter(centerX_, centerY_); - originalAngle_ = accessor_.GetLayer().GetAngle(); + originalAngle_ = accessor_.GetLayer().GetGeometry().GetAngle(); double sceneX, sceneY; view.MapDisplayToScene(sceneX, sceneY, x, y); diff -r b70e9be013e4 -r a750f11892ec Framework/Radiography/RadiographyScene.cpp --- a/Framework/Radiography/RadiographyScene.cpp Mon Dec 24 13:41:12 2018 +0100 +++ b/Framework/Radiography/RadiographyScene.cpp Fri Dec 28 10:40:34 2018 +0100 @@ -21,6 +21,9 @@ #include "RadiographyScene.h" +#include "RadiographyAlphaLayer.h" +#include "RadiographyDicomLayer.h" +#include "RadiographyTextLayer.h" #include "../Toolbox/DicomFrameConverter.h" #include @@ -55,7 +58,7 @@ } } - + RadiographyScene::LayerAccessor::LayerAccessor(RadiographyScene& scene, double x, double y) : @@ -65,7 +68,7 @@ if (scene.LookupLayer(index_, x, y)) { Layers::iterator layer = scene.layers_.find(index_); - + if (layer == scene.layers_.end()) { throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); @@ -119,289 +122,7 @@ { throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); } - } - - - - class RadiographyScene::AlphaLayer : public RadiographyLayer - { - private: - const RadiographyScene& scene_; - std::auto_ptr alpha_; // Grayscale8 - bool useWindowing_; - float foreground_; - - public: - AlphaLayer(const RadiographyScene& scene) : - scene_(scene), - useWindowing_(true), - foreground_(0) - { - } - - - void SetForegroundValue(float foreground) - { - useWindowing_ = false; - foreground_ = foreground; - } - - - void SetAlpha(Orthanc::ImageAccessor* image) - { - std::auto_ptr raii(image); - - if (image == NULL) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); - } - - if (image->GetFormat() != Orthanc::PixelFormat_Grayscale8) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageFormat); - } - - SetSize(image->GetWidth(), image->GetHeight()); - alpha_ = raii; - } - - - void LoadText(const Orthanc::Font& font, - const std::string& utf8) - { - SetAlpha(font.RenderAlpha(utf8)); - } - - - virtual bool GetDefaultWindowing(float& center, - float& width) const - { - return false; - } - - - virtual void Render(Orthanc::ImageAccessor& buffer, - const AffineTransform2D& viewTransform, - ImageInterpolation interpolation) const - { - if (alpha_.get() == NULL) - { - return; - } - - if (buffer.GetFormat() != Orthanc::PixelFormat_Float32) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageFormat); - } - - unsigned int cropX, cropY, cropWidth, cropHeight; - GetCrop(cropX, cropY, cropWidth, cropHeight); - - const AffineTransform2D t = AffineTransform2D::Combine( - viewTransform, GetTransform(), - AffineTransform2D::CreateOffset(cropX, cropY)); - - Orthanc::ImageAccessor cropped; - alpha_->GetRegion(cropped, cropX, cropY, cropWidth, cropHeight); - - Orthanc::Image tmp(Orthanc::PixelFormat_Grayscale8, buffer.GetWidth(), buffer.GetHeight(), false); - - t.Apply(tmp, cropped, interpolation, true /* clear */); - - // Blit - const unsigned int width = buffer.GetWidth(); - const unsigned int height = buffer.GetHeight(); - - float value = foreground_; - - if (useWindowing_) - { - float center, width; - if (scene_.GetWindowing(center, width)) - { - value = center + width / 2.0f; - } - } - - for (unsigned int y = 0; y < height; y++) - { - float *q = reinterpret_cast(buffer.GetRow(y)); - const uint8_t *p = reinterpret_cast(tmp.GetRow(y)); - - for (unsigned int x = 0; x < width; x++, p++, q++) - { - float a = static_cast(*p) / 255.0f; - - *q = (a * value + (1.0f - a) * (*q)); - } - } - } - - - virtual bool GetRange(float& minValue, - float& maxValue) const - { - if (useWindowing_) - { - return false; - } - else - { - minValue = 0; - maxValue = 0; - - if (foreground_ < 0) - { - minValue = foreground_; - } - - if (foreground_ > 0) - { - maxValue = foreground_; - } - - return true; - } - } - }; - - - - class RadiographyScene::DicomLayer : public RadiographyLayer - { - private: - std::auto_ptr source_; // Content of PixelData - std::auto_ptr converter_; - std::auto_ptr converted_; // Float32 - - static OrthancPlugins::DicomTag ConvertTag(const Orthanc::DicomTag& tag) - { - return OrthancPlugins::DicomTag(tag.GetGroup(), tag.GetElement()); - } - - - void ApplyConverter() - { - if (source_.get() != NULL && - converter_.get() != NULL) - { - converted_.reset(converter_->ConvertFrame(*source_)); - } - } - - public: - void SetDicomTags(const OrthancPlugins::FullOrthancDataset& dataset) - { - converter_.reset(new DicomFrameConverter); - converter_->ReadParameters(dataset); - ApplyConverter(); - - std::string tmp; - Vector pixelSpacing; - - if (dataset.GetStringValue(tmp, ConvertTag(Orthanc::DICOM_TAG_PIXEL_SPACING)) && - LinearAlgebra::ParseVector(pixelSpacing, tmp) && - pixelSpacing.size() == 2) - { - SetPixelSpacing(pixelSpacing[0], pixelSpacing[1]); - } - - //SetPan(-0.5 * GetPixelSpacingX(), -0.5 * GetPixelSpacingY()); - - OrthancPlugins::DicomDatasetReader reader(dataset); - - unsigned int width, height; - if (!reader.GetUnsignedIntegerValue(width, ConvertTag(Orthanc::DICOM_TAG_COLUMNS)) || - !reader.GetUnsignedIntegerValue(height, ConvertTag(Orthanc::DICOM_TAG_ROWS))) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat); - } - else - { - SetSize(width, height); - } - } - - - void SetSourceImage(Orthanc::ImageAccessor* image) // Takes ownership - { - std::auto_ptr raii(image); - - if (image == NULL) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); - } - - SetSize(image->GetWidth(), image->GetHeight()); - - source_ = raii; - ApplyConverter(); - } - - - virtual void Render(Orthanc::ImageAccessor& buffer, - const AffineTransform2D& viewTransform, - ImageInterpolation interpolation) const - { - if (converted_.get() != NULL) - { - if (converted_->GetFormat() != Orthanc::PixelFormat_Float32) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); - } - - unsigned int cropX, cropY, cropWidth, cropHeight; - GetCrop(cropX, cropY, cropWidth, cropHeight); - - AffineTransform2D t = AffineTransform2D::Combine( - viewTransform, GetTransform(), - AffineTransform2D::CreateOffset(cropX, cropY)); - - Orthanc::ImageAccessor cropped; - converted_->GetRegion(cropped, cropX, cropY, cropWidth, cropHeight); - - t.Apply(buffer, cropped, interpolation, false); - } - } - - - virtual bool GetDefaultWindowing(float& center, - float& width) const - { - if (converter_.get() != NULL && - converter_->HasDefaultWindow()) - { - center = static_cast(converter_->GetDefaultWindowCenter()); - width = static_cast(converter_->GetDefaultWindowWidth()); - return true; - } - else - { - return false; - } - } - - - virtual bool GetRange(float& minValue, - float& maxValue) const - { - if (converted_.get() != NULL) - { - if (converted_->GetFormat() != Orthanc::PixelFormat_Float32) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); - } - - Orthanc::ImageProcessing::GetMinMaxFloatValue(minValue, maxValue, *converted_); - return true; - } - else - { - return false; - } - } - }; - + } RadiographyLayer& RadiographyScene::RegisterLayer(RadiographyLayer* layer) { @@ -411,17 +132,19 @@ } std::auto_ptr raii(layer); - + + LOG(INFO) << "Registering layer: " << countLayers_; + size_t index = countLayers_++; raii->SetIndex(index); layers_[index] = raii.release(); - EmitMessage(GeometryChangedMessage(*this)); - EmitMessage(ContentChangedMessage(*this)); + EmitMessage(GeometryChangedMessage(*this, *layer)); + EmitMessage(ContentChangedMessage(*this, *layer)); return *layer; } - + RadiographyScene::RadiographyScene(MessageBroker& broker) : IObserver(broker), @@ -443,6 +166,52 @@ } } + PhotometricDisplayMode RadiographyScene::GetPreferredPhotomotricDisplayMode() const + { + // return the mode of the first layer who "cares" about its display mode (normaly, the one and only layer that is a DicomLayer) + for (Layers::const_iterator it = layers_.begin(); it != layers_.end(); it++) + { + if (it->second->GetPreferredPhotomotricDisplayMode() != PhotometricDisplayMode_Default) + { + return it->second->GetPreferredPhotomotricDisplayMode(); + } + } + + return PhotometricDisplayMode_Default; + } + + + void RadiographyScene::GetLayersIndexes(std::vector& output) const + { + for (Layers::const_iterator it = layers_.begin(); it != layers_.end(); it++) + { + output.push_back(it->first); + } + } + + void RadiographyScene::RemoveLayer(size_t layerIndex) + { + LOG(INFO) << "Removing layer: " << layerIndex; + + if (layerIndex > countLayers_) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + delete layers_[layerIndex]; + layers_.erase(layerIndex); + countLayers_--; + LOG(INFO) << "Removing layer, there are now : " << countLayers_ << " layers"; + } + + const RadiographyLayer& RadiographyScene::GetLayer(size_t layerIndex) const + { + if (layerIndex > countLayers_) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + + return *(layers_.at(layerIndex)); + } bool RadiographyScene::GetWindowing(float& center, float& width) const @@ -481,17 +250,23 @@ RadiographyLayer& RadiographyScene::LoadText(const Orthanc::Font& font, - const std::string& utf8) + const std::string& utf8, + RadiographyLayer::Geometry* geometry) { - std::auto_ptr alpha(new AlphaLayer(*this)); + std::auto_ptr alpha(new RadiographyTextLayer(*this)); alpha->LoadText(font, utf8); + if (geometry != NULL) + { + alpha->SetGeometry(*geometry); + } return RegisterLayer(alpha.release()); } - + RadiographyLayer& RadiographyScene::LoadTestBlock(unsigned int width, - unsigned int height) + unsigned int height, + RadiographyLayer::Geometry* geometry) { std::auto_ptr block(new Orthanc::Image(Orthanc::PixelFormat_Grayscale8, width, height, false)); @@ -514,29 +289,44 @@ Orthanc::ImageProcessing::Set(region, color); } - std::auto_ptr alpha(new AlphaLayer(*this)); - alpha->SetAlpha(block.release()); + return LoadAlphaBitmap(block.release(), geometry); + } + + RadiographyLayer& RadiographyScene::LoadAlphaBitmap(Orthanc::ImageAccessor* bitmap, RadiographyLayer::Geometry *geometry) + { + std::auto_ptr alpha(new RadiographyAlphaLayer(*this)); + alpha->SetAlpha(bitmap); + if (geometry != NULL) + { + alpha->SetGeometry(*geometry); + } return RegisterLayer(alpha.release()); } - RadiographyLayer& RadiographyScene::LoadDicomFrame(OrthancApiClient& orthanc, const std::string& instance, unsigned int frame, - bool httpCompression) + bool httpCompression, + RadiographyLayer::Geometry* geometry) { - RadiographyLayer& layer = RegisterLayer(new DicomLayer); + RadiographyDicomLayer& layer = dynamic_cast(RegisterLayer(new RadiographyDicomLayer)); + layer.SetInstance(instance, frame); + + if (geometry != NULL) + { + layer.SetGeometry(*geometry); + } { IWebService::HttpHeaders headers; std::string uri = "/instances/" + instance + "/tags"; - + orthanc.GetBinaryAsync( - uri, headers, - new Callable - (*this, &RadiographyScene::OnTagsReceived), NULL, - new Orthanc::SingleValueObject(layer.GetIndex())); + uri, headers, + new Callable + (*this, &RadiographyScene::OnTagsReceived), NULL, + new Orthanc::SingleValueObject(layer.GetIndex())); } { @@ -547,15 +337,15 @@ { headers["Accept-Encoding"] = "gzip"; } - + std::string uri = ("/instances/" + instance + "/frames/" + boost::lexical_cast(frame) + "/image-uint16"); - + orthanc.GetBinaryAsync( - uri, headers, - new Callable - (*this, &RadiographyScene::OnFrameReceived), NULL, - new Orthanc::SingleValueObject(layer.GetIndex())); + uri, headers, + new Callable + (*this, &RadiographyScene::OnFrameReceived), NULL, + new Orthanc::SingleValueObject(layer.GetIndex())); } return layer; @@ -564,29 +354,29 @@ RadiographyLayer& RadiographyScene::LoadDicomWebFrame(IWebService& web) { - RadiographyLayer& layer = RegisterLayer(new DicomLayer); + RadiographyLayer& layer = RegisterLayer(new RadiographyDicomLayer); - + return layer; } - + void RadiographyScene::OnTagsReceived(const OrthancApiClient::BinaryResponseReadyMessage& message) { size_t index = dynamic_cast&> - (message.GetPayload()).GetValue(); + (message.GetPayload()).GetValue(); LOG(INFO) << "JSON received: " << message.GetUri().c_str() << " (" << message.GetAnswerSize() << " bytes) for layer " << index; - + Layers::iterator layer = layers_.find(index); if (layer != layers_.end()) { assert(layer->second != NULL); - + OrthancPlugins::FullOrthancDataset dicom(message.GetAnswer(), message.GetAnswerSize()); - dynamic_cast(layer->second)->SetDicomTags(dicom); + dynamic_cast(layer->second)->SetDicomTags(dicom); float c, w; if (!hasWindowing_ && @@ -597,18 +387,18 @@ windowingWidth_ = w; } - EmitMessage(GeometryChangedMessage(*this)); + EmitMessage(GeometryChangedMessage(*this, *(layer->second))); } } - + void RadiographyScene::OnFrameReceived(const OrthancApiClient::BinaryResponseReadyMessage& message) { size_t index = dynamic_cast&>(message.GetPayload()).GetValue(); - + LOG(INFO) << "DICOM frame received: " << message.GetUri().c_str() << " (" << message.GetAnswerSize() << " bytes) for layer " << index; - + Layers::iterator layer = layers_.find(index); if (layer != layers_.end()) { @@ -619,12 +409,12 @@ { content.assign(reinterpret_cast(message.GetAnswer()), message.GetAnswerSize()); } - + std::auto_ptr reader(new Orthanc::PamReader); reader->ReadFromMemory(content); - dynamic_cast(layer->second)->SetSourceImage(reader.release()); + dynamic_cast(layer->second)->SetSourceImage(reader.release()); - EmitMessage(ContentChangedMessage(*this)); + EmitMessage(ContentChangedMessage(*this, *(layer->second))); } } @@ -642,14 +432,12 @@ return extent; } - + void RadiographyScene::Render(Orthanc::ImageAccessor& buffer, const AffineTransform2D& viewTransform, ImageInterpolation interpolation) const { - Orthanc::ImageProcessing::Set(buffer, 0); - // Render layers in the background-to-foreground order for (size_t index = 0; index < countLayers_; index++) { @@ -685,13 +473,13 @@ return false; } - + void RadiographyScene::DrawBorder(CairoContext& context, unsigned int layer, double zoom) { Layers::const_iterator found = layers_.find(layer); - + if (found != layers_.end()) { context.SetSourceColor(255, 0, 0); @@ -704,7 +492,7 @@ float& maxValue) const { bool first = true; - + for (Layers::const_iterator it = layers_.begin(); it != layers_.end(); it++) { @@ -735,22 +523,47 @@ } - // Export using PAM is faster than using PNG, but requires Orthanc - // core >= 1.4.3 void RadiographyScene::ExportDicom(OrthancApiClient& orthanc, const Orthanc::DicomMap& dicom, + const std::string& parentOrthancId, double pixelSpacingX, double pixelSpacingY, bool invert, ImageInterpolation interpolation, bool usePam) { + Json::Value createDicomRequestContent; + + ExportToCreateDicomRequest(createDicomRequestContent, dicom, pixelSpacingX, pixelSpacingY, invert, interpolation, usePam); + + if (!parentOrthancId.empty()) + { + createDicomRequestContent["Parent"] = parentOrthancId; + } + + orthanc.PostJsonAsyncExpectJson( + "/tools/create-dicom", createDicomRequestContent, + new Callable + (*this, &RadiographyScene::OnDicomExported), + NULL, NULL); + } + + // Export using PAM is faster than using PNG, but requires Orthanc + // core >= 1.4.3 + void RadiographyScene::ExportToCreateDicomRequest(Json::Value& createDicomRequestContent, + const Orthanc::DicomMap& dicom, + double pixelSpacingX, + double pixelSpacingY, + bool invert, + ImageInterpolation interpolation, + bool usePam) + { if (pixelSpacingX <= 0 || pixelSpacingY <= 0) { throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); } - + LOG(INFO) << "Exporting DICOM"; Extent2D extent = GetSceneExtent(); @@ -768,9 +581,12 @@ static_cast(h), false); AffineTransform2D view = AffineTransform2D::Combine( - AffineTransform2D::CreateScaling(1.0 / pixelSpacingX, 1.0 / pixelSpacingY), - AffineTransform2D::CreateOffset(-extent.GetX1(), -extent.GetY1())); - + AffineTransform2D::CreateScaling(1.0 / pixelSpacingX, 1.0 / pixelSpacingY), + AffineTransform2D::CreateOffset(-extent.GetX1(), -extent.GetY1())); + + // wipe background before rendering + Orthanc::ImageProcessing::Set(layers, 0); + Render(layers, view, interpolation); Orthanc::Image rendered(Orthanc::PixelFormat_Grayscale16, @@ -799,22 +615,29 @@ std::set tags; dicom.GetTags(tags); - Json::Value json = Json::objectValue; - json["Tags"] = Json::objectValue; - + createDicomRequestContent["Tags"] = Json::objectValue; + for (std::set::const_iterator - tag = tags.begin(); tag != tags.end(); ++tag) + tag = tags.begin(); tag != tags.end(); ++tag) { const Orthanc::DicomValue& value = dicom.GetValue(*tag); if (!value.IsNull() && !value.IsBinary()) { - json["Tags"][tag->Format()] = value.GetContent(); + createDicomRequestContent["Tags"][tag->Format()] = value.GetContent(); } } - json["Tags"][Orthanc::DICOM_TAG_PHOTOMETRIC_INTERPRETATION.Format()] = - (invert ? "MONOCHROME1" : "MONOCHROME2"); + PhotometricDisplayMode photometricMode = GetPreferredPhotomotricDisplayMode(); + if ((invert && photometricMode != PhotometricDisplayMode_Monochrome2) || + (!invert && photometricMode == PhotometricDisplayMode_Monochrome1)) + { + createDicomRequestContent["Tags"][Orthanc::DICOM_TAG_PHOTOMETRIC_INTERPRETATION.Format()] = "MONOCHROME1"; + } + else + { + createDicomRequestContent["Tags"][Orthanc::DICOM_TAG_PHOTOMETRIC_INTERPRETATION.Format()] = "MONOCHROME2"; + } // WARNING: The order of PixelSpacing is Y/X. We use "%0.8f" to // avoid floating-point numbers to grow over 16 characters, @@ -822,29 +645,24 @@ // ("dciodvfy" would complain). char buf[32]; sprintf(buf, "%0.8f\\%0.8f", pixelSpacingY, pixelSpacingX); - - json["Tags"][Orthanc::DICOM_TAG_PIXEL_SPACING.Format()] = buf; + + createDicomRequestContent["Tags"][Orthanc::DICOM_TAG_PIXEL_SPACING.Format()] = buf; float center, width; if (GetWindowing(center, width)) { - json["Tags"][Orthanc::DICOM_TAG_WINDOW_CENTER.Format()] = - boost::lexical_cast(boost::math::iround(center)); + createDicomRequestContent["Tags"][Orthanc::DICOM_TAG_WINDOW_CENTER.Format()] = + boost::lexical_cast(boost::math::iround(center)); - json["Tags"][Orthanc::DICOM_TAG_WINDOW_WIDTH.Format()] = - boost::lexical_cast(boost::math::iround(width)); + createDicomRequestContent["Tags"][Orthanc::DICOM_TAG_WINDOW_WIDTH.Format()] = + boost::lexical_cast(boost::math::iround(width)); } + // This is Data URI scheme: https://en.wikipedia.org/wiki/Data_URI_scheme - json["Content"] = ("data:" + - std::string(usePam ? Orthanc::MIME_PAM : Orthanc::MIME_PNG) + - ";base64," + base64); - - orthanc.PostJsonAsyncExpectJson( - "/tools/create-dicom", json, - new Callable - (*this, &RadiographyScene::OnDicomExported), - NULL, NULL); + createDicomRequestContent["Content"] = ("data:" + + std::string(usePam ? Orthanc::MIME_PAM : Orthanc::MIME_PNG) + + ";base64," + base64); } @@ -861,7 +679,7 @@ const IWebService::HttpHeaders& h = message.GetAnswerHttpHeaders(); for (IWebService::HttpHeaders::const_iterator - it = h.begin(); it != h.end(); ++it) + it = h.begin(); it != h.end(); ++it) { printf("[%s] = [%s]\n", it->first.c_str(), it->second.c_str()); } diff -r b70e9be013e4 -r a750f11892ec Framework/Radiography/RadiographyScene.h --- a/Framework/Radiography/RadiographyScene.h Mon Dec 24 13:41:12 2018 +0100 +++ b/Framework/Radiography/RadiographyScene.h Fri Dec 28 10:40:34 2018 +0100 @@ -23,17 +23,55 @@ #include "RadiographyLayer.h" #include "../Toolbox/OrthancApiClient.h" - +#include "Framework/StoneEnumerations.h" namespace OrthancStone { class RadiographyScene : - public IObserver, - public IObservable + public IObserver, + public IObservable { public: - typedef OriginMessage GeometryChangedMessage; - typedef OriginMessage ContentChangedMessage; + class GeometryChangedMessage : + public OriginMessage + { + private: + RadiographyLayer& layer_; + + public: + GeometryChangedMessage(const RadiographyScene& origin, + RadiographyLayer& layer) : + OriginMessage(origin), + layer_(layer) + { + } + + RadiographyLayer& GetLayer() const + { + return layer_; + } + }; + + class ContentChangedMessage : + public OriginMessage + { + private: + RadiographyLayer& layer_; + + public: + ContentChangedMessage(const RadiographyScene& origin, + RadiographyLayer& layer) : + OriginMessage(origin), + layer_(layer) + { + } + + RadiographyLayer& GetLayer() const + { + return layer_; + } + }; + class LayerAccessor : public boost::noncopyable { @@ -69,17 +107,15 @@ private: - class AlphaLayer; - class DicomLayer; + typedef std::map Layers; - typedef std::map Layers; - size_t countLayers_; bool hasWindowing_; float windowingCenter_; float windowingWidth_; Layers layers_; + protected: RadiographyLayer& RegisterLayer(RadiographyLayer* layer); void OnTagsReceived(const OrthancApiClient::BinaryResponseReadyMessage& message); @@ -104,19 +140,33 @@ void SetWindowing(float center, float width); + PhotometricDisplayMode GetPreferredPhotomotricDisplayMode() const; + RadiographyLayer& LoadText(const Orthanc::Font& font, - const std::string& utf8); + const std::string& utf8, + RadiographyLayer::Geometry* geometry); RadiographyLayer& LoadTestBlock(unsigned int width, - unsigned int height); - - RadiographyLayer& LoadDicomFrame(OrthancApiClient& orthanc, - const std::string& instance, - unsigned int frame, - bool httpCompression); + unsigned int height, + RadiographyLayer::Geometry* geometry); + + RadiographyLayer& LoadAlphaBitmap(Orthanc::ImageAccessor* bitmap, // takes ownership + RadiographyLayer::Geometry* geometry); + + virtual RadiographyLayer& LoadDicomFrame(OrthancApiClient& orthanc, + const std::string& instance, + unsigned int frame, + bool httpCompression, + RadiographyLayer::Geometry* geometry); // pass NULL if you want default geometry RadiographyLayer& LoadDicomWebFrame(IWebService& web); + void RemoveLayer(size_t layerIndex); + + const RadiographyLayer& GetLayer(size_t layerIndex) const; + + void GetLayersIndexes(std::vector& output) const; + Extent2D GetSceneExtent() const; void Render(Orthanc::ImageAccessor& buffer, @@ -138,10 +188,20 @@ // core >= 1.4.3 void ExportDicom(OrthancApiClient& orthanc, const Orthanc::DicomMap& dicom, + const std::string& parentOrthancId, double pixelSpacingX, double pixelSpacingY, bool invert, ImageInterpolation interpolation, bool usePam); + + // temporary version used by VSOL because we need to send the same request at another url + void ExportToCreateDicomRequest(Json::Value& createDicomRequestContent, + const Orthanc::DicomMap& dicom, + double pixelSpacingX, + double pixelSpacingY, + bool invert, + ImageInterpolation interpolation, + bool usePam); }; } diff -r b70e9be013e4 -r a750f11892ec Framework/Radiography/RadiographySceneReader.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Radiography/RadiographySceneReader.cpp Fri Dec 28 10:40:34 2018 +0100 @@ -0,0 +1,112 @@ +/** + * 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 . + **/ + + +#include "RadiographySceneReader.h" + +#include +#include +#include +#include + +namespace OrthancStone +{ + void RadiographySceneReader::Read(const Json::Value& input) + { + unsigned int version = input["version"].asUInt(); + + if (version != 1) + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); + + for(size_t layerIndex = 0; layerIndex < input["layers"].size(); layerIndex++) + { + const Json::Value& jsonLayer = input["layers"][(int)layerIndex]; + RadiographyLayer::Geometry geometry; + + if (jsonLayer["type"].asString() == "dicom") + { + ReadLayerGeometry(geometry, jsonLayer); + scene_.LoadDicomFrame(orthancApiClient_, jsonLayer["instanceId"].asString(), jsonLayer["frame"].asUInt(), false, &geometry); + } + else if (jsonLayer["type"].asString() == "text") + { + if (fontRegistry_ == NULL || fontRegistry_->GetSize() == 0) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); // you must provide a FontRegistry if you need to re-create text layers. + } + + ReadLayerGeometry(geometry, jsonLayer); + const Orthanc::Font* font = fontRegistry_->FindFont(jsonLayer["fontName"].asString()); + if (font == NULL) // if not found, take the first font in the registry + { + font = &(fontRegistry_->GetFont(0)); + } + scene_.LoadText(*font, jsonLayer["text"].asString(), &geometry); + } + else if (jsonLayer["type"].asString() == "alpha") + { + ReadLayerGeometry(geometry, jsonLayer); + + const std::string& pngContentBase64 = jsonLayer["content"].asString(); + std::string pngContent; + std::string mimeType; + Orthanc::Toolbox::DecodeDataUriScheme(mimeType, pngContent, pngContentBase64); + + std::auto_ptr image; + if (mimeType == "image/png") + { + image.reset(new Orthanc::PngReader()); + dynamic_cast(image.get())->ReadFromMemory(pngContent); + } + else + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); + + RadiographyAlphaLayer& layer = dynamic_cast(scene_.LoadAlphaBitmap(image.release(), &geometry)); + + if (!jsonLayer["isUsingWindowing"].asBool()) + { + layer.SetForegroundValue((float)(jsonLayer["foreground"].asDouble())); + } + } + else + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); + } + } + + void RadiographySceneReader::ReadLayerGeometry(RadiographyLayer::Geometry& geometry, const Json::Value& jsonLayer) + { + {// crop + unsigned int x, y, width, height; + if (jsonLayer["crop"]["hasCrop"].asBool()) + { + x = jsonLayer["crop"]["x"].asUInt(); + y = jsonLayer["crop"]["y"].asUInt(); + width = jsonLayer["crop"]["width"].asUInt(); + height = jsonLayer["crop"]["height"].asUInt(); + geometry.SetCrop(x, y, width, height); + } + } + + geometry.SetAngle(jsonLayer["angle"].asDouble()); + geometry.SetResizeable(jsonLayer["isResizable"].asBool()); + geometry.SetPan(jsonLayer["pan"]["x"].asDouble(), jsonLayer["pan"]["y"].asDouble()); + geometry.SetPixelSpacing(jsonLayer["pixelSpacing"]["x"].asDouble(), jsonLayer["pixelSpacing"]["y"].asDouble()); + } +} diff -r b70e9be013e4 -r a750f11892ec Framework/Radiography/RadiographySceneReader.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Radiography/RadiographySceneReader.h Fri Dec 28 10:40:34 2018 +0100 @@ -0,0 +1,59 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "RadiographyScene.h" +#include "RadiographyAlphaLayer.h" +#include "RadiographyDicomLayer.h" +#include "RadiographyTextLayer.h" +#include +#include + +namespace OrthancStone +{ + class OrthancApiClient; + + class RadiographySceneReader : public boost::noncopyable + { + RadiographyScene& scene_; + OrthancApiClient& orthancApiClient_; + const Orthanc::FontRegistry* fontRegistry_; + + public: + RadiographySceneReader(RadiographyScene& scene, OrthancApiClient& orthancApiClient) : + scene_(scene), + orthancApiClient_(orthancApiClient), + fontRegistry_(NULL) + { + } + + void Read(const Json::Value& output); + + void SetFontRegistry(const Orthanc::FontRegistry& fontRegistry) + { + fontRegistry_ = &fontRegistry; + } + + private: + void ReadLayerGeometry(RadiographyLayer::Geometry& geometry, const Json::Value& output); + }; +} diff -r b70e9be013e4 -r a750f11892ec Framework/Radiography/RadiographySceneWriter.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Radiography/RadiographySceneWriter.cpp Fri Dec 28 10:40:34 2018 +0100 @@ -0,0 +1,136 @@ +/** + * 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 . + **/ + + +#include "RadiographySceneWriter.h" + +#include +#include +#include + +namespace OrthancStone +{ + void RadiographySceneWriter::Write(Json::Value& output, const RadiographyScene& scene) + { + output["version"] = 1; + output["layers"] = Json::arrayValue; + + std::vector layersIndexes; + scene.GetLayersIndexes(layersIndexes); + + for (std::vector::iterator itLayerIndex = layersIndexes.begin(); itLayerIndex < layersIndexes.end(); itLayerIndex++) + { + Json::Value layer; + WriteLayer(layer, scene.GetLayer(*itLayerIndex)); + output["layers"].append(layer); + } + } + + void RadiographySceneWriter::WriteLayer(Json::Value& output, const RadiographyDicomLayer& layer) + { + output["type"] = "dicom"; + output["instanceId"] = layer.GetInstanceId(); + output["frame"] = layer.GetFrame(); + } + + void RadiographySceneWriter::WriteLayer(Json::Value& output, const RadiographyTextLayer& layer) + { + output["type"] = "text"; + output["text"] = layer.GetText(); + output["fontName"] = layer.GetFontName(); + } + + void RadiographySceneWriter::WriteLayer(Json::Value& output, const RadiographyAlphaLayer& layer) + { + output["type"] = "alpha"; + + //output["bitmap"] = + const Orthanc::ImageAccessor& alpha = layer.GetAlpha(); + + Orthanc::PngWriter pngWriter; + std::string pngContent; + std::string pngContentBase64; + pngWriter.WriteToMemory(pngContent, alpha); + + Orthanc::Toolbox::EncodeDataUriScheme(pngContentBase64, "image/png", pngContent); + output["content"] = pngContentBase64; + output["foreground"] = layer.GetForegroundValue(); + output["isUsingWindowing"] = layer.IsUsingWindowing(); + } + + void RadiographySceneWriter::WriteLayer(Json::Value& output, const RadiographyLayer& layer) + { + const RadiographyLayer::Geometry& geometry = layer.GetGeometry(); + + {// crop + Json::Value crop; + if (geometry.HasCrop()) + { + unsigned int x, y, width, height; + geometry.GetCrop(x, y, width, height); + crop["hasCrop"] = true; + crop["x"] = x; + crop["y"] = y; + crop["width"] = width; + crop["height"] = height; + } + else + { + crop["hasCrop"] = false; + } + + output["crop"] = crop; + } + + output["angle"] = geometry.GetAngle(); + output["isResizable"] = geometry.IsResizeable(); + + {// pan + Json::Value pan; + pan["x"] = geometry.GetPanX(); + pan["y"] = geometry.GetPanY(); + output["pan"] = pan; + } + + {// pixelSpacing + Json::Value pan; + pan["x"] = geometry.GetPixelSpacingX(); + pan["y"] = geometry.GetPixelSpacingY(); + output["pixelSpacing"] = pan; + } + + if (dynamic_cast(&layer) != NULL) + { + WriteLayer(output, dynamic_cast(layer)); + } + else if (dynamic_cast(&layer) != NULL) + { + WriteLayer(output, dynamic_cast(layer)); + } + else if (dynamic_cast(&layer) != NULL) + { + WriteLayer(output, dynamic_cast(layer)); + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); + } + } +} diff -r b70e9be013e4 -r a750f11892ec Framework/Radiography/RadiographySceneWriter.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Radiography/RadiographySceneWriter.h Fri Dec 28 10:40:34 2018 +0100 @@ -0,0 +1,50 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "RadiographyScene.h" +#include "RadiographyAlphaLayer.h" +#include "RadiographyDicomLayer.h" +#include "RadiographyTextLayer.h" +#include + +namespace OrthancStone +{ + class RadiographyScene; + + class RadiographySceneWriter : public boost::noncopyable + { + + public: + RadiographySceneWriter() + { + } + + void Write(Json::Value& output, const RadiographyScene& scene); + + private: + void WriteLayer(Json::Value& output, const RadiographyLayer& layer); + void WriteLayer(Json::Value& output, const RadiographyDicomLayer& layer); + void WriteLayer(Json::Value& output, const RadiographyTextLayer& layer); + void WriteLayer(Json::Value& output, const RadiographyAlphaLayer& layer); + }; +} diff -r b70e9be013e4 -r a750f11892ec Framework/Radiography/RadiographyTextLayer.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Radiography/RadiographyTextLayer.cpp Fri Dec 28 10:40:34 2018 +0100 @@ -0,0 +1,36 @@ +/** + * 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 . + **/ + + +#include "RadiographyTextLayer.h" + +#include "RadiographyScene.h" + +namespace OrthancStone +{ + void RadiographyTextLayer::LoadText(const Orthanc::Font& font, + const std::string& utf8) + { + text_ = utf8; + fontName_ = font.GetName(); + + SetAlpha(font.RenderAlpha(utf8)); + } +} diff -r b70e9be013e4 -r a750f11892ec Framework/Radiography/RadiographyTextLayer.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Radiography/RadiographyTextLayer.h Fri Dec 28 10:40:34 2018 +0100 @@ -0,0 +1,55 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "RadiographyAlphaLayer.h" + +namespace OrthancStone +{ + class RadiographyScene; + + class RadiographyTextLayer : public RadiographyAlphaLayer + { + private: + std::string text_; + std::string fontName_; + + public: + RadiographyTextLayer(const RadiographyScene& scene) : + RadiographyAlphaLayer(scene) + { + } + + void LoadText(const Orthanc::Font& font, + const std::string& utf8); + + const std::string& GetText() const + { + return text_; + } + + const std::string& GetFontName() const + { + return fontName_; + } + }; +} diff -r b70e9be013e4 -r a750f11892ec Framework/Radiography/RadiographyWidget.cpp --- a/Framework/Radiography/RadiographyWidget.cpp Mon Dec 24 13:41:12 2018 +0100 +++ b/Framework/Radiography/RadiographyWidget.cpp Fri Dec 28 10:40:34 2018 +0100 @@ -21,18 +21,53 @@ #include "RadiographyWidget.h" +#include #include +#include namespace OrthancStone { + + bool RadiographyWidget::IsInvertedInternal() const + { + return (scene_->GetPreferredPhotomotricDisplayMode() == PhotometricDisplayMode_Monochrome1) ^ invert_; // MONOCHROME1 images must be inverted and the user can invert the image too -> XOR the two + } + + void RadiographyWidget::RenderBackground(Orthanc::ImageAccessor& image, float minValue, float maxValue) + { + // wipe background before rendering + float backgroundValue = minValue; + + switch (scene_->GetPreferredPhotomotricDisplayMode()) + { + case PhotometricDisplayMode_Monochrome1: + case PhotometricDisplayMode_Default: + if (IsInvertedInternal()) + backgroundValue = maxValue; + else + backgroundValue = minValue; + break; + case PhotometricDisplayMode_Monochrome2: + if (IsInvertedInternal()) + backgroundValue = minValue; + else + backgroundValue = maxValue; + break; + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); + } + + Orthanc::ImageProcessing::Set(image, backgroundValue); + } + bool RadiographyWidget::RenderInternal(unsigned int width, unsigned int height, ImageInterpolation interpolation) { float windowCenter, windowWidth; - scene_.GetWindowingWithDefault(windowCenter, windowWidth); - + scene_->GetWindowingWithDefault(windowCenter, windowWidth); + float x0 = windowCenter - windowWidth / 2.0f; float x1 = windowCenter + windowWidth / 2.0f; @@ -56,8 +91,10 @@ cairoBuffer_.reset(new CairoSurface(width, height)); } - scene_.Render(*floatBuffer_, GetView().GetMatrix(), interpolation); - + RenderBackground(*floatBuffer_, x0, x1); + + scene_->Render(*floatBuffer_, GetView().GetMatrix(), interpolation); + // Conversion from Float32 to BGRA32 (cairo). Very similar to // GrayscaleFrameRenderer => TODO MERGE? @@ -65,7 +102,9 @@ cairoBuffer_->GetWriteableAccessor(target); float scaling = 255.0f / (x1 - x0); - + + bool invert = IsInvertedInternal(); + for (unsigned int y = 0; y < height; y++) { const float* p = reinterpret_cast(floatBuffer_->GetConstRow(y)); @@ -88,7 +127,7 @@ v = static_cast(scaling * (*p - x0)); // (*) } - if (invert_) + if (invert) { v = 255 - v; } @@ -128,7 +167,7 @@ if (hasSelection_) { - scene_.DrawBorder(context, selectedLayer_, view.GetZoom()); + scene_->DrawBorder(context, selectedLayer_, view.GetZoom()); } return true; @@ -136,23 +175,16 @@ RadiographyWidget::RadiographyWidget(MessageBroker& broker, - RadiographyScene& scene, + boost::shared_ptr scene, const std::string& name) : WorldSceneWidget(name), IObserver(broker), - scene_(scene), invert_(false), interpolation_(ImageInterpolation_Nearest), hasSelection_(false), selectedLayer_(0) // Dummy initialization { - scene.RegisterObserverCallback( - new Callable - (*this, &RadiographyWidget::OnGeometryChanged)); - - scene.RegisterObserverCallback( - new Callable - (*this, &RadiographyWidget::OnContentChanged)); + SetScene(scene); } @@ -179,14 +211,15 @@ void RadiographyWidget::OnGeometryChanged(const RadiographyScene::GeometryChangedMessage& message) { - LOG(INFO) << "Geometry has changed"; + LOG(INFO) << "Scene geometry has changed"; + FitContent(); } void RadiographyWidget::OnContentChanged(const RadiographyScene::ContentChangedMessage& message) { - LOG(INFO) << "Content has changed"; + LOG(INFO) << "Scene content has changed"; NotifyContentChanged(); } @@ -216,4 +249,27 @@ NotifyContentChanged(); } } + + void RadiographyWidget::SetScene(boost::shared_ptr scene) + { + if (scene_ != NULL) + { + scene_->Unregister(this); + } + + scene_ = scene; + + scene_->RegisterObserverCallback( + new Callable + (*this, &RadiographyWidget::OnGeometryChanged)); + + scene_->RegisterObserverCallback( + new Callable + (*this, &RadiographyWidget::OnContentChanged)); + + NotifyContentChanged(); + + // force redraw + FitContent(); + } } diff -r b70e9be013e4 -r a750f11892ec Framework/Radiography/RadiographyWidget.h --- a/Framework/Radiography/RadiographyWidget.h Mon Dec 24 13:41:12 2018 +0100 +++ b/Framework/Radiography/RadiographyWidget.h Fri Dec 28 10:40:34 2018 +0100 @@ -32,7 +32,7 @@ public IObserver { private: - RadiographyScene& scene_; + boost::shared_ptr scene_; std::auto_ptr floatBuffer_; std::auto_ptr cairoBuffer_; bool invert_; @@ -47,22 +47,28 @@ protected: virtual Extent2D GetSceneExtent() { - return scene_.GetSceneExtent(); + return scene_->GetSceneExtent(); } virtual bool RenderScene(CairoContext& context, const ViewportGeometry& view); + virtual void RenderBackground(Orthanc::ImageAccessor& image, float minValue, float maxValue); + + bool IsInvertedInternal() const; + public: RadiographyWidget(MessageBroker& broker, - RadiographyScene& scene, + boost::shared_ptr scene, // TODO: check how we can avoid boost::shared_ptr here since we don't want them in the public API (app is keeping a boost::shared_ptr to this right now) const std::string& name); RadiographyScene& GetScene() const { - return scene_; + return *scene_; } + void SetScene(boost::shared_ptr scene); + void Unselect() { hasSelection_ = false; diff -r b70e9be013e4 -r a750f11892ec Framework/StoneEnumerations.h --- a/Framework/StoneEnumerations.h Mon Dec 24 13:41:12 2018 +0100 +++ b/Framework/StoneEnumerations.h Fri Dec 28 10:40:34 2018 +0100 @@ -157,8 +157,13 @@ MessageType_OrthancApi_GenericHttpError_Ready, MessageType_OrthancApi_GenericEmptyResponse_Ready, + MessageType_Scene_GeometryChanged, + MessageType_Scene_ContentChanged, + MessageType_ViewportChanged, + MessageType_Timeout, + // used in unit tests only MessageType_Test1, MessageType_Test2, @@ -175,6 +180,14 @@ Corner_BottomRight }; + enum PhotometricDisplayMode + { + PhotometricDisplayMode_Default, + + PhotometricDisplayMode_Monochrome1, + PhotometricDisplayMode_Monochrome2 + }; + bool StringToSopClassUid(SopClassUid& result, const std::string& source); diff -r b70e9be013e4 -r a750f11892ec Framework/Toolbox/BaseWebService.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Toolbox/BaseWebService.cpp Fri Dec 28 10:40:34 2018 +0100 @@ -0,0 +1,143 @@ +/** + * 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 . + **/ + + +#include "BaseWebService.h" + +#include +#include "Framework/Messages/IObservable.h" +#include "Platforms/Generic/IOracleCommand.h" +#include + +namespace OrthancStone +{ + + + class BaseWebService::BaseWebServicePayload : public Orthanc::IDynamicObject + { + private: + std::auto_ptr< MessageHandler > userSuccessHandler_; + std::auto_ptr< MessageHandler > userFailureHandler_; + std::auto_ptr< Orthanc::IDynamicObject> userPayload_; + + public: + BaseWebServicePayload(MessageHandler* userSuccessHandler, + MessageHandler* userFailureHandler, + Orthanc::IDynamicObject* userPayload) : + userSuccessHandler_(userSuccessHandler), + userFailureHandler_(userFailureHandler), + userPayload_(userPayload) + { + } + + void HandleSuccess(const IWebService::HttpRequestSuccessMessage& message) const + { + if (userSuccessHandler_.get() != NULL) + { + // recreate a success message with the user payload + IWebService::HttpRequestSuccessMessage successMessage(message.GetUri(), + message.GetAnswer(), + message.GetAnswerSize(), + message.GetAnswerHttpHeaders(), + userPayload_.get()); + userSuccessHandler_->Apply(successMessage); + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + } + + void HandleFailure(const IWebService::HttpRequestErrorMessage& message) const + { + if (userFailureHandler_.get() != NULL) + { + // recreate a failure message with the user payload + IWebService::HttpRequestErrorMessage failureMessage(message.GetUri(), + userPayload_.get()); + + userFailureHandler_->Apply(failureMessage); + } + } + + }; + + + void BaseWebService::GetAsync(const std::string& uri, + const HttpHeaders& headers, + Orthanc::IDynamicObject* payload /* takes ownership */, + MessageHandler* successCallback, + MessageHandler* failureCallback, + unsigned int timeoutInSeconds) + { + if (cache_.find(uri) == cache_.end()) + { + GetAsyncInternal(uri, headers, + new BaseWebService::BaseWebServicePayload(successCallback, failureCallback, payload), // ownership is transfered + new Callable + (*this, &BaseWebService::CacheAndNotifyHttpSuccess), + new Callable + (*this, &BaseWebService::NotifyHttpError), + timeoutInSeconds); + } + else + { + // create a command and "post" it to the Oracle so it is executed and commited "later" + NotifyHttpSuccessLater(cache_[uri], payload, successCallback); + } + + } + + + + void BaseWebService::NotifyHttpSuccess(const IWebService::HttpRequestSuccessMessage& message) + { + if (message.HasPayload()) + { + dynamic_cast(message.GetPayload()).HandleSuccess(message); + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + } + + void BaseWebService::CacheAndNotifyHttpSuccess(const IWebService::HttpRequestSuccessMessage& message) + { + cache_[message.GetUri()] = boost::shared_ptr(new CachedHttpRequestSuccessMessage(message)); + NotifyHttpSuccess(message); + } + + void BaseWebService::NotifyHttpError(const IWebService::HttpRequestErrorMessage& message) + { + if (message.HasPayload()) + { + dynamic_cast(message.GetPayload()).HandleFailure(message); + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + } + + + + +} diff -r b70e9be013e4 -r a750f11892ec Framework/Toolbox/BaseWebService.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Toolbox/BaseWebService.h Fri Dec 28 10:40:34 2018 +0100 @@ -0,0 +1,131 @@ +/** + * 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 . + **/ + + +#pragma once + +#include "IWebService.h" + +#include +#include + +namespace OrthancStone +{ + // This is an intermediate of IWebService that implements some caching on + // the HTTP GET requests + class BaseWebService : public IWebService, public IObserver + { + public: + class CachedHttpRequestSuccessMessage + { + protected: + std::string uri_; + void* answer_; + size_t answerSize_; + IWebService::HttpHeaders answerHeaders_; + + public: + CachedHttpRequestSuccessMessage(const IWebService::HttpRequestSuccessMessage& message) : + uri_(message.GetUri()), + answerSize_(message.GetAnswerSize()), + answerHeaders_(message.GetAnswerHttpHeaders()) + { + answer_ = malloc(answerSize_); + memcpy(answer_, message.GetAnswer(), answerSize_); + } + + ~CachedHttpRequestSuccessMessage() + { + free(answer_); + } + + const std::string& GetUri() const + { + return uri_; + } + + const void* GetAnswer() const + { + return answer_; + } + + size_t GetAnswerSize() const + { + return answerSize_; + } + + const IWebService::HttpHeaders& GetAnswerHttpHeaders() const + { + return answerHeaders_; + } + + }; + protected: + class BaseWebServicePayload; + + bool cacheEnabled_; + std::map> cache_; // TODO: this is currently an infinite cache ! + + public: + + BaseWebService(MessageBroker& broker) : + IWebService(broker), + IObserver(broker), + cacheEnabled_(true) + { + } + + virtual ~BaseWebService() + { + } + + virtual void EnableCache(bool enable) + { + cacheEnabled_ = enable; + } + + virtual void GetAsync(const std::string& uri, + const HttpHeaders& headers, + Orthanc::IDynamicObject* payload /* takes ownership */, + MessageHandler* successCallback, + MessageHandler* failureCallback = NULL, + unsigned int timeoutInSeconds = 60); + + protected: + virtual void GetAsyncInternal(const std::string& uri, + const HttpHeaders& headers, + Orthanc::IDynamicObject* payload /* takes ownership */, + MessageHandler* successCallback, + MessageHandler* failureCallback = NULL, + unsigned int timeoutInSeconds = 60) = 0; + + virtual void NotifyHttpSuccessLater(boost::shared_ptr cachedHttpMessage, + Orthanc::IDynamicObject* payload, // takes ownership + MessageHandler* successCallback) = 0; + + private: + void NotifyHttpSuccess(const IWebService::HttpRequestSuccessMessage& message); + + void NotifyHttpError(const IWebService::HttpRequestErrorMessage& message); + + void CacheAndNotifyHttpSuccess(const IWebService::HttpRequestSuccessMessage& message); + + }; +} diff -r b70e9be013e4 -r a750f11892ec Framework/Toolbox/IDelayedCallExecutor.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Toolbox/IDelayedCallExecutor.h Fri Dec 28 10:40:34 2018 +0100 @@ -0,0 +1,59 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "../../Framework/Messages/IObserver.h" +#include "../../Framework/Messages/ICallable.h" + +#include +#include + +#include +#include + +namespace OrthancStone +{ + // The IDelayedCall executes a callback after a delay (equivalent to timeout() function in javascript). + class IDelayedCallExecutor : public boost::noncopyable + { + protected: + MessageBroker& broker_; + + public: + + typedef NoPayloadMessage TimeoutMessage; + + IDelayedCallExecutor(MessageBroker& broker) : + broker_(broker) + { + } + + + virtual ~IDelayedCallExecutor() + { + } + + + virtual void Schedule(MessageHandler* callback, + unsigned int timeoutInMs = 1000) = 0; + }; +} diff -r b70e9be013e4 -r a750f11892ec Framework/Toolbox/IWebService.h --- a/Framework/Toolbox/IWebService.h Mon Dec 24 13:41:12 2018 +0100 +++ b/Framework/Toolbox/IWebService.h Fri Dec 28 10:40:34 2018 +0100 @@ -135,6 +135,7 @@ { } + virtual void EnableCache(bool enable) = 0; virtual void GetAsync(const std::string& uri, const HttpHeaders& headers, diff -r b70e9be013e4 -r a750f11892ec Framework/Toolbox/OrthancApiClient.cpp --- a/Framework/Toolbox/OrthancApiClient.cpp Mon Dec 24 13:41:12 2018 +0100 +++ b/Framework/Toolbox/OrthancApiClient.cpp Fri Dec 28 10:40:34 2018 +0100 @@ -154,7 +154,7 @@ { throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); } - } + } void HandleFailure(const IWebService::HttpRequestErrorMessage& message) const { @@ -163,7 +163,7 @@ failureHandler_->Apply(IWebService::HttpRequestErrorMessage (message.GetUri(), userPayload_.get())); } - } + } }; @@ -179,13 +179,14 @@ void OrthancApiClient::GetJsonAsync( - const std::string& uri, - MessageHandler* successCallback, - MessageHandler* failureCallback, - Orthanc::IDynamicObject* payload) + const std::string& uri, + MessageHandler* successCallback, + MessageHandler* failureCallback, + Orthanc::IDynamicObject* payload) { + IWebService::HttpHeaders emptyHeaders; web_.GetAsync(baseUrl_ + uri, - IWebService::HttpHeaders(), + emptyHeaders, new WebServicePayload(successCallback, failureCallback, payload), new Callable (*this, &OrthancApiClient::NotifyHttpSuccess), @@ -195,27 +196,26 @@ void OrthancApiClient::GetBinaryAsync( - const std::string& uri, - const std::string& contentType, - MessageHandler* successCallback, - MessageHandler* failureCallback, - Orthanc::IDynamicObject* payload) + const std::string& uri, + const std::string& contentType, + MessageHandler* successCallback, + MessageHandler* failureCallback, + Orthanc::IDynamicObject* payload) { IWebService::HttpHeaders headers; headers["Accept"] = contentType; GetBinaryAsync(uri, headers, successCallback, failureCallback, payload); } - void OrthancApiClient::GetBinaryAsync( - const std::string& uri, - const IWebService::HttpHeaders& headers, - MessageHandler* successCallback, - MessageHandler* failureCallback, - Orthanc::IDynamicObject* payload) + const std::string& uri, + const IWebService::HttpHeaders& headers, + MessageHandler* successCallback, + MessageHandler* failureCallback, + Orthanc::IDynamicObject* payload) { - printf("GET [%s] [%s]\n", baseUrl_.c_str(), uri.c_str()); - + // printf("GET [%s] [%s]\n", baseUrl_.c_str(), uri.c_str()); + web_.GetAsync(baseUrl_ + uri, headers, new WebServicePayload(successCallback, failureCallback, payload), new Callable @@ -226,11 +226,11 @@ void OrthancApiClient::PostBinaryAsyncExpectJson( - const std::string& uri, - const std::string& body, - MessageHandler* successCallback, - MessageHandler* failureCallback, - Orthanc::IDynamicObject* payload) + const std::string& uri, + const std::string& body, + MessageHandler* successCallback, + MessageHandler* failureCallback, + Orthanc::IDynamicObject* payload) { web_.PostAsync(baseUrl_ + uri, IWebService::HttpHeaders(), body, new WebServicePayload(successCallback, failureCallback, payload), @@ -241,25 +241,39 @@ } - + void OrthancApiClient::PostBinaryAsync( + const std::string& uri, + const std::string& body) + { + web_.PostAsync(baseUrl_ + uri, IWebService::HttpHeaders(), body, NULL, NULL, NULL); + } + void OrthancApiClient::PostJsonAsyncExpectJson( - const std::string& uri, - const Json::Value& data, - MessageHandler* successCallback, - MessageHandler* failureCallback, - Orthanc::IDynamicObject* payload) + const std::string& uri, + const Json::Value& data, + MessageHandler* successCallback, + MessageHandler* failureCallback, + Orthanc::IDynamicObject* payload) { std::string body; MessagingToolbox::JsonToString(body, data); return PostBinaryAsyncExpectJson(uri, body, successCallback, failureCallback, payload); } - + void OrthancApiClient::PostJsonAsync( + const std::string& uri, + const Json::Value& data) + { + std::string body; + MessagingToolbox::JsonToString(body, data); + return PostBinaryAsync(uri, body); + } + void OrthancApiClient::DeleteAsync( - const std::string& uri, - MessageHandler* successCallback, - MessageHandler* failureCallback, - Orthanc::IDynamicObject* payload) + const std::string& uri, + MessageHandler* successCallback, + MessageHandler* failureCallback, + Orthanc::IDynamicObject* payload) { web_.DeleteAsync(baseUrl_ + uri, IWebService::HttpHeaders(), new WebServicePayload(successCallback, failureCallback, payload), @@ -282,7 +296,6 @@ } } - void OrthancApiClient::NotifyHttpError(const IWebService::HttpRequestErrorMessage& message) { if (message.HasPayload()) @@ -293,5 +306,5 @@ { throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); } - } + } } diff -r b70e9be013e4 -r a750f11892ec Framework/Toolbox/OrthancApiClient.h --- a/Framework/Toolbox/OrthancApiClient.h Mon Dec 24 13:41:12 2018 +0100 +++ b/Framework/Toolbox/OrthancApiClient.h Fri Dec 28 10:40:34 2018 +0100 @@ -31,12 +31,12 @@ namespace OrthancStone { class OrthancApiClient : - public IObservable, - public IObserver + public IObservable, + public IObserver { public: class JsonResponseReadyMessage : - public BaseMessage + public BaseMessage { private: const std::string& uri_; @@ -73,7 +73,7 @@ class BinaryResponseReadyMessage : - public BaseMessage + public BaseMessage { private: const std::string& uri_; @@ -118,7 +118,7 @@ class EmptyResponseReadyMessage : - public BaseMessage + public BaseMessage { private: const std::string& uri_; @@ -149,7 +149,7 @@ private: class WebServicePayload; - + protected: IWebService& web_; std::string baseUrl_; @@ -197,6 +197,14 @@ MessageHandler* failureCallback = NULL, Orthanc::IDynamicObject* payload = NULL /* takes ownership */); + // schedule a POST request and don't mind the response. + void PostJsonAsync(const std::string& uri, + const Json::Value& data); + + // schedule a POST request and don't mind the response. + void PostBinaryAsync(const std::string& uri, + const std::string& body); + // schedule a DELETE request expecting an empty response. void DeleteAsync(const std::string& uri, MessageHandler* successCallback, @@ -206,5 +214,10 @@ void NotifyHttpSuccess(const IWebService::HttpRequestSuccessMessage& message); void NotifyHttpError(const IWebService::HttpRequestErrorMessage& message); + + private: + void HandleFromCache(const std::string& uri, + const IWebService::HttpHeaders& headers, + Orthanc::IDynamicObject* payload /* takes ownership */); }; } diff -r b70e9be013e4 -r a750f11892ec Platforms/Generic/DelayedCallCommand.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Platforms/Generic/DelayedCallCommand.cpp Fri Dec 28 10:40:34 2018 +0100 @@ -0,0 +1,66 @@ +/** + * 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 . + **/ + + +#include "DelayedCallCommand.h" +#include "boost/thread/thread.hpp" + +#include + +namespace OrthancStone +{ + DelayedCallCommand::DelayedCallCommand(MessageBroker& broker, + MessageHandler* callback, // takes ownership + unsigned int timeoutInMs, + Orthanc::IDynamicObject* payload /* takes ownership */, + NativeStoneApplicationContext& context + ) : + IObservable(broker), + callback_(callback), + payload_(payload), + context_(context), + expirationTimePoint_(boost::chrono::system_clock::now() + boost::chrono::milliseconds(timeoutInMs)), + timeoutInMs_(timeoutInMs) + { + } + + + void DelayedCallCommand::Execute() + { + while (boost::chrono::system_clock::now() < expirationTimePoint_) + { + boost::this_thread::sleep_for(boost::chrono::milliseconds(1)); + } + } + + void DelayedCallCommand::Commit() + { + // We want to make sure that, i.e, the UpdateThread is not + // triggered while we are updating the "model" with the result of + // an OracleCommand + NativeStoneApplicationContext::GlobalMutexLocker lock(context_); + + if (callback_.get() != NULL) + { + IDelayedCallExecutor::TimeoutMessage message; // TODO: add payload + callback_->Apply(message); + } + } +} diff -r b70e9be013e4 -r a750f11892ec Platforms/Generic/DelayedCallCommand.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Platforms/Generic/DelayedCallCommand.h Fri Dec 28 10:40:34 2018 +0100 @@ -0,0 +1,56 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "IOracleCommand.h" + +#include "../../Framework/Toolbox/IDelayedCallExecutor.h" +#include "../../Framework/Messages/IObservable.h" +#include "../../Framework/Messages/ICallable.h" +#include "../../Applications/Generic/NativeStoneApplicationContext.h" +#include + +namespace OrthancStone +{ + class DelayedCallCommand : public IOracleCommand, IObservable + { + protected: + std::auto_ptr > callback_; + std::auto_ptr payload_; + NativeStoneApplicationContext& context_; + boost::chrono::system_clock::time_point expirationTimePoint_; + unsigned int timeoutInMs_; + + public: + DelayedCallCommand(MessageBroker& broker, + MessageHandler* callback, // takes ownership + unsigned int timeoutInMs, + Orthanc::IDynamicObject* payload /* takes ownership */, + NativeStoneApplicationContext& context + ); + + virtual void Execute(); + + virtual void Commit(); + }; + +} diff -r b70e9be013e4 -r a750f11892ec Platforms/Generic/OracleDelayedCallExecutor.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Platforms/Generic/OracleDelayedCallExecutor.h Fri Dec 28 10:40:34 2018 +0100 @@ -0,0 +1,54 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "../../Framework/Toolbox/IDelayedCallExecutor.h" +#include "Oracle.h" +#include "../../Applications/Generic/NativeStoneApplicationContext.h" +#include "DelayedCallCommand.h" + +namespace OrthancStone +{ + // The OracleTimeout executes callbacks after a delay. + class OracleDelayedCallExecutor : public IDelayedCallExecutor + { + private: + Oracle& oracle_; + NativeStoneApplicationContext& context_; + + public: + OracleDelayedCallExecutor(MessageBroker& broker, + Oracle& oracle, + NativeStoneApplicationContext& context) : + IDelayedCallExecutor(broker), + oracle_(oracle), + context_(context) + { + } + + virtual void Schedule(MessageHandler* callback, + unsigned int timeoutInMs = 1000) + { + oracle_.Submit(new DelayedCallCommand(broker_, callback, timeoutInMs, NULL, context_)); + } + }; +} diff -r b70e9be013e4 -r a750f11892ec Platforms/Generic/OracleWebService.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Platforms/Generic/OracleWebService.cpp Fri Dec 28 10:40:34 2018 +0100 @@ -0,0 +1,82 @@ +/** + * 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 . + **/ + + +#include "OracleWebService.h" +#include "../../Framework/Toolbox/IWebService.h" + +namespace OrthancStone +{ + + + class OracleWebService::WebServiceCachedGetCommand : public IOracleCommand, IObservable + { + protected: + std::auto_ptr > successCallback_; + std::auto_ptr payload_; + boost::shared_ptr cachedMessage_; + NativeStoneApplicationContext& context_; + + public: + WebServiceCachedGetCommand(MessageBroker& broker, + MessageHandler* successCallback, // takes ownership + boost::shared_ptr cachedMessage, + Orthanc::IDynamicObject* payload /* takes ownership */, + NativeStoneApplicationContext& context + ) : + IObservable(broker), + successCallback_(successCallback), + payload_(payload), + cachedMessage_(cachedMessage), + context_(context) + { + } + + virtual void Execute() + { + // nothing to do, everything is in the commit + } + + virtual void Commit() + { + // We want to make sure that, i.e, the UpdateThread is not + // triggered while we are updating the "model" with the result of + // a WebServiceCommand + NativeStoneApplicationContext::GlobalMutexLocker lock(context_); + + IWebService::HttpRequestSuccessMessage successMessage(cachedMessage_->GetUri(), + cachedMessage_->GetAnswer(), + cachedMessage_->GetAnswerSize(), + cachedMessage_->GetAnswerHttpHeaders(), + payload_.get()); + + successCallback_->Apply(successMessage); + } + }; + + void OracleWebService::NotifyHttpSuccessLater(boost::shared_ptr cachedMessage, + Orthanc::IDynamicObject* payload, // takes ownership + MessageHandler* successCallback) + { + oracle_.Submit(new WebServiceCachedGetCommand(GetBroker(), successCallback, cachedMessage, payload, context_)); + } + + +} diff -r b70e9be013e4 -r a750f11892ec Platforms/Generic/OracleWebService.h --- a/Platforms/Generic/OracleWebService.h Mon Dec 24 13:41:12 2018 +0100 +++ b/Platforms/Generic/OracleWebService.h Fri Dec 28 10:40:34 2018 +0100 @@ -21,7 +21,7 @@ #pragma once -#include "../../Framework/Toolbox/IWebService.h" +#include "../../Framework/Toolbox/BaseWebService.h" #include "Oracle.h" #include "WebServiceGetCommand.h" #include "WebServicePostCommand.h" @@ -33,35 +33,27 @@ // The OracleWebService performs HTTP requests in a native environment. // It uses a thread pool to handle multiple HTTP requests in a same time. // It works asynchronously to mimick the behaviour of the WebService running in a WASM environment. - class OracleWebService : public IWebService + class OracleWebService : public BaseWebService { private: Oracle& oracle_; NativeStoneApplicationContext& context_; Orthanc::WebServiceParameters parameters_; + class WebServiceCachedGetCommand; + public: OracleWebService(MessageBroker& broker, Oracle& oracle, const Orthanc::WebServiceParameters& parameters, NativeStoneApplicationContext& context) : - IWebService(broker), + BaseWebService(broker), oracle_(oracle), context_(context), parameters_(parameters) { } - virtual void GetAsync(const std::string& uri, - const HttpHeaders& headers, - Orthanc::IDynamicObject* payload, // takes ownership - MessageHandler* successCallback, // takes ownership - MessageHandler* failureCallback = NULL,// takes ownership - unsigned int timeoutInSeconds = 60) - { - oracle_.Submit(new WebServiceGetCommand(broker_, successCallback, failureCallback, parameters_, uri, headers, timeoutInSeconds, payload, context_)); - } - virtual void PostAsync(const std::string& uri, const HttpHeaders& headers, const std::string& body, @@ -70,7 +62,7 @@ MessageHandler* failureCallback = NULL, // takes ownership unsigned int timeoutInSeconds = 60) { - oracle_.Submit(new WebServicePostCommand(broker_, successCallback, failureCallback, parameters_, uri, headers, timeoutInSeconds, body, payload, context_)); + oracle_.Submit(new WebServicePostCommand(GetBroker(), successCallback, failureCallback, parameters_, uri, headers, timeoutInSeconds, body, payload, context_)); } virtual void DeleteAsync(const std::string& uri, @@ -80,7 +72,23 @@ MessageHandler* failureCallback = NULL, unsigned int timeoutInSeconds = 60) { - oracle_.Submit(new WebServiceDeleteCommand(broker_, successCallback, failureCallback, parameters_, uri, headers, timeoutInSeconds, payload, context_)); + oracle_.Submit(new WebServiceDeleteCommand(GetBroker(), successCallback, failureCallback, parameters_, uri, headers, timeoutInSeconds, payload, context_)); } + + protected: + virtual void GetAsyncInternal(const std::string& uri, + const HttpHeaders& headers, + Orthanc::IDynamicObject* payload, // takes ownership + MessageHandler* successCallback, // takes ownership + MessageHandler* failureCallback = NULL,// takes ownership + unsigned int timeoutInSeconds = 60) + { + oracle_.Submit(new WebServiceGetCommand(GetBroker(), successCallback, failureCallback, parameters_, uri, headers, timeoutInSeconds, payload, context_)); + } + + virtual void NotifyHttpSuccessLater(boost::shared_ptr cachedHttpMessage, + Orthanc::IDynamicObject* payload, // takes ownership + MessageHandler* successCallback); + }; } diff -r b70e9be013e4 -r a750f11892ec Platforms/Wasm/Defaults.cpp --- a/Platforms/Wasm/Defaults.cpp Mon Dec 24 13:41:12 2018 +0100 +++ b/Platforms/Wasm/Defaults.cpp Fri Dec 28 10:40:34 2018 +0100 @@ -1,6 +1,7 @@ #include "Defaults.h" #include "WasmWebService.h" +#include "WasmDelayedCallExecutor.h" #include #include "Framework/Widgets/TestCairoWidget.h" #include @@ -73,6 +74,7 @@ application.reset(CreateUserApplication(broker)); applicationWasmAdapter.reset(CreateWasmApplicationAdapter(broker, application.get())); WasmWebService::SetBroker(broker); + WasmDelayedCallExecutor::SetBroker(broker); startupParametersBuilder.Clear(); } @@ -96,6 +98,7 @@ context->SetOrthancBaseUrl(baseUri); printf("Base URL to Orthanc API: [%s]\n", baseUri); context->SetWebService(OrthancStone::WasmWebService::GetInstance()); + context->SetDelayedCallExecutor(OrthancStone::WasmDelayedCallExecutor::GetInstance()); application->Initialize(context.get(), statusBar_, parameters); application->InitializeWasm(); diff -r b70e9be013e4 -r a750f11892ec Platforms/Wasm/WasmDelayedCallExecutor.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Platforms/Wasm/WasmDelayedCallExecutor.cpp Fri Dec 28 10:40:34 2018 +0100 @@ -0,0 +1,46 @@ +#include "WasmDelayedCallExecutor.h" +#include "json/value.h" +#include "json/writer.h" +#include + +#ifdef __cplusplus +extern "C" { +#endif + + extern void WasmDelayedCallExecutor_Schedule(void* callable, + unsigned int timeoutInMs + /*void* payload*/); + + void EMSCRIPTEN_KEEPALIVE WasmDelayedCallExecutor_ExecuteCallback(void* callable + //void* payload + ) + { + if (callable == NULL) + { + throw; + } + else + { + reinterpret_cast*>(callable)-> + Apply(OrthancStone::IDelayedCallExecutor::TimeoutMessage()); // uri, reinterpret_cast(payload))); + } + } + + +#ifdef __cplusplus +} +#endif + + + +namespace OrthancStone +{ + MessageBroker* WasmDelayedCallExecutor::broker_ = NULL; + + + void WasmDelayedCallExecutor::Schedule(MessageHandler* callback, + unsigned int timeoutInMs) + { + WasmDelayedCallExecutor_Schedule(callback, timeoutInMs); + } +} diff -r b70e9be013e4 -r a750f11892ec Platforms/Wasm/WasmDelayedCallExecutor.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Platforms/Wasm/WasmDelayedCallExecutor.h Fri Dec 28 10:40:34 2018 +0100 @@ -0,0 +1,40 @@ +#pragma once + +#include +#include + +namespace OrthancStone +{ + class WasmDelayedCallExecutor : public IDelayedCallExecutor + { + private: + static MessageBroker* broker_; + + // Private constructor => Singleton design pattern + WasmDelayedCallExecutor(MessageBroker& broker) : + IDelayedCallExecutor(broker) + { + } + + public: + static WasmDelayedCallExecutor& GetInstance() + { + if (broker_ == NULL) + { + printf("WasmDelayedCallExecutor::GetInstance(): broker not initialized\n"); + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + static WasmDelayedCallExecutor instance(*broker_); + return instance; + } + + static void SetBroker(MessageBroker& broker) + { + broker_ = &broker; + } + + virtual void Schedule(MessageHandler* callback, + unsigned int timeoutInMs = 1000); + + }; +} diff -r b70e9be013e4 -r a750f11892ec Platforms/Wasm/WasmDelayedCallExecutor.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Platforms/Wasm/WasmDelayedCallExecutor.js Fri Dec 28 10:40:34 2018 +0100 @@ -0,0 +1,7 @@ +mergeInto(LibraryManager.library, { + WasmDelayedCallExecutor_Schedule: function(callable, timeoutInMs/*, payload*/) { + setTimeout(function() { + WasmDelayedCallExecutor_ExecuteCallback(callable/*, payload*/); + }, timeoutInMs); + } +}); diff -r b70e9be013e4 -r a750f11892ec Platforms/Wasm/WasmWebService.cpp --- a/Platforms/Wasm/WasmWebService.cpp Mon Dec 24 13:41:12 2018 +0100 +++ b/Platforms/Wasm/WasmWebService.cpp Fri Dec 28 10:40:34 2018 +0100 @@ -2,6 +2,15 @@ #include "json/value.h" #include "json/writer.h" #include +#include + +struct CachedSuccessNotification +{ + boost::shared_ptr cachedMessage; + std::auto_ptr payload; + OrthancStone::MessageHandler* successCallback; +}; + #ifdef __cplusplus extern "C" { @@ -14,6 +23,8 @@ void* payload, unsigned int timeoutInSeconds); + extern void WasmWebService_ScheduleLaterCachedSuccessNotification(void* brol); + extern void WasmWebService_PostAsync(void* callableSuccess, void* callableFailure, const char* uri, @@ -45,6 +56,20 @@ } } + void EMSCRIPTEN_KEEPALIVE WasmWebService_NotifyCachedSuccess(void* notification_) + { + // notification has been allocated in C++ and passed to JS. It must be deleted by this method + std::auto_ptr notification(reinterpret_cast(notification_)); + + notification->successCallback->Apply(OrthancStone::IWebService::HttpRequestSuccessMessage( + notification->cachedMessage->GetUri(), + notification->cachedMessage->GetAnswer(), + notification->cachedMessage->GetAnswerSize(), + notification->cachedMessage->GetAnswerHttpHeaders(), + notification->payload.get() + )); + } + void EMSCRIPTEN_KEEPALIVE WasmWebService_NotifySuccess(void* successCallable, const char* uri, const void* body, @@ -61,7 +86,7 @@ OrthancStone::IWebService::HttpHeaders headers; // TODO - Parse "answerHeaders" - printf("[%s]\n", answerHeaders); + printf("TODO: parse headers [%s]\n", answerHeaders); reinterpret_cast*>(successCallable)-> Apply(OrthancStone::IWebService::HttpRequestSuccessMessage(uri, body, bodySize, headers, @@ -122,16 +147,29 @@ payload, timeoutInSeconds); } - void WasmWebService::GetAsync(const std::string& relativeUri, - const HttpHeaders& headers, - Orthanc::IDynamicObject* payload, - MessageHandler* successCallable, - MessageHandler* failureCallable, - unsigned int timeoutInSeconds) + void WasmWebService::GetAsyncInternal(const std::string &relativeUri, + const HttpHeaders &headers, + Orthanc::IDynamicObject *payload, + MessageHandler *successCallable, + MessageHandler *failureCallable, + unsigned int timeoutInSeconds) { std::string headersInJsonString; ToJsonString(headersInJsonString, headers); WasmWebService_GetAsync(successCallable, failureCallable, relativeUri.c_str(), headersInJsonString.c_str(), payload, timeoutInSeconds); } + + void WasmWebService::NotifyHttpSuccessLater(boost::shared_ptr cachedMessage, + Orthanc::IDynamicObject* payload, // takes ownership + MessageHandler* successCallback) + { + CachedSuccessNotification* notification = new CachedSuccessNotification(); // allocated on the heap, it will be passed to JS and deleted when coming back to C++ + notification->cachedMessage = cachedMessage; + notification->payload.reset(payload); + notification->successCallback = successCallback; + + WasmWebService_ScheduleLaterCachedSuccessNotification(notification); + } + } diff -r b70e9be013e4 -r a750f11892ec Platforms/Wasm/WasmWebService.h --- a/Platforms/Wasm/WasmWebService.h Mon Dec 24 13:41:12 2018 +0100 +++ b/Platforms/Wasm/WasmWebService.h Fri Dec 28 10:40:34 2018 +0100 @@ -1,66 +1,62 @@ #pragma once -#include +#include #include namespace OrthancStone { - class WasmWebService : public IWebService - { - private: - static MessageBroker* broker_; +class WasmWebService : public BaseWebService +{ +private: + static MessageBroker *broker_; - // Private constructor => Singleton design pattern - WasmWebService(MessageBroker& broker) : - IWebService(broker) - { - } + // Private constructor => Singleton design pattern + WasmWebService(MessageBroker &broker) : BaseWebService(broker) + { + } - public: - static WasmWebService& GetInstance() +public: + static WasmWebService &GetInstance() + { + if (broker_ == NULL) { - if (broker_ == NULL) - { - printf("WasmWebService::GetInstance(): broker not initialized\n"); - throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); - } - static WasmWebService instance(*broker_); - return instance; + printf("WasmWebService::GetInstance(): broker not initialized\n"); + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); } + static WasmWebService instance(*broker_); + return instance; + } - static void SetBroker(MessageBroker& broker) - { - broker_ = &broker; - } + static void SetBroker(MessageBroker &broker) + { + broker_ = &broker; + } - virtual void GetAsync(const std::string& uri, - const HttpHeaders& headers, - Orthanc::IDynamicObject* payload, - MessageHandler* successCallable, - MessageHandler* failureCallable = NULL, - unsigned int timeoutInSeconds = 60); + virtual void PostAsync(const std::string &uri, + const HttpHeaders &headers, + const std::string &body, + Orthanc::IDynamicObject *payload, + MessageHandler *successCallable, + MessageHandler *failureCallable = NULL, + unsigned int timeoutInSeconds = 60); - virtual void PostAsync(const std::string& uri, - const HttpHeaders& headers, - const std::string& body, - Orthanc::IDynamicObject* payload, - MessageHandler* successCallable, - MessageHandler* failureCallable = NULL, + virtual void DeleteAsync(const std::string &uri, + const HttpHeaders &headers, + Orthanc::IDynamicObject *payload, + MessageHandler *successCallable, + MessageHandler *failureCallable = NULL, unsigned int timeoutInSeconds = 60); - virtual void DeleteAsync(const std::string& uri, - const HttpHeaders& headers, - Orthanc::IDynamicObject* payload, - MessageHandler* successCallable, - MessageHandler* failureCallable = NULL, - unsigned int timeoutInSeconds = 60); +protected: + virtual void GetAsyncInternal(const std::string &uri, + const HttpHeaders &headers, + Orthanc::IDynamicObject *payload, + MessageHandler *successCallable, + MessageHandler *failureCallable = NULL, + unsigned int timeoutInSeconds = 60); - virtual void Start() - { - } - - virtual void Stop() - { - } - }; -} + virtual void NotifyHttpSuccessLater(boost::shared_ptr cachedHttpMessage, + Orthanc::IDynamicObject *payload, // takes ownership + MessageHandler *successCallback); +}; +} // namespace OrthancStone diff -r b70e9be013e4 -r a750f11892ec Platforms/Wasm/WasmWebService.js --- a/Platforms/Wasm/WasmWebService.js Mon Dec 24 13:41:12 2018 +0100 +++ b/Platforms/Wasm/WasmWebService.js Fri Dec 28 10:40:34 2018 +0100 @@ -35,6 +35,12 @@ xhr.send(); }, + WasmWebService_ScheduleLaterCachedSuccessNotification: function (brol) { + setTimeout(function() { + WasmWebService_NotifyCachedSuccess(brol); + }, 0); + }, + WasmWebService_PostAsync: function(callableSuccess, callableFailure, url, headersInJsonString, body, bodySize, payload, timeoutInSeconds) { var xhr = new XMLHttpRequest(); var url_ = UTF8ToString(url); diff -r b70e9be013e4 -r a750f11892ec Platforms/Wasm/wasm-application-runner.ts --- a/Platforms/Wasm/wasm-application-runner.ts Mon Dec 24 13:41:12 2018 +0100 +++ b/Platforms/Wasm/wasm-application-runner.ts Fri Dec 28 10:40:34 2018 +0100 @@ -10,6 +10,8 @@ // global functions var WasmWebService_NotifyError: Function = null; var WasmWebService_NotifySuccess: Function = null; +var WasmWebService_NotifyCachedSuccess: Function = null; +var WasmDelayedCallExecutor_ExecuteCallback: Function = null; var WasmDoAnimation: Function = null; var SetStartupParameter: Function = null; var CreateWasmApplication: Function = null; @@ -91,8 +93,10 @@ ReleaseCppViewport = StoneFrameworkModule.cwrap('ReleaseCppViewport', null, ['number']); StartWasmApplication = StoneFrameworkModule.cwrap('StartWasmApplication', null, ['string']); + WasmWebService_NotifyCachedSuccess = StoneFrameworkModule.cwrap('WasmWebService_NotifyCachedSuccess', null, ['number']); WasmWebService_NotifySuccess = StoneFrameworkModule.cwrap('WasmWebService_NotifySuccess', null, ['number', 'string', 'array', 'number', 'number']); WasmWebService_NotifyError = StoneFrameworkModule.cwrap('WasmWebService_NotifyError', null, ['number', 'string', 'number']); + WasmDelayedCallExecutor_ExecuteCallback = StoneFrameworkModule.cwrap('WasmDelayedCallExecutor_ExecuteCallback', null, ['number']); WasmDoAnimation = StoneFrameworkModule.cwrap('WasmDoAnimation', null, []); SendMessageToStoneApplication = StoneFrameworkModule.cwrap('SendMessageToStoneApplication', 'string', ['string']); diff -r b70e9be013e4 -r a750f11892ec Resources/CMake/OrthancStoneConfiguration.cmake --- a/Resources/CMake/OrthancStoneConfiguration.cmake Mon Dec 24 13:41:12 2018 +0100 +++ b/Resources/CMake/OrthancStoneConfiguration.cmake Fri Dec 28 10:40:34 2018 +0100 @@ -186,8 +186,10 @@ ${ORTHANC_STONE_ROOT}/Platforms/Generic/WebServiceGetCommand.cpp ${ORTHANC_STONE_ROOT}/Platforms/Generic/WebServicePostCommand.cpp ${ORTHANC_STONE_ROOT}/Platforms/Generic/WebServiceDeleteCommand.cpp + ${ORTHANC_STONE_ROOT}/Platforms/Generic/DelayedCallCommand.cpp ${ORTHANC_STONE_ROOT}/Platforms/Generic/Oracle.cpp - ${ORTHANC_STONE_ROOT}/Platforms/Generic/OracleWebService.h + ${ORTHANC_STONE_ROOT}/Platforms/Generic/OracleWebService.cpp + ${ORTHANC_STONE_ROOT}/Platforms/Generic/OracleDelayedCallExecutor.h ) if (ENABLE_SDL OR ENABLE_QT) @@ -212,6 +214,7 @@ set(STONE_WASM_SOURCES ${ORTHANC_STONE_ROOT}/Platforms/Wasm/Defaults.cpp + ${ORTHANC_STONE_ROOT}/Platforms/Wasm/WasmDelayedCallExecutor.cpp ${ORTHANC_STONE_ROOT}/Platforms/Wasm/WasmWebService.cpp ${ORTHANC_STONE_ROOT}/Platforms/Wasm/WasmViewport.cpp ${ORTHANC_STONE_ROOT}/Platforms/Wasm/WasmPlatformApplicationAdapter.cpp @@ -226,6 +229,10 @@ COMMAND ${CMAKE_COMMAND} -E touch "${AUTOGENERATED_DIR}/WasmWebService.c" "" DEPENDS "${ORTHANC_STONE_ROOT}/Platforms/Wasm/WasmWebService.js") add_custom_command( + OUTPUT "${AUTOGENERATED_DIR}/WasmDelayedCallExecutor.c" + COMMAND ${CMAKE_COMMAND} -E touch "${AUTOGENERATED_DIR}/WasmDelayedCallExecutor.c" "" + DEPENDS "${ORTHANC_STONE_ROOT}/Platforms/Wasm/WasmDelayedCallExecutor.js") + add_custom_command( OUTPUT "${AUTOGENERATED_DIR}/default-library.c" COMMAND ${CMAKE_COMMAND} -E touch "${AUTOGENERATED_DIR}/default-library.c" "" DEPENDS "${ORTHANC_STONE_ROOT}/Platforms/Wasm/default-library.js") @@ -246,6 +253,8 @@ ${ORTHANC_STONE_ROOT}/Framework/Layers/LineMeasureTracker.cpp ${ORTHANC_STONE_ROOT}/Framework/Layers/RenderStyle.cpp ${ORTHANC_STONE_ROOT}/Framework/Layers/SliceOutlineRenderer.cpp + ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographyAlphaLayer.cpp + ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographyDicomLayer.cpp ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographyLayer.cpp ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographyLayerCropTracker.cpp ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographyLayerMoveTracker.cpp @@ -253,12 +262,16 @@ ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographyLayerRotateTracker.cpp ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographyScene.cpp ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographySceneCommand.cpp + ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographySceneReader.cpp + ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographySceneWriter.cpp + ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographyTextLayer.cpp ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographyWidget.cpp ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographyWindowingTracker.cpp ${ORTHANC_STONE_ROOT}/Framework/SmartLoader.cpp ${ORTHANC_STONE_ROOT}/Framework/StoneEnumerations.cpp ${ORTHANC_STONE_ROOT}/Framework/StoneException.h ${ORTHANC_STONE_ROOT}/Framework/Toolbox/AffineTransform2D.cpp + ${ORTHANC_STONE_ROOT}/Framework/Toolbox/BaseWebService.cpp ${ORTHANC_STONE_ROOT}/Framework/Toolbox/CoordinateSystem3D.cpp ${ORTHANC_STONE_ROOT}/Framework/Toolbox/DicomFrameConverter.cpp ${ORTHANC_STONE_ROOT}/Framework/Toolbox/DicomStructureSet.cpp @@ -266,6 +279,7 @@ ${ORTHANC_STONE_ROOT}/Framework/Toolbox/Extent2D.cpp ${ORTHANC_STONE_ROOT}/Framework/Toolbox/FiniteProjectiveCamera.cpp ${ORTHANC_STONE_ROOT}/Framework/Toolbox/GeometryToolbox.cpp + ${ORTHANC_STONE_ROOT}/Framework/Toolbox/IDelayedCallExecutor.h ${ORTHANC_STONE_ROOT}/Framework/Toolbox/IWebService.cpp ${ORTHANC_STONE_ROOT}/Framework/Toolbox/ImageGeometry.cpp ${ORTHANC_STONE_ROOT}/Framework/Toolbox/LinearAlgebra.cpp diff -r b70e9be013e4 -r a750f11892ec Resources/CMake/QtConfiguration.cmake --- a/Resources/CMake/QtConfiguration.cmake Mon Dec 24 13:41:12 2018 +0100 +++ b/Resources/CMake/QtConfiguration.cmake Fri Dec 28 10:40:34 2018 +0100 @@ -64,6 +64,11 @@ ${ORTHANC_STONE_ROOT}/Applications/Qt/QStoneMainWindow.cpp ) +ORTHANC_QT_WRAP_CPP(QT_SOURCES + ${ORTHANC_STONE_ROOT}/Applications/Qt/QCairoWidget.h + ${ORTHANC_STONE_ROOT}/Applications/Qt/QStoneMainWindow.h + ) + # NB: Including CMAKE_CURRENT_BINARY_DIR is mandatory, as the CMake # macros for Qt will put their result in that directory, which cannot diff -r b70e9be013e4 -r a750f11892ec UnitTestsSources/TestMessageBroker.cpp --- a/UnitTestsSources/TestMessageBroker.cpp Mon Dec 24 13:41:12 2018 +0100 +++ b/UnitTestsSources/TestMessageBroker.cpp Fri Dec 28 10:40:34 2018 +0100 @@ -1,158 +1,416 @@ -///** -// * 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 . -// **/ +/** + * 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 . + **/ -//#include "gtest/gtest.h" +#include "gtest/gtest.h" + +#include "Framework/Messages/MessageBroker.h" +#include "Framework/Messages/Promise.h" +#include "Framework/Messages/IObservable.h" +#include "Framework/Messages/IObserver.h" +#include "Framework/Messages/MessageForwarder.h" + -//#include "../Framework/Messages/MessageBroker.h" -//#include "../Framework/Messages/IMessage.h" -//#include "../Framework/Messages/IObservable.h" -//#include "../Framework/Messages/IObserver.h" -//#include "../Framework/StoneEnumerations.h" +int testCounter = 0; +namespace { + + using namespace OrthancStone; + + + enum CustomMessageType + { + CustomMessageType_First = MessageType_CustomMessage + 1, + + CustomMessageType_Completed, + CustomMessageType_Increment + }; -//static int test1Counter = 0; -//static int test2Counter = 0; -//class MyFullObserver : public OrthancStone::IObserver -//{ + class MyObservable : public IObservable + { + public: + struct MyCustomMessage: public BaseMessage + { + int payload_; + + MyCustomMessage(int payload) + : BaseMessage(), + payload_(payload) + {} + }; + + MyObservable(MessageBroker& broker) + : IObservable(broker) + {} + + }; -//public: -// MyFullObserver(OrthancStone::MessageBroker& broker) -// : OrthancStone::IObserver(broker) -// { -//// DeclareHandledMessage(OrthancStone::MessageType_Test1); -//// DeclareIgnoredMessage(OrthancStone::MessageType_Test2); -// } + class MyObserver : public IObserver + { + public: + MyObserver(MessageBroker& broker) + : IObserver(broker) + {} + + void HandleCompletedMessage(const MyObservable::MyCustomMessage& message) + { + testCounter += message.payload_; + } + + }; + + + class MyIntermediate : public IObserver, public IObservable + { + IObservable& observedObject_; + public: + MyIntermediate(MessageBroker& broker, IObservable& observedObject) + : IObserver(broker), + IObservable(broker), + observedObject_(observedObject) + { + observedObject_.RegisterObserverCallback(new MessageForwarder(broker, *this)); + } + }; -// void HandleMessage(OrthancStone::IObservable& from, const OrthancStone::IMessage& message) { -// switch (message.GetType()) -// { -// case OrthancStone::MessageType_Test1: -// test1Counter++; -// break; -// case OrthancStone::MessageType_Test2: -// test2Counter++; -// break; -// default: -// throw OrthancStone::MessageNotDeclaredException(message.GetType()); -// } -// } + class MyPromiseSource : public IObservable + { + Promise* currentPromise_; + public: + struct MyPromiseMessage: public BaseMessage + { + int increment; + + MyPromiseMessage(int increment) + : BaseMessage(), + increment(increment) + {} + }; + + MyPromiseSource(MessageBroker& broker) + : IObservable(broker), + currentPromise_(NULL) + {} + + Promise& StartSomethingAsync() + { + currentPromise_ = new Promise(GetBroker()); + return *currentPromise_; + } -//}; + void CompleteSomethingAsyncWithSuccess(int payload) + { + currentPromise_->Success(MyPromiseMessage(payload)); + delete currentPromise_; + } -//class MyPartialObserver : public OrthancStone::IObserver -//{ + void CompleteSomethingAsyncWithFailure(int payload) + { + currentPromise_->Failure(MyPromiseMessage(payload)); + delete currentPromise_; + } + }; + -//public: -// MyPartialObserver(OrthancStone::MessageBroker& broker) -// : OrthancStone::IObserver(broker) -// { -//// DeclareHandledMessage(OrthancStone::MessageType_Test1); -// // don't declare Test2 on purpose -// } + class MyPromiseTarget : public IObserver + { + public: + MyPromiseTarget(MessageBroker& broker) + : IObserver(broker) + {} + + void IncrementCounter(const MyPromiseSource::MyPromiseMessage& args) + { + testCounter += args.increment; + } + + void DecrementCounter(const MyPromiseSource::MyPromiseMessage& args) + { + testCounter -= args.increment; + } + }; +} -// void HandleMessage(OrthancStone::IObservable& from, const OrthancStone::IMessage& message) { -// switch (message.GetType()) -// { -// case OrthancStone::MessageType_Test1: -// test1Counter++; -// break; -// case OrthancStone::MessageType_Test2: -// test2Counter++; -// break; -// default: -// throw OrthancStone::MessageNotDeclaredException(message.GetType()); -// } -// } +TEST(MessageBroker, TestPermanentConnectionSimpleUseCase) +{ + MessageBroker broker; + MyObservable observable(broker); + MyObserver observer(broker); + + // create a permanent connection between an observable and an observer + observable.RegisterObserverCallback(new Callable(observer, &MyObserver::HandleCompletedMessage)); + + testCounter = 0; + observable.EmitMessage(MyObservable::MyCustomMessage(12)); + ASSERT_EQ(12, testCounter); + + // the connection is permanent; if we emit the same message again, the observer will be notified again + testCounter = 0; + observable.EmitMessage(MyObservable::MyCustomMessage(20)); + ASSERT_EQ(20, testCounter); + + // Unregister the observer; make sure it's not called anymore + observable.Unregister(&observer); + testCounter = 0; + observable.EmitMessage(MyObservable::MyCustomMessage(20)); + ASSERT_EQ(0, testCounter); +} + +TEST(MessageBroker, TestMessageForwarderSimpleUseCase) +{ + MessageBroker broker; + MyObservable observable(broker); + MyIntermediate intermediate(broker, observable); + MyObserver observer(broker); + + // let the observer observers the intermediate that is actually forwarding the messages from the observable + intermediate.RegisterObserverCallback(new Callable(observer, &MyObserver::HandleCompletedMessage)); + + testCounter = 0; + observable.EmitMessage(MyObservable::MyCustomMessage(12)); + ASSERT_EQ(12, testCounter); + + // the connection is permanent; if we emit the same message again, the observer will be notified again + testCounter = 0; + observable.EmitMessage(MyObservable::MyCustomMessage(20)); + ASSERT_EQ(20, testCounter); +} + +TEST(MessageBroker, TestPermanentConnectionDeleteObserver) +{ + MessageBroker broker; + MyObservable observable(broker); + MyObserver* observer = new MyObserver(broker); -//}; + // create a permanent connection between an observable and an observer + observable.RegisterObserverCallback(new Callable(*observer, &MyObserver::HandleCompletedMessage)); + testCounter = 0; + observable.EmitMessage(MyObservable::MyCustomMessage(12)); + ASSERT_EQ(12, testCounter); + + // delete the observer and check that the callback is not called anymore + delete observer; -//class MyObservable : public OrthancStone::IObservable -//{ + // the connection is permanent; if we emit the same message again, the observer will be notified again + testCounter = 0; + observable.EmitMessage(MyObservable::MyCustomMessage(20)); + ASSERT_EQ(0, testCounter); +} + +TEST(MessageBroker, TestMessageForwarderDeleteIntermediate) +{ + MessageBroker broker; + MyObservable observable(broker); + MyIntermediate* intermediate = new MyIntermediate(broker, observable); + MyObserver observer(broker); + + // let the observer observers the intermediate that is actually forwarding the messages from the observable + intermediate->RegisterObserverCallback(new Callable(observer, &MyObserver::HandleCompletedMessage)); -//public: -// MyObservable(OrthancStone::MessageBroker& broker) -// : OrthancStone::IObservable(broker) -// { -// DeclareEmittableMessage(OrthancStone::MessageType_Test1); -// DeclareEmittableMessage(OrthancStone::MessageType_Test2); -// } + testCounter = 0; + observable.EmitMessage(MyObservable::MyCustomMessage(12)); + ASSERT_EQ(12, testCounter); + + delete intermediate; + + observable.EmitMessage(MyObservable::MyCustomMessage(20)); + ASSERT_EQ(12, testCounter); +} -//}; +TEST(MessageBroker, TestCustomMessage) +{ + MessageBroker broker; + MyObservable observable(broker); + MyIntermediate intermediate(broker, observable); + MyObserver observer(broker); + + // let the observer observers the intermediate that is actually forwarding the messages from the observable + intermediate.RegisterObserverCallback(new Callable(observer, &MyObserver::HandleCompletedMessage)); + + testCounter = 0; + observable.EmitMessage(MyObservable::MyCustomMessage(12)); + ASSERT_EQ(12, testCounter); + + // the connection is permanent; if we emit the same message again, the observer will be notified again + testCounter = 0; + observable.EmitMessage(MyObservable::MyCustomMessage(20)); + ASSERT_EQ(20, testCounter); +} -//TEST(MessageBroker, NormalUsage) -//{ -// OrthancStone::MessageBroker broker; -// MyObservable observable(broker); +TEST(MessageBroker, TestPromiseSuccessFailure) +{ + MessageBroker broker; + MyPromiseSource source(broker); + MyPromiseTarget target(broker); + + // test a successful promise + source.StartSomethingAsync() + .Then(new Callable(target, &MyPromiseTarget::IncrementCounter)) + .Else(new Callable(target, &MyPromiseTarget::DecrementCounter)); + + testCounter = 0; + source.CompleteSomethingAsyncWithSuccess(10); + ASSERT_EQ(10, testCounter); -// test1Counter = 0; + // test a failing promise + source.StartSomethingAsync() + .Then(new Callable(target, &MyPromiseTarget::IncrementCounter)) + .Else(new Callable(target, &MyPromiseTarget::DecrementCounter)); + + testCounter = 0; + source.CompleteSomethingAsyncWithFailure(15); + ASSERT_EQ(-15, testCounter); +} -// // no observers have been registered -> nothing shall happen -// observable.EmitMessage(OrthancStone::IMessage(OrthancStone::MessageType_Test1)); +TEST(MessageBroker, TestPromiseDeleteTarget) +{ + MessageBroker broker; + MyPromiseSource source(broker); + MyPromiseTarget* target = new MyPromiseTarget(broker); -// ASSERT_EQ(0, test1Counter); + // create the promise + source.StartSomethingAsync() + .Then(new Callable(*target, &MyPromiseTarget::IncrementCounter)) + .Else(new Callable(*target, &MyPromiseTarget::DecrementCounter)); -// // register an observer, check it is called -// MyFullObserver fullObserver(broker); -// ASSERT_NO_THROW(observable.RegisterObserver(fullObserver)); + // delete the promise target + delete target; + + // trigger the promise, make sure it does not throw and does not call the callback + testCounter = 0; + source.CompleteSomethingAsyncWithSuccess(10); + ASSERT_EQ(0, testCounter); -// observable.EmitMessage(OrthancStone::IMessage(OrthancStone::MessageType_Test1)); + // test a failing promise + source.StartSomethingAsync() + .Then(new Callable(*target, &MyPromiseTarget::IncrementCounter)) + .Else(new Callable(*target, &MyPromiseTarget::DecrementCounter)); + + testCounter = 0; + source.CompleteSomethingAsyncWithFailure(15); + ASSERT_EQ(0, testCounter); +} + +#if __cplusplus >= 201103L -// ASSERT_EQ(1, test1Counter); +#include + +namespace OrthancStone { + + template + class LambdaCallable : public MessageHandler + { + private: -// // register an invalid observer, check it raises an exception -// MyPartialObserver partialObserver(broker); -// ASSERT_THROW(observable.RegisterObserver(partialObserver), OrthancStone::MessageNotDeclaredException); + IObserver& observer_; + std::function lambda_; -// // check an exception is thrown when the observable emits an undeclared message -// ASSERT_THROW(observable.EmitMessage(OrthancStone::IMessage(OrthancStone::MessageType_VolumeSlicer_GeometryReady)), OrthancStone::MessageNotDeclaredException); + public: + LambdaCallable(IObserver& observer, + std::function lambdaFunction) : + observer_(observer), + lambda_(lambdaFunction) + { + } -// // unregister the observer, make sure nothing happens afterwards -// observable.UnregisterObserver(fullObserver); -// observable.EmitMessage(OrthancStone::IMessage(OrthancStone::MessageType_Test1)); -// ASSERT_EQ(1, test1Counter); -//} + virtual void Apply(const IMessage& message) + { + lambda_(dynamic_cast(message)); + } + + virtual MessageType GetMessageType() const + { + return static_cast(TMessage::Type); + } + + virtual IObserver* GetObserver() const + { + return &observer_; + } + }; + -//TEST(MessageBroker, DeleteObserverWhileRegistered) -//{ -// OrthancStone::MessageBroker broker; -// MyObservable observable(broker); +} + +TEST(MessageBroker, TestLambdaSimpleUseCase) +{ + MessageBroker broker; + MyObservable observable(broker); + MyObserver* observer = new MyObserver(broker); -// test1Counter = 0; + // create a permanent connection between an observable and an observer + observable.RegisterObserverCallback(new LambdaCallable(*observer, [&](const MyObservable::MyCustomMessage& message) {testCounter += 2 * message.payload_;})); + + testCounter = 0; + observable.EmitMessage(MyObservable::MyCustomMessage(12)); + ASSERT_EQ(24, testCounter); + + // delete the observer and check that the callback is not called anymore + delete observer; -// { -// // register an observer, check it is called -// MyFullObserver observer(broker); -// observable.RegisterObserver(observer); + // the connection is permanent; if we emit the same message again, the observer will be notified again + testCounter = 0; + observable.EmitMessage(MyObservable::MyCustomMessage(20)); + ASSERT_EQ(0, testCounter); +} -// observable.EmitMessage(OrthancStone::IMessage(OrthancStone::MessageType_Test1)); +namespace { + class MyObserverWithLambda : public IObserver { + private: + int multiplier_; // this is a private variable we want to access in a lambda + + public: + MyObserverWithLambda(MessageBroker& broker, int multiplier, MyObservable& observable) + : IObserver(broker), + multiplier_(multiplier) + { + // register a callable to a lambda that access private members + observable.RegisterObserverCallback(new LambdaCallable(*this, [this](const MyObservable::MyCustomMessage& message) { + testCounter += multiplier_ * message.payload_; + })); -// ASSERT_EQ(1, test1Counter); -// } + } + }; +} + +TEST(MessageBroker, TestLambdaCaptureThisAndAccessPrivateMembers) +{ + MessageBroker broker; + MyObservable observable(broker); + MyObserverWithLambda* observer = new MyObserverWithLambda(broker, 3, observable); -// // at this point, the observer has been deleted, the handle shall not be called again (and it shall not crash !) -// observable.EmitMessage(OrthancStone::IMessage(OrthancStone::MessageType_Test1)); + testCounter = 0; + observable.EmitMessage(MyObservable::MyCustomMessage(12)); + ASSERT_EQ(36, testCounter); + + // delete the observer and check that the callback is not called anymore + delete observer; -// ASSERT_EQ(1, test1Counter); -//} + // the connection is permanent; if we emit the same message again, the observer will be notified again + testCounter = 0; + observable.EmitMessage(MyObservable::MyCustomMessage(20)); + ASSERT_EQ(0, testCounter); +} + +#endif // C++ 11 diff -r b70e9be013e4 -r a750f11892ec UnitTestsSources/TestMessageBroker2.cpp --- a/UnitTestsSources/TestMessageBroker2.cpp Mon Dec 24 13:41:12 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,410 +0,0 @@ -/** - * 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 . - **/ - - -#include "gtest/gtest.h" - -#include "Framework/Messages/MessageBroker.h" -#include "Framework/Messages/Promise.h" -#include "Framework/Messages/IObservable.h" -#include "Framework/Messages/IObserver.h" -#include "Framework/Messages/MessageForwarder.h" - - -int testCounter = 0; -namespace { - - using namespace OrthancStone; - - - enum CustomMessageType - { - CustomMessageType_First = MessageType_CustomMessage + 1, - - CustomMessageType_Completed, - CustomMessageType_Increment - }; - - - class MyObservable : public IObservable - { - public: - struct MyCustomMessage: public BaseMessage - { - int payload_; - - MyCustomMessage(int payload) - : BaseMessage(), - payload_(payload) - {} - }; - - MyObservable(MessageBroker& broker) - : IObservable(broker) - {} - - }; - - class MyObserver : public IObserver - { - public: - MyObserver(MessageBroker& broker) - : IObserver(broker) - {} - - void HandleCompletedMessage(const MyObservable::MyCustomMessage& message) - { - testCounter += message.payload_; - } - - }; - - - class MyIntermediate : public IObserver, public IObservable - { - IObservable& observedObject_; - public: - MyIntermediate(MessageBroker& broker, IObservable& observedObject) - : IObserver(broker), - IObservable(broker), - observedObject_(observedObject) - { - observedObject_.RegisterObserverCallback(new MessageForwarder(broker, *this)); - } - }; - - - class MyPromiseSource : public IObservable - { - Promise* currentPromise_; - public: - struct MyPromiseMessage: public BaseMessage - { - int increment; - - MyPromiseMessage(int increment) - : BaseMessage(), - increment(increment) - {} - }; - - MyPromiseSource(MessageBroker& broker) - : IObservable(broker), - currentPromise_(NULL) - {} - - Promise& StartSomethingAsync() - { - currentPromise_ = new Promise(GetBroker()); - return *currentPromise_; - } - - void CompleteSomethingAsyncWithSuccess(int payload) - { - currentPromise_->Success(MyPromiseMessage(payload)); - delete currentPromise_; - } - - void CompleteSomethingAsyncWithFailure(int payload) - { - currentPromise_->Failure(MyPromiseMessage(payload)); - delete currentPromise_; - } - }; - - - class MyPromiseTarget : public IObserver - { - public: - MyPromiseTarget(MessageBroker& broker) - : IObserver(broker) - {} - - void IncrementCounter(const MyPromiseSource::MyPromiseMessage& args) - { - testCounter += args.increment; - } - - void DecrementCounter(const MyPromiseSource::MyPromiseMessage& args) - { - testCounter -= args.increment; - } - }; -} - - -TEST(MessageBroker2, TestPermanentConnectionSimpleUseCase) -{ - MessageBroker broker; - MyObservable observable(broker); - MyObserver observer(broker); - - // create a permanent connection between an observable and an observer - observable.RegisterObserverCallback(new Callable(observer, &MyObserver::HandleCompletedMessage)); - - testCounter = 0; - observable.EmitMessage(MyObservable::MyCustomMessage(12)); - ASSERT_EQ(12, testCounter); - - // the connection is permanent; if we emit the same message again, the observer will be notified again - testCounter = 0; - observable.EmitMessage(MyObservable::MyCustomMessage(20)); - ASSERT_EQ(20, testCounter); -} - -TEST(MessageBroker2, TestMessageForwarderSimpleUseCase) -{ - MessageBroker broker; - MyObservable observable(broker); - MyIntermediate intermediate(broker, observable); - MyObserver observer(broker); - - // let the observer observers the intermediate that is actually forwarding the messages from the observable - intermediate.RegisterObserverCallback(new Callable(observer, &MyObserver::HandleCompletedMessage)); - - testCounter = 0; - observable.EmitMessage(MyObservable::MyCustomMessage(12)); - ASSERT_EQ(12, testCounter); - - // the connection is permanent; if we emit the same message again, the observer will be notified again - testCounter = 0; - observable.EmitMessage(MyObservable::MyCustomMessage(20)); - ASSERT_EQ(20, testCounter); -} - -TEST(MessageBroker2, TestPermanentConnectionDeleteObserver) -{ - MessageBroker broker; - MyObservable observable(broker); - MyObserver* observer = new MyObserver(broker); - - // create a permanent connection between an observable and an observer - observable.RegisterObserverCallback(new Callable(*observer, &MyObserver::HandleCompletedMessage)); - - testCounter = 0; - observable.EmitMessage(MyObservable::MyCustomMessage(12)); - ASSERT_EQ(12, testCounter); - - // delete the observer and check that the callback is not called anymore - delete observer; - - // the connection is permanent; if we emit the same message again, the observer will be notified again - testCounter = 0; - observable.EmitMessage(MyObservable::MyCustomMessage(20)); - ASSERT_EQ(0, testCounter); -} - -TEST(MessageBroker2, TestMessageForwarderDeleteIntermediate) -{ - MessageBroker broker; - MyObservable observable(broker); - MyIntermediate* intermediate = new MyIntermediate(broker, observable); - MyObserver observer(broker); - - // let the observer observers the intermediate that is actually forwarding the messages from the observable - intermediate->RegisterObserverCallback(new Callable(observer, &MyObserver::HandleCompletedMessage)); - - testCounter = 0; - observable.EmitMessage(MyObservable::MyCustomMessage(12)); - ASSERT_EQ(12, testCounter); - - delete intermediate; - - observable.EmitMessage(MyObservable::MyCustomMessage(20)); - ASSERT_EQ(12, testCounter); -} - -TEST(MessageBroker2, TestCustomMessage) -{ - MessageBroker broker; - MyObservable observable(broker); - MyIntermediate intermediate(broker, observable); - MyObserver observer(broker); - - // let the observer observers the intermediate that is actually forwarding the messages from the observable - intermediate.RegisterObserverCallback(new Callable(observer, &MyObserver::HandleCompletedMessage)); - - testCounter = 0; - observable.EmitMessage(MyObservable::MyCustomMessage(12)); - ASSERT_EQ(12, testCounter); - - // the connection is permanent; if we emit the same message again, the observer will be notified again - testCounter = 0; - observable.EmitMessage(MyObservable::MyCustomMessage(20)); - ASSERT_EQ(20, testCounter); -} - - -TEST(MessageBroker2, TestPromiseSuccessFailure) -{ - MessageBroker broker; - MyPromiseSource source(broker); - MyPromiseTarget target(broker); - - // test a successful promise - source.StartSomethingAsync() - .Then(new Callable(target, &MyPromiseTarget::IncrementCounter)) - .Else(new Callable(target, &MyPromiseTarget::DecrementCounter)); - - testCounter = 0; - source.CompleteSomethingAsyncWithSuccess(10); - ASSERT_EQ(10, testCounter); - - // test a failing promise - source.StartSomethingAsync() - .Then(new Callable(target, &MyPromiseTarget::IncrementCounter)) - .Else(new Callable(target, &MyPromiseTarget::DecrementCounter)); - - testCounter = 0; - source.CompleteSomethingAsyncWithFailure(15); - ASSERT_EQ(-15, testCounter); -} - -TEST(MessageBroker2, TestPromiseDeleteTarget) -{ - MessageBroker broker; - MyPromiseSource source(broker); - MyPromiseTarget* target = new MyPromiseTarget(broker); - - // create the promise - source.StartSomethingAsync() - .Then(new Callable(*target, &MyPromiseTarget::IncrementCounter)) - .Else(new Callable(*target, &MyPromiseTarget::DecrementCounter)); - - // delete the promise target - delete target; - - // trigger the promise, make sure it does not throw and does not call the callback - testCounter = 0; - source.CompleteSomethingAsyncWithSuccess(10); - ASSERT_EQ(0, testCounter); - - // test a failing promise - source.StartSomethingAsync() - .Then(new Callable(*target, &MyPromiseTarget::IncrementCounter)) - .Else(new Callable(*target, &MyPromiseTarget::DecrementCounter)); - - testCounter = 0; - source.CompleteSomethingAsyncWithFailure(15); - ASSERT_EQ(0, testCounter); -} - -#if __cplusplus >= 201103L - -#include - -namespace OrthancStone { - - template - class LambdaCallable : public MessageHandler - { - private: - - IObserver& observer_; - std::function lambda_; - - public: - LambdaCallable(IObserver& observer, - std::function lambdaFunction) : - observer_(observer), - lambda_(lambdaFunction) - { - } - - virtual void Apply(const IMessage& message) - { - lambda_(dynamic_cast(message)); - } - - virtual MessageType GetMessageType() const - { - return static_cast(TMessage::Type); - } - - virtual IObserver* GetObserver() const - { - return &observer_; - } - }; - - -} - -TEST(MessageBroker2, TestLambdaSimpleUseCase) -{ - MessageBroker broker; - MyObservable observable(broker); - MyObserver* observer = new MyObserver(broker); - - // create a permanent connection between an observable and an observer - observable.RegisterObserverCallback(new LambdaCallable(*observer, [&](const MyObservable::MyCustomMessage& message) {testCounter += 2 * message.payload_;})); - - testCounter = 0; - observable.EmitMessage(MyObservable::MyCustomMessage(12)); - ASSERT_EQ(24, testCounter); - - // delete the observer and check that the callback is not called anymore - delete observer; - - // the connection is permanent; if we emit the same message again, the observer will be notified again - testCounter = 0; - observable.EmitMessage(MyObservable::MyCustomMessage(20)); - ASSERT_EQ(0, testCounter); -} - -namespace { - class MyObserverWithLambda : public IObserver { - private: - int multiplier_; // this is a private variable we want to access in a lambda - - public: - MyObserverWithLambda(MessageBroker& broker, int multiplier, MyObservable& observable) - : IObserver(broker), - multiplier_(multiplier) - { - // register a callable to a lambda that access private members - observable.RegisterObserverCallback(new LambdaCallable(*this, [this](const MyObservable::MyCustomMessage& message) { - testCounter += multiplier_ * message.payload_; - })); - - } - }; -} - -TEST(MessageBroker2, TestLambdaCaptureThisAndAccessPrivateMembers) -{ - MessageBroker broker; - MyObservable observable(broker); - MyObserverWithLambda* observer = new MyObserverWithLambda(broker, 3, observable); - - testCounter = 0; - observable.EmitMessage(MyObservable::MyCustomMessage(12)); - ASSERT_EQ(36, testCounter); - - // delete the observer and check that the callback is not called anymore - delete observer; - - // the connection is permanent; if we emit the same message again, the observer will be notified again - testCounter = 0; - observable.EmitMessage(MyObservable::MyCustomMessage(20)); - ASSERT_EQ(0, testCounter); -} - -#endif // C++ 11 diff -r b70e9be013e4 -r a750f11892ec UnitTestsSources/TestMessageBroker2_connect_ok.cpp --- a/UnitTestsSources/TestMessageBroker2_connect_ok.cpp Mon Dec 24 13:41:12 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,226 +0,0 @@ -/** - * 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 . - **/ - - -#include "gtest/gtest.h" - -#include -#include -#include - -#include -#include -#include - -int testCounter = 0; -namespace { - - enum MessageType - { - // used in unit tests only - MessageType_Test1, - MessageType_Test2, - - MessageType_LastGenericStoneMessage - }; - - struct IMessage : public boost::noncopyable - { - MessageType messageType_; - public: - IMessage(const MessageType& messageType) - : messageType_(messageType) - {} - virtual ~IMessage() {} - - MessageType GetType() const {return messageType_;} - }; - - - class IObserver; - class IObservable; - - /* - * This is a central message broker. It keeps track of all observers and knows - * when an observer is deleted. - * This way, it can prevent an observable to send a message to a dead observer. - */ - class MessageBroker : public boost::noncopyable - { - - std::set activeObservers_; // the list of observers that are currently alive (that have not been deleted) - - public: - - void Register(IObserver& observer) - { - activeObservers_.insert(&observer); - } - - void Unregister(IObserver& observer) - { - activeObservers_.erase(&observer); - } - - void EmitMessage(IObservable& from, std::set observers, const IMessage& message); - }; - - - class IObserver : public boost::noncopyable - { - protected: - MessageBroker& broker_; - - public: - IObserver(MessageBroker& broker) - : broker_(broker) - { - broker_.Register(*this); - } - - virtual ~IObserver() - { - broker_.Unregister(*this); - } - - void HandleMessage_(IObservable &from, const IMessage &message) - { - - HandleMessage(from, message); - } - - virtual void HandleMessage(IObservable& from, const IMessage& message) = 0; - - - protected: - - - }; - -// struct ICallableObserver -// { -// IObserver* observer; -// }; - -// typedef void (IObserver::*ObserverSingleMesssageHandler)(IObservable& from, const IMessage& message); - -// template -// struct CallableObserver : public ICallableObserver -// { -// void (TObserver::*ptrToMemberHandler)(IObservable& from, const IMessage& message); -// }; - - struct CallableObserver - { - IObserver* observer; - boost::function f; - }; - - class IObservable : public boost::noncopyable - { - protected: - MessageBroker& broker_; - - std::set observers_; - - std::map > callables_; - public: - - IObservable(MessageBroker& broker) - : broker_(broker) - { - } - virtual ~IObservable() - { - } - - void EmitMessage(const IMessage& message) - { - //broker_.EmitMessage(*this, observers_, message); - - // TODO check if observer is still alive and call ! - CallableObserver* callable = *(callables_[message.GetType()].begin()); - callable->f(*this, message); - } - - void RegisterObserver(IObserver& observer) - { - observers_.insert(&observer); - } - - void UnregisterObserver(IObserver& observer) - { - observers_.erase(&observer); - } - - - //template void Connect(MessageType messageType, IObserver& observer, void (TObserver::*ptrToMemberHandler)(IObservable& from, const IMessage& message)) - void Connect(MessageType messageType, IObserver& observer, boost::function f) - { - callables_[messageType] = std::set(); - CallableObserver* callable = new CallableObserver(); - callable->observer = &observer; - callable->f = f; - callables_[messageType].insert(callable); - } - }; - - - class MyObservable : public IObservable - { - public: - MyObservable(MessageBroker& broker) - : IObservable(broker) - {} - }; - - class MyObserver : public IObserver - { - public: - MyObserver(MessageBroker& broker) - : IObserver(broker) - {} - virtual void HandleMessage(IObservable& from, const IMessage& message) {} - void HandleSpecificMessage(IObservable& from, const IMessage& message) - { - testCounter++; - } - - }; - -} - -//#define STONE_CONNECT(observabe, messageType, observerPtr, observerMemberFnPtr) - -TEST(MessageBroker2, Test1) -{ - MessageBroker broker; - MyObservable observable(broker); - MyObserver observer(broker); - - - observable.Connect(MessageType_Test1, observer, boost::bind(&MyObserver::HandleSpecificMessage, &observer, _1, _2)); - //STONE_CONNECT(observable, MessageType_Test1, observer, &MyObserver::HandleSpecificMessage) - observable.EmitMessage(IMessage(MessageType_Test1)); - - ASSERT_EQ(1, testCounter); -} - - diff -r b70e9be013e4 -r a750f11892ec UnitTestsSources/TestMessageBroker2_promise_and_connect_ok.cpp --- a/UnitTestsSources/TestMessageBroker2_promise_and_connect_ok.cpp Mon Dec 24 13:41:12 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,520 +0,0 @@ -/** - * 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 . - **/ - - -#include "gtest/gtest.h" - -#include -#include -#include - -#include -#include -#include - -int testCounter = 0; -namespace { - - enum MessageType - { - MessageType_Test1, - MessageType_Test2, - - MessageType_CustomMessage, - MessageType_LastGenericStoneMessage - }; - - struct IMessage : public boost::noncopyable - { - MessageType messageType_; - public: - IMessage(const MessageType& messageType) - : messageType_(messageType) - {} - virtual ~IMessage() {} - - virtual int GetType() const {return messageType_;} - }; - - - struct ICustomMessage : public IMessage - { - int customMessageType_; - public: - ICustomMessage(int customMessageType) - : IMessage(MessageType_CustomMessage), - customMessageType_(customMessageType) - {} - virtual ~ICustomMessage() {} - - virtual int GetType() const {return customMessageType_;} - }; - - - class IObserver; - class IObservable; - class IPromiseTarget; - class IPromiseSource; - class Promise; - - /* - * This is a central message broker. It keeps track of all observers and knows - * when an observer is deleted. - * This way, it can prevent an observable to send a message to a delete observer. - * It does the same book-keeping for the IPromiseTarget and IPromiseSource - */ - class MessageBroker : public boost::noncopyable - { - - std::set activeObservers_; // the list of observers that are currently alive (that have not been deleted) - std::set activePromiseTargets_; - std::set activePromiseSources_; - - public: - - void Register(IObserver& observer) - { - activeObservers_.insert(&observer); - } - - void Unregister(IObserver& observer) - { - activeObservers_.erase(&observer); - } - - void Register(IPromiseTarget& target) - { - activePromiseTargets_.insert(&target); - } - - void Unregister(IPromiseTarget& target) - { - activePromiseTargets_.erase(&target); - } - - void Register(IPromiseSource& source) - { - activePromiseSources_.insert(&source); - } - - void Unregister(IPromiseSource& source) - { - activePromiseSources_.erase(&source); - } - - void EmitMessage(IObservable& from, std::set observers, const IMessage& message); - - bool IsActive(IPromiseTarget* target) - { - return activePromiseTargets_.find(target) != activePromiseTargets_.end(); - } - - bool IsActive(IPromiseSource* source) - { - return activePromiseSources_.find(source) != activePromiseSources_.end(); - } - - bool IsActive(IObserver* observer) - { - return activeObservers_.find(observer) != activeObservers_.end(); - } - }; - - struct IPromiseArgs - { -public: - virtual ~IPromiseArgs() {} - }; - - class EmptyPromiseArguments : public IPromiseArgs - { - - }; - - class Promise : public boost::noncopyable - { - protected: - MessageBroker& broker_; - - IPromiseTarget* successTarget_; - boost::function successCallable_; - - IPromiseTarget* failureTarget_; - boost::function failureCallable_; - - public: - Promise(MessageBroker& broker) - : broker_(broker), - successTarget_(NULL), - failureTarget_(NULL) - { - } - - void Success(const IPromiseArgs& message) - { - // check the target is still alive in the broker - if (broker_.IsActive(successTarget_)) - { - successCallable_(message); - } - } - - void Failure(const IPromiseArgs& message) - { - // check the target is still alive in the broker - if (broker_.IsActive(failureTarget_)) - { - failureCallable_(message); - } - } - - Promise& Then(IPromiseTarget* target, boost::function f) - { - if (successTarget_ != NULL) - { - // TODO: throw throw new "Promise may only have a single success target" - } - successTarget_ = target; - successCallable_ = f; - return *this; - } - - Promise& Else(IPromiseTarget* target, boost::function f) - { - if (failureTarget_ != NULL) - { - // TODO: throw throw new "Promise may only have a single failure target" - } - failureTarget_ = target; - failureCallable_ = f; - return *this; - } - - }; - - class IObserver : public boost::noncopyable - { - protected: - MessageBroker& broker_; - - public: - IObserver(MessageBroker& broker) - : broker_(broker) - { - broker_.Register(*this); - } - - virtual ~IObserver() - { - broker_.Unregister(*this); - } - - }; - - class IPromiseTarget : public boost::noncopyable - { - protected: - MessageBroker& broker_; - - public: - IPromiseTarget(MessageBroker& broker) - : broker_(broker) - { - broker_.Register(*this); - } - - virtual ~IPromiseTarget() - { - broker_.Unregister(*this); - } - }; - - class IPromiseSource : public boost::noncopyable - { - protected: - MessageBroker& broker_; - - public: - IPromiseSource(MessageBroker& broker) - : broker_(broker) - { - broker_.Register(*this); - } - - virtual ~IPromiseSource() - { - broker_.Unregister(*this); - } - }; - - - struct CallableObserver - { - IObserver* observer; - boost::function f; - }; - - class IObservable : public boost::noncopyable - { - protected: - MessageBroker& broker_; - - std::set observers_; - - std::map > callables_; - public: - - IObservable(MessageBroker& broker) - : broker_(broker) - { - } - virtual ~IObservable() - { - } - - void EmitMessage(const IMessage& message) - { - //broker_.EmitMessage(*this, observers_, message); - int messageType = message.GetType(); - if (callables_.find(messageType) != callables_.end()) - { - for (std::set::iterator observer = callables_[messageType].begin(); observer != callables_[messageType].end(); observer++) - { - CallableObserver* callable = *observer; - if (broker_.IsActive(callable->observer)) - { - callable->f(*this, message); - } - } - } - - } - - void RegisterObserver(IObserver& observer) - { - observers_.insert(&observer); - } - - void UnregisterObserver(IObserver& observer) - { - observers_.erase(&observer); - } - - //template void Connect(MessageType messageType, IObserver& observer, void (TObserver::*ptrToMemberHandler)(IObservable& from, const IMessage& message)) - void Connect(int messageType, IObserver& observer, boost::function f) - { - callables_[messageType] = std::set(); - CallableObserver* callable = new CallableObserver(); - callable->observer = &observer; - callable->f = f; - callables_[messageType].insert(callable); - } - }; - - - enum CustomMessageType - { - CustomMessageType_First = MessageType_LastGenericStoneMessage + 1, - - CustomMessageType_Completed - }; - - class MyObservable : public IObservable - { - public: - struct MyCustomMessage: public ICustomMessage - { - int payload_; - MyCustomMessage(int payload) - : ICustomMessage(CustomMessageType_Completed), - payload_(payload) - {} - }; - - MyObservable(MessageBroker& broker) - : IObservable(broker) - {} - - }; - - class MyObserver : public IObserver - { - public: - MyObserver(MessageBroker& broker) - : IObserver(broker) - {} - void HandleCompletedMessage(IObservable& from, const IMessage& message) - { - const MyObservable::MyCustomMessage& msg = dynamic_cast(message); - testCounter += msg.payload_; - } - - }; - - - class MyPromiseSource : public IPromiseSource - { - Promise* currentPromise_; - public: - struct MyPromiseArgs : public IPromiseArgs - { - int increment; - }; - - MyPromiseSource(MessageBroker& broker) - : IPromiseSource(broker), - currentPromise_(NULL) - {} - - Promise& StartSomethingAsync() - { - currentPromise_ = new Promise(broker_); - return *currentPromise_; - } - - void CompleteSomethingAsyncWithSuccess() - { - currentPromise_->Success(EmptyPromiseArguments()); - delete currentPromise_; - } - - void CompleteSomethingAsyncWithFailure() - { - currentPromise_->Failure(EmptyPromiseArguments()); - delete currentPromise_; - } - }; - - - class MyPromiseTarget : public IPromiseTarget - { - public: - MyPromiseTarget(MessageBroker& broker) - : IPromiseTarget(broker) - {} - - void IncrementCounter(const IPromiseArgs& args) - { - testCounter++; - } - - void DecrementCounter(const IPromiseArgs& args) - { - testCounter--; - } - }; -} - -#define CONNECT_MESSAGES(observablePtr, messageType, observerPtr, observerFnPtr) (observablePtr)->Connect(messageType, *(observerPtr), boost::bind(observerFnPtr, observerPtr, _1, _2)) -#define PTHEN(targetPtr, targetFnPtr) Then(targetPtr, boost::bind(targetFnPtr, targetPtr, _1)) -#define PELSE(targetPtr, targetFnPtr) Else(targetPtr, boost::bind(targetFnPtr, targetPtr, _1)) - - -TEST(MessageBroker2, TestPermanentConnectionSimpleUseCase) -{ - MessageBroker broker; - MyObservable observable(broker); - MyObserver observer(broker); - - // create a permanent connection between an observable and an observer - CONNECT_MESSAGES(&observable, CustomMessageType_Completed, &observer, &MyObserver::HandleCompletedMessage); - - testCounter = 0; - observable.EmitMessage(MyObservable::MyCustomMessage(12)); - ASSERT_EQ(12, testCounter); - - // the connection is permanent; if we emit the same message again, the observer will be notified again - testCounter = 0; - observable.EmitMessage(MyObservable::MyCustomMessage(20)); - ASSERT_EQ(20, testCounter); -} - -TEST(MessageBroker2, TestPermanentConnectionDeleteObserver) -{ - MessageBroker broker; - MyObservable observable(broker); - MyObserver* observer = new MyObserver(broker); - - // create a permanent connection between an observable and an observer - CONNECT_MESSAGES(&observable, CustomMessageType_Completed, observer, &MyObserver::HandleCompletedMessage); - - testCounter = 0; - observable.EmitMessage(MyObservable::MyCustomMessage(12)); - ASSERT_EQ(12, testCounter); - - // delete the observer and check that the callback is not called anymore - delete observer; - - // the connection is permanent; if we emit the same message again, the observer will be notified again - testCounter = 0; - observable.EmitMessage(MyObservable::MyCustomMessage(20)); - ASSERT_EQ(0, testCounter); -} - - -TEST(MessageBroker2, TestPromiseSuccessFailure) -{ - MessageBroker broker; - MyPromiseSource source(broker); - MyPromiseTarget target(broker); - - // test a successful promise - source.StartSomethingAsync() - .PTHEN(&target, &MyPromiseTarget::IncrementCounter) - .PELSE(&target, &MyPromiseTarget::DecrementCounter); - - testCounter = 0; - source.CompleteSomethingAsyncWithSuccess(); - ASSERT_EQ(1, testCounter); - - // test a failing promise - source.StartSomethingAsync() - .PTHEN(&target, &MyPromiseTarget::IncrementCounter) - .PELSE(&target, &MyPromiseTarget::DecrementCounter); - - testCounter = 0; - source.CompleteSomethingAsyncWithFailure(); - ASSERT_EQ(-1, testCounter); -} - -//TEST(MessageBroker2, TestPromiseDeleteTarget) -//{ -// MessageBroker broker; -// MyPromiseSource source(broker); -// MyPromiseTarget target(broker); - -// // test a successful promise -// source.StartSomethingAsync() -// .PTHEN(&target, &MyPromiseTarget::IncrementCounter) -// .PELSE(&target, &MyPromiseTarget::DecrementCounter); - -// testCounter = 0; -// source.CompleteSomethingAsyncWithSuccess(); -// ASSERT_EQ(1, testCounter); - -// // test a failing promise -// source.StartSomethingAsync() -// .PTHEN(&target, &MyPromiseTarget::IncrementCounter) -// .PELSE(&target, &MyPromiseTarget::DecrementCounter); - -// testCounter = 0; -// source.CompleteSomethingAsyncWithFailure(); -// ASSERT_EQ(-1, testCounter); -//}