# HG changeset patch # User Benjamin Golinvaux # Date 1559139304 -7200 # Node ID 28f99af358fac42f0f97ed6d6d393f1f4cf2f4ed # Parent 2fd96a637a596950eb4d3034619c74665205aee5# Parent 5c7b08bf84af23123bd90b19e132888afc0442cf Merge + FusionMprSdl diff -r 2fd96a637a59 -r 28f99af358fa Framework/Loaders/DicomStructureSetLoader.cpp --- a/Framework/Loaders/DicomStructureSetLoader.cpp Wed May 29 13:44:55 2019 +0200 +++ b/Framework/Loaders/DicomStructureSetLoader.cpp Wed May 29 16:15:04 2019 +0200 @@ -128,7 +128,7 @@ std::set instances; loader.content_->GetReferencedInstances(instances); - loader.countReferencedInstances_ = instances.size(); + loader.countReferencedInstances_ = static_cast(instances.size()); for (std::set::const_iterator it = instances.begin(); it != instances.end(); ++it) diff -r 2fd96a637a59 -r 28f99af358fa Framework/Loaders/OrthancSeriesVolumeProgressiveLoader.cpp --- a/Framework/Loaders/OrthancSeriesVolumeProgressiveLoader.cpp Wed May 29 13:44:55 2019 +0200 +++ b/Framework/Loaders/OrthancSeriesVolumeProgressiveLoader.cpp Wed May 29 16:15:04 2019 +0200 @@ -42,20 +42,22 @@ DicomVolumeImageMPRSlicer::Slice(*that.volume_, plane), that_(that) { - if (GetProjection() == VolumeProjection_Axial) + if (IsValid()) { - // For coronal and sagittal projections, we take the global - // revision of the volume because even if a single slice changes, - // this means the projection will yield a different result --> - // we must increase the revision as soon as any slice changes - SetRevision(that_.seriesGeometry_.GetSliceRevision(GetSliceIndex())); - } + if (GetProjection() == VolumeProjection_Axial) + { + // For coronal and sagittal projections, we take the global + // revision of the volume because even if a single slice changes, + // this means the projection will yield a different result --> + // we must increase the revision as soon as any slice changes + SetRevision(that_.seriesGeometry_.GetSliceRevision(GetSliceIndex())); + } - if (that_.strategy_.get() != NULL && - IsValid() && - GetProjection() == VolumeProjection_Axial) - { - that_.strategy_->SetCurrent(GetSliceIndex()); + if (that_.strategy_.get() != NULL && + GetProjection() == VolumeProjection_Axial) + { + that_.strategy_->SetCurrent(GetSliceIndex()); + } } } }; @@ -264,7 +266,7 @@ std::auto_ptr tmp(new GetOrthancImageCommand); tmp->SetHttpHeader("Accept-Encoding", "gzip"); tmp->SetHttpHeader("Accept", std::string(Orthanc::EnumerationToString(Orthanc::MimeType_Pam))); - tmp->SetInstanceUri(instance, slice.GetExpectedPixelFormat()); + tmp->SetInstanceUri(instance, slice.GetExpectedPixelFormat()); tmp->SetExpectedPixelFormat(slice.GetExpectedPixelFormat()); command.reset(tmp.release()); } diff -r 2fd96a637a59 -r 28f99af358fa Framework/Messages/MessageBroker.h --- a/Framework/Messages/MessageBroker.h Wed May 29 13:44:55 2019 +0200 +++ b/Framework/Messages/MessageBroker.h Wed May 29 16:15:04 2019 +0200 @@ -18,8 +18,9 @@ * along with this program. If not, see . **/ +#pragma once -#pragma once +#include "../StoneException.h" #include "boost/noncopyable.hpp" @@ -40,6 +41,13 @@ std::set activeObservers_; // the list of observers that are currently alive (that have not been deleted) public: + MessageBroker() + { + static bool created = false; + ORTHANC_ASSERT(!created, "One broker to rule them all!"); + created = true; + } + void Register(const IObserver& observer) { activeObservers_.insert(&observer); diff -r 2fd96a637a59 -r 28f99af358fa Framework/Oracle/OrthancRestApiCommand.h --- a/Framework/Oracle/OrthancRestApiCommand.h Wed May 29 13:44:55 2019 +0200 +++ b/Framework/Oracle/OrthancRestApiCommand.h Wed May 29 16:15:04 2019 +0200 @@ -95,6 +95,11 @@ void SetBody(const Json::Value& json); + void SwapBody(std::string& body) + { + body_.swap(body); + } + void SetHttpHeader(const std::string& key, const std::string& value) { diff -r 2fd96a637a59 -r 28f99af358fa Framework/Oracle/ThreadedOracle.cpp --- a/Framework/Oracle/ThreadedOracle.cpp Wed May 29 13:44:55 2019 +0200 +++ b/Framework/Oracle/ThreadedOracle.cpp Wed May 29 16:15:04 2019 +0200 @@ -248,7 +248,7 @@ client.SetTimeout(command.GetTimeout()); CopyHttpHeaders(client, command.GetHttpHeaders()); - + std::string answer; Orthanc::HttpClient::HttpHeaders answerHeaders; client.ApplyAndThrowException(answer, answerHeaders); diff -r 2fd96a637a59 -r 28f99af358fa Framework/Oracle/ThreadedOracle.h --- a/Framework/Oracle/ThreadedOracle.h Wed May 29 13:44:55 2019 +0200 +++ b/Framework/Oracle/ThreadedOracle.h Wed May 29 16:15:04 2019 +0200 @@ -74,6 +74,7 @@ virtual ~ThreadedOracle(); + // The reference is not stored. void SetOrthancParameters(const Orthanc::WebServiceParameters& orthanc); void SetThreadsCount(unsigned int count); diff -r 2fd96a637a59 -r 28f99af358fa Samples/Sdl/FusionMprSdl.cpp --- a/Samples/Sdl/FusionMprSdl.cpp Wed May 29 13:44:55 2019 +0200 +++ b/Samples/Sdl/FusionMprSdl.cpp Wed May 29 16:15:04 2019 +0200 @@ -24,8 +24,6 @@ #include "../../Framework/StoneInitialization.h" -#include "../../Framework/Messages/IMessageEmitter.h" - #include "../../Framework/Scene2D/CairoCompositor.h" #include "../../Framework/Scene2D/ColorTextureSceneLayer.h" #include "../../Framework/Scene2D/OpenGLCompositor.h" @@ -50,7 +48,6 @@ #include #include #include -#include #include #include "../../Framework/Oracle/GetOrthancWebViewerJpegCommand.h" @@ -65,76 +62,6 @@ namespace OrthancStone { - - class NativeApplicationContext : public IMessageEmitter - { - private: - boost::shared_mutex mutex_; - MessageBroker broker_; - IObservable oracleObservable_; - - public: - NativeApplicationContext() : - oracleObservable_(broker_) - { - } - - - virtual void EmitMessage(const IObserver& observer, - const IMessage& message) ORTHANC_OVERRIDE - { - try - { - boost::unique_lock lock(mutex_); - oracleObservable_.EmitMessage(observer, message); - } - catch (Orthanc::OrthancException& e) - { - LOG(ERROR) << "Exception while emitting a message: " << e.What(); - } - } - - - class ReaderLock : public boost::noncopyable - { - private: - NativeApplicationContext& that_; - boost::shared_lock lock_; - - public: - ReaderLock(NativeApplicationContext& that) : - that_(that), - lock_(that.mutex_) - { - } - }; - - - class WriterLock : public boost::noncopyable - { - private: - NativeApplicationContext& that_; - boost::unique_lock lock_; - - public: - WriterLock(NativeApplicationContext& that) : - that_(that), - lock_(that.mutex_) - { - } - - MessageBroker& GetBroker() - { - return that_.broker_; - } - - IObservable& GetOracleObservable() - { - return that_.oracleObservable_; - } - }; - }; - const char* FusionMprMeasureToolToString(size_t i) { static const char* descs[] = { @@ -474,10 +401,31 @@ } - FusionMprSdlApp::FusionMprSdlApp(MessageBroker& broker) : IObserver(broker) + FusionMprSdlApp::FusionMprSdlApp(MessageBroker& broker) + : IObserver(broker) + , broker_(broker) + , oracleObservable_(broker) + , oracle_(*this) , currentTool_(FusionMprGuiTool_Rotate) { - controller_ = boost::shared_ptr(new ViewportController(broker)); + //oracleObservable.RegisterObserverCallback + //(new Callable + // (*this, &FusionMprSdlApp::Handle)); + + //oracleObservable.RegisterObserverCallback + //(new Callable + // (*this, &FusionMprSdlApp::Handle)); + + //oracleObservable.RegisterObserverCallback + //(new Callable + // (*this, &ToFusionMprSdlAppto::Handle)); + + oracleObservable_.RegisterObserverCallback + (new Callable + (*this, &FusionMprSdlApp::Handle)); + + controller_ = boost::shared_ptr( + new ViewportController(broker_)); controller_->RegisterObserverCallback( new Callable @@ -653,18 +601,11 @@ //////// from loader - - // from main - NativeApplicationContext context; - ThreadedOracle oracle(context); - - - { Orthanc::WebServiceParameters p; //p.SetUrl("http://localhost:8043/"); p.SetCredentials("orthanc", "orthanc"); - oracle.SetOrthancParameters(p); + oracle_.SetOrthancParameters(p); } //////// from Run @@ -678,38 +619,16 @@ boost::shared_ptr rtstructLoader; { - NativeApplicationContext::WriterLock lock(context); - //toto.reset(new Toto(oracle, lock.GetOracleObservable())); - oracle_ = &oracle; - IObservable* oracleObservable = &lock.GetOracleObservable(); - - - //oracleObservable->RegisterObserverCallback - //(new Callable - // (*this, &FusionMprSdlApp::Handle)); - - - //oracleObservable->RegisterObserverCallback - //(new Callable - // (*this, &FusionMprSdlApp::Handle)); - - //oracleObservable->RegisterObserverCallback - //(new Callable - // (*this, &ToFusionMprSdlAppto::Handle)); - - oracleObservable->RegisterObserverCallback - (new Callable - (*this, &FusionMprSdlApp::Handle)); - - - ctLoader.reset(new OrthancSeriesVolumeProgressiveLoader(ct, oracle, lock.GetOracleObservable())); - doseLoader.reset(new OrthancMultiframeVolumeLoader(dose, oracle, lock.GetOracleObservable())); - rtstructLoader.reset(new DicomStructureSetLoader(oracle, lock.GetOracleObservable())); + ctLoader.reset(new OrthancSeriesVolumeProgressiveLoader(ct, oracle_, oracleObservable_)); + doseLoader.reset(new OrthancMultiframeVolumeLoader(dose, oracle_, oracleObservable_)); + rtstructLoader.reset(new DicomStructureSetLoader(oracle_, oracleObservable_)); } - //toto->SetReferenceLoader(*ctLoader); - doseLoader->RegisterObserverCallback + //doseLoader->RegisterObserverCallback + //(new Callable + // (*this, &FusionMprSdlApp::Handle)); + ctLoader->RegisterObserverCallback (new Callable (*this, &FusionMprSdlApp::Handle)); @@ -725,10 +644,10 @@ this->SetStructureSet(2, rtstructLoader); -#if 0 +#if 1 // BGO data ctLoader->LoadSeries("a04ecf01-79b2fc33-58239f7e-ad9db983-28e81afa"); // CT - doseLoader->LoadInstance("830a69ff-8e4b5ee3-b7f966c8-bccc20fb-d322dceb"); // RT-DOSE + //doseLoader->LoadInstance("830a69ff-8e4b5ee3-b7f966c8-bccc20fb-d322dceb"); // RT-DOSE //rtstructLoader->LoadInstance("54460695-ba3885ee-ddf61ac0-f028e31d-a6e474d9"); // RT-STRUCT #else //ctLoader->LoadSeries("cb3ea4d1-d08f3856-ad7b6314-74d88d77-60b05618"); // CT @@ -741,7 +660,7 @@ rtstructLoader->LoadInstance("54460695-ba3885ee-ddf61ac0-f028e31d-a6e474d9"); // RT-STRUCT #endif - oracle.Start(); + oracle_.Start(); //// END from loader @@ -818,7 +737,7 @@ * as in (*). **/ - oracle.Stop(); + oracle_.Stop(); //// END from loader } @@ -861,7 +780,7 @@ try { - MessageBroker broker; + OrthancStone::MessageBroker broker; boost::shared_ptr app(new FusionMprSdlApp(broker)); g_app = app; app->PrepareScene(); diff -r 2fd96a637a59 -r 28f99af358fa Samples/Sdl/FusionMprSdl.h --- a/Samples/Sdl/FusionMprSdl.h Wed May 29 13:44:55 2019 +0200 +++ b/Samples/Sdl/FusionMprSdl.h Wed May 29 16:15:04 2019 +0200 @@ -19,12 +19,17 @@ **/ #include "../../Framework/Messages/IObserver.h" +#include "../../Framework/Messages/IMessageEmitter.h" +#include "../../Framework/Oracle/OracleCommandExceptionMessage.h" #include "../../Framework/Scene2DViewport/ViewportController.h" +#include "../../Framework/Volumes/DicomVolumeImage.h" +#include "../../Framework/Oracle/ThreadedOracle.h" #include +#include +#include + #include -#include "../../Framework/Volumes/DicomVolumeImage.h" -#include "../../Framework/Oracle/OracleCommandExceptionMessage.h" namespace OrthancStone { @@ -33,8 +38,11 @@ class ILayerStyleConfigurator; class DicomStructureSetLoader; class IOracle; + class ThreadedOracle; class VolumeSceneLayerSource; + class NativeFusionMprApplicationContext; + enum FusionMprGuiTool { FusionMprGuiTool_Rotate = 0, @@ -54,12 +62,18 @@ class Scene2D; + /** + This application subclasses IMessageEmitter to use a mutex before forwarding Oracle messages (that + can be sent from multiple threads) + */ class FusionMprSdlApp : public IObserver , public boost::enable_shared_from_this + , public IMessageEmitter { public: // 12 because. FusionMprSdlApp(MessageBroker& broker); + void PrepareScene(); void Run(); void SetInfoDisplayMessage(std::string key, std::string value); @@ -77,7 +91,31 @@ void OnSceneTransformChanged( const ViewportController::SceneTransformChanged& message); + + virtual void EmitMessage(const IObserver& observer, + const IMessage& message) ORTHANC_OVERRIDE + { + try + { + boost::unique_lock lock(mutex_); + oracleObservable_.EmitMessage(observer, message); + } + catch (Orthanc::OrthancException& e) + { + LOG(ERROR) << "Exception while emitting a message: " << e.What(); + throw; + } + } + private: +#if 1 + // if threaded (not wasm) + MessageBroker& broker_; + IObservable oracleObservable_; + ThreadedOracle oracle_; + boost::shared_mutex mutex_; // to serialize messages from the ThreadedOracle +#endif + void SelectNextTool(); /** @@ -132,7 +170,7 @@ private: CoordinateSystem3D plane_; - IOracle* oracle_ = nullptr; + boost::shared_ptr source1_, source2_, source3_; std::auto_ptr compositor_; diff -r 2fd96a637a59 -r 28f99af358fa Samples/Sdl/Loader.cpp --- a/Samples/Sdl/Loader.cpp Wed May 29 13:44:55 2019 +0200 +++ b/Samples/Sdl/Loader.cpp Wed May 29 16:15:04 2019 +0200 @@ -166,9 +166,9 @@ { printf("Geometry ready\n"); - //plane_ = message.GetOrigin().GetGeometry().GetSagittalGeometry(); + plane_ = message.GetOrigin().GetGeometry().GetSagittalGeometry(); //plane_ = message.GetOrigin().GetGeometry().GetAxialGeometry(); - plane_ = message.GetOrigin().GetGeometry().GetCoronalGeometry(); + //plane_ = message.GetOrigin().GetGeometry().GetCoronalGeometry(); plane_.SetOrigin(message.GetOrigin().GetGeometry().GetCoordinates(0.5f, 0.5f, 0.5f)); Refresh(); @@ -302,6 +302,8 @@ void Run(OrthancStone::NativeApplicationContext& context, OrthancStone::ThreadedOracle& oracle) { + // the oracle has been supplied with the context (as an IEmitter) upon + // creation boost::shared_ptr ct(new OrthancStone::DicomVolumeImage); boost::shared_ptr dose(new OrthancStone::DicomVolumeImage); @@ -314,6 +316,11 @@ { OrthancStone::NativeApplicationContext::WriterLock lock(context); toto.reset(new Toto(oracle, lock.GetOracleObservable())); + + // the oracle is used to schedule commands + // the oracleObservable is used by the loaders to: + // - request the broker (lifetime mgmt) + // - register the loader callbacks (called indirectly by the oracle) ctLoader.reset(new OrthancStone::OrthancSeriesVolumeProgressiveLoader(ct, oracle, lock.GetOracleObservable())); doseLoader.reset(new OrthancStone::OrthancMultiframeVolumeLoader(dose, oracle, lock.GetOracleObservable())); rtstructLoader.reset(new OrthancStone::DicomStructureSetLoader(oracle, lock.GetOracleObservable())); diff -r 2fd96a637a59 -r 28f99af358fa Samples/WebAssembly/BasicMPR.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Samples/WebAssembly/BasicMPR.cpp Wed May 29 16:15:04 2019 +0200 @@ -0,0 +1,880 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2019 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + + +#include +#include +#include + +#include "../../Framework/Loaders/OrthancSeriesVolumeProgressiveLoader.h" +#include "../../Framework/OpenGL/WebAssemblyOpenGLContext.h" +#include "../../Framework/Scene2D/GrayscaleStyleConfigurator.h" +#include "../../Framework/Scene2D/OpenGLCompositor.h" +#include "../../Framework/Scene2D/PanSceneTracker.h" +#include "../../Framework/Scene2D/RotateSceneTracker.h" +#include "../../Framework/Scene2D/ZoomSceneTracker.h" +#include "../../Framework/Scene2DViewport/IFlexiblePointerTracker.h" +#include "../../Framework/Scene2DViewport/ViewportController.h" +#include "../../Framework/StoneInitialization.h" +#include "../../Framework/Volumes/VolumeSceneLayerSource.h" + +#include +#include + + +static const unsigned int FONT_SIZE = 32; + + +namespace OrthancStone +{ + class WebAssemblyViewport : public boost::noncopyable + { + private: + // the construction order is important because compositor_ + // will hold a reference to the scene that belong to the + // controller_ object + OpenGL::WebAssemblyOpenGLContext context_; + boost::shared_ptr controller_; + OpenGLCompositor compositor_; + + void SetupEvents(const std::string& canvas); + + public: + WebAssemblyViewport(MessageBroker& broker, + const std::string& canvas) : + context_(canvas), + controller_(new ViewportController(broker)), + compositor_(context_, *controller_->GetScene()) + { + compositor_.SetFont(0, Orthanc::EmbeddedResources::UBUNTU_FONT, + FONT_SIZE, Orthanc::Encoding_Latin1); + SetupEvents(canvas); + } + + Scene2D& GetScene() + { + return *controller_->GetScene(); + } + + const boost::shared_ptr& GetController() + { + return controller_; + } + + void UpdateSize() + { + context_.UpdateSize(); + compositor_.UpdateSize(); + Refresh(); + } + + void Refresh() + { + compositor_.Refresh(); + } + + const std::string& GetCanvasIdentifier() const + { + return context_.GetCanvasIdentifier(); + } + + ScenePoint2D GetPixelCenterCoordinates(int x, int y) const + { + return compositor_.GetPixelCenterCoordinates(x, y); + } + + unsigned int GetCanvasWidth() const + { + return context_.GetCanvasWidth(); + } + + unsigned int GetCanvasHeight() const + { + return context_.GetCanvasHeight(); + } + }; + + class ActiveTracker : public boost::noncopyable + { + private: + boost::shared_ptr tracker_; + std::string canvasIdentifier_; + bool insideCanvas_; + + public: + ActiveTracker(const boost::shared_ptr& tracker, + const WebAssemblyViewport& viewport) : + tracker_(tracker), + canvasIdentifier_(viewport.GetCanvasIdentifier()), + insideCanvas_(true) + { + if (tracker_.get() == NULL) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); + } + } + + bool IsAlive() const + { + return tracker_->IsAlive(); + } + + void PointerMove(const PointerEvent& event) + { + tracker_->PointerMove(event); + } + + void PointerUp(const PointerEvent& event) + { + tracker_->PointerUp(event); + } + }; +} + +static OrthancStone::PointerEvent* ConvertMouseEvent( + const EmscriptenMouseEvent& source, + OrthancStone::WebAssemblyViewport& viewport) +{ + std::auto_ptr target( + new OrthancStone::PointerEvent); + + target->AddPosition(viewport.GetPixelCenterCoordinates( + source.targetX, source.targetY)); + target->SetAltModifier(source.altKey); + target->SetControlModifier(source.ctrlKey); + target->SetShiftModifier(source.shiftKey); + + return target.release(); +} + +std::auto_ptr tracker_; + +EM_BOOL OnMouseEvent(int eventType, + const EmscriptenMouseEvent *mouseEvent, + void *userData) +{ + if (mouseEvent != NULL && + userData != NULL) + { + OrthancStone::WebAssemblyViewport& viewport = + *reinterpret_cast(userData); + + switch (eventType) + { + case EMSCRIPTEN_EVENT_CLICK: + { + static unsigned int count = 0; + char buf[64]; + sprintf(buf, "click %d", count++); + + std::auto_ptr layer(new OrthancStone::TextSceneLayer); + layer->SetText(buf); + viewport.GetScene().SetLayer(100, layer.release()); + viewport.Refresh(); + break; + } + + case EMSCRIPTEN_EVENT_MOUSEDOWN: + { + boost::shared_ptr t; + + { + std::auto_ptr event( + ConvertMouseEvent(*mouseEvent, viewport)); + + switch (mouseEvent->button) + { + case 0: // Left button + emscripten_console_log("Creating RotateSceneTracker"); + t.reset(new OrthancStone::RotateSceneTracker( + viewport.GetController(), *event)); + break; + + case 1: // Middle button + emscripten_console_log("Creating PanSceneTracker"); + LOG(INFO) << "Creating PanSceneTracker" ; + t.reset(new OrthancStone::PanSceneTracker( + viewport.GetController(), *event)); + break; + + case 2: // Right button + emscripten_console_log("Creating ZoomSceneTracker"); + t.reset(new OrthancStone::ZoomSceneTracker( + viewport.GetController(), *event, viewport.GetCanvasWidth())); + break; + + default: + break; + } + } + + if (t.get() != NULL) + { + tracker_.reset( + new OrthancStone::ActiveTracker(t, viewport)); + viewport.Refresh(); + } + + break; + } + + case EMSCRIPTEN_EVENT_MOUSEMOVE: + if (tracker_.get() != NULL) + { + std::auto_ptr event( + ConvertMouseEvent(*mouseEvent, viewport)); + tracker_->PointerMove(*event); + viewport.Refresh(); + } + break; + + case EMSCRIPTEN_EVENT_MOUSEUP: + if (tracker_.get() != NULL) + { + std::auto_ptr event( + ConvertMouseEvent(*mouseEvent, viewport)); + tracker_->PointerUp(*event); + viewport.Refresh(); + if (!tracker_->IsAlive()) + tracker_.reset(); + } + break; + + default: + break; + } + } + + return true; +} + + +void OrthancStone::WebAssemblyViewport::SetupEvents(const std::string& canvas) +{ + emscripten_set_mousedown_callback(canvas.c_str(), this, false, OnMouseEvent); + emscripten_set_mousemove_callback(canvas.c_str(), this, false, OnMouseEvent); + emscripten_set_mouseup_callback(canvas.c_str(), this, false, OnMouseEvent); +} + + + + +namespace OrthancStone +{ + class VolumeSlicerViewport : public IObserver + { + private: + OrthancStone::WebAssemblyViewport viewport_; + std::auto_ptr source_; + VolumeProjection projection_; + std::vector planes_; + size_t currentPlane_; + + void Handle(const DicomVolumeImage::GeometryReadyMessage& message) + { + LOG(INFO) << "Geometry is available"; + + const VolumeImageGeometry& geometry = message.GetOrigin().GetGeometry(); + + const unsigned int depth = geometry.GetProjectionDepth(projection_); + currentPlane_ = depth / 2; + + planes_.resize(depth); + + for (unsigned int z = 0; z < depth; z++) + { + planes_[z] = geometry.GetProjectionSlice(projection_, z); + } + } + + public: + VolumeSlicerViewport(MessageBroker& broker, + const std::string& canvas, + VolumeProjection projection) : + IObserver(broker), + viewport_(broker, canvas), + projection_(projection), + currentPlane_(0) + { + } + + void UpdateSize() + { + viewport_.UpdateSize(); + } + + void SetSlicer(int layerDepth, + const boost::shared_ptr& slicer, + IObservable& loader, + ILayerStyleConfigurator* configurator) + { + if (source_.get() != NULL) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls, + "Only one slicer can be registered"); + } + + loader.RegisterObserverCallback( + new Callable + (*this, &VolumeSlicerViewport::Handle)); + + source_.reset(new VolumeSceneLayerSource(viewport_.GetScene(), layerDepth, slicer)); + + if (configurator != NULL) + { + source_->SetConfigurator(configurator); + } + } + + void Refresh() + { + if (source_.get() != NULL && + currentPlane_ < planes_.size()) + { + source_->Update(planes_[currentPlane_]); + } + } + }; + + + + + class WebAssemblyOracle : + public IOracle, + public IObservable + { + private: + typedef std::map HttpHeaders; + + class FetchContext : public boost::noncopyable + { + private: + class Emitter : public IMessageEmitter + { + private: + WebAssemblyOracle& oracle_; + + public: + Emitter(WebAssemblyOracle& oracle) : + oracle_(oracle) + { + } + + virtual void EmitMessage(const IObserver& receiver, + const IMessage& message) + { + oracle_.EmitMessage(receiver, message); + } + }; + + Emitter emitter_; + const IObserver& receiver_; + std::auto_ptr command_; + std::string expectedContentType_; + + public: + FetchContext(WebAssemblyOracle& oracle, + const IObserver& receiver, + IOracleCommand* command, + const std::string& expectedContentType) : + emitter_(oracle), + receiver_(receiver), + command_(command), + expectedContentType_(expectedContentType) + { + if (command == NULL) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); + } + } + + const std::string& GetExpectedContentType() const + { + return expectedContentType_; + } + + void EmitMessage(const IMessage& message) + { + emitter_.EmitMessage(receiver_, message); + } + + IMessageEmitter& GetEmitter() + { + return emitter_; + } + + const IObserver& GetReceiver() const + { + return receiver_; + } + + IOracleCommand& GetCommand() const + { + return *command_; + } + + template + const T& GetTypedCommand() const + { + return dynamic_cast(*command_); + } + }; + + static void FetchSucceeded(emscripten_fetch_t *fetch) + { + /** + * Firstly, make a local copy of the fetched information, and + * free data associated with the fetch. + **/ + + std::auto_ptr context(reinterpret_cast(fetch->userData)); + + std::string answer; + if (fetch->numBytes > 0) + { + answer.assign(fetch->data, fetch->numBytes); + } + + /** + * TODO - HACK - As of emscripten-1.38.31, the fetch API does + * not contain a way to retrieve the HTTP headers of the + * answer. We make the assumption that the "Content-Type" header + * of the response is the same as the "Accept" header of the + * query. This should be fixed in future versions of emscripten. + * https://github.com/emscripten-core/emscripten/pull/8486 + **/ + + HttpHeaders headers; + if (!context->GetExpectedContentType().empty()) + { + headers["Content-Type"] = context->GetExpectedContentType(); + } + + + emscripten_fetch_close(fetch); + + + /** + * Secondly, use the retrieved data. + **/ + + try + { + if (context.get() == NULL) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); + } + else + { + switch (context->GetCommand().GetType()) + { + case IOracleCommand::Type_OrthancRestApi: + { + OrthancRestApiCommand::SuccessMessage message + (context->GetTypedCommand(), headers, answer); + context->EmitMessage(message); + break; + } + + case IOracleCommand::Type_GetOrthancImage: + { + context->GetTypedCommand().ProcessHttpAnswer + (context->GetEmitter(), context->GetReceiver(), answer, headers); + break; + } + + case IOracleCommand::Type_GetOrthancWebViewerJpeg: + { + context->GetTypedCommand().ProcessHttpAnswer + (context->GetEmitter(), context->GetReceiver(), answer); + break; + } + + default: + LOG(ERROR) << "Command type not implemented by the WebAssembly Oracle: " + << context->GetCommand().GetType(); + } + } + } + catch (Orthanc::OrthancException& e) + { + LOG(ERROR) << "Error while processing a fetch answer in the oracle: " << e.What(); + } + } + + static void FetchFailed(emscripten_fetch_t *fetch) + { + std::auto_ptr context(reinterpret_cast(fetch->userData)); + + LOG(ERROR) << "Fetching " << fetch->url << " failed, HTTP failure status code: " << fetch->status; + + /** + * TODO - The following code leads to an infinite recursion, at + * least with Firefox running on incognito mode => WHY? + **/ + //emscripten_fetch_close(fetch); // Also free data on failure. + } + + + class FetchCommand : public boost::noncopyable + { + private: + WebAssemblyOracle& oracle_; + const IObserver& receiver_; + std::auto_ptr command_; + Orthanc::HttpMethod method_; + std::string uri_; + std::string body_; + HttpHeaders headers_; + unsigned int timeout_; + std::string expectedContentType_; + + public: + FetchCommand(WebAssemblyOracle& oracle, + const IObserver& receiver, + IOracleCommand* command) : + oracle_(oracle), + receiver_(receiver), + command_(command), + method_(Orthanc::HttpMethod_Get), + timeout_(0) + { + if (command == NULL) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); + } + } + + void SetMethod(Orthanc::HttpMethod method) + { + method_ = method; + } + + void SetUri(const std::string& uri) + { + uri_ = uri; + } + + void SetBody(std::string& body /* will be swapped */) + { + body_.swap(body); + } + + void SetHttpHeaders(const HttpHeaders& headers) + { + headers_ = headers; + } + + void SetTimeout(unsigned int timeout) + { + timeout_ = timeout; + } + + void Execute() + { + if (command_.get() == NULL) + { + // Cannot call Execute() twice + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + + emscripten_fetch_attr_t attr; + emscripten_fetch_attr_init(&attr); + + const char* method; + + switch (method_) + { + case Orthanc::HttpMethod_Get: + method = "GET"; + break; + + case Orthanc::HttpMethod_Post: + method = "POST"; + break; + + case Orthanc::HttpMethod_Delete: + method = "DELETE"; + break; + + case Orthanc::HttpMethod_Put: + method = "PUT"; + break; + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); + } + + strcpy(attr.requestMethod, method); + + attr.attributes = EMSCRIPTEN_FETCH_LOAD_TO_MEMORY; + attr.onsuccess = FetchSucceeded; + attr.onerror = FetchFailed; + attr.timeoutMSecs = timeout_ * 1000; + + std::vector headers; + headers.reserve(2 * headers_.size() + 1); + + std::string expectedContentType; + + for (HttpHeaders::const_iterator it = headers_.begin(); it != headers_.end(); ++it) + { + std::string key; + Orthanc::Toolbox::ToLowerCase(key, it->first); + + if (key == "accept") + { + expectedContentType = it->second; + } + + if (key != "accept-encoding") // Web browsers forbid the modification of this HTTP header + { + headers.push_back(it->first.c_str()); + headers.push_back(it->second.c_str()); + } + } + + headers.push_back(NULL); // Termination of the array of HTTP headers + + attr.requestHeaders = &headers[0]; + + if (!body_.empty()) + { + attr.requestDataSize = body_.size(); + attr.requestData = body_.c_str(); + } + + // Must be the last call to prevent memory leak on error + attr.userData = new FetchContext(oracle_, receiver_, command_.release(), expectedContentType); + emscripten_fetch(&attr, uri_.c_str()); + } + }; + + + void Execute(const IObserver& receiver, + OrthancRestApiCommand* command) + { + FetchCommand fetch(*this, receiver, command); + + fetch.SetMethod(command->GetMethod()); + fetch.SetUri(command->GetUri()); + fetch.SetHttpHeaders(command->GetHttpHeaders()); + fetch.SetTimeout(command->GetTimeout()); + + if (command->GetMethod() == Orthanc::HttpMethod_Put || + command->GetMethod() == Orthanc::HttpMethod_Put) + { + std::string body; + command->SwapBody(body); + fetch.SetBody(body); + } + + fetch.Execute(); + } + + + void Execute(const IObserver& receiver, + GetOrthancImageCommand* command) + { + FetchCommand fetch(*this, receiver, command); + + fetch.SetUri(command->GetUri()); + fetch.SetHttpHeaders(command->GetHttpHeaders()); + fetch.SetTimeout(command->GetTimeout()); + + fetch.Execute(); + } + + + void Execute(const IObserver& receiver, + GetOrthancWebViewerJpegCommand* command) + { + FetchCommand fetch(*this, receiver, command); + + fetch.SetUri(command->GetUri()); + fetch.SetHttpHeaders(command->GetHttpHeaders()); + fetch.SetTimeout(command->GetTimeout()); + + fetch.Execute(); + } + + + public: + WebAssemblyOracle(MessageBroker& broker) : + IObservable(broker) + { + } + + virtual void Schedule(const IObserver& receiver, + IOracleCommand* command) + { + std::auto_ptr protection(command); + + if (command == NULL) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); + } + + switch (command->GetType()) + { + case IOracleCommand::Type_OrthancRestApi: + Execute(receiver, dynamic_cast(protection.release())); + break; + + case IOracleCommand::Type_GetOrthancImage: + Execute(receiver, dynamic_cast(protection.release())); + break; + + case IOracleCommand::Type_GetOrthancWebViewerJpeg: + Execute(receiver, dynamic_cast(protection.release())); + break; + + default: + LOG(ERROR) << "Command type not implemented by the WebAssembly Oracle: " << command->GetType(); + } + } + + virtual void Start() + { + } + + virtual void Stop() + { + } + }; +} + + + + +boost::shared_ptr ct_(new OrthancStone::DicomVolumeImage); + +boost::shared_ptr loader_; + +std::auto_ptr viewport1_; +std::auto_ptr viewport2_; +std::auto_ptr viewport3_; + +OrthancStone::MessageBroker broker_; +OrthancStone::WebAssemblyOracle oracle_(broker_); + + +EM_BOOL OnWindowResize(int eventType, const EmscriptenUiEvent *uiEvent, void *userData) +{ + try + { + if (viewport1_.get() != NULL) + { + viewport1_->UpdateSize(); + } + + if (viewport2_.get() != NULL) + { + viewport2_->UpdateSize(); + } + + if (viewport3_.get() != NULL) + { + viewport3_->UpdateSize(); + } + } + catch (Orthanc::OrthancException& e) + { + LOG(ERROR) << "Exception while updating canvas size: " << e.What(); + } + + return true; +} + + + + +EM_BOOL OnAnimationFrame(double time, void *userData) +{ + try + { + if (viewport1_.get() != NULL) + { + viewport1_->Refresh(); + } + + if (viewport2_.get() != NULL) + { + viewport2_->Refresh(); + } + + if (viewport3_.get() != NULL) + { + viewport3_->Refresh(); + } + + return true; + } + catch (Orthanc::OrthancException& e) + { + LOG(ERROR) << "Exception in the animation loop, stopping now: " << e.What(); + return false; + } +} + + +extern "C" +{ + int main(int argc, char const *argv[]) + { + OrthancStone::StoneInitialize(); + Orthanc::Logging::EnableInfoLevel(true); + // Orthanc::Logging::EnableTraceLevel(true); + EM_ASM(window.dispatchEvent(new CustomEvent("WebAssemblyLoaded"));); + } + + EMSCRIPTEN_KEEPALIVE + void Initialize() + { + try + { + loader_.reset(new OrthancStone::OrthancSeriesVolumeProgressiveLoader(ct_, oracle_, oracle_)); + + viewport1_.reset(new OrthancStone::VolumeSlicerViewport(broker_, "mycanvas1", OrthancStone::VolumeProjection_Axial)); + viewport1_->SetSlicer(0, loader_, *loader_, new OrthancStone::GrayscaleStyleConfigurator); + viewport1_->UpdateSize(); + + viewport2_.reset(new OrthancStone::VolumeSlicerViewport(broker_, "mycanvas2", OrthancStone::VolumeProjection_Coronal)); + viewport2_->SetSlicer(0, loader_, *loader_, new OrthancStone::GrayscaleStyleConfigurator); + viewport2_->UpdateSize(); + + viewport3_.reset(new OrthancStone::VolumeSlicerViewport(broker_, "mycanvas3", OrthancStone::VolumeProjection_Sagittal)); + viewport3_->SetSlicer(0, loader_, *loader_, new OrthancStone::GrayscaleStyleConfigurator); + viewport3_->UpdateSize(); + + emscripten_set_resize_callback("#window", NULL, false, OnWindowResize); + + emscripten_request_animation_frame_loop(OnAnimationFrame, NULL); + + oracle_.Start(); + loader_->LoadSeries("a04ecf01-79b2fc33-58239f7e-ad9db983-28e81afa"); + } + catch (Orthanc::OrthancException& e) + { + LOG(ERROR) << "Exception during Initialize(): " << e.What(); + } + } +} diff -r 2fd96a637a59 -r 28f99af358fa Samples/WebAssembly/BasicMPR.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Samples/WebAssembly/BasicMPR.html Wed May 29 16:15:04 2019 +0200 @@ -0,0 +1,69 @@ + + + + + + + + + + + + Stone of Orthanc + + + + + + + + + + + + + diff -r 2fd96a637a59 -r 28f99af358fa Samples/WebAssembly/BasicScene.cpp --- a/Samples/WebAssembly/BasicScene.cpp Wed May 29 13:44:55 2019 +0200 +++ b/Samples/WebAssembly/BasicScene.cpp Wed May 29 16:15:04 2019 +0200 @@ -113,14 +113,14 @@ chain.push_back(ScenePoint2D(0 - 0.5, 2 - 0.5)); chain.push_back(ScenePoint2D(2 - 0.5, 2 - 0.5)); chain.push_back(ScenePoint2D(2 - 0.5, 0 - 0.5)); - layer->AddChain(chain, true); + layer->AddChain(chain, true, 255, 0, 0); chain.clear(); chain.push_back(ScenePoint2D(-5, -5)); chain.push_back(ScenePoint2D(5, -5)); chain.push_back(ScenePoint2D(5, 5)); chain.push_back(ScenePoint2D(-5, 5)); - layer->AddChain(chain, true); + layer->AddChain(chain, true, 0, 255, 0); double dy = 1.01; chain.clear(); @@ -128,9 +128,8 @@ chain.push_back(ScenePoint2D(4, -4 + dy)); chain.push_back(ScenePoint2D(-4, -4 + 2.0 * dy)); chain.push_back(ScenePoint2D(4, 2)); - layer->AddChain(chain, false); + layer->AddChain(chain, false, 0, 0, 255); - layer->SetColor(0,255, 255); scene->SetLayer(50, layer.release()); } diff -r 2fd96a637a59 -r 28f99af358fa Samples/WebAssembly/CMakeLists.txt --- a/Samples/WebAssembly/CMakeLists.txt Wed May 29 13:44:55 2019 +0200 +++ b/Samples/WebAssembly/CMakeLists.txt Wed May 29 16:15:04 2019 +0200 @@ -13,6 +13,7 @@ set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s ERROR_ON_UNDEFINED_SYMBOLS=1") set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s ALLOW_MEMORY_GROWTH=1") set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s ASSERTIONS=1 -s DISABLE_EXCEPTION_CATCHING=0") +#set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s EXIT_RUNTIME=1") ##################################################################### @@ -68,20 +69,40 @@ ${ORTHANC_STONE_SOURCES} ) -add_executable(BasicScene - BasicScene.cpp - ) + +if (OFF) + add_executable(BasicScene + BasicScene.cpp + ) + + target_link_libraries(BasicScene OrthancStone) + + install( + TARGETS BasicScene + RUNTIME DESTINATION ${CMAKE_INSTALL_PREFIX} + ) +endif() -target_link_libraries(BasicScene OrthancStone) + +if (ON) + add_executable(BasicMPR + BasicMPR.cpp + ) -install( - TARGETS BasicScene - RUNTIME DESTINATION ${CMAKE_INSTALL_PREFIX} - ) + target_link_libraries(BasicMPR OrthancStone) + + install( + TARGETS BasicMPR + RUNTIME DESTINATION ${CMAKE_INSTALL_PREFIX} + ) +endif() + install( FILES + ${CMAKE_CURRENT_BINARY_DIR}/BasicMPR.wasm ${CMAKE_CURRENT_BINARY_DIR}/BasicScene.wasm + ${CMAKE_SOURCE_DIR}/BasicMPR.html ${CMAKE_SOURCE_DIR}/BasicScene.html ${CMAKE_SOURCE_DIR}/Configuration.json ${CMAKE_SOURCE_DIR}/index.html diff -r 2fd96a637a59 -r 28f99af358fa Samples/WebAssembly/NOTES.txt --- a/Samples/WebAssembly/NOTES.txt Wed May 29 13:44:55 2019 +0200 +++ b/Samples/WebAssembly/NOTES.txt Wed May 29 16:15:04 2019 +0200 @@ -1,7 +1,7 @@ $ source ~/Downloads/emsdk/emsdk_env.sh $ cmake -G Ninja -DCMAKE_TOOLCHAIN_FILE=${EMSCRIPTEN}/cmake/Modules/Platform/Emscripten.cmake -DCMAKE_BUILD_TYPE=Release -DALLOW_DOWNLOADS=ON .. -DCMAKE_INSTALL_PREFIX=/tmp/stone $ ninja install -$ sudo docker run -p 4242:4242 -p 8042:8042 --rm -v /tmp/stone:/root/stone:ro jodogne/orthanc-plugins:1.5.6 /root/stone/Configuration.json --verbose +$ docker run -p 4242:4242 -p 8042:8042 --rm -v /tmp/stone:/root/stone:ro -v /tmp/stone-db/:/var/lib/orthanc/db/ jodogne/orthanc-plugins:1.5.6 /root/stone/Configuration.json --verbose notes BGO : source ~/apps/emsdk/emsdk_env.sh @@ -11,4 +11,4 @@ cmake -G Ninja -DCMAKE_TOOLCHAIN_FILE=${EMSCRIPTEN}/cmake/Modules/Platform/Emscripten.cmake -DCMAKE_BUILD_TYPE=Release -DALLOW_DOWNLOADS=ON .. -DCMAKE_INSTALL_PREFIX=/mnt/c/osi/dev/orthanc-stone/Samples/WebAssembly/installDir ninja install -docker run -p 4242:4242 -p 8042:8042 --rm -v "C:/osi/dev/orthanc-stone/Samples/WebAssembly/installDir:/root/stone:ro" jodogne/orthanc-plugins:1.5.6 /root/stone/Configuration.json --verbose \ No newline at end of file +docker run -p 4242:4242 -p 8042:8042 --rm -v "C:/osi/dev/orthanc-stone/Samples/WebAssembly/installDir:/root/stone:ro" jodogne/orthanc-plugins:1.5.6 /root/stone/Configuration.json --verbose diff -r 2fd96a637a59 -r 28f99af358fa Samples/WebAssembly/index.html --- a/Samples/WebAssembly/index.html Wed May 29 13:44:55 2019 +0200 +++ b/Samples/WebAssembly/index.html Wed May 29 16:15:04 2019 +0200 @@ -10,6 +10,7 @@

Available samples