Mercurial > hg > orthanc-stone
changeset 430:b85f635f1eb5 am-vsol-upgrade
added serialization for RadiographyScene
line wrap: on
line diff
--- a/Applications/Samples/CMakeLists.txt Fri Nov 23 16:06:23 2018 +0100 +++ b/Applications/Samples/CMakeLists.txt Thu Nov 29 15:11:19 2018 +0100 @@ -193,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 )
--- a/Applications/Samples/SingleFrameEditorApplication.h Fri Nov 23 16:06:23 2018 +0100 +++ b/Applications/Samples/SingleFrameEditorApplication.h Thu Nov 29 15:11:19 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 <Core/HttpClient.h> #include <Core/Images/FontRegistry.h> @@ -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<RadiographyScene> 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<RadiographyScene> scene_; - RadiographyEditorInteractor interactor_; + boost::shared_ptr<RadiographyScene> 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<std::string>(), - "Orthanc ID of the instance") - ("frame", boost::program_options::value<unsigned int>()->default_value(0), - "Number of the frame, for multi-frame DICOM instances") - ; + ("instance", boost::program_options::value<std::string>(), + "Orthanc ID of the instance") + ("frame", boost::program_options::value<unsigned int>()->default_value(0), + "Number of the frame, for multi-frame DICOM instances") + ; options.add(generic); } @@ -440,12 +465,11 @@ std::string instance = parameters["instance"].as<std::string>(); int frame = parameters["frame"].as<unsigned int>(); - 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_);
--- a/Framework/Messages/IObservable.cpp Fri Nov 23 16:06:23 2018 +0100 +++ b/Framework/Messages/IObservable.cpp Thu Nov 29 15:11:19 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<ICallable*>::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) {
--- a/Framework/Messages/IObservable.h Fri Nov 23 16:06:23 2018 +0100 +++ b/Framework/Messages/IObservable.h Thu Nov 29 15:11:19 2018 +0100 @@ -58,6 +58,8 @@ // Takes ownsership void RegisterObserverCallback(ICallable* callable); + void Unregister(IObserver* observer); + void EmitMessage(const IMessage& message); // Takes ownsership
--- a/Framework/Messages/MessageForwarder.h Fri Nov 23 16:06:23 2018 +0100 +++ b/Framework/Messages/MessageForwarder.h Thu Nov 29 15:11:19 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<A::MessageType>(broker, *this) // where this is B + * A.RegisterObserverCallback(new MessageForwarder<A::MessageType>(broker, *this) // where "this" is B * * in C: - * B.RegisterObserverCallback(new Callable<C, A:MessageTyper>(*this, &B::MyCallback)) // where this is C + * B.RegisterObserverCallback(new Callable<C, A:MessageTyper>(*this, &B::MyCallback)) // where "this" is C */ template<typename TMessage> class MessageForwarder : public IMessageForwarder, public Callable<MessageForwarder<TMessage>, TMessage>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Radiography/RadiographyAlphaLayer.cpp Thu Nov 29 15:11:19 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 <http://www.gnu.org/licenses/>. + **/ + + +#include "RadiographyAlphaLayer.h" + +#include "RadiographyScene.h" + +#include <Core/Images/Image.h> +#include <Core/OrthancException.h> + +namespace OrthancStone +{ + + void RadiographyAlphaLayer::SetAlpha(Orthanc::ImageAccessor* image) + { + std::auto_ptr<Orthanc::ImageAccessor> raii(image); + + if (image == NULL) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); + } + + if (image->GetFormat() != Orthanc::PixelFormat_Grayscale8) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageFormat); + } + + SetSize(image->GetWidth(), image->GetHeight()); + alpha_ = raii; + } + + void 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; // TODO: shouldn't it be center alone ? + } + } + + for (unsigned int y = 0; y < height; y++) + { + float *q = reinterpret_cast<float*>(buffer.GetRow(y)); + const uint8_t *p = reinterpret_cast<uint8_t*>(tmp.GetRow(y)); + + for (unsigned int x = 0; x < width; x++, p++, q++) + { + float a = static_cast<float>(*p) / 255.0f; + + *q = (a * value + (1.0f - a) * (*q)); + } + } + } + + 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; + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Radiography/RadiographyAlphaLayer.h Thu Nov 29 15:11:19 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 <http://www.gnu.org/licenses/>. + **/ + + +#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<Orthanc::ImageAccessor> 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()); + } + + + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Radiography/RadiographyDicomLayer.cpp Thu Nov 29 15:11:19 2018 +0100 @@ -0,0 +1,159 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + **/ + + +#include "RadiographyDicomLayer.h" + +#include "RadiographyScene.h" +#include "../Toolbox/DicomFrameConverter.h" + +#include <Core/OrthancException.h> +#include <Core/Images/Image.h> +#include <Core/Images/ImageProcessing.h> +#include <Plugins/Samples/Common/DicomDatasetReader.h> + +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]); + } + + //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 RadiographyDicomLayer::SetSourceImage(Orthanc::ImageAccessor* image) // Takes ownership + { + std::auto_ptr<Orthanc::ImageAccessor> raii(image); + + if (image == NULL) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); + } + + SetSize(image->GetWidth(), image->GetHeight()); + + source_ = raii; + ApplyConverter(); + } + + 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<float>(converter_->GetDefaultWindowCenter()); + width = static_cast<float>(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; + } + } + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Radiography/RadiographyDicomLayer.h Thu Nov 29 15:11:19 2018 +0100 @@ -0,0 +1,74 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + **/ + + +#pragma once + +#include "RadiographyLayer.h" +#include <Plugins/Samples/Common/FullOrthancDataset.h> + +namespace OrthancStone +{ + class RadiographyScene; + class DicomFrameConverter; + + class RadiographyDicomLayer : public RadiographyLayer + { + private: + std::auto_ptr<Orthanc::ImageAccessor> source_; // Content of PixelData + std::auto_ptr<DicomFrameConverter> converter_; + std::auto_ptr<Orthanc::ImageAccessor> 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; + }; +}
--- a/Framework/Radiography/RadiographyLayer.cpp Fri Nov 23 16:06:23 2018 +0100 +++ b/Framework/Radiography/RadiographyLayer.cpp Thu Nov 29 15:11:19 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 Radiography::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; @@ -162,18 +185,16 @@ index_(0), hasSize_(false), width_(0), - height_(0), - hasCrop_(false), - pixelSpacingX_(1), - pixelSpacingY_(1), - panX_(0), - panY_(0), - angle_(0), - resizeable_(false) + height_(0) { UpdateTransform(); } + void RadiographyLayer::ResetCrop() + { + geometry_.ResetCrop(); + UpdateTransform(); + } void RadiographyLayer::SetCrop(unsigned int x, unsigned int y, @@ -184,34 +205,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 +245,10 @@ } } - + void RadiographyLayer::SetAngle(double angle) { - angle_ = angle; + geometry_.SetAngle(angle); UpdateTransform(); } @@ -239,7 +262,7 @@ { throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageSize); } - + hasSize_ = true; width_ = width; height_ = height; @@ -251,7 +274,7 @@ Extent2D RadiographyLayer::GetExtent() const { Extent2D extent; - + unsigned int x, y, width, height; GetCrop(x, y, width, height); @@ -264,7 +287,7 @@ AddToExtent(extent, dx + dwidth, dy); AddToExtent(extent, dx, dy + dheight); AddToExtent(extent, dx + dwidth, dy + dheight); - + return extent; } @@ -282,7 +305,7 @@ else { transformInverse_.Apply(sceneX, sceneY); - + int x = static_cast<int>(std::floor(sceneX)); int y = static_cast<int>(std::floor(sceneY)); @@ -320,8 +343,7 @@ void RadiographyLayer::SetPan(double x, double y) { - panX_ = x; - panY_ = y; + geometry_.SetPan(x, y); UpdateTransform(); } @@ -329,8 +351,7 @@ void RadiographyLayer::SetPixelSpacing(double x, double y) { - pixelSpacingX_ = x; - pixelSpacingY_ = y; + geometry_.SetPixelSpacing(x, y); UpdateTransform(); } @@ -352,8 +373,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 +387,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; } }
--- a/Framework/Radiography/RadiographyLayer.h Fri Nov 23 16:06:23 2018 +0100 +++ b/Framework/Radiography/RadiographyLayer.h Thu Nov 29 15:11:19 2018 +0100 @@ -31,24 +31,114 @@ { 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) + { + 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_; protected: const AffineTransform2D& GetTransform() const @@ -94,11 +184,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 +205,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 +238,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,16 +254,6 @@ 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;
--- a/Framework/Radiography/RadiographyLayerCropTracker.cpp Fri Nov 23 16:06:23 2018 +0100 +++ b/Framework/Radiography/RadiographyLayerCropTracker.cpp Thu Nov 29 15:11:19 2018 +0100 @@ -58,7 +58,7 @@ sourceCropWidth_(tracker.cropWidth_), sourceCropHeight_(tracker.cropHeight_) { - tracker.accessor_.GetLayer().GetCrop(targetCropX_, targetCropY_, + tracker.accessor_.GetLayer().GetGeometry().GetCrop(targetCropX_, targetCropY_, targetCropWidth_, targetCropHeight_); } }; @@ -77,7 +77,7 @@ { if (accessor_.IsValid()) { - accessor_.GetLayer().GetCrop(cropX_, cropY_, cropWidth_, cropHeight_); + accessor_.GetLayer().GetGeometry().GetCrop(cropX_, cropY_, cropWidth_, cropHeight_); } }
--- a/Framework/Radiography/RadiographyLayerMoveTracker.cpp Fri Nov 23 16:06:23 2018 +0100 +++ b/Framework/Radiography/RadiographyLayerMoveTracker.cpp Thu Nov 29 15:11:19 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(); } }
--- a/Framework/Radiography/RadiographyLayerResizeTracker.cpp Fri Nov 23 16:06:23 2018 +0100 +++ b/Framework/Radiography/RadiographyLayerResizeTracker.cpp Thu Nov 29 15:11:19 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); } } }
--- a/Framework/Radiography/RadiographyLayerRotateTracker.cpp Fri Nov 23 16:06:23 2018 +0100 +++ b/Framework/Radiography/RadiographyLayerRotateTracker.cpp Thu Nov 29 15:11:19 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);
--- a/Framework/Radiography/RadiographyScene.cpp Fri Nov 23 16:06:23 2018 +0100 +++ b/Framework/Radiography/RadiographyScene.cpp Thu Nov 29 15:11:19 2018 +0100 @@ -21,6 +21,9 @@ #include "RadiographyScene.h" +#include "RadiographyAlphaLayer.h" +#include "RadiographyDicomLayer.h" +#include "RadiographyTextLayer.h" #include "../Toolbox/DicomFrameConverter.h" #include <Core/Images/Image.h> @@ -121,299 +124,6 @@ } } - - - class RadiographyScene::AlphaLayer : public RadiographyLayer - { - private: - const RadiographyScene& scene_; - std::auto_ptr<Orthanc::ImageAccessor> 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<Orthanc::ImageAccessor> raii(image); - - if (image == NULL) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); - } - - if (image->GetFormat() != Orthanc::PixelFormat_Grayscale8) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageFormat); - } - - SetSize(image->GetWidth(), image->GetHeight()); - alpha_ = raii; - } - - 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<float*>(buffer.GetRow(y)); - const uint8_t *p = reinterpret_cast<uint8_t*>(tmp.GetRow(y)); - - for (unsigned int x = 0; x < width; x++, p++, q++) - { - float a = static_cast<float>(*p) / 255.0f; - - *q = (a * value + (1.0f - a) * (*q)); - } - } - } - - - virtual bool GetRange(float& minValue, - float& maxValue) const - { - if (useWindowing_) - { - return false; - } - else - { - minValue = 0; - maxValue = 0; - - if (foreground_ < 0) - { - minValue = foreground_; - } - - if (foreground_ > 0) - { - maxValue = foreground_; - } - - return true; - } - } - }; - - - class RadiographyScene::TextLayer : public AlphaLayer - { - private: - std::string text_; - - public: - TextLayer(const RadiographyScene& scene) : - AlphaLayer(scene) - { - } - - void LoadText(const Orthanc::Font& font, - const std::string& utf8) - { - SetAlpha(font.RenderAlpha(utf8)); - } - - }; - - - class RadiographyScene::DicomLayer : public RadiographyLayer - { - private: - std::auto_ptr<Orthanc::ImageAccessor> source_; // Content of PixelData - std::auto_ptr<DicomFrameConverter> converter_; - std::auto_ptr<Orthanc::ImageAccessor> converted_; // Float32 - - static OrthancPlugins::DicomTag ConvertTag(const Orthanc::DicomTag& tag) - { - return OrthancPlugins::DicomTag(tag.GetGroup(), tag.GetElement()); - } - - - void ApplyConverter() - { - if (source_.get() != NULL && - converter_.get() != NULL) - { - converted_.reset(converter_->ConvertFrame(*source_)); - } - } - - public: - void SetDicomTags(const OrthancPlugins::FullOrthancDataset& dataset) - { - converter_.reset(new DicomFrameConverter); - converter_->ReadParameters(dataset); - ApplyConverter(); - - std::string tmp; - Vector pixelSpacing; - - if (dataset.GetStringValue(tmp, ConvertTag(Orthanc::DICOM_TAG_PIXEL_SPACING)) && - LinearAlgebra::ParseVector(pixelSpacing, tmp) && - pixelSpacing.size() == 2) - { - SetPixelSpacing(pixelSpacing[0], pixelSpacing[1]); - } - - //SetPan(-0.5 * GetPixelSpacingX(), -0.5 * GetPixelSpacingY()); - - OrthancPlugins::DicomDatasetReader reader(dataset); - - unsigned int width, height; - if (!reader.GetUnsignedIntegerValue(width, ConvertTag(Orthanc::DICOM_TAG_COLUMNS)) || - !reader.GetUnsignedIntegerValue(height, ConvertTag(Orthanc::DICOM_TAG_ROWS))) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat); - } - else - { - SetSize(width, height); - } - } - - - void SetSourceImage(Orthanc::ImageAccessor* image) // Takes ownership - { - std::auto_ptr<Orthanc::ImageAccessor> raii(image); - - if (image == NULL) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); - } - - SetSize(image->GetWidth(), image->GetHeight()); - - source_ = raii; - ApplyConverter(); - } - - - virtual void Render(Orthanc::ImageAccessor& buffer, - const 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<float>(converter_->GetDefaultWindowCenter()); - width = static_cast<float>(converter_->GetDefaultWindowWidth()); - return true; - } - else - { - return false; - } - } - - - virtual bool GetRange(float& minValue, - float& maxValue) const - { - if (converted_.get() != NULL) - { - if (converted_->GetFormat() != Orthanc::PixelFormat_Float32) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); - } - - Orthanc::ImageProcessing::GetMinMaxFloatValue(minValue, maxValue, *converted_); - return true; - } - else - { - return false; - } - } - }; - - RadiographyLayer& RadiographyScene::RegisterLayer(RadiographyLayer* layer) { if (layer == NULL) @@ -423,12 +133,14 @@ std::auto_ptr<RadiographyLayer> 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; } @@ -454,8 +166,18 @@ } } + void RadiographyScene::GetLayersIndexes(std::vector<size_t>& 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); @@ -463,15 +185,17 @@ delete layers_[layerIndex]; layers_.erase(layerIndex); countLayers_--; + LOG(INFO) << "Removing layer, there are now : " << countLayers_ << " layers"; } - RadiographyLayer& RadiographyScene::GetLayer(size_t layerIndex) + const RadiographyLayer& RadiographyScene::GetLayer(size_t layerIndex) const { if (layerIndex > countLayers_) { throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); } - return *(layers_[layerIndex]); + + return *(layers_.at(layerIndex)); } bool RadiographyScene::GetWindowing(float& center, @@ -511,17 +235,23 @@ RadiographyLayer& RadiographyScene::LoadText(const Orthanc::Font& font, - const std::string& utf8) + const std::string& utf8, + RadiographyLayer::Geometry* geometry) { - std::auto_ptr<AlphaLayer> alpha(new AlphaLayer(*this)); + std::auto_ptr<RadiographyTextLayer> 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<Orthanc::Image> block(new Orthanc::Image(Orthanc::PixelFormat_Grayscale8, width, height, false)); @@ -544,19 +274,34 @@ Orthanc::ImageProcessing::Set(region, color); } - std::auto_ptr<AlphaLayer> 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<RadiographyAlphaLayer> 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<RadiographyDicomLayer&>(RegisterLayer(new RadiographyDicomLayer)); + layer.SetInstance(instance, frame); + + if (geometry != NULL) + { + layer.SetGeometry(*geometry); + } { IWebService::HttpHeaders headers; @@ -594,7 +339,7 @@ RadiographyLayer& RadiographyScene::LoadDicomWebFrame(IWebService& web) { - RadiographyLayer& layer = RegisterLayer(new DicomLayer); + RadiographyLayer& layer = RegisterLayer(new RadiographyDicomLayer); return layer; @@ -616,7 +361,7 @@ assert(layer->second != NULL); OrthancPlugins::FullOrthancDataset dicom(message.GetAnswer(), message.GetAnswerSize()); - dynamic_cast<DicomLayer*>(layer->second)->SetDicomTags(dicom); + dynamic_cast<RadiographyDicomLayer*>(layer->second)->SetDicomTags(dicom); float c, w; if (!hasWindowing_ && @@ -627,7 +372,7 @@ windowingWidth_ = w; } - EmitMessage(GeometryChangedMessage(*this)); + EmitMessage(GeometryChangedMessage(*this, *(layer->second))); } } @@ -652,9 +397,9 @@ std::auto_ptr<Orthanc::PamReader> reader(new Orthanc::PamReader); reader->ReadFromMemory(content); - dynamic_cast<DicomLayer*>(layer->second)->SetSourceImage(reader.release()); + dynamic_cast<RadiographyDicomLayer*>(layer->second)->SetSourceImage(reader.release()); - EmitMessage(ContentChangedMessage(*this)); + EmitMessage(ContentChangedMessage(*this, *(layer->second))); } } @@ -678,7 +423,7 @@ const AffineTransform2D& viewTransform, ImageInterpolation interpolation) const { - Orthanc::ImageProcessing::Set(buffer, 0); + Orthanc::ImageProcessing::Set(buffer, 0); // TODO: get background color (depending on inverted state) // Render layers in the background-to-foreground order for (size_t index = 0; index < countLayers_; index++) @@ -765,16 +510,6 @@ } - void RadiographyScene::ExportToJson(Json::Value &output) - { - for (Layers::iterator it = layers_.begin(); it != layers_.end(); it++) - { - Json::Value jsonLayer; - it-> - - } - } - void RadiographyScene::ExportDicom(OrthancApiClient& orthanc, const Orthanc::DicomMap& dicom, const std::string& parentOrthancId,
--- a/Framework/Radiography/RadiographyScene.h Fri Nov 23 16:06:23 2018 +0100 +++ b/Framework/Radiography/RadiographyScene.h Thu Nov 29 15:11:19 2018 +0100 @@ -32,8 +32,46 @@ public IObservable { public: - typedef OriginMessage<MessageType_Widget_GeometryChanged, RadiographyScene> GeometryChangedMessage; - typedef OriginMessage<MessageType_Widget_ContentChanged, RadiographyScene> ContentChangedMessage; + class GeometryChangedMessage : + public OriginMessage<MessageType_Scene_GeometryChanged, RadiographyScene> + { + private: + RadiographyLayer& layer_; + + public: + GeometryChangedMessage(const RadiographyScene& origin, + RadiographyLayer& layer) : + OriginMessage(origin), + layer_(layer) + { + } + + RadiographyLayer& GetLayer() const + { + return layer_; + } + }; + + class ContentChangedMessage : + public OriginMessage<MessageType_Scene_ContentChanged, RadiographyScene> + { + 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,9 +107,6 @@ private: - class AlphaLayer; - class DicomLayer; - typedef std::map<size_t, RadiographyLayer*> Layers; size_t countLayers_; @@ -105,26 +140,29 @@ float width); 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); - + unsigned int height, + RadiographyLayer::Geometry* geometry); + + RadiographyLayer& LoadAlphaBitmap(Orthanc::ImageAccessor* bitmap, // takes ownership + RadiographyLayer::Geometry* geometry); + RadiographyLayer& LoadDicomFrame(OrthancApiClient& orthanc, const std::string& instance, unsigned int frame, - bool httpCompression); + bool httpCompression, + RadiographyLayer::Geometry* geometry); // pass NULL if you want default geometry RadiographyLayer& LoadDicomWebFrame(IWebService& web); void RemoveLayer(size_t layerIndex); - size_t GetLayerCount() - { - return countLayers_; - } + const RadiographyLayer& GetLayer(size_t layerIndex) const; - RadiographyLayer& GetLayer(size_t layerIndex); + void GetLayersIndexes(std::vector<size_t>& output) const; Extent2D GetSceneExtent() const; @@ -162,8 +200,5 @@ bool invert, ImageInterpolation interpolation, bool usePam); - - // temporary version used by VSOL - void ExportToJson(Json::Value& output); }; }
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Radiography/RadiographySceneReader.cpp Thu Nov 29 15:11:19 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 <http://www.gnu.org/licenses/>. + **/ + + +#include "RadiographySceneReader.h" + +#include <Core/Images/FontRegistry.h> +#include <Core/Images/PngReader.h> +#include <Core/OrthancException.h> +#include <Core/Toolbox.h> + +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(), true, &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<Orthanc::ImageAccessor> image; + if (mimeType == "image/png") + { + image.reset(new Orthanc::PngReader()); + dynamic_cast<Orthanc::PngReader*>(image.get())->ReadFromMemory(pngContent); + } + else + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); + + RadiographyAlphaLayer& layer = dynamic_cast<RadiographyAlphaLayer&>(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()); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Radiography/RadiographySceneReader.h Thu Nov 29 15:11:19 2018 +0100 @@ -0,0 +1,59 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + **/ + + +#pragma once + +#include "RadiographyScene.h" +#include "RadiographyAlphaLayer.h" +#include "RadiographyDicomLayer.h" +#include "RadiographyTextLayer.h" +#include <json/value.h> +#include <Core/Images/FontRegistry.h> + +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); + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Radiography/RadiographySceneWriter.cpp Thu Nov 29 15:11:19 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 <http://www.gnu.org/licenses/>. + **/ + + +#include "RadiographySceneWriter.h" + +#include <Core/OrthancException.h> +#include <Core/Images/PngWriter.h> +#include <Core/Toolbox.h> + +namespace OrthancStone +{ + void RadiographySceneWriter::Write(Json::Value& output, const RadiographyScene& scene) + { + output["version"] = 1; + output["layers"] = Json::arrayValue; + + std::vector<size_t> layersIndexes; + scene.GetLayersIndexes(layersIndexes); + + for (std::vector<size_t>::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<const RadiographyTextLayer*>(&layer) != NULL) + { + WriteLayer(output, dynamic_cast<const RadiographyTextLayer&>(layer)); + } + else if (dynamic_cast<const RadiographyDicomLayer*>(&layer) != NULL) + { + WriteLayer(output, dynamic_cast<const RadiographyDicomLayer&>(layer)); + } + else if (dynamic_cast<const RadiographyAlphaLayer*>(&layer) != NULL) + { + WriteLayer(output, dynamic_cast<const RadiographyAlphaLayer&>(layer)); + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Radiography/RadiographySceneWriter.h Thu Nov 29 15:11:19 2018 +0100 @@ -0,0 +1,50 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + **/ + + +#pragma once + +#include "RadiographyScene.h" +#include "RadiographyAlphaLayer.h" +#include "RadiographyDicomLayer.h" +#include "RadiographyTextLayer.h" +#include <json/value.h> + +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); + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Radiography/RadiographyTextLayer.cpp Thu Nov 29 15:11:19 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 <http://www.gnu.org/licenses/>. + **/ + + +#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)); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Radiography/RadiographyTextLayer.h Thu Nov 29 15:11:19 2018 +0100 @@ -0,0 +1,55 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + **/ + + +#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_; + } + }; +}
--- a/Framework/Radiography/RadiographyWidget.cpp Fri Nov 23 16:06:23 2018 +0100 +++ b/Framework/Radiography/RadiographyWidget.cpp Thu Nov 29 15:11:19 2018 +0100 @@ -31,7 +31,7 @@ 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,7 +56,7 @@ cairoBuffer_.reset(new CairoSurface(width, height)); } - scene_.Render(*floatBuffer_, GetView().GetMatrix(), interpolation); + scene_->Render(*floatBuffer_, GetView().GetMatrix(), interpolation); // Conversion from Float32 to BGRA32 (cairo). Very similar to // GrayscaleFrameRenderer => TODO MERGE? @@ -128,7 +128,7 @@ if (hasSelection_) { - scene_.DrawBorder(context, selectedLayer_, view.GetZoom()); + scene_->DrawBorder(context, selectedLayer_, view.GetZoom()); } return true; @@ -136,23 +136,16 @@ RadiographyWidget::RadiographyWidget(MessageBroker& broker, - RadiographyScene& scene, + boost::shared_ptr<RadiographyScene> 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<RadiographyWidget, RadiographyScene::GeometryChangedMessage> - (*this, &RadiographyWidget::OnGeometryChanged)); - - scene.RegisterObserverCallback( - new Callable<RadiographyWidget, RadiographyScene::ContentChangedMessage> - (*this, &RadiographyWidget::OnContentChanged)); + SetScene(scene); } @@ -216,4 +209,25 @@ NotifyContentChanged(); } } + + void RadiographyWidget::SetScene(boost::shared_ptr<RadiographyScene> scene) + { + if (scene_ != NULL) + { + scene_->Unregister(this); + } + + scene_ = scene; + + scene_->RegisterObserverCallback( + new Callable<RadiographyWidget, RadiographyScene::GeometryChangedMessage> + (*this, &RadiographyWidget::OnGeometryChanged)); + + scene_->RegisterObserverCallback( + new Callable<RadiographyWidget, RadiographyScene::ContentChangedMessage> + (*this, &RadiographyWidget::OnContentChanged)); + + // force redraw + FitContent(); + } }
--- a/Framework/Radiography/RadiographyWidget.h Fri Nov 23 16:06:23 2018 +0100 +++ b/Framework/Radiography/RadiographyWidget.h Thu Nov 29 15:11:19 2018 +0100 @@ -32,7 +32,7 @@ public IObserver { private: - RadiographyScene& scene_; + boost::shared_ptr<RadiographyScene> scene_; std::auto_ptr<Orthanc::ImageAccessor> floatBuffer_; std::auto_ptr<CairoSurface> cairoBuffer_; bool invert_; @@ -47,7 +47,7 @@ protected: virtual Extent2D GetSceneExtent() { - return scene_.GetSceneExtent(); + return scene_->GetSceneExtent(); } virtual bool RenderScene(CairoContext& context, @@ -55,14 +55,16 @@ public: RadiographyWidget(MessageBroker& broker, - RadiographyScene& scene, + boost::shared_ptr<RadiographyScene> 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<RadiographyScene> scene); + void Unselect() { hasSelection_ = false;
--- a/Framework/StoneEnumerations.h Fri Nov 23 16:06:23 2018 +0100 +++ b/Framework/StoneEnumerations.h Thu Nov 29 15:11:19 2018 +0100 @@ -157,6 +157,9 @@ MessageType_OrthancApi_GenericHttpError_Ready, MessageType_OrthancApi_GenericEmptyResponse_Ready, + MessageType_Scene_GeometryChanged, + MessageType_Scene_ContentChanged, + MessageType_ViewportChanged, // used in unit tests only
--- a/Framework/Toolbox/OrthancApiClient.cpp Fri Nov 23 16:06:23 2018 +0100 +++ b/Framework/Toolbox/OrthancApiClient.cpp Thu Nov 29 15:11:19 2018 +0100 @@ -170,7 +170,7 @@ class OrthancApiClient::CachedHttpRequestSuccessMessage { protected: - const std::string& uri_; + std::string uri_; void* answer_; size_t answerSize_; IWebService::HttpHeaders answerHeaders_;
--- a/Resources/CMake/OrthancStoneConfiguration.cmake Fri Nov 23 16:06:23 2018 +0100 +++ b/Resources/CMake/OrthancStoneConfiguration.cmake Thu Nov 29 15:11:19 2018 +0100 @@ -246,6 +246,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,6 +255,9 @@ ${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
--- a/UnitTestsSources/TestMessageBroker.cpp Fri Nov 23 16:06:23 2018 +0100 +++ b/UnitTestsSources/TestMessageBroker.cpp Thu Nov 29 15:11:19 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-2018 Osimis S.A., Belgium -// * -// * This program is free software: you can redistribute it and/or -// * modify it under the terms of the GNU Affero General Public License -// * as published by the Free Software Foundation, either version 3 of -// * the License, or (at your option) any later version. -// * -// * This program is distributed in the hope that it will be useful, but -// * WITHOUT ANY WARRANTY; without even the implied warranty of -// * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -// * Affero General Public License for more details. -// * -// * You should have received a copy of the GNU Affero General Public License -// * along with this program. If not, see <http://www.gnu.org/licenses/>. -// **/ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + **/ -//#include "gtest/gtest.h" +#include "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<CustomMessageType_Completed> + { + 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<MyObservable::MyCustomMessage>(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<MessageType_Test1> + { + 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<MyObserver, MyObservable::MyCustomMessage>(observer, &MyObserver::HandleCompletedMessage)); + + testCounter = 0; + observable.EmitMessage(MyObservable::MyCustomMessage(12)); + ASSERT_EQ(12, testCounter); + + // the connection is permanent; if we emit the same message again, the observer will be notified again + testCounter = 0; + observable.EmitMessage(MyObservable::MyCustomMessage(20)); + ASSERT_EQ(20, testCounter); + + // 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<MyObserver, MyObservable::MyCustomMessage>(observer, &MyObserver::HandleCompletedMessage)); + + testCounter = 0; + observable.EmitMessage(MyObservable::MyCustomMessage(12)); + ASSERT_EQ(12, testCounter); + + // the connection is permanent; if we emit the same message again, the observer will be notified again + testCounter = 0; + observable.EmitMessage(MyObservable::MyCustomMessage(20)); + ASSERT_EQ(20, testCounter); +} + +TEST(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<MyObserver, MyObservable::MyCustomMessage>(*observer, &MyObserver::HandleCompletedMessage)); + testCounter = 0; + observable.EmitMessage(MyObservable::MyCustomMessage(12)); + ASSERT_EQ(12, testCounter); + + // delete the observer and check that the callback is not called anymore + delete observer; -//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<MyObserver, MyObservable::MyCustomMessage>(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<MyObserver, MyObservable::MyCustomMessage>(observer, &MyObserver::HandleCompletedMessage)); + + testCounter = 0; + observable.EmitMessage(MyObservable::MyCustomMessage(12)); + ASSERT_EQ(12, testCounter); + + // the connection is permanent; if we emit the same message again, the observer will be notified again + testCounter = 0; + observable.EmitMessage(MyObservable::MyCustomMessage(20)); + ASSERT_EQ(20, testCounter); +} -//TEST(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<MyPromiseTarget, MyPromiseSource::MyPromiseMessage>(target, &MyPromiseTarget::IncrementCounter)) + .Else(new Callable<MyPromiseTarget, MyPromiseSource::MyPromiseMessage>(target, &MyPromiseTarget::DecrementCounter)); + + testCounter = 0; + source.CompleteSomethingAsyncWithSuccess(10); + ASSERT_EQ(10, testCounter); -// test1Counter = 0; + // test a failing promise + source.StartSomethingAsync() + .Then(new Callable<MyPromiseTarget, MyPromiseSource::MyPromiseMessage>(target, &MyPromiseTarget::IncrementCounter)) + .Else(new Callable<MyPromiseTarget, MyPromiseSource::MyPromiseMessage>(target, &MyPromiseTarget::DecrementCounter)); + + testCounter = 0; + source.CompleteSomethingAsyncWithFailure(15); + ASSERT_EQ(-15, testCounter); +} -// // 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<MyPromiseTarget, MyPromiseSource::MyPromiseMessage>(*target, &MyPromiseTarget::IncrementCounter)) + .Else(new Callable<MyPromiseTarget, MyPromiseSource::MyPromiseMessage>(*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<MyPromiseTarget, MyPromiseSource::MyPromiseMessage>(*target, &MyPromiseTarget::IncrementCounter)) + .Else(new Callable<MyPromiseTarget, MyPromiseSource::MyPromiseMessage>(*target, &MyPromiseTarget::DecrementCounter)); + + testCounter = 0; + source.CompleteSomethingAsyncWithFailure(15); + ASSERT_EQ(0, testCounter); +} + +#if __cplusplus >= 201103L -// ASSERT_EQ(1, test1Counter); +#include <functional> + +namespace OrthancStone { + + template <typename TMessage> + class LambdaCallable : public MessageHandler<TMessage> + { + private: -// // register an invalid observer, check it raises an exception -// MyPartialObserver partialObserver(broker); -// ASSERT_THROW(observable.RegisterObserver(partialObserver), OrthancStone::MessageNotDeclaredException); + IObserver& observer_; + std::function<void (const TMessage&)> 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<void (const TMessage&)> 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<const TMessage&>(message)); + } + + virtual MessageType GetMessageType() const + { + return static_cast<MessageType>(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<MyObservable::MyCustomMessage>(*observer, [&](const MyObservable::MyCustomMessage& message) {testCounter += 2 * message.payload_;})); + + testCounter = 0; + observable.EmitMessage(MyObservable::MyCustomMessage(12)); + ASSERT_EQ(24, testCounter); + + // delete the observer and check that the callback is not called anymore + delete observer; -// { -// // 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<MyObservable::MyCustomMessage>(*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
--- a/UnitTestsSources/TestMessageBroker2.cpp Fri Nov 23 16:06:23 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-2018 Osimis S.A., Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU Affero General Public License - * as published by the Free Software Foundation, either version 3 of - * the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - **/ - - -#include "gtest/gtest.h" - -#include "Framework/Messages/MessageBroker.h" -#include "Framework/Messages/Promise.h" -#include "Framework/Messages/IObservable.h" -#include "Framework/Messages/IObserver.h" -#include "Framework/Messages/MessageForwarder.h" - - -int testCounter = 0; -namespace { - - using namespace OrthancStone; - - - enum CustomMessageType - { - CustomMessageType_First = MessageType_CustomMessage + 1, - - CustomMessageType_Completed, - CustomMessageType_Increment - }; - - - class MyObservable : public IObservable - { - public: - struct MyCustomMessage: public BaseMessage<CustomMessageType_Completed> - { - int payload_; - - MyCustomMessage(int payload) - : BaseMessage(), - payload_(payload) - {} - }; - - MyObservable(MessageBroker& broker) - : IObservable(broker) - {} - - }; - - class MyObserver : public IObserver - { - public: - MyObserver(MessageBroker& broker) - : IObserver(broker) - {} - - void HandleCompletedMessage(const MyObservable::MyCustomMessage& message) - { - testCounter += message.payload_; - } - - }; - - - class MyIntermediate : public IObserver, public IObservable - { - IObservable& observedObject_; - public: - MyIntermediate(MessageBroker& broker, IObservable& observedObject) - : IObserver(broker), - IObservable(broker), - observedObject_(observedObject) - { - observedObject_.RegisterObserverCallback(new MessageForwarder<MyObservable::MyCustomMessage>(broker, *this)); - } - }; - - - class MyPromiseSource : public IObservable - { - Promise* currentPromise_; - public: - struct MyPromiseMessage: public BaseMessage<MessageType_Test1> - { - int increment; - - MyPromiseMessage(int increment) - : BaseMessage(), - increment(increment) - {} - }; - - MyPromiseSource(MessageBroker& broker) - : IObservable(broker), - currentPromise_(NULL) - {} - - Promise& StartSomethingAsync() - { - currentPromise_ = new Promise(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<MyObserver, MyObservable::MyCustomMessage>(observer, &MyObserver::HandleCompletedMessage)); - - testCounter = 0; - observable.EmitMessage(MyObservable::MyCustomMessage(12)); - ASSERT_EQ(12, testCounter); - - // the connection is permanent; if we emit the same message again, the observer will be notified again - testCounter = 0; - observable.EmitMessage(MyObservable::MyCustomMessage(20)); - ASSERT_EQ(20, testCounter); -} - -TEST(MessageBroker2, TestMessageForwarderSimpleUseCase) -{ - MessageBroker broker; - MyObservable observable(broker); - MyIntermediate intermediate(broker, observable); - MyObserver observer(broker); - - // let the observer observers the intermediate that is actually forwarding the messages from the observable - intermediate.RegisterObserverCallback(new Callable<MyObserver, MyObservable::MyCustomMessage>(observer, &MyObserver::HandleCompletedMessage)); - - testCounter = 0; - observable.EmitMessage(MyObservable::MyCustomMessage(12)); - ASSERT_EQ(12, testCounter); - - // the connection is permanent; if we emit the same message again, the observer will be notified again - testCounter = 0; - observable.EmitMessage(MyObservable::MyCustomMessage(20)); - ASSERT_EQ(20, testCounter); -} - -TEST(MessageBroker2, TestPermanentConnectionDeleteObserver) -{ - MessageBroker broker; - MyObservable observable(broker); - MyObserver* observer = new MyObserver(broker); - - // create a permanent connection between an observable and an observer - observable.RegisterObserverCallback(new Callable<MyObserver, MyObservable::MyCustomMessage>(*observer, &MyObserver::HandleCompletedMessage)); - - testCounter = 0; - observable.EmitMessage(MyObservable::MyCustomMessage(12)); - ASSERT_EQ(12, testCounter); - - // delete the observer and check that the callback is not called anymore - delete observer; - - // the connection is permanent; if we emit the same message again, the observer will be notified again - testCounter = 0; - observable.EmitMessage(MyObservable::MyCustomMessage(20)); - ASSERT_EQ(0, testCounter); -} - -TEST(MessageBroker2, TestMessageForwarderDeleteIntermediate) -{ - MessageBroker broker; - MyObservable observable(broker); - MyIntermediate* intermediate = new MyIntermediate(broker, observable); - MyObserver observer(broker); - - // let the observer observers the intermediate that is actually forwarding the messages from the observable - intermediate->RegisterObserverCallback(new Callable<MyObserver, MyObservable::MyCustomMessage>(observer, &MyObserver::HandleCompletedMessage)); - - testCounter = 0; - observable.EmitMessage(MyObservable::MyCustomMessage(12)); - ASSERT_EQ(12, testCounter); - - delete intermediate; - - observable.EmitMessage(MyObservable::MyCustomMessage(20)); - ASSERT_EQ(12, testCounter); -} - -TEST(MessageBroker2, TestCustomMessage) -{ - MessageBroker broker; - MyObservable observable(broker); - MyIntermediate intermediate(broker, observable); - MyObserver observer(broker); - - // let the observer observers the intermediate that is actually forwarding the messages from the observable - intermediate.RegisterObserverCallback(new Callable<MyObserver, MyObservable::MyCustomMessage>(observer, &MyObserver::HandleCompletedMessage)); - - testCounter = 0; - observable.EmitMessage(MyObservable::MyCustomMessage(12)); - ASSERT_EQ(12, testCounter); - - // the connection is permanent; if we emit the same message again, the observer will be notified again - testCounter = 0; - observable.EmitMessage(MyObservable::MyCustomMessage(20)); - ASSERT_EQ(20, testCounter); -} - - -TEST(MessageBroker2, TestPromiseSuccessFailure) -{ - MessageBroker broker; - MyPromiseSource source(broker); - MyPromiseTarget target(broker); - - // test a successful promise - source.StartSomethingAsync() - .Then(new Callable<MyPromiseTarget, MyPromiseSource::MyPromiseMessage>(target, &MyPromiseTarget::IncrementCounter)) - .Else(new Callable<MyPromiseTarget, MyPromiseSource::MyPromiseMessage>(target, &MyPromiseTarget::DecrementCounter)); - - testCounter = 0; - source.CompleteSomethingAsyncWithSuccess(10); - ASSERT_EQ(10, testCounter); - - // test a failing promise - source.StartSomethingAsync() - .Then(new Callable<MyPromiseTarget, MyPromiseSource::MyPromiseMessage>(target, &MyPromiseTarget::IncrementCounter)) - .Else(new Callable<MyPromiseTarget, MyPromiseSource::MyPromiseMessage>(target, &MyPromiseTarget::DecrementCounter)); - - testCounter = 0; - source.CompleteSomethingAsyncWithFailure(15); - ASSERT_EQ(-15, testCounter); -} - -TEST(MessageBroker2, TestPromiseDeleteTarget) -{ - MessageBroker broker; - MyPromiseSource source(broker); - MyPromiseTarget* target = new MyPromiseTarget(broker); - - // create the promise - source.StartSomethingAsync() - .Then(new Callable<MyPromiseTarget, MyPromiseSource::MyPromiseMessage>(*target, &MyPromiseTarget::IncrementCounter)) - .Else(new Callable<MyPromiseTarget, MyPromiseSource::MyPromiseMessage>(*target, &MyPromiseTarget::DecrementCounter)); - - // delete the promise target - delete target; - - // trigger the promise, make sure it does not throw and does not call the callback - testCounter = 0; - source.CompleteSomethingAsyncWithSuccess(10); - ASSERT_EQ(0, testCounter); - - // test a failing promise - source.StartSomethingAsync() - .Then(new Callable<MyPromiseTarget, MyPromiseSource::MyPromiseMessage>(*target, &MyPromiseTarget::IncrementCounter)) - .Else(new Callable<MyPromiseTarget, MyPromiseSource::MyPromiseMessage>(*target, &MyPromiseTarget::DecrementCounter)); - - testCounter = 0; - source.CompleteSomethingAsyncWithFailure(15); - ASSERT_EQ(0, testCounter); -} - -#if __cplusplus >= 201103L - -#include <functional> - -namespace OrthancStone { - - template <typename TMessage> - class LambdaCallable : public MessageHandler<TMessage> - { - private: - - IObserver& observer_; - std::function<void (const TMessage&)> lambda_; - - public: - LambdaCallable(IObserver& observer, - std::function<void (const TMessage&)> lambdaFunction) : - observer_(observer), - lambda_(lambdaFunction) - { - } - - virtual void Apply(const IMessage& message) - { - lambda_(dynamic_cast<const TMessage&>(message)); - } - - virtual MessageType GetMessageType() const - { - return static_cast<MessageType>(TMessage::Type); - } - - virtual IObserver* GetObserver() const - { - return &observer_; - } - }; - - -} - -TEST(MessageBroker2, TestLambdaSimpleUseCase) -{ - MessageBroker broker; - MyObservable observable(broker); - MyObserver* observer = new MyObserver(broker); - - // create a permanent connection between an observable and an observer - observable.RegisterObserverCallback(new LambdaCallable<MyObservable::MyCustomMessage>(*observer, [&](const MyObservable::MyCustomMessage& message) {testCounter += 2 * message.payload_;})); - - testCounter = 0; - observable.EmitMessage(MyObservable::MyCustomMessage(12)); - ASSERT_EQ(24, testCounter); - - // delete the observer and check that the callback is not called anymore - delete observer; - - // the connection is permanent; if we emit the same message again, the observer will be notified again - testCounter = 0; - observable.EmitMessage(MyObservable::MyCustomMessage(20)); - ASSERT_EQ(0, testCounter); -} - -namespace { - class MyObserverWithLambda : public IObserver { - private: - int multiplier_; // this is a private variable we want to access in a lambda - - public: - MyObserverWithLambda(MessageBroker& broker, int multiplier, MyObservable& observable) - : IObserver(broker), - multiplier_(multiplier) - { - // register a callable to a lambda that access private members - observable.RegisterObserverCallback(new LambdaCallable<MyObservable::MyCustomMessage>(*this, [this](const MyObservable::MyCustomMessage& message) { - testCounter += multiplier_ * message.payload_; - })); - - } - }; -} - -TEST(MessageBroker2, TestLambdaCaptureThisAndAccessPrivateMembers) -{ - MessageBroker broker; - MyObservable observable(broker); - MyObserverWithLambda* observer = new MyObserverWithLambda(broker, 3, observable); - - testCounter = 0; - observable.EmitMessage(MyObservable::MyCustomMessage(12)); - ASSERT_EQ(36, testCounter); - - // delete the observer and check that the callback is not called anymore - delete observer; - - // the connection is permanent; if we emit the same message again, the observer will be notified again - testCounter = 0; - observable.EmitMessage(MyObservable::MyCustomMessage(20)); - ASSERT_EQ(0, testCounter); -} - -#endif // C++ 11
--- a/UnitTestsSources/TestMessageBroker2_connect_ok.cpp Fri Nov 23 16:06:23 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-2018 Osimis S.A., Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU Affero General Public License - * as published by the Free Software Foundation, either version 3 of - * the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - **/ - - -#include "gtest/gtest.h" - -#include <boost/noncopyable.hpp> -#include <boost/function.hpp> -#include <boost/bind.hpp> - -#include <string> -#include <map> -#include <set> - -int testCounter = 0; -namespace { - - enum MessageType - { - // used in unit tests only - MessageType_Test1, - MessageType_Test2, - - MessageType_LastGenericStoneMessage - }; - - struct IMessage : public boost::noncopyable - { - MessageType messageType_; - public: - IMessage(const MessageType& messageType) - : messageType_(messageType) - {} - virtual ~IMessage() {} - - MessageType GetType() const {return messageType_;} - }; - - - class IObserver; - class IObservable; - - /* - * This is a central message broker. It keeps track of all observers and knows - * when an observer is deleted. - * This way, it can prevent an observable to send a message to a dead observer. - */ - class MessageBroker : public boost::noncopyable - { - - std::set<IObserver*> activeObservers_; // the list of observers that are currently alive (that have not been deleted) - - public: - - void Register(IObserver& observer) - { - activeObservers_.insert(&observer); - } - - void Unregister(IObserver& observer) - { - activeObservers_.erase(&observer); - } - - void EmitMessage(IObservable& from, std::set<IObserver*> observers, const IMessage& message); - }; - - - class IObserver : public boost::noncopyable - { - protected: - MessageBroker& broker_; - - public: - IObserver(MessageBroker& broker) - : broker_(broker) - { - broker_.Register(*this); - } - - virtual ~IObserver() - { - broker_.Unregister(*this); - } - - void HandleMessage_(IObservable &from, const IMessage &message) - { - - HandleMessage(from, message); - } - - virtual void HandleMessage(IObservable& from, const IMessage& message) = 0; - - - protected: - - - }; - -// struct ICallableObserver -// { -// IObserver* observer; -// }; - -// typedef void (IObserver::*ObserverSingleMesssageHandler)(IObservable& from, const IMessage& message); - -// template <typename TObserver> -// struct CallableObserver : public ICallableObserver -// { -// void (TObserver::*ptrToMemberHandler)(IObservable& from, const IMessage& message); -// }; - - struct CallableObserver - { - IObserver* observer; - boost::function<void (IObservable& from, const IMessage& message)> f; - }; - - class IObservable : public boost::noncopyable - { - protected: - MessageBroker& broker_; - - std::set<IObserver*> observers_; - - std::map<MessageType, std::set<CallableObserver*> > callables_; - public: - - IObservable(MessageBroker& broker) - : broker_(broker) - { - } - virtual ~IObservable() - { - } - - void EmitMessage(const IMessage& message) - { - //broker_.EmitMessage(*this, observers_, message); - - // TODO check if observer is still alive and call ! - CallableObserver* callable = *(callables_[message.GetType()].begin()); - callable->f(*this, message); - } - - void RegisterObserver(IObserver& observer) - { - observers_.insert(&observer); - } - - void UnregisterObserver(IObserver& observer) - { - observers_.erase(&observer); - } - - - //template<typename TObserver> void Connect(MessageType messageType, IObserver& observer, void (TObserver::*ptrToMemberHandler)(IObservable& from, const IMessage& message)) - void Connect(MessageType messageType, IObserver& observer, boost::function<void (IObservable& from, const IMessage& message)> f) - { - callables_[messageType] = std::set<CallableObserver*>(); - CallableObserver* callable = new CallableObserver(); - callable->observer = &observer; - callable->f = f; - callables_[messageType].insert(callable); - } - }; - - - class MyObservable : public IObservable - { - public: - MyObservable(MessageBroker& broker) - : IObservable(broker) - {} - }; - - class MyObserver : public IObserver - { - public: - MyObserver(MessageBroker& broker) - : IObserver(broker) - {} - virtual void HandleMessage(IObservable& from, const IMessage& message) {} - void HandleSpecificMessage(IObservable& from, const IMessage& message) - { - testCounter++; - } - - }; - -} - -//#define STONE_CONNECT(observabe, messageType, observerPtr, observerMemberFnPtr) - -TEST(MessageBroker2, Test1) -{ - MessageBroker broker; - MyObservable observable(broker); - MyObserver observer(broker); - - - observable.Connect(MessageType_Test1, observer, boost::bind(&MyObserver::HandleSpecificMessage, &observer, _1, _2)); - //STONE_CONNECT(observable, MessageType_Test1, observer, &MyObserver::HandleSpecificMessage) - observable.EmitMessage(IMessage(MessageType_Test1)); - - ASSERT_EQ(1, testCounter); -} - -
--- a/UnitTestsSources/TestMessageBroker2_promise_and_connect_ok.cpp Fri Nov 23 16:06:23 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-2018 Osimis S.A., Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU Affero General Public License - * as published by the Free Software Foundation, either version 3 of - * the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - **/ - - -#include "gtest/gtest.h" - -#include <boost/noncopyable.hpp> -#include <boost/function.hpp> -#include <boost/bind.hpp> - -#include <string> -#include <map> -#include <set> - -int testCounter = 0; -namespace { - - enum MessageType - { - MessageType_Test1, - MessageType_Test2, - - MessageType_CustomMessage, - MessageType_LastGenericStoneMessage - }; - - struct IMessage : public boost::noncopyable - { - MessageType messageType_; - public: - IMessage(const MessageType& messageType) - : messageType_(messageType) - {} - virtual ~IMessage() {} - - virtual int GetType() const {return messageType_;} - }; - - - struct ICustomMessage : public IMessage - { - int customMessageType_; - public: - ICustomMessage(int customMessageType) - : IMessage(MessageType_CustomMessage), - customMessageType_(customMessageType) - {} - virtual ~ICustomMessage() {} - - virtual int GetType() const {return customMessageType_;} - }; - - - class IObserver; - class IObservable; - class IPromiseTarget; - class IPromiseSource; - class Promise; - - /* - * This is a central message broker. It keeps track of all observers and knows - * when an observer is deleted. - * This way, it can prevent an observable to send a message to a delete observer. - * It does the same book-keeping for the IPromiseTarget and IPromiseSource - */ - class MessageBroker : public boost::noncopyable - { - - std::set<IObserver*> activeObservers_; // the list of observers that are currently alive (that have not been deleted) - std::set<IPromiseTarget*> activePromiseTargets_; - std::set<IPromiseSource*> activePromiseSources_; - - public: - - void Register(IObserver& observer) - { - activeObservers_.insert(&observer); - } - - void Unregister(IObserver& observer) - { - activeObservers_.erase(&observer); - } - - void Register(IPromiseTarget& target) - { - activePromiseTargets_.insert(&target); - } - - void Unregister(IPromiseTarget& target) - { - activePromiseTargets_.erase(&target); - } - - void Register(IPromiseSource& source) - { - activePromiseSources_.insert(&source); - } - - void Unregister(IPromiseSource& source) - { - activePromiseSources_.erase(&source); - } - - void EmitMessage(IObservable& from, std::set<IObserver*> observers, const IMessage& message); - - bool IsActive(IPromiseTarget* target) - { - return activePromiseTargets_.find(target) != activePromiseTargets_.end(); - } - - bool IsActive(IPromiseSource* source) - { - return activePromiseSources_.find(source) != activePromiseSources_.end(); - } - - bool IsActive(IObserver* observer) - { - return activeObservers_.find(observer) != activeObservers_.end(); - } - }; - - struct IPromiseArgs - { -public: - virtual ~IPromiseArgs() {} - }; - - class EmptyPromiseArguments : public IPromiseArgs - { - - }; - - class Promise : public boost::noncopyable - { - protected: - MessageBroker& broker_; - - IPromiseTarget* successTarget_; - boost::function<void (const IPromiseArgs& message)> successCallable_; - - IPromiseTarget* failureTarget_; - boost::function<void (const IPromiseArgs& message)> failureCallable_; - - public: - Promise(MessageBroker& broker) - : broker_(broker), - successTarget_(NULL), - failureTarget_(NULL) - { - } - - void Success(const IPromiseArgs& message) - { - // check the target is still alive in the broker - if (broker_.IsActive(successTarget_)) - { - successCallable_(message); - } - } - - void Failure(const IPromiseArgs& message) - { - // check the target is still alive in the broker - if (broker_.IsActive(failureTarget_)) - { - failureCallable_(message); - } - } - - Promise& Then(IPromiseTarget* target, boost::function<void (const IPromiseArgs& message)> f) - { - if (successTarget_ != NULL) - { - // TODO: throw throw new "Promise may only have a single success target" - } - successTarget_ = target; - successCallable_ = f; - return *this; - } - - Promise& Else(IPromiseTarget* target, boost::function<void (const IPromiseArgs& message)> f) - { - if (failureTarget_ != NULL) - { - // TODO: throw throw new "Promise may only have a single failure target" - } - failureTarget_ = target; - failureCallable_ = f; - return *this; - } - - }; - - class IObserver : public boost::noncopyable - { - protected: - MessageBroker& broker_; - - public: - IObserver(MessageBroker& broker) - : broker_(broker) - { - broker_.Register(*this); - } - - virtual ~IObserver() - { - broker_.Unregister(*this); - } - - }; - - class IPromiseTarget : public boost::noncopyable - { - protected: - MessageBroker& broker_; - - public: - IPromiseTarget(MessageBroker& broker) - : broker_(broker) - { - broker_.Register(*this); - } - - virtual ~IPromiseTarget() - { - broker_.Unregister(*this); - } - }; - - class IPromiseSource : public boost::noncopyable - { - protected: - MessageBroker& broker_; - - public: - IPromiseSource(MessageBroker& broker) - : broker_(broker) - { - broker_.Register(*this); - } - - virtual ~IPromiseSource() - { - broker_.Unregister(*this); - } - }; - - - struct CallableObserver - { - IObserver* observer; - boost::function<void (IObservable& from, const IMessage& message)> f; - }; - - class IObservable : public boost::noncopyable - { - protected: - MessageBroker& broker_; - - std::set<IObserver*> observers_; - - std::map<int, std::set<CallableObserver*> > callables_; - public: - - IObservable(MessageBroker& broker) - : broker_(broker) - { - } - virtual ~IObservable() - { - } - - void EmitMessage(const IMessage& message) - { - //broker_.EmitMessage(*this, observers_, message); - int messageType = message.GetType(); - if (callables_.find(messageType) != callables_.end()) - { - for (std::set<CallableObserver*>::iterator observer = callables_[messageType].begin(); observer != callables_[messageType].end(); observer++) - { - CallableObserver* callable = *observer; - if (broker_.IsActive(callable->observer)) - { - callable->f(*this, message); - } - } - } - - } - - void RegisterObserver(IObserver& observer) - { - observers_.insert(&observer); - } - - void UnregisterObserver(IObserver& observer) - { - observers_.erase(&observer); - } - - //template<typename TObserver> void Connect(MessageType messageType, IObserver& observer, void (TObserver::*ptrToMemberHandler)(IObservable& from, const IMessage& message)) - void Connect(int messageType, IObserver& observer, boost::function<void (IObservable& from, const IMessage& message)> f) - { - callables_[messageType] = std::set<CallableObserver*>(); - CallableObserver* callable = new CallableObserver(); - callable->observer = &observer; - callable->f = f; - callables_[messageType].insert(callable); - } - }; - - - enum CustomMessageType - { - CustomMessageType_First = MessageType_LastGenericStoneMessage + 1, - - CustomMessageType_Completed - }; - - class MyObservable : public IObservable - { - public: - struct MyCustomMessage: public ICustomMessage - { - int payload_; - MyCustomMessage(int payload) - : ICustomMessage(CustomMessageType_Completed), - payload_(payload) - {} - }; - - MyObservable(MessageBroker& broker) - : IObservable(broker) - {} - - }; - - class MyObserver : public IObserver - { - public: - MyObserver(MessageBroker& broker) - : IObserver(broker) - {} - void HandleCompletedMessage(IObservable& from, const IMessage& message) - { - const MyObservable::MyCustomMessage& msg = dynamic_cast<const MyObservable::MyCustomMessage&>(message); - testCounter += msg.payload_; - } - - }; - - - class MyPromiseSource : public IPromiseSource - { - Promise* currentPromise_; - public: - struct MyPromiseArgs : public IPromiseArgs - { - int increment; - }; - - MyPromiseSource(MessageBroker& broker) - : IPromiseSource(broker), - currentPromise_(NULL) - {} - - Promise& StartSomethingAsync() - { - currentPromise_ = new Promise(broker_); - return *currentPromise_; - } - - void CompleteSomethingAsyncWithSuccess() - { - currentPromise_->Success(EmptyPromiseArguments()); - delete currentPromise_; - } - - void CompleteSomethingAsyncWithFailure() - { - currentPromise_->Failure(EmptyPromiseArguments()); - delete currentPromise_; - } - }; - - - class MyPromiseTarget : public IPromiseTarget - { - public: - MyPromiseTarget(MessageBroker& broker) - : IPromiseTarget(broker) - {} - - void IncrementCounter(const IPromiseArgs& args) - { - testCounter++; - } - - void DecrementCounter(const IPromiseArgs& args) - { - testCounter--; - } - }; -} - -#define CONNECT_MESSAGES(observablePtr, messageType, observerPtr, observerFnPtr) (observablePtr)->Connect(messageType, *(observerPtr), boost::bind(observerFnPtr, observerPtr, _1, _2)) -#define PTHEN(targetPtr, targetFnPtr) Then(targetPtr, boost::bind(targetFnPtr, targetPtr, _1)) -#define PELSE(targetPtr, targetFnPtr) Else(targetPtr, boost::bind(targetFnPtr, targetPtr, _1)) - - -TEST(MessageBroker2, TestPermanentConnectionSimpleUseCase) -{ - MessageBroker broker; - MyObservable observable(broker); - MyObserver observer(broker); - - // create a permanent connection between an observable and an observer - CONNECT_MESSAGES(&observable, CustomMessageType_Completed, &observer, &MyObserver::HandleCompletedMessage); - - testCounter = 0; - observable.EmitMessage(MyObservable::MyCustomMessage(12)); - ASSERT_EQ(12, testCounter); - - // the connection is permanent; if we emit the same message again, the observer will be notified again - testCounter = 0; - observable.EmitMessage(MyObservable::MyCustomMessage(20)); - ASSERT_EQ(20, testCounter); -} - -TEST(MessageBroker2, TestPermanentConnectionDeleteObserver) -{ - MessageBroker broker; - MyObservable observable(broker); - MyObserver* observer = new MyObserver(broker); - - // create a permanent connection between an observable and an observer - CONNECT_MESSAGES(&observable, CustomMessageType_Completed, observer, &MyObserver::HandleCompletedMessage); - - testCounter = 0; - observable.EmitMessage(MyObservable::MyCustomMessage(12)); - ASSERT_EQ(12, testCounter); - - // delete the observer and check that the callback is not called anymore - delete observer; - - // the connection is permanent; if we emit the same message again, the observer will be notified again - testCounter = 0; - observable.EmitMessage(MyObservable::MyCustomMessage(20)); - ASSERT_EQ(0, testCounter); -} - - -TEST(MessageBroker2, TestPromiseSuccessFailure) -{ - MessageBroker broker; - MyPromiseSource source(broker); - MyPromiseTarget target(broker); - - // test a successful promise - source.StartSomethingAsync() - .PTHEN(&target, &MyPromiseTarget::IncrementCounter) - .PELSE(&target, &MyPromiseTarget::DecrementCounter); - - testCounter = 0; - source.CompleteSomethingAsyncWithSuccess(); - ASSERT_EQ(1, testCounter); - - // test a failing promise - source.StartSomethingAsync() - .PTHEN(&target, &MyPromiseTarget::IncrementCounter) - .PELSE(&target, &MyPromiseTarget::DecrementCounter); - - testCounter = 0; - source.CompleteSomethingAsyncWithFailure(); - ASSERT_EQ(-1, testCounter); -} - -//TEST(MessageBroker2, TestPromiseDeleteTarget) -//{ -// MessageBroker broker; -// MyPromiseSource source(broker); -// MyPromiseTarget target(broker); - -// // test a successful promise -// source.StartSomethingAsync() -// .PTHEN(&target, &MyPromiseTarget::IncrementCounter) -// .PELSE(&target, &MyPromiseTarget::DecrementCounter); - -// testCounter = 0; -// source.CompleteSomethingAsyncWithSuccess(); -// ASSERT_EQ(1, testCounter); - -// // test a failing promise -// source.StartSomethingAsync() -// .PTHEN(&target, &MyPromiseTarget::IncrementCounter) -// .PELSE(&target, &MyPromiseTarget::DecrementCounter); - -// testCounter = 0; -// source.CompleteSomethingAsyncWithFailure(); -// ASSERT_EQ(-1, testCounter); -//}