Mercurial > hg > orthanc-stone
changeset 1336:379c00958553 broker
integration mainline->broker
author | Sebastien Jodogne <s.jodogne@gmail.com> |
---|---|
date | Tue, 31 Mar 2020 15:51:02 +0200 |
parents | 04055b6b9e2c (diff) 35469f75faa2 (current diff) |
children | c7dbe89c82d7 |
files | Applications/Samples/SingleFrameEditorApplication.h Framework/Radiography/RadiographyDicomLayer.cpp Framework/Radiography/RadiographyLayer.cpp Framework/Radiography/RadiographyLayer.h Framework/Radiography/RadiographyMaskLayer.h Framework/Radiography/RadiographyScene.cpp Framework/Radiography/RadiographyScene.h Framework/Radiography/RadiographyWidget.h |
diffstat | 290 files changed, 17352 insertions(+), 7616 deletions(-) [+] |
line wrap: on
line diff
--- a/Applications/Generic/GuiAdapter.cpp Tue Mar 31 15:47:29 2020 +0200 +++ b/Applications/Generic/GuiAdapter.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -31,7 +31,7 @@ #endif #if ORTHANC_ENABLE_THREADS == 1 -# include "../../Framework/Messages/LockingEmitter.h" +# include "../../Framework/Deprecated/Messages/LockingEmitter.h" #endif #include <Core/Compatibility.h> @@ -358,6 +358,8 @@ func); } +#if 0 + // useless under Wasm where canvas resize is handled automatically void GuiAdapter::SetResizeCallback( std::string canvasId, void* userData, bool capture, OnWindowResizeFunc func) { @@ -368,6 +370,7 @@ capture, func); } +#endif void GuiAdapter::RequestAnimationFrame( OnAnimationFrameFunc func, void* userData) @@ -393,10 +396,11 @@ emscripten_set_keyup_callback(canvasId.c_str(), userData, static_cast<EM_BOOL>(capture), func); } - void GuiAdapter::SetResizeCallback(std::string canvasId, void* userData, bool capture, OnWindowResizeFunc func) - { - emscripten_set_resize_callback(canvasId.c_str(), userData, static_cast<EM_BOOL>(capture), func); - } + // handled from within WebAssemblyViewport + //void GuiAdapter::SetResizeCallback(std::string canvasId, void* userData, bool capture, OnWindowResizeFunc func) + //{ + // emscripten_set_resize_callback(canvasId.c_str(), userData, static_cast<EM_BOOL>(capture), func); + //} void GuiAdapter::RequestAnimationFrame(OnAnimationFrameFunc func, void* userData) { @@ -516,13 +520,11 @@ dest.altKey = false; } - - // SDL ONLY - void GuiAdapter::SetResizeCallback( - std::string canvasId, void* userData, bool capture, OnWindowResizeFunc func) + void GuiAdapter::SetSdlResizeCallback( + std::string canvasId, void* userData, bool capture, OnSdlWindowResizeFunc func) { - resizeHandlers_.push_back(EventHandlerData<OnWindowResizeFunc>(canvasId, func, userData)); + resizeHandlers_.push_back(EventHandlerData<OnSdlWindowResizeFunc>(canvasId, func, userData)); } // SDL ONLY @@ -575,6 +577,13 @@ } // SDL ONLY + void GuiAdapter::SetGenericSdlEventCallback( + std::string canvasId, void* userData, bool capture, OnSdlEventCallback func) + { + sdlEventHandlers_.push_back(EventHandlerData<OnSdlEventCallback>(canvasId, func, userData)); + } + + // SDL ONLY void GuiAdapter::OnAnimationFrame() { std::vector<size_t> disabledAnimationHandlers; @@ -596,19 +605,91 @@ } // SDL ONLY - void GuiAdapter::OnResize() + void GuiAdapter::OnResize(unsigned int width, unsigned int height) { for (size_t i = 0; i < resizeHandlers_.size(); i++) { (*(resizeHandlers_[i].func))( - resizeHandlers_[i].canvasName, 0, resizeHandlers_[i].userData); + resizeHandlers_[i].canvasName, NULL, width, height, resizeHandlers_[i].userData); + } + } + + + + void GuiAdapter::OnSdlGenericEvent(const SDL_Event& sdlEvent) + { + // Events related to a window are only sent to the related canvas + // User events are sent to everyone (we can't filter them here) + + /* + SDL_WindowEvent SDL_WINDOWEVENT + SDL_KeyboardEvent SDL_KEYDOWN + SDL_KEYUP + SDL_TextEditingEvent SDL_TEXTEDITING + SDL_TextInputEvent SDL_TEXTINPUT + SDL_MouseMotionEvent SDL_MOUSEMOTION + SDL_MouseButtonEvent SDL_MOUSEBUTTONDOWN + SDL_MOUSEBUTTONUP + SDL_MouseWheelEvent SDL_MOUSEWHEEL + SDL_UserEvent SDL_USEREVENT through ::SDL_LASTEVENT-1 + */ + + // if this string is left empty, it means the message will be sent to + // all widgets. + // otherwise, it contains the originating message window title + + std::string windowTitle; + uint32_t windowId = 0; + + if (sdlEvent.type == SDL_WINDOWEVENT) + windowId = sdlEvent.window.windowID; + else if (sdlEvent.type == SDL_KEYDOWN || sdlEvent.type == SDL_KEYUP) + windowId = sdlEvent.key.windowID; + else if (sdlEvent.type == SDL_TEXTEDITING) + windowId = sdlEvent.edit.windowID; + else if (sdlEvent.type == SDL_TEXTINPUT) + windowId = sdlEvent.text.windowID; + else if (sdlEvent.type == SDL_MOUSEMOTION) + windowId = sdlEvent.motion.windowID; + else if (sdlEvent.type == SDL_MOUSEBUTTONDOWN || sdlEvent.type == SDL_MOUSEBUTTONUP) + windowId = sdlEvent.button.windowID; + else if (sdlEvent.type == SDL_MOUSEWHEEL) + windowId = sdlEvent.wheel.windowID; + else if (sdlEvent.type >= SDL_USEREVENT && sdlEvent.type <= (SDL_LASTEVENT-1)) + windowId = sdlEvent.user.windowID; + + if (windowId != 0) + { + SDL_Window* sdlWindow = SDL_GetWindowFromID(windowId); + ORTHANC_ASSERT(sdlWindow != NULL, "Window ID \"" << windowId << "\" is not a valid SDL window ID!"); + const char* windowTitleSz = SDL_GetWindowTitle(sdlWindow); + ORTHANC_ASSERT(windowTitleSz != NULL, "Window ID \"" << windowId << "\" has a NULL window title!"); + windowTitle = windowTitleSz; + ORTHANC_ASSERT(windowTitle != "", "Window ID \"" << windowId << "\" has an empty window title!"); + } + + for (size_t i = 0; i < sdlEventHandlers_.size(); i++) + { + // normally, the handlers return a bool indicating whether they + // have handled the event or not, but we don't really care about this + std::string& canvasName = sdlEventHandlers_[i].canvasName; + + bool sendEvent = true; + + if (windowTitle != "" && (canvasName != windowTitle)) + sendEvent = false; + + if (sendEvent) + { + OnSdlEventCallback func = sdlEventHandlers_[i].func; + (*func)(canvasName, sdlEvent, sdlEventHandlers_[i].userData); + } } } // SDL ONLY void GuiAdapter::OnMouseWheelEvent(uint32_t windowID, const GuiAdapterWheelEvent& event) { - // the SDL window name IS the canvas name ("canvas" is used because this lib // is designed for Wasm SDL_Window* sdlWindow = SDL_GetWindowFromID(windowID); @@ -726,17 +807,6 @@ ORTHANC_ASSERT(false, "Wrong event.type: " << event.type << " in GuiAdapter::OnMouseEvent(...)"); break; } - - ////boost::shared_ptr<IGuiAdapterWidget> GetWidgetFromWindowId(); - //boost::shared_ptr<IGuiAdapterWidget> foundWidget; - //VisitWidgets([foundWidget, windowID](auto widget) - // { - // if (widget->GetSdlWindowID() == windowID) - // foundWidget = widget; - // }); - //ORTHANC_ASSERT(foundWidget, "WindowID " << windowID << " was not found in the registered widgets!"); - //if(foundWidget) - // foundWidget-> } } @@ -771,6 +841,31 @@ } # endif +#if 0 + // TODO: remove this when generic sdl event handlers are implemented in + // the VolumeSlicerWidget + // SDL ONLY + bool GuiAdapter::IsSdlViewPortRefreshEvent(const SDL_Event& event) const + { + SDL_Window* sdlWindow = SDL_GetWindowFromID(event.window.windowID); + + ORTHANC_ASSERT(sdlWindow != NULL, "Window ID \"" << event.window.windowID << "\" is not a valid SDL window ID!"); + + const char* windowTitleSz = SDL_GetWindowTitle(sdlWindow); + + // now we need to find the VolumeSlicerWidget from from the canvas name! + // (and retrieve the SdlViewport) + boost::shared_ptr<IGuiAdapterWidget> foundWidget; + VisitWidgets([&foundWidget, windowTitleSz](auto widget) + { + if (widget->GetCanvasIdentifier() == std::string(windowTitleSz)) + foundWidget = widget; + }); + ORTHANC_ASSERT(foundWidget, "The window named: \"" << windowTitleSz << "\" was not found in the registered widgets!"); + return foundWidget->GetSdlViewport().IsRefreshEvent(event); + } +#endif + // SDL ONLY void GuiAdapter::Run(GuiAdapterRunFunc func, void* cookie) { @@ -789,141 +884,171 @@ while (!stop) { { - LockingEmitter::WriterLock lock(lockingEmitter_); + // TODO: lock all viewports here! (use a scoped object) if(func != NULL) (*func)(cookie); OnAnimationFrame(); // in SDL we must call it } - SDL_Event event; - - while (!stop && SDL_PollEvent(&event)) + while (!stop) { - LockingEmitter::WriterLock lock(lockingEmitter_); - - if (event.type == SDL_QUIT) - { - // TODO: call exit callbacks here - stop = true; - break; - } - else if ((event.type == SDL_MOUSEMOTION) || - (event.type == SDL_MOUSEBUTTONDOWN) || - (event.type == SDL_MOUSEBUTTONUP)) - { - int scancodeCount = 0; - const uint8_t* keyboardState = SDL_GetKeyboardState(&scancodeCount); - bool ctrlPressed(false); - bool shiftPressed(false); - bool altPressed(false); - - if (SDL_SCANCODE_LCTRL < scancodeCount && keyboardState[SDL_SCANCODE_LCTRL]) - ctrlPressed = true; - if (SDL_SCANCODE_RCTRL < scancodeCount && keyboardState[SDL_SCANCODE_RCTRL]) - ctrlPressed = true; - if (SDL_SCANCODE_LSHIFT < scancodeCount && keyboardState[SDL_SCANCODE_LSHIFT]) - shiftPressed = true; - if (SDL_SCANCODE_RSHIFT < scancodeCount && keyboardState[SDL_SCANCODE_RSHIFT]) - shiftPressed = true; - if (SDL_SCANCODE_LALT < scancodeCount && keyboardState[SDL_SCANCODE_LALT]) - altPressed = true; - - GuiAdapterMouseEvent dest; - ConvertFromPlatform(dest, ctrlPressed, shiftPressed, altPressed, event); - OnMouseEvent(event.window.windowID, dest); -#if 0 - // for reference, how to create trackers - if (tracker) - { - PointerEvent e; - e.AddPosition(compositor.GetPixelCenterCoordinates( - event.button.x, event.button.y)); - tracker->PointerMove(e); - } -#endif - } - else if (event.type == SDL_MOUSEWHEEL) - { - - int scancodeCount = 0; - const uint8_t* keyboardState = SDL_GetKeyboardState(&scancodeCount); - bool ctrlPressed(false); - bool shiftPressed(false); - bool altPressed(false); + std::vector<SDL_Event> sdlEvents; + std::map<Uint32,SDL_Event> userEventsMap; + + SDL_Event sdlEvent; - if (SDL_SCANCODE_LCTRL < scancodeCount && keyboardState[SDL_SCANCODE_LCTRL]) - ctrlPressed = true; - if (SDL_SCANCODE_RCTRL < scancodeCount && keyboardState[SDL_SCANCODE_RCTRL]) - ctrlPressed = true; - if (SDL_SCANCODE_LSHIFT < scancodeCount && keyboardState[SDL_SCANCODE_LSHIFT]) - shiftPressed = true; - if (SDL_SCANCODE_RSHIFT < scancodeCount && keyboardState[SDL_SCANCODE_RSHIFT]) - shiftPressed = true; - if (SDL_SCANCODE_LALT < scancodeCount && keyboardState[SDL_SCANCODE_LALT]) - altPressed = true; - - GuiAdapterWheelEvent dest; - ConvertFromPlatform(dest, ctrlPressed, shiftPressed, altPressed, event); - OnMouseWheelEvent(event.window.windowID, dest); - - //KeyboardModifiers modifiers = GetKeyboardModifiers(keyboardState, scancodeCount); - - //int x, y; - //SDL_GetMouseState(&x, &y); - - //if (event.wheel.y > 0) - //{ - // locker.GetCentralViewport().MouseWheel(MouseWheelDirection_Up, x, y, modifiers); - //} - //else if (event.wheel.y < 0) - //{ - // locker.GetCentralViewport().MouseWheel(MouseWheelDirection_Down, x, y, modifiers); - //} - } - else if (event.type == SDL_WINDOWEVENT && event.window.event == SDL_WINDOWEVENT_SIZE_CHANGED) + // FIRST: collect all pending events + while (SDL_PollEvent(&sdlEvent) != 0) { -#if 0 - tracker.reset(); -#endif - OnResize(); + if ( (sdlEvent.type >= SDL_USEREVENT) && + (sdlEvent.type <= SDL_USEREVENT) ) + { + // we don't want to have multiple events with the same event.type + userEventsMap[sdlEvent.type] = sdlEvent; + } + else + { + sdlEvents.push_back(sdlEvent); + } } - else if (event.type == SDL_KEYDOWN && event.key.repeat == 0 /* Ignore key bounce */) - { - switch (event.key.keysym.sym) - { - case SDLK_f: - // window.GetWindow().ToggleMaximize(); //TODO: move to particular handler - break; - // This commented out code was used to debug the context - // loss/restoring code (2019-08-10) - // case SDLK_k: - // { - // SDL_Window* window = SDL_GetWindowFromID(event.window.windowID); - // std::string windowTitle(SDL_GetWindowTitle(window)); - // Debug_SetContextToBeKilled(windowTitle); - // } - // break; - // case SDLK_l: - // { - // SDL_Window* window = SDL_GetWindowFromID(event.window.windowID); - // std::string windowTitle(SDL_GetWindowTitle(window)); - // Debug_SetContextToBeRestored(windowTitle); - // } - // break; + // SECOND: collect all user events + for (std::map<Uint32,SDL_Event>::const_iterator it = userEventsMap.begin(); it != userEventsMap.end(); ++it) + sdlEvents.push_back(it->second); + + // now process the events + for (std::vector<SDL_Event>::const_iterator it = sdlEvents.begin(); it != sdlEvents.end(); ++it) + { + const SDL_Event& sdlEvent = *it; + // TODO: lock all viewports here! (use a scoped object) - case SDLK_q: + if (sdlEvent.type == SDL_QUIT) + { + // TODO: call exit callbacks here stop = true; break; + } + else if ((sdlEvent.type == SDL_MOUSEMOTION) || + (sdlEvent.type == SDL_MOUSEBUTTONDOWN) || + (sdlEvent.type == SDL_MOUSEBUTTONUP)) + { + int scancodeCount = 0; + const uint8_t* keyboardState = SDL_GetKeyboardState(&scancodeCount); + bool ctrlPressed(false); + bool shiftPressed(false); + bool altPressed(false); - default: - GuiAdapterKeyboardEvent dest; - ConvertFromPlatform(dest, event); - OnKeyboardEvent(event.window.windowID, dest); - break; + if (SDL_SCANCODE_LCTRL < scancodeCount && keyboardState[SDL_SCANCODE_LCTRL]) + ctrlPressed = true; + if (SDL_SCANCODE_RCTRL < scancodeCount && keyboardState[SDL_SCANCODE_RCTRL]) + ctrlPressed = true; + if (SDL_SCANCODE_LSHIFT < scancodeCount && keyboardState[SDL_SCANCODE_LSHIFT]) + shiftPressed = true; + if (SDL_SCANCODE_RSHIFT < scancodeCount && keyboardState[SDL_SCANCODE_RSHIFT]) + shiftPressed = true; + if (SDL_SCANCODE_LALT < scancodeCount && keyboardState[SDL_SCANCODE_LALT]) + altPressed = true; + + GuiAdapterMouseEvent dest; + ConvertFromPlatform(dest, ctrlPressed, shiftPressed, altPressed, sdlEvent); + OnMouseEvent(sdlEvent.window.windowID, dest); + #if 0 + // for reference, how to create trackers + if (tracker) + { + PointerEvent e; + e.AddPosition(compositor.GetPixelCenterCoordinates( + sdlEvent.button.x, sdlEvent.button.y)); + tracker->PointerMove(e); + } + #endif } + else if (sdlEvent.type == SDL_MOUSEWHEEL) + { + + int scancodeCount = 0; + const uint8_t* keyboardState = SDL_GetKeyboardState(&scancodeCount); + bool ctrlPressed(false); + bool shiftPressed(false); + bool altPressed(false); + + if (SDL_SCANCODE_LCTRL < scancodeCount && keyboardState[SDL_SCANCODE_LCTRL]) + ctrlPressed = true; + if (SDL_SCANCODE_RCTRL < scancodeCount && keyboardState[SDL_SCANCODE_RCTRL]) + ctrlPressed = true; + if (SDL_SCANCODE_LSHIFT < scancodeCount && keyboardState[SDL_SCANCODE_LSHIFT]) + shiftPressed = true; + if (SDL_SCANCODE_RSHIFT < scancodeCount && keyboardState[SDL_SCANCODE_RSHIFT]) + shiftPressed = true; + if (SDL_SCANCODE_LALT < scancodeCount && keyboardState[SDL_SCANCODE_LALT]) + altPressed = true; + + GuiAdapterWheelEvent dest; + ConvertFromPlatform(dest, ctrlPressed, shiftPressed, altPressed, sdlEvent); + OnMouseWheelEvent(sdlEvent.window.windowID, dest); + + //KeyboardModifiers modifiers = GetKeyboardModifiers(keyboardState, scancodeCount); + + //int x, y; + //SDL_GetMouseState(&x, &y); + + //if (sdlEvent.wheel.y > 0) + //{ + // locker.GetCentralViewport().MouseWheel(MouseWheelDirection_Up, x, y, modifiers); + //} + //else if (sdlEvent.wheel.y < 0) + //{ + // locker.GetCentralViewport().MouseWheel(MouseWheelDirection_Down, x, y, modifiers); + //} + } + else if (sdlEvent.type == SDL_WINDOWEVENT && + (sdlEvent.window.event == SDL_WINDOWEVENT_RESIZED || + sdlEvent.window.event == SDL_WINDOWEVENT_SIZE_CHANGED)) + { + #if 0 + tracker.reset(); + #endif + OnResize(sdlEvent.window.data1, sdlEvent.window.data2); + } + else if (sdlEvent.type == SDL_KEYDOWN && sdlEvent.key.repeat == 0 /* Ignore key bounce */) + { + switch (sdlEvent.key.keysym.sym) + { + case SDLK_f: + // window.GetWindow().ToggleMaximize(); //TODO: move to particular handler + break; + + // This commented out code was used to debug the context + // loss/restoring code (2019-08-10) + // case SDLK_k: + // { + // SDL_Window* window = SDL_GetWindowFromID(sdlEvent.window.windowID); + // std::string windowTitle(SDL_GetWindowTitle(window)); + // Debug_SetContextToBeKilled(windowTitle); + // } + // break; + // case SDLK_l: + // { + // SDL_Window* window = SDL_GetWindowFromID(sdlEvent.window.windowID); + // std::string windowTitle(SDL_GetWindowTitle(window)); + // Debug_SetContextToBeRestored(windowTitle); + // } + // break; + + case SDLK_q: + stop = true; + break; + + default: + GuiAdapterKeyboardEvent dest; + ConvertFromPlatform(dest, sdlEvent); + OnKeyboardEvent(sdlEvent.window.windowID, dest); + break; + } + } + + OnSdlGenericEvent(sdlEvent); } - // HandleApplicationEvent(controller, compositor, event, tracker); } SDL_Delay(1);
--- a/Applications/Generic/GuiAdapter.h Tue Mar 31 15:47:29 2020 +0200 +++ b/Applications/Generic/GuiAdapter.h Tue Mar 31 15:51:02 2020 +0200 @@ -43,8 +43,8 @@ #include "../../Framework/StoneException.h" -#if ORTHANC_ENABLE_THREADS != 1 -# include "../../Framework/Messages/LockingEmitter.h" +#if ORTHANC_ENABLE_THREADS == 1 +# include "../../Framework/Deprecated/Messages/LockingEmitter.h" #endif #include <vector> @@ -53,6 +53,14 @@ namespace OrthancStone { +#if ORTHANC_ENABLE_SDL == 1 + class SdlViewport; +#endif + +#if 0 + + // events are now handled directly by the VolumeSlicerWidget... The + // GuiAdapter doesn't know about widgets anymore /** This interface is used to store the widgets that are controlled by the @@ -64,8 +72,17 @@ { public: virtual ~IGuiAdapterWidget() {} + +#if #if ORTHANC_ENABLE_SDL == 1 + /** + Returns the SdlViewport that this widget contains. If the underlying + viewport type is *not* SDL, then an error is returned. + */ + virtual SdlViewport& GetSdlViewport() = 0; +#endif + }; - }; +#endif enum GuiAdapterMouseButtonType { @@ -95,16 +112,24 @@ struct GuiAdapterWheelEvent; struct GuiAdapterKeyboardEvent; - class LockingEmitter; - #if 1 - typedef bool (*OnMouseEventFunc)(std::string canvasId, const GuiAdapterMouseEvent* mouseEvent, void* userData); - typedef bool (*OnMouseWheelFunc)(std::string canvasId, const GuiAdapterWheelEvent* wheelEvent, void* userData); - typedef bool (*OnKeyDownFunc) (std::string canvasId, const GuiAdapterKeyboardEvent* keyEvent, void* userData); - typedef bool (*OnKeyUpFunc) (std::string canvasId, const GuiAdapterKeyboardEvent* keyEvent, void* userData); + typedef bool (*OnMouseEventFunc) (std::string canvasId, const GuiAdapterMouseEvent* mouseEvent, void* userData); + typedef bool (*OnMouseWheelFunc) (std::string canvasId, const GuiAdapterWheelEvent* wheelEvent, void* userData); + typedef bool (*OnKeyDownFunc) (std::string canvasId, const GuiAdapterKeyboardEvent* keyEvent, void* userData); + typedef bool (*OnKeyUpFunc) (std::string canvasId, const GuiAdapterKeyboardEvent* keyEvent, void* userData); + typedef bool (*OnAnimationFrameFunc)(double time, void* userData); + +#if ORTHANC_ENABLE_SDL == 1 + typedef bool (*OnSdlEventCallback) (std::string canvasId, const SDL_Event& sdlEvent, void* userData); - typedef bool (*OnAnimationFrameFunc)(double time, void* userData); - typedef bool (*OnWindowResizeFunc)(std::string canvasId, const GuiAdapterUiEvent* uiEvent, void* userData); + typedef bool (*OnSdlWindowResizeFunc)(std::string canvasId, + const GuiAdapterUiEvent* uiEvent, + unsigned int width, + unsigned int height, + void* userData); + + +#endif #else @@ -224,11 +249,7 @@ class GuiAdapter { public: -#if ORTHANC_ENABLE_THREADS == 1 - GuiAdapter(LockingEmitter& lockingEmitter) : lockingEmitter_(lockingEmitter) -#else GuiAdapter() -#endif { static int instanceCount = 0; ORTHANC_ASSERT(instanceCount == 0); @@ -252,27 +273,32 @@ */ - void SetMouseDownCallback (std::string canvasId, void* userData, bool capture, OnMouseEventFunc func); - void SetMouseDblClickCallback (std::string canvasId, void* userData, bool capture, OnMouseEventFunc func); - void SetMouseMoveCallback (std::string canvasId, void* userData, bool capture, OnMouseEventFunc func); - void SetMouseUpCallback (std::string canvasId, void* userData, bool capture, OnMouseEventFunc func); - void SetWheelCallback (std::string canvasId, void* userData, bool capture, OnMouseWheelFunc func); - void SetKeyDownCallback (std::string canvasId, void* userData, bool capture, OnKeyDownFunc func); - void SetKeyUpCallback (std::string canvasId, void* userData, bool capture, OnKeyUpFunc func); - - // if you pass "#window", under SDL, then any Window resize will trigger the callback - void SetResizeCallback (std::string canvasId, void* userData, bool capture, OnWindowResizeFunc func); + void SetMouseDownCallback (std::string canvasId, void* userData, bool capture, OnMouseEventFunc func); + void SetMouseDblClickCallback (std::string canvasId, void* userData, bool capture, OnMouseEventFunc func); + void SetMouseMoveCallback (std::string canvasId, void* userData, bool capture, OnMouseEventFunc func); + void SetMouseUpCallback (std::string canvasId, void* userData, bool capture, OnMouseEventFunc func); + void SetWheelCallback (std::string canvasId, void* userData, bool capture, OnMouseWheelFunc func); + void SetKeyDownCallback (std::string canvasId, void* userData, bool capture, OnKeyDownFunc func); + void SetKeyUpCallback (std::string canvasId, void* userData, bool capture, OnKeyUpFunc func); + +#if ORTHANC_ENABLE_SDL == 1 + + void SetGenericSdlEventCallback (std::string canvasId, void* userData, bool capture, OnSdlEventCallback func); + + typedef bool (*OnSdlEventCallback) (std::string canvasId, const SDL_Event& sdlEvent, void* userData); + + // if you pass "#window", then any Window resize will trigger the callback + void SetSdlResizeCallback(std::string canvasId, + void* userData, + bool capture, + OnSdlWindowResizeFunc func); +#endif void RequestAnimationFrame(OnAnimationFrameFunc func, void* userData); // TODO: implement and call to remove canvases [in SDL, although code should be generic] void SetOnExitCallback(); - // void - // OnWindowResize - // oracle - // broker - /** Under SDL, this function does NOT return until all windows have been closed. Under wasm, it returns without doing anything, since the event loop is managed @@ -280,26 +306,14 @@ */ void Run(GuiAdapterRunFunc func = NULL, void* cookie = NULL); -#if ORTHANC_ENABLE_WASM != 1 - /** - This method must be called in order for callback handler to be allowed to - be registered. - - We'll retrieve its name and use it as the canvas name in all subsequent - calls - */ - //void RegisterSdlWindow(SDL_Window* window); - //void UnregisterSdlWindow(SDL_Window* window); -#endif - private: -#if ORTHANC_ENABLE_THREADS == 1 +#if ORTHANC_ENABLE_SDL == 1 /** - This object is used by the multithreaded Oracle to serialize access to - shared data. We need to use it as soon as we access the state. + Gives observers a chance to react based on generic event handlers. This + is used, for instance, when the viewport lock interface is invalidated. */ - LockingEmitter& lockingEmitter_; + void OnSdlGenericEvent(const SDL_Event& sdlEvent); #endif /** @@ -311,7 +325,7 @@ std::vector<std::pair<OnAnimationFrameFunc, void*> > animationFrameHandlers_; - void OnResize(); + void OnResize(unsigned int width, unsigned int height); #if ORTHANC_ENABLE_SDL == 1 template<typename Func> @@ -328,14 +342,15 @@ Func func; void* userData; }; - std::vector<EventHandlerData<OnWindowResizeFunc> > resizeHandlers_; - std::vector<EventHandlerData<OnMouseEventFunc > > mouseDownHandlers_; - std::vector<EventHandlerData<OnMouseEventFunc > > mouseDblCickHandlers_; - std::vector<EventHandlerData<OnMouseEventFunc > > mouseMoveHandlers_; - std::vector<EventHandlerData<OnMouseEventFunc > > mouseUpHandlers_; - std::vector<EventHandlerData<OnMouseWheelFunc > > mouseWheelHandlers_; - std::vector<EventHandlerData<OnKeyDownFunc > > keyDownHandlers_; - std::vector<EventHandlerData<OnKeyUpFunc > > keyUpHandlers_; + std::vector<EventHandlerData<OnSdlWindowResizeFunc> > resizeHandlers_; + std::vector<EventHandlerData<OnMouseEventFunc > > mouseDownHandlers_; + std::vector<EventHandlerData<OnMouseEventFunc > > mouseDblCickHandlers_; + std::vector<EventHandlerData<OnMouseEventFunc > > mouseMoveHandlers_; + std::vector<EventHandlerData<OnMouseEventFunc > > mouseUpHandlers_; + std::vector<EventHandlerData<OnMouseWheelFunc > > mouseWheelHandlers_; + std::vector<EventHandlerData<OnKeyDownFunc > > keyDownHandlers_; + std::vector<EventHandlerData<OnKeyUpFunc > > keyUpHandlers_; + std::vector<EventHandlerData<OnSdlEventCallback > > sdlEventHandlers_; /** This executes all the registered headers if needed (in wasm, the browser @@ -350,8 +365,6 @@ */ void OnMouseWheelEvent(uint32_t windowID, const GuiAdapterWheelEvent& event); - boost::shared_ptr<IGuiAdapterWidget> GetWidgetFromWindowId(); - #endif /**
--- a/Applications/Generic/NativeStoneApplicationContext.cpp Tue Mar 31 15:47:29 2020 +0200 +++ b/Applications/Generic/NativeStoneApplicationContext.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -24,10 +24,10 @@ namespace OrthancStone { - Deprecated::IWidget& NativeStoneApplicationContext::GlobalMutexLocker::SetCentralWidget(Deprecated::IWidget* widget) + void NativeStoneApplicationContext::GlobalMutexLocker::SetCentralWidget( + boost::shared_ptr<Deprecated::IWidget> widget) { that_.centralViewport_.SetCentralWidget(widget); - return *widget; } @@ -45,9 +45,7 @@ } - NativeStoneApplicationContext::NativeStoneApplicationContext(MessageBroker& broker) : - StoneApplicationContext(broker), - centralViewport_(broker), + NativeStoneApplicationContext::NativeStoneApplicationContext() : stopped_(true), updateDelayInMs_(100) // By default, 100ms between each refresh of the content {
--- a/Applications/Generic/NativeStoneApplicationContext.h Tue Mar 31 15:47:29 2020 +0200 +++ b/Applications/Generic/NativeStoneApplicationContext.h Tue Mar 31 15:51:02 2020 +0200 @@ -56,7 +56,7 @@ { } - Deprecated::IWidget& SetCentralWidget(Deprecated::IWidget* widget); // Takes ownership + void SetCentralWidget(boost::shared_ptr<Deprecated::IWidget> widget); Deprecated::IViewport& GetCentralViewport() { @@ -67,14 +67,9 @@ { that_.updateDelayInMs_ = delayInMs; } - - MessageBroker& GetMessageBroker() - { - return that_.GetMessageBroker(); - } }; - NativeStoneApplicationContext(MessageBroker& broker); + NativeStoneApplicationContext(); void Start();
--- a/Applications/Generic/NativeStoneApplicationRunner.cpp Tue Mar 31 15:47:29 2020 +0200 +++ b/Applications/Generic/NativeStoneApplicationRunner.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -97,7 +97,7 @@ DeclareCommandLineOptions(options); // application specific options - application_.DeclareStartupOptions(options); + application_->DeclareStartupOptions(options); boost::program_options::variables_map parameters; bool error = false; @@ -197,7 +197,7 @@ LogStatusBar statusBar; - NativeStoneApplicationContext context(broker_); + NativeStoneApplicationContext context; { // use multiple threads to execute asynchronous tasks like @@ -206,24 +206,23 @@ oracle.Start(); { - Deprecated::OracleWebService webService( - broker_, oracle, webServiceParameters, context); - + boost::shared_ptr<Deprecated::OracleWebService> webService + (new Deprecated::OracleWebService(oracle, webServiceParameters, context)); context.SetWebService(webService); context.SetOrthancBaseUrl(webServiceParameters.GetUrl()); - Deprecated::OracleDelayedCallExecutor delayedExecutor(broker_, oracle, context); + Deprecated::OracleDelayedCallExecutor delayedExecutor(oracle, context); context.SetDelayedCallExecutor(delayedExecutor); - application_.Initialize(&context, statusBar, parameters); + application_->Initialize(&context, statusBar, parameters); { NativeStoneApplicationContext::GlobalMutexLocker locker(context); - locker.SetCentralWidget(application_.GetCentralWidget()); + locker.SetCentralWidget(application_->GetCentralWidget()); locker.GetCentralViewport().SetStatusBar(statusBar); } - std::string title = application_.GetTitle(); + std::string title = application_->GetTitle(); if (title.empty()) { title = "Stone of Orthanc"; @@ -244,7 +243,7 @@ } LOG(WARNING) << "The application is stopping"; - application_.Finalize(); + application_->Finalize(); } catch (Orthanc::OrthancException& e) {
--- a/Applications/Generic/NativeStoneApplicationRunner.h Tue Mar 31 15:47:29 2020 +0200 +++ b/Applications/Generic/NativeStoneApplicationRunner.h Tue Mar 31 15:51:02 2020 +0200 @@ -34,14 +34,11 @@ class NativeStoneApplicationRunner { protected: - MessageBroker& broker_; - IStoneApplication& application_; + boost::shared_ptr<IStoneApplication> application_; + public: - - NativeStoneApplicationRunner(MessageBroker& broker, - IStoneApplication& application) - : broker_(broker), - application_(application) + NativeStoneApplicationRunner(boost::shared_ptr<IStoneApplication> application) + : application_(application) { } int Execute(int argc,
--- a/Applications/IStoneApplication.h Tue Mar 31 15:47:29 2020 +0200 +++ b/Applications/IStoneApplication.h Tue Mar 31 15:51:02 2020 +0200 @@ -64,7 +64,11 @@ #endif virtual std::string GetTitle() const = 0; - virtual Deprecated::IWidget* GetCentralWidget() = 0; + + virtual void SetCentralWidget(boost::shared_ptr<Deprecated::IWidget> widget) = 0; + + virtual boost::shared_ptr<Deprecated::IWidget> GetCentralWidget() = 0; + virtual void Finalize() = 0; }; }
--- a/Applications/Samples/SampleApplicationBase.h Tue Mar 31 15:47:29 2020 +0200 +++ b/Applications/Samples/SampleApplicationBase.h Tue Mar 31 15:51:02 2020 +0200 @@ -40,9 +40,8 @@ { class SampleApplicationBase : public IStoneApplication { - protected: - // ownership is transferred to the application context - Deprecated::WorldSceneWidget* mainWidget_; + private: + boost::shared_ptr<Deprecated::IWidget> mainWidget_; public: virtual void Initialize(StoneApplicationContext* context, @@ -64,7 +63,16 @@ virtual void Finalize() ORTHANC_OVERRIDE {} - virtual Deprecated::IWidget* GetCentralWidget() ORTHANC_OVERRIDE {return mainWidget_;} + + virtual void SetCentralWidget(boost::shared_ptr<Deprecated::IWidget> widget) ORTHANC_OVERRIDE + { + mainWidget_ = widget; + } + + virtual boost::shared_ptr<Deprecated::IWidget> GetCentralWidget() ORTHANC_OVERRIDE + { + return mainWidget_; + } #if ORTHANC_ENABLE_WASM==1 // default implementations for a single canvas named "canvas" in the HTML and an emtpy WasmApplicationAdapter
--- a/Applications/Samples/SampleMainNative.cpp Tue Mar 31 15:47:29 2020 +0200 +++ b/Applications/Samples/SampleMainNative.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -26,19 +26,18 @@ #if ORTHANC_ENABLE_QT==1 #include "Qt/SampleQtApplicationRunner.h" #endif -#include "../../Framework/Messages/MessageBroker.h" int main(int argc, char* argv[]) { - OrthancStone::MessageBroker broker; - SampleApplication sampleStoneApplication(broker); + boost::shared_ptr<SampleApplication> sampleStoneApplication(new SampleApplication); #if ORTHANC_ENABLE_SDL==1 - OrthancStone::SdlStoneApplicationRunner sdlApplicationRunner(broker, sampleStoneApplication); + OrthancStone::SdlStoneApplicationRunner sdlApplicationRunner(sampleStoneApplication); return sdlApplicationRunner.Execute(argc, argv); #endif + #if ORTHANC_ENABLE_QT==1 - OrthancStone::Samples::SampleQtApplicationRunner qtAppRunner(broker, sampleStoneApplication); + OrthancStone::Samples::SampleQtApplicationRunner qtAppRunner(sampleStoneApplication); return qtAppRunner.Execute(argc, argv); #endif }
--- a/Applications/Samples/SimpleViewerApplicationSingleFile.h Tue Mar 31 15:47:29 2020 +0200 +++ b/Applications/Samples/SimpleViewerApplicationSingleFile.h Tue Mar 31 15:51:02 2020 +0200 @@ -44,7 +44,7 @@ { class SimpleViewerApplication : public SampleSingleCanvasWithButtonsApplicationBase, - public IObserver + public ObserverBase<SimpleViewerApplication> { private: class ThumbnailInteractor : public Deprecated::IWorldSceneInteractor @@ -199,8 +199,8 @@ SimpleViewerApplication& viewerApplication_; public: - SimpleViewerApplicationAdapter(MessageBroker& broker, SimpleViewerApplication& application) - : WasmPlatformApplicationAdapter(broker, application), + SimpleViewerApplicationAdapter(SimpleViewerApplication& application) + : WasmPlatformApplicationAdapter(application), viewerApplication_(application) { } @@ -243,7 +243,7 @@ std::unique_ptr<ThumbnailInteractor> thumbnailInteractor_; Deprecated::LayoutWidget* mainLayout_; Deprecated::LayoutWidget* thumbnailsLayout_; - std::vector<Deprecated::SliceViewerWidget*> thumbnails_; + std::vector<boost::shared_ptr<Deprecated::SliceViewerWidget> > thumbnails_; std::map<std::string, std::vector<std::string> > instancesIdsPerSeriesId_; std::map<std::string, Json::Value> seriesTags_; @@ -258,8 +258,7 @@ Orthanc::Font font_; public: - SimpleViewerApplication(MessageBroker& broker) : - IObserver(broker), + SimpleViewerApplication() : currentTool_(Tool_LineMeasure), mainLayout_(NULL), currentInstanceIndex_(0), @@ -297,26 +296,28 @@ mainLayout_->SetBackgroundColor(0, 0, 0); mainLayout_->SetHorizontal(); - thumbnailsLayout_ = new Deprecated::LayoutWidget("thumbnail-layout"); + boost::shared_ptr<Deprecated::LayoutWidget> thumbnailsLayout_(new Deprecated::LayoutWidget("thumbnail-layout")); thumbnailsLayout_->SetPadding(10); thumbnailsLayout_->SetBackgroundCleared(true); thumbnailsLayout_->SetBackgroundColor(50, 50, 50); thumbnailsLayout_->SetVertical(); - mainWidget_ = new Deprecated::SliceViewerWidget(GetBroker(), "main-viewport"); + boost::shared_ptr<Deprecated::SliceViewerWidget> widget + (new Deprecated::SliceViewerWidget("main-viewport")); + SetCentralWidget(widget); //mainWidget_->RegisterObserver(*this); // hierarchy mainLayout_->AddWidget(thumbnailsLayout_); - mainLayout_->AddWidget(mainWidget_); + mainLayout_->AddWidget(widget); // sources - smartLoader_.reset(new Deprecated::SmartLoader(GetBroker(), context->GetOrthancApiClient())); + smartLoader_.reset(new Deprecated::SmartLoader(context->GetOrthancApiClient())); smartLoader_->SetImageQuality(Deprecated::SliceImageQuality_FullPam); mainLayout_->SetTransmitMouseOver(true); mainWidgetInteractor_.reset(new MainWidgetInteractor(*this)); - mainWidget_->SetInteractor(*mainWidgetInteractor_); + widget->SetInteractor(*mainWidgetInteractor_); thumbnailInteractor_.reset(new ThumbnailInteractor(*this)); } @@ -327,10 +328,10 @@ if (parameters.count("studyId") < 1) { LOG(WARNING) << "The study ID is missing, will take the first studyId found in Orthanc"; - context->GetOrthancApiClient().GetJsonAsync( + context->GetOrthancApiClient()->GetJsonAsync( "/studies", - new Callable<SimpleViewerApplication, Deprecated::OrthancApiClient::JsonResponseReadyMessage> - (*this, &SimpleViewerApplication::OnStudyListReceived)); + new Deprecated::DeprecatedCallable<SimpleViewerApplication, Deprecated::OrthancApiClient::JsonResponseReadyMessage> + (GetSharedObserver(), &SimpleViewerApplication::OnStudyListReceived)); } else { @@ -357,10 +358,10 @@ { for (size_t i=0; i < response["Series"].size(); i++) { - context_->GetOrthancApiClient().GetJsonAsync( + context_->GetOrthancApiClient()->GetJsonAsync( "/series/" + response["Series"][(int)i].asString(), - new Callable<SimpleViewerApplication, Deprecated::OrthancApiClient::JsonResponseReadyMessage> - (*this, &SimpleViewerApplication::OnSeriesReceived)); + new Deprecated::DeprecatedCallable<SimpleViewerApplication, Deprecated::OrthancApiClient::JsonResponseReadyMessage> + (GetSharedObserver(), &SimpleViewerApplication::OnSeriesReceived)); } } } @@ -387,7 +388,7 @@ LoadThumbnailForSeries(seriesId, instancesIdsPerSeriesId_[seriesId][0]); // if this is the first thumbnail loaded, load the first instance in the mainWidget - Deprecated::SliceViewerWidget& widget = *dynamic_cast<Deprecated::SliceViewerWidget*>(mainWidget_); + Deprecated::SliceViewerWidget& widget = dynamic_cast<Deprecated::SliceViewerWidget&>(*GetCentralWidget()); if (widget.GetLayerCount() == 0) { smartLoader_->SetFrameInWidget(widget, 0, instancesIdsPerSeriesId_[seriesId][0], 0); @@ -398,10 +399,10 @@ void LoadThumbnailForSeries(const std::string& seriesId, const std::string& instanceId) { LOG(INFO) << "Loading thumbnail for series " << seriesId; - Deprecated::SliceViewerWidget* thumbnailWidget = new Deprecated::SliceViewerWidget(GetBroker(), "thumbnail-series-" + seriesId); + boost::shared_ptr<Deprecated::SliceViewerWidget> thumbnailWidget(new Deprecated::SliceViewerWidget("thumbnail-series-" + seriesId)); thumbnails_.push_back(thumbnailWidget); thumbnailsLayout_->AddWidget(thumbnailWidget); - thumbnailWidget->RegisterObserverCallback(new Callable<SimpleViewerApplication, Deprecated::SliceViewerWidget::GeometryChangedMessage>(*this, &SimpleViewerApplication::OnWidgetGeometryChanged)); + Register<Deprecated::SliceViewerWidget::GeometryChangedMessage>(*thumbnailWidget, &SimpleViewerApplication::OnWidgetGeometryChanged); smartLoader_->SetFrameInWidget(*thumbnailWidget, 0, instanceId, 0); thumbnailWidget->SetInteractor(*thumbnailInteractor_); } @@ -409,9 +410,9 @@ void SelectStudy(const std::string& studyId) { LOG(INFO) << "Selecting study: " << studyId; - context_->GetOrthancApiClient().GetJsonAsync( - "/studies/" + studyId, new Callable<SimpleViewerApplication, Deprecated::OrthancApiClient::JsonResponseReadyMessage> - (*this, &SimpleViewerApplication::OnStudyReceived)); + context_->GetOrthancApiClient()->GetJsonAsync( + "/studies/" + studyId, new Deprecated::DeprecatedCallable<SimpleViewerApplication, Deprecated::OrthancApiClient::JsonResponseReadyMessage> + (GetSharedObserver(), &SimpleViewerApplication::OnStudyReceived)); } void OnWidgetGeometryChanged(const Deprecated::SliceViewerWidget::GeometryChangedMessage& message) @@ -422,7 +423,7 @@ void SelectSeriesInMainViewport(const std::string& seriesId) { - Deprecated::SliceViewerWidget& widget = *dynamic_cast<Deprecated::SliceViewerWidget*>(mainWidget_); + Deprecated::SliceViewerWidget& widget = dynamic_cast<Deprecated::SliceViewerWidget&>(*GetCentralWidget()); smartLoader_->SetFrameInWidget(widget, 0, instancesIdsPerSeriesId_[seriesId][0], 0); } @@ -451,7 +452,7 @@ virtual void InitializeWasm() { AttachWidgetToWasmViewport("canvas", thumbnailsLayout_); - AttachWidgetToWasmViewport("canvas2", mainWidget_); + AttachWidgetToWasmViewport("canvas2", widget); } #endif
--- a/Applications/Samples/SingleFrameApplication.h Tue Mar 31 15:47:29 2020 +0200 +++ b/Applications/Samples/SingleFrameApplication.h Tue Mar 31 15:51:02 2020 +0200 @@ -38,7 +38,7 @@ { class SingleFrameApplication : public SampleSingleCanvasApplicationBase, - public IObserver + public ObserverBase<SingleFrameApplication> { private: class Interactor : public Deprecated::IWorldSceneInteractor @@ -127,7 +127,7 @@ void OffsetSlice(int offset) { - if (source_ != NULL) + if (source_) { int slice = static_cast<int>(slice_) + offset; @@ -149,21 +149,15 @@ } - Deprecated::SliceViewerWidget& GetMainWidget() - { - return *dynamic_cast<Deprecated::SliceViewerWidget*>(mainWidget_); - } - - void SetSlice(size_t index) { - if (source_ != NULL && + if (source_ && index < source_->GetSlicesCount()) { slice_ = static_cast<unsigned int>(index); #if 1 - GetMainWidget().SetSlice(source_->GetSlice(slice_).GetGeometry()); + widget_->SetSlice(source_->GetSlice(slice_).GetGeometry()); #else // TEST for scene extents - Rotate the axes double a = 15.0 / 180.0 * boost::math::constants::pi<double>(); @@ -189,22 +183,22 @@ // Once the geometry of the series is downloaded from Orthanc, // display its middle slice, and adapt the viewport to fit this // slice - if (source_ == &message.GetOrigin()) + if (source_ && + source_.get() == &message.GetOrigin()) { SetSlice(source_->GetSlicesCount() / 2); } - GetMainWidget().FitContent(); + widget_->FitContent(); } - + + boost::shared_ptr<Deprecated::SliceViewerWidget> widget_; std::unique_ptr<Interactor> mainWidgetInteractor_; - const Deprecated::DicomSeriesVolumeSlicer* source_; + boost::shared_ptr<Deprecated::DicomSeriesVolumeSlicer> source_; unsigned int slice_; public: - SingleFrameApplication(MessageBroker& broker) : - IObserver(broker), - source_(NULL), + SingleFrameApplication() : slice_(0) { } @@ -243,13 +237,16 @@ std::string instance = parameters["instance"].as<std::string>(); int frame = parameters["frame"].as<unsigned int>(); - mainWidget_ = new Deprecated::SliceViewerWidget(GetBroker(), "main-widget"); + widget_.reset(new Deprecated::SliceViewerWidget("main-widget")); + SetCentralWidget(widget_); - std::unique_ptr<Deprecated::DicomSeriesVolumeSlicer> layer(new Deprecated::DicomSeriesVolumeSlicer(GetBroker(), context->GetOrthancApiClient())); - source_ = layer.get(); + boost::shared_ptr<Deprecated::DicomSeriesVolumeSlicer> layer(new Deprecated::DicomSeriesVolumeSlicer); + layer->Connect(context->GetOrthancApiClient()); + source_ = layer; + layer->LoadFrame(instance, frame); - layer->RegisterObserverCallback(new Callable<SingleFrameApplication, Deprecated::IVolumeSlicer::GeometryReadyMessage>(*this, &SingleFrameApplication::OnMainWidgetGeometryReady)); - GetMainWidget().AddLayer(layer.release()); + Register<Deprecated::IVolumeSlicer::GeometryReadyMessage>(*layer, &SingleFrameApplication::OnMainWidgetGeometryReady); + widget_->AddLayer(layer); Deprecated::RenderStyle s; @@ -258,11 +255,11 @@ s.interpolation_ = ImageInterpolation_Bilinear; } - GetMainWidget().SetLayerStyle(0, s); - GetMainWidget().SetTransmitMouseOver(true); + widget_->SetLayerStyle(0, s); + widget_->SetTransmitMouseOver(true); mainWidgetInteractor_.reset(new Interactor(*this)); - GetMainWidget().SetInteractor(*mainWidgetInteractor_); + widget_->SetInteractor(*mainWidgetInteractor_); } };
--- a/Applications/Samples/SingleFrameEditorApplication.h Tue Mar 31 15:47:29 2020 +0200 +++ b/Applications/Samples/SingleFrameEditorApplication.h Tue Mar 31 15:51:02 2020 +0200 @@ -28,13 +28,13 @@ #include "../../Framework/Radiography/RadiographyLayerMoveTracker.h" #include "../../Framework/Radiography/RadiographyLayerResizeTracker.h" #include "../../Framework/Radiography/RadiographyLayerRotateTracker.h" +#include "../../Framework/Radiography/RadiographyMaskLayer.h" #include "../../Framework/Radiography/RadiographyScene.h" #include "../../Framework/Radiography/RadiographySceneCommand.h" +#include "../../Framework/Radiography/RadiographySceneReader.h" +#include "../../Framework/Radiography/RadiographySceneWriter.h" #include "../../Framework/Radiography/RadiographyWidget.h" #include "../../Framework/Radiography/RadiographyWindowingTracker.h" -#include "../../Framework/Radiography/RadiographySceneWriter.h" -#include "../../Framework/Radiography/RadiographySceneReader.h" -#include "../../Framework/Radiography/RadiographyMaskLayer.h" #include "../../Framework/Toolbox/TextRenderer.h" #include <Core/HttpClient.h> @@ -55,7 +55,7 @@ { class RadiographyEditorInteractor : public Deprecated::IWorldSceneInteractor, - public IObserver + public ObserverBase<RadiographyEditorInteractor> { private: enum Tool @@ -82,8 +82,7 @@ public: - RadiographyEditorInteractor(MessageBroker& broker) : - IObserver(broker), + RadiographyEditorInteractor() : context_(NULL), tool_(Tool_Move), maskLayer_(NULL) @@ -315,8 +314,8 @@ LOG(INFO) << "JSON export was successful: " << snapshot.toStyledString(); - boost::shared_ptr<RadiographyScene> scene(new RadiographyScene(GetBroker())); - RadiographySceneReader reader(*scene, context_->GetOrthancApiClient()); + boost::shared_ptr<RadiographyScene> scene(new RadiographyScene); + RadiographySceneReader reader(*scene, *context_->GetOrthancApiClient()); reader.Read(snapshot); widget.SetScene(scene); @@ -346,7 +345,7 @@ if (context_ != NULL) { - widget.GetScene().ExportDicom(context_->GetOrthancApiClient(), + widget.GetScene().ExportDicom(*context_->GetOrthancApiClient(), tags, std::string(), 0.1, 0.1, widget.IsInverted(), false /* autoCrop */, widget.GetInterpolation(), EXPORT_USING_PAM); } @@ -426,16 +425,9 @@ private: boost::shared_ptr<RadiographyScene> scene_; RadiographyEditorInteractor interactor_; - Orthanc::FontRegistry fontRegistry_; RadiographyMaskLayer* maskLayer_; public: - SingleFrameEditorApplication(MessageBroker& broker) : - IObserver(broker), - interactor_(broker) - { - } - virtual ~SingleFrameEditorApplication() { LOG(WARNING) << "Destroying the application"; @@ -487,9 +479,9 @@ std::string instance = parameters["instance"].as<std::string>(); //int frame = parameters["frame"].as<unsigned int>(); - scene_.reset(new RadiographyScene(GetBroker())); + scene_.reset(new RadiographyScene); - RadiographyLayer& dicomLayer = scene_->LoadDicomFrame(context->GetOrthancApiClient(), instance, 0, false, NULL); + RadiographyLayer& dicomLayer = scene_->LoadDicomFrame(*context->GetOrthancApiClient(), instance, 0, false, NULL); //scene_->LoadDicomFrame(instance, frame, false); //.SetPan(200, 0); // = scene_->LoadDicomFrame(context->GetOrthancApiClient(), "61f3143e-96f34791-ad6bbb8d-62559e75-45943e1b", 0, false, NULL); @@ -527,10 +519,10 @@ layer.SetPan(0, 200); } - - mainWidget_ = new RadiographyWidget(GetBroker(), scene_, "main-widget"); - mainWidget_->SetTransmitMouseOver(true); - mainWidget_->SetInteractor(interactor_); + boost::shared_ptr<RadiographyWidget> widget(new RadiographyWidget(scene_, "main-widget")); + widget->SetTransmitMouseOver(true); + widget->SetInteractor(interactor_); + SetCentralWidget(widget); //scene_->SetWindowing(128, 256); }
--- a/Applications/Sdl/SdlCairoSurface.h Tue Mar 31 15:47:29 2020 +0200 +++ b/Applications/Sdl/SdlCairoSurface.h Tue Mar 31 15:51:02 2020 +0200 @@ -29,6 +29,7 @@ #include <Core/Compatibility.h> +#include <SDL_render.h> #include <boost/thread/mutex.hpp> namespace OrthancStone
--- a/Applications/Sdl/SdlEngine.cpp Tue Mar 31 15:47:29 2020 +0200 +++ b/Applications/Sdl/SdlEngine.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -99,9 +99,7 @@ SdlEngine::SdlEngine(SdlWindow& window, - NativeStoneApplicationContext& context, - MessageBroker& broker) : - IObserver(broker), + NativeStoneApplicationContext& context) : window_(window), context_(context), surface_(window),
--- a/Applications/Sdl/SdlEngine.h Tue Mar 31 15:47:29 2020 +0200 +++ b/Applications/Sdl/SdlEngine.h Tue Mar 31 15:51:02 2020 +0200 @@ -23,12 +23,13 @@ #if ORTHANC_ENABLE_SDL == 1 +#include "../../Framework/Messages/ObserverBase.h" +#include "../Generic/NativeStoneApplicationContext.h" #include "SdlCairoSurface.h" -#include "../Generic/NativeStoneApplicationContext.h" namespace OrthancStone { - class SdlEngine : public IObserver + class SdlEngine : public ObserverBase<SdlEngine> { private: SdlWindow& window_; @@ -46,8 +47,7 @@ public: SdlEngine(SdlWindow& window, - NativeStoneApplicationContext& context, - MessageBroker& broker); + NativeStoneApplicationContext& context); void OnViewportChanged(const Deprecated::IViewport::ViewportChangedMessage& message) {
--- a/Applications/Sdl/SdlOrthancSurface.cpp Tue Mar 31 15:47:29 2020 +0200 +++ b/Applications/Sdl/SdlOrthancSurface.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -27,6 +27,8 @@ #include <Core/OrthancException.h> #include <Core/Images/Image.h> +#include <SDL_render.h> + namespace OrthancStone { SdlOrthancSurface::SdlOrthancSurface(SdlWindow& window) :
--- a/Applications/Sdl/SdlStoneApplicationRunner.cpp Tue Mar 31 15:47:29 2020 +0200 +++ b/Applications/Sdl/SdlStoneApplicationRunner.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -103,20 +103,19 @@ LOG(WARNING) << "Starting the application"; SdlWindow window(title.c_str(), width_, height_, enableOpenGl_); - SdlEngine sdl(window, context, broker_); + boost::shared_ptr<SdlEngine> sdl(new SdlEngine(window, context)); { NativeStoneApplicationContext::GlobalMutexLocker locker(context); - locker.GetCentralViewport().RegisterObserverCallback( - new Callable<SdlEngine, Deprecated::IViewport::ViewportChangedMessage> - (sdl, &SdlEngine::OnViewportChanged)); + sdl->Register<Deprecated::IViewport::ViewportChangedMessage> + (locker.GetCentralViewport(), &SdlEngine::OnViewportChanged); //context.GetCentralViewport().Register(sdl); // (*) } context.Start(); - sdl.Run(); + sdl->Run(); LOG(WARNING) << "Stopping the application";
--- a/Applications/Sdl/SdlStoneApplicationRunner.h Tue Mar 31 15:47:29 2020 +0200 +++ b/Applications/Sdl/SdlStoneApplicationRunner.h Tue Mar 31 15:51:02 2020 +0200 @@ -39,9 +39,8 @@ bool enableOpenGl_; public: - SdlStoneApplicationRunner(MessageBroker& broker, - IStoneApplication& application) : - NativeStoneApplicationRunner(broker, application) + SdlStoneApplicationRunner(boost::shared_ptr<IStoneApplication> application) : + NativeStoneApplicationRunner(application) { }
--- a/Applications/StoneApplicationContext.cpp Tue Mar 31 15:47:29 2020 +0200 +++ b/Applications/StoneApplicationContext.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -32,35 +32,35 @@ throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); } - orthanc_.reset(new Deprecated::OrthancApiClient(broker_, *webService_, orthancBaseUrl_)); + orthanc_.reset(new Deprecated::OrthancApiClient(*webService_, orthancBaseUrl_)); } - Deprecated::IWebService& StoneApplicationContext::GetWebService() + boost::shared_ptr<Deprecated::IWebService> StoneApplicationContext::GetWebService() { if (webService_ == NULL) { throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); } - return *webService_; + return webService_; } - Deprecated::OrthancApiClient& StoneApplicationContext::GetOrthancApiClient() + boost::shared_ptr<Deprecated::OrthancApiClient> StoneApplicationContext::GetOrthancApiClient() { if (orthanc_.get() == NULL) { throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); } - return *orthanc_; + return orthanc_; } - void StoneApplicationContext::SetWebService(Deprecated::IWebService& webService) + void StoneApplicationContext::SetWebService(boost::shared_ptr<Deprecated::IWebService> webService) { - webService_ = &webService; + webService_ = webService; InitializeOrthanc(); }
--- a/Applications/StoneApplicationContext.h Tue Mar 31 15:47:29 2020 +0200 +++ b/Applications/StoneApplicationContext.h Tue Mar 31 15:51:02 2020 +0200 @@ -59,18 +59,15 @@ class StoneApplicationContext : public boost::noncopyable { private: - MessageBroker& broker_; - Deprecated::IWebService* webService_; - Deprecated::IDelayedCallExecutor* delayedCallExecutor_; - std::unique_ptr<Deprecated::OrthancApiClient> orthanc_; + boost::shared_ptr<Deprecated::IWebService> webService_; + Deprecated::IDelayedCallExecutor* delayedCallExecutor_; // TODO => shared_ptr ?? + boost::shared_ptr<Deprecated::OrthancApiClient> orthanc_; std::string orthancBaseUrl_; void InitializeOrthanc(); public: - StoneApplicationContext(MessageBroker& broker) : - broker_(broker), - webService_(NULL), + StoneApplicationContext() : delayedCallExecutor_(NULL) { } @@ -79,21 +76,11 @@ { } - MessageBroker& GetMessageBroker() - { - return broker_; - } + boost::shared_ptr<Deprecated::IWebService> GetWebService(); - bool HasWebService() const - { - return webService_ != NULL; - } + boost::shared_ptr<Deprecated::OrthancApiClient> GetOrthancApiClient(); - Deprecated::IWebService& GetWebService(); - - Deprecated::OrthancApiClient& GetOrthancApiClient(); - - void SetWebService(Deprecated::IWebService& webService); + void SetWebService(boost::shared_ptr<Deprecated::IWebService> webService); void SetOrthancBaseUrl(const std::string& baseUrl);
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Docs/stone-object-model-reference.md Tue Mar 31 15:51:02 2020 +0200 @@ -0,0 +1,439 @@ +## Scene2D and viewport-related object reference + +### `Scene2D` + +Represents a collection of layers that display 2D data. + +These layers must implement `ISceneLayer` + +The layers must be created externally and set to a specific Z-order index +with the `SetLayer` method. + +The `Scene2D` object merely acts as a layer container. It has no rendering +or layer creation facility on its own. + +The `Scene2D` contains an `AffineTransform2D` structure that defines how +the various layer item coordinates are transformed before being displayed +on the viewport (aka canvas) + +It is up to each layer type-specific renderer to choose how this transformation +is used. See the various kinds of layer below for more details. + +Examining the `Scene2D` contents can be done either by implementing the +`Scene2D::IVisitor` interface and calling `Apply(IVisitor& visitor)` or by +iterating between `GetMinDepth()` and `GetMaxDepth()` and calling the +`ISceneLayer& GetLayer(int depth)` getter. + +### `ISceneLayer` + +Interface that must be implemented by `Scene2D` layers. This is a closed list +that, as of 2020-03, contains: + +``` + Type_InfoPanel, + Type_ColorTexture, + Type_Polyline, + Type_Text, + Type_FloatTexture, + Type_LookupTableTexture +``` + +Please note that this interface mandates the implementation of a `GetRevision` +method returning an `uint64_t`. + +The idea is that when a model gets converted to a set of `ISceneLayer` +instances, changes in the model that result in changes to the layers must +increase the revision number of these layers. + +That allows the rendering process to safely assume that a given layers whose +revision does not change hasn't been modified (this helps with caching). + +Every mutable method in `ISceneLayer` instances that possibly change the visual +representation of an `ISceneLayer` must increase this revision number. + +### Implementation: `FloatTextureSceneLayer` + +Layer that renders an `Orthanc::ImageAccessor` object that must be convertible +to `Float32` image. + +The constructor only uses the image accessor to perform a copy. It can safely +be deleted afterwards. + +The input values are mapped to the output values by taking into account various +properties that can be modified with: + +- `SetWindowing`: uses windowing presets like "bone" or "lung" +- `SetCustomWindowing`: with manual window center and width +- `SetInverted`: toggles black <-> white inversion after windowing +- `SetApplyLog`: uses a non-linear response curve described in + https://theailearner.com/2019/01/01/log-transformation/ that expands contrast + in dark areas while compressing contrast in bright ones. This is **not** + implemented in the OpenGL renderer! + +The corresponding renderers are `OpenGLFloatTextureRenderer` and +`CairoFloatTextureRenderer`. The scene transformation is applied during +rendering. + +### Implementation: `ColorTextureSceneLayer` + +Layer that renders an `Orthanc::ImageAccessor` object an RGBA image (alpha must +be premultiplied). + +The constructor only uses the image accessor to perform a copy. It can safely +be deleted afterwards. + +The corresponding renderers are `OpenGLColorTextureRenderer` and +`CairoColorTextureRenderer`. The scene transformation is applied during +rendering. + +### Implementation: `LookupTableTextureSceneLayer` + +Layer that renders an `Orthanc::ImageAccessor` object that must be convertible +to `Float32` image. + +The constructor only uses the image accessor to perform a copy. It can safely +be deleted afterwards. + +The final on-screen color of each pixel is determined by passing the input +`Float32` value through a 256-entry look-up table (LUT) that can be passed as +an array of either 256 x 3 bytes (for opaque RGB colors) or 256 x 4 bytes (for +RGBA pixels). The LUT is not specified at construction time, but with +calls to `SetLookupTable` or `SetLookupTableGrayscale` (that fills the LUT +with a gradient from black to white, fully opaque) + +The range of input values that is mapped to the entirety of the LUT is, by +default, the full image range, but can be customized with `SetRange`. + +The corresponding renderers are `OpenGLLookupTableTextureRenderer` and +`CairoLookupTableTextureRenderer`. The scene transformation is applied during +rendering. + +### Implementation: `PolylineSceneLayer` + +Layer that renders vector-based polygonal lines. + +Polylines can be added with the `AddChain` method, that accepts a `Chain`, that +is a typedef to `std::vector<ScenePoint2D>`, a flag to specify whether the +chain must be automatically close (last point of the vector connected to the +first one) and the chain color (a `Color` structure). + +Please note that the line thickness is, contrary to the color, specified +per-chain but rather per-layer. + +If you need multiple line thicknesses, multiple `PolylineSceneLayer` must be +created. + +The corresponding renderers are `OpenGLAdvancedPolylineRenderer` and +`CairoPolylineRenderer`. The scene transformation is applied during +rendering. + +### Implementation: `TextSceneLayer` + +This layers renders a paragraph of text. + +The inputs to the layer can be changed after creation and are: +- The text iself, supplied as an UTF-8 encoded string in `SetText` +- The font used for rendering, set by `SetFontIndex`. +- The text anchoring, through `SetAnchor`: the text can be anchored to + various positions, such as top lef, center, bottom center,... These + various anchors are part of the `BitmapAnchor` enumeration. +- The text position, relative to its anchor, through `SetPosition`. + +The font is supplied as an index. This is an index in the set of fonts +that has been registered in the viewport compositor. The following code +shows how to set such a font: + +``` +std::unique_ptr<OrthancStone::IViewport::ILock> lock(viewport_.Lock()); +lock->GetCompositor().SetFont(0, + Orthanc::EmbeddedResources::UBUNTU_FONT, + 32, Orthanc::Encoding_Latin1); +// where 32 is the font size in pixels +``` + +This call uses the embedded `UBUNTU_FONT` resource that has been defined in +the `CMakeLists.txt` file with: + +``` +set(ORTHANC_STONE_APPLICATION_RESOURCES + UBUNTU_FONT ${CMAKE_BINARY_DIR}/ubuntu-font-family-0.83/Ubuntu-R.ttf +) +``` + +Please note that you must supply a font: there is no default font provided by +the OpenGL or Cairo compositors. + +The corresponding renderers are `OpenGLTextRenderer` and +`CairoTextRenderer`. The scene transformation is not applied during rendering, +because the text anchoring, position and scaling are computed relative to the +viewport/canvas. + +### Implementation: `InfoPanelSceneLayer` + +This layer is designed to display an image, supplied through an +`Orthanc::ImageAccessor` reference (only used at construction time). + +The image is not transformed according to the normal layer transformation but +is rather positioned relative to the canvas, with the same mechanism as the +`TextSceneLayer` described above. + +The image position is specified with the sole means of the `SetAnchor` method. + +The corresponding renderers are `OpenGLInfoPanelRenderer` and +`CairoInfoPanelRenderer`. + +### `IViewport` + +https://bitbucket.org/sjodogne/orthanc-stone/src/broker/Framework/Viewport/IViewport.h + +(**not** the one in `Deprecated`) +- Implemented by classes that: + - manage the on-screen display of a `Scene2D` trough a compositor. + - Own the `ICompositor` object that performs the rendering. + - Own the `Scene2D` (TODO: currently through `ViewportController` --> `Scene2D`) + - Provide a `Lock` method that returns a RAII, that must be kept alive when + modifying the underlying objects (controller, compositor, scene), but not + longer. + +#### Implementation: `SdlOpenGLViewport` +- Implementation of a viewport rendered on a SDL window, that uses OpenGL for + rendering. +- Instantiating this object creates an SDL window. Automatic scaling for hiDPI + displays can be toggled on or off. + +#### Implementation: `WebGLViewport` +- Implementation of a viewport rendered on a DOM canvas, that uses OpenGL for + rendering. +- Contrary to the SDL OpenGL viewport, the canvas must already be existing + when the ctor is called. + +### `ICompositor` +The interface providing a rendering service for `Scene2D` objects. + +**Subclasses:** `CairoCompositor`, `OpenGLCompositor` + +You do not need to create compositor instances. They are created for you when +instantiating a viewport. + +### `ViewportController` +This concrete class is instantiated by its `IViewport` owner. + +**TODO:** its functionality is not well defined and should be moved into the +viewport base class. Support for measuring tools should be moved to a special +interactor. + +- contains: + - array of `MeasureTool` + - ref to `IViewport` + - `activeTracker_` + - owns a `Scene2D` + - weak ref to `UndoStack` + - cached `canvasToSceneFactor_` + +- contains logic to: + - pass commands to undostack (trivial) + - logic to locate `MeasureTool` in the HitTest + - OTOH, the meat of the measuring tool logic (highlighting etc..) is + done in app-specific code (`VolumeSlicerWidget` for IBA) + - accept new Scene transform and notify listeners + - **the code that uses the interactor** (`HandleMousePress`) is only + called by the new `WebAssemblyViewport` !!! **TODO** clean this mess + +### `IViewportInteractor` +- must provide logic to respond to `CreateTracker` + +### `DefaultViewportInteractor` +- provides Pan+Rotate+Zoom trackers + +### `WebGLViewportsRegistry` + +This class is a singleton (accessible through `GetWebGLViewportsRegistry()` +that deals with context losses in the WebGL contexts. + +You use it by creating a WebGLViewport in the following fashion: + +``` +boost::shared_ptr<OrthancStone::WebGLViewport> viewport( + OrthancStone::GetWebGLViewportsRegistry().Add(canvasId)); +``` + +## Source data related + +### `IVolumeSlicer` + +A very simple interface with a single method: +`IVolumeSlicer::IExtractedSlice* ExtractSlice(const CoordinateSystem3D& cuttingPlane)` + +### `IVolumeSlicer::IExtractedSlice` + +On a slice has been extracted from a volume by an `IVolumeSlicer`, it can +report its *revision number*. + +If another call to `ExtractSlice` with the same cutting plane is made, but +the returned slice revision is different, it means that the volume has +changed and the scene layer must be refreshed. + +Please see `VolumeSceneLayerSource::Update` to check how this logic is +implemented. + + +### `OrthancSeriesVolumeProgressiveLoader` + +This class implements `IVolumeSlicer` (and `IObservable`) and can be used to +load a volume stored in a Dicom series on an Orthanc server. + +Over the course of the series loading, various notifications are sent: + +The first one is `OrthancStone::DicomVolumeImage::GeometryReadyMessage` that +is sent when the volume extent and geometric properties are known. + +Then, as slices get loaded and the volume is filled, +`OrthancStone::DicomVolumeImage::ContentUpdatedMessage` are sent. + +Once all the highest-quality slices have been loaded, the +`OrthancSeriesVolumeProgressiveLoader::VolumeImageReadyInHighQuality` +notification is sent. + +Please note that calling `ExtractSlice` *before* the geometry is loaded will +yield an instance of `InvalidSlice` that cannot be used to create a layer. + +On the other hand, + +### `VolumeSceneLayerSource` + +This class makes the bridge between a volume (supplied by an `IVolumeSlicer` +interface) and a `Scene2D`. + +Please note that the bulk of the work is done the objects implementing +`IVolumeSlicer` and this object merely connects things together. + +For instance, deciding whether an image (texture) or vector (polyline) layer +is done by the `IVolumeSlicer` implementation. + +- contains: + - reference to Scene2D + - `layerIndex_` (fixed at ctor) that is the index, in the Scene2D layer + stack, of the layer that will be created/updated + - `IVolumeSlicer` + +- contains logic to: + - extract a slice from the slicer and set/refresh the Scene2D layer at + the supplied `layerIndex_` + - refresh this based on the slice revision or configuration revision + - accept a configuration that will be applied to the layer + - the `Update()` method will + +## Updates and the configurators + +`ISceneLayer` does not expose mutable methods. + +The way to change a layer once it has been created is through configurator +objets. + +If you plan to set (even only once) or modify some layer properties after +layer creation, you need to create a matching configurator objet. + +For instance, in the `VolumeSceneLayerSource`, the `SetConfigurator` method +will store a `ILayerStyleConfigurator* configurator_`. + +In the `OrthancView` ctor, you can see how it is used: + +``` +std::unique_ptr<GrayscaleStyleConfigurator> style( + new GrayscaleStyleConfigurator); + +style->SetLinearInterpolation(true); + +...<some more code>... + +std::unique_ptr<LookupTableStyleConfigurator> config( + new LookupTableStyleConfigurator); + +config->SetLookupTable(Orthanc::EmbeddedResources::COLORMAP_HOT); + +``` + +The configurator type are created according to the type of layer.¸ + +Later, in `VolumeSceneLayerSource::Update(const CoordinateSystem3D& plane)`, +if the cutting plane has **not** changed and if the layer revision has **not** +changed, we test `configurator_->GetRevision() != lastConfiguratorRevision_` +and, if different, we call `configurator_->ApplyStyle(scene_.GetLayer(layerDepth_));` + +This allows to change layer properties that do not depend on the layer model +contents. + +On the other hand, if the layer revision has changed, when compared to the +last time it has been rendered (stored in `lastRevision_`), then we need to +ask the slice to create a brand new layer. + +Another way to see it is that layer rendering depend on model data and view +data. The model data is not mutable in the layer and, if the model changes, the +layer must be recreated. + +If only the view properties change (the configurator), we call ApplyStyle +(that **will** mutate some of the layer internals) + +Please note that the renderer does **not** know about the configurator : the +renderer uses properies in the layer and does not care whether those have +been set once at construction time or at every frame (configuration time). + + +## Cookbook + +### Simple application + +#### Building + +In order to create a Stone application, you need to: + +- CMake-based application: + ``` + include(${STONE_SOURCES_DIR}/Resources/CMake/OrthancStoneConfiguration.cmake) + ``` + with this library target that you have to define: + ``` + add_library(OrthancStone STATIC ${ORTHANC_STONE_SOURCES}) + ``` + then link with this library: + ``` + target_link_libraries(MyStoneApplication OrthancStone) + ``` + +Building is supported with emscripten, Visual C++ (>= 9.0), gcc... + +emscripten recommended version >= 1.38.41 + +These are very rough guidelines. See the `Samples` folder for actual examples. + +#### Structure + +The code requires a loader (object that ) + +Initialize: + +``` +Orthanc::Logging::Initialize(); +Orthanc::Logging::EnableInfoLevel(true); +``` +Call, in WASM: +``` +DISPATCH_JAVASCRIPT_EVENT("StoneInitialized"); +``` + + + +# TODO order +- **IBA** remove SDL Console failed attempt +- **IBA** understand refresh issue --> push fix +- **IBA** move main.cpp to tests/ +- **IBA** Remove deprecated tag from loaders and rename loaders (ensure "Progressive" only means spatially progressive, not in terms of quality.. make sure Yoann doesn't need Jpeg) +- **IBA** + + +# Notes + +- It is NOT possible to abandon the existing loaders : they contain too much loader-specific getters + + +
--- a/Framework/Deprecated/Layers/DicomSeriesVolumeSlicer.cpp Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Deprecated/Layers/DicomSeriesVolumeSlicer.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -87,59 +87,72 @@ } - DicomSeriesVolumeSlicer::DicomSeriesVolumeSlicer(OrthancStone::MessageBroker& broker, - OrthancApiClient& orthanc) : - IVolumeSlicer(broker), - IObserver(broker), - loader_(broker, orthanc), + DicomSeriesVolumeSlicer::DicomSeriesVolumeSlicer() : quality_(SliceImageQuality_FullPng) { - loader_.RegisterObserverCallback( - new OrthancStone::Callable<DicomSeriesVolumeSlicer, OrthancSlicesLoader::SliceGeometryReadyMessage> - (*this, &DicomSeriesVolumeSlicer::OnSliceGeometryReady)); - - loader_.RegisterObserverCallback( - new OrthancStone::Callable<DicomSeriesVolumeSlicer, OrthancSlicesLoader::SliceGeometryErrorMessage> - (*this, &DicomSeriesVolumeSlicer::OnSliceGeometryError)); - - loader_.RegisterObserverCallback( - new OrthancStone::Callable<DicomSeriesVolumeSlicer, OrthancSlicesLoader::SliceImageReadyMessage> - (*this, &DicomSeriesVolumeSlicer::OnSliceImageReady)); - - loader_.RegisterObserverCallback( - new OrthancStone::Callable<DicomSeriesVolumeSlicer, OrthancSlicesLoader::SliceImageErrorMessage> - (*this, &DicomSeriesVolumeSlicer::OnSliceImageError)); } + void DicomSeriesVolumeSlicer::Connect(boost::shared_ptr<OrthancApiClient> orthanc) + { + loader_.reset(new OrthancSlicesLoader(orthanc)); + Register<OrthancSlicesLoader::SliceGeometryReadyMessage>(*loader_, &DicomSeriesVolumeSlicer::OnSliceGeometryReady); + Register<OrthancSlicesLoader::SliceGeometryErrorMessage>(*loader_, &DicomSeriesVolumeSlicer::OnSliceGeometryError); + Register<OrthancSlicesLoader::SliceImageReadyMessage>(*loader_, &DicomSeriesVolumeSlicer::OnSliceImageReady); + Register<OrthancSlicesLoader::SliceImageErrorMessage>(*loader_, &DicomSeriesVolumeSlicer::OnSliceImageError); + } void DicomSeriesVolumeSlicer::LoadSeries(const std::string& seriesId) { - loader_.ScheduleLoadSeries(seriesId); + if (loader_.get() == NULL) + { + // Should have called "Connect()" + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + + loader_->ScheduleLoadSeries(seriesId); } void DicomSeriesVolumeSlicer::LoadInstance(const std::string& instanceId) { - loader_.ScheduleLoadInstance(instanceId); + if (loader_.get() == NULL) + { + // Should have called "Connect()" + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + + loader_->ScheduleLoadInstance(instanceId); } void DicomSeriesVolumeSlicer::LoadFrame(const std::string& instanceId, unsigned int frame) { - loader_.ScheduleLoadFrame(instanceId, frame); + if (loader_.get() == NULL) + { + // Should have called "Connect()" + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + + loader_->ScheduleLoadFrame(instanceId, frame); } bool DicomSeriesVolumeSlicer::GetExtent(std::vector<OrthancStone::Vector>& points, const OrthancStone::CoordinateSystem3D& viewportSlice) { + if (loader_.get() == NULL) + { + // Should have called "Connect()" + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + size_t index; - if (loader_.IsGeometryReady() && - loader_.LookupSlice(index, viewportSlice)) + if (loader_->IsGeometryReady() && + loader_->LookupSlice(index, viewportSlice)) { - loader_.GetSlice(index).GetExtent(points); + loader_->GetSlice(index).GetExtent(points); return true; } else @@ -151,12 +164,18 @@ void DicomSeriesVolumeSlicer::ScheduleLayerCreation(const OrthancStone::CoordinateSystem3D& viewportSlice) { + if (loader_.get() == NULL) + { + // Should have called "Connect()" + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + size_t index; - if (loader_.IsGeometryReady() && - loader_.LookupSlice(index, viewportSlice)) + if (loader_->IsGeometryReady() && + loader_->LookupSlice(index, viewportSlice)) { - loader_.ScheduleLoadSliceImage(index, quality_); + loader_->ScheduleLoadSliceImage(index, quality_); } } }
--- a/Framework/Deprecated/Layers/DicomSeriesVolumeSlicer.h Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Deprecated/Layers/DicomSeriesVolumeSlicer.h Tue Mar 31 15:51:02 2020 +0200 @@ -22,6 +22,7 @@ #pragma once #include "IVolumeSlicer.h" +#include "../../Messages/ObserverBase.h" #include "../Toolbox/IWebService.h" #include "../Toolbox/OrthancSlicesLoader.h" #include "../Toolbox/OrthancApiClient.h" @@ -33,7 +34,7 @@ // messages are sent to observers so they can use it class DicomSeriesVolumeSlicer : public IVolumeSlicer, - public OrthancStone::IObserver + public OrthancStone::ObserverBase<DicomSeriesVolumeSlicer> //private OrthancSlicesLoader::ISliceLoaderObserver { public: @@ -79,13 +80,14 @@ private: class RendererFactory; - OrthancSlicesLoader loader_; + boost::shared_ptr<OrthancSlicesLoader> loader_; SliceImageQuality quality_; public: - DicomSeriesVolumeSlicer(OrthancStone::MessageBroker& broker, - OrthancApiClient& orthanc); + DicomSeriesVolumeSlicer(); + void Connect(boost::shared_ptr<OrthancApiClient> orthanc); + void LoadSeries(const std::string& seriesId); void LoadInstance(const std::string& instanceId); @@ -105,12 +107,12 @@ size_t GetSlicesCount() const { - return loader_.GetSlicesCount(); + return loader_->GetSlicesCount(); } const Slice& GetSlice(size_t slice) const { - return loader_.GetSlice(slice); + return loader_->GetSlice(slice); } virtual bool GetExtent(std::vector<OrthancStone::Vector>& points,
--- a/Framework/Deprecated/Layers/DicomStructureSetSlicer.cpp Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Deprecated/Layers/DicomStructureSetSlicer.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -20,6 +20,8 @@ #include "DicomStructureSetSlicer.h" +#include "../../Toolbox/DicomStructureSet.h" + namespace Deprecated { class DicomStructureSetSlicer::Renderer : public ILayerRenderer @@ -28,13 +30,18 @@ class Structure { private: - bool visible_; - uint8_t red_; - uint8_t green_; - uint8_t blue_; - std::string name_; + bool visible_; + uint8_t red_; + uint8_t green_; + uint8_t blue_; + std::string name_; + +#if USE_BOOST_UNION_FOR_POLYGONS == 1 + std::vector< std::vector<OrthancStone::Point2D> > polygons_; +#else std::vector< std::pair<OrthancStone::Point2D, OrthancStone::Point2D> > segments_; - +#endif + public: Structure(OrthancStone::DicomStructureSet& structureSet, const OrthancStone::CoordinateSystem3D& plane, @@ -42,7 +49,12 @@ name_(structureSet.GetStructureName(index)) { structureSet.GetStructureColor(red_, green_, blue_, index); + +#if USE_BOOST_UNION_FOR_POLYGONS == 1 + visible_ = structureSet.ProjectStructure(polygons_, index, plane); +#else visible_ = structureSet.ProjectStructure(segments_, index, plane); +#endif } void Render(OrthancStone::CairoContext& context) @@ -53,12 +65,25 @@ context.SetSourceColor(red_, green_, blue_); +#if USE_BOOST_UNION_FOR_POLYGONS == 1 + for (size_t i = 0; i < polygons_.size(); i++) + { + cairo_move_to(cr, polygons_[i][0].x, polygons_[i][0].y); + for (size_t j = 0; j < polygons_[i].size(); j++) + { + cairo_line_to(cr, polygons_[i][j].x, polygons_[i][j].y); + } + cairo_line_to(cr, polygons_[i][0].x, polygons_[i][0].y); + cairo_stroke(cr); + } +#else for (size_t i = 0; i < segments_.size(); i++) { cairo_move_to(cr, segments_[i].first.x, segments_[i].first.y); - cairo_move_to(cr, segments_[i].second.x, segments_[i].second.y); + cairo_line_to(cr, segments_[i].second.x, segments_[i].second.y); cairo_stroke(cr); } +#endif } } }; @@ -140,15 +165,10 @@ }; - DicomStructureSetSlicer::DicomStructureSetSlicer(OrthancStone::MessageBroker& broker, - StructureSetLoader& loader) : - IVolumeSlicer(broker), - IObserver(broker), + DicomStructureSetSlicer::DicomStructureSetSlicer(StructureSetLoader& loader) : loader_(loader) { - loader_.RegisterObserverCallback( - new OrthancStone::Callable<DicomStructureSetSlicer, StructureSetLoader::ContentChangedMessage> - (*this, &DicomStructureSetSlicer::OnStructureSetLoaded)); + Register<StructureSetLoader::ContentChangedMessage>(loader_, &DicomStructureSetSlicer::OnStructureSetLoaded); }
--- a/Framework/Deprecated/Layers/DicomStructureSetSlicer.h Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Deprecated/Layers/DicomStructureSetSlicer.h Tue Mar 31 15:51:02 2020 +0200 @@ -28,7 +28,7 @@ { class DicomStructureSetSlicer : public IVolumeSlicer, - public OrthancStone::IObserver + public OrthancStone::ObserverBase<DicomStructureSetSlicer> { private: class Renderer; @@ -42,8 +42,7 @@ } public: - DicomStructureSetSlicer(OrthancStone::MessageBroker& broker, - StructureSetLoader& loader); + DicomStructureSetSlicer(StructureSetLoader& loader); virtual bool GetExtent(std::vector<OrthancStone::Vector>& points, const OrthancStone::CoordinateSystem3D& viewportPlane)
--- a/Framework/Deprecated/Layers/IVolumeSlicer.h Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Deprecated/Layers/IVolumeSlicer.h Tue Mar 31 15:51:02 2020 +0200 @@ -122,11 +122,6 @@ }; - IVolumeSlicer(OrthancStone::MessageBroker& broker) : - IObservable(broker) - { - } - virtual ~IVolumeSlicer() { }
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Deprecated/Loaders/DicomStructureSetLoader.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -0,0 +1,428 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 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 "DicomStructureSetLoader.h" + +#include "../../Scene2D/PolylineSceneLayer.h" +#include "../../StoneException.h" +#include "../../Toolbox/GeometryToolbox.h" + +#include <Core/Toolbox.h> + +#include <algorithm> + +#if 0 +bool logbgo233 = false; +bool logbgo115 = false; +#endif + +namespace Deprecated +{ + +#if 0 + void DumpDicomMap(std::ostream& o, const Orthanc::DicomMap& dicomMap) + { + using namespace std; + //ios_base::fmtflags state = o.flags(); + //o.flags(ios::right | ios::hex); + //o << "(" << setfill('0') << setw(4) << tag.GetGroup() + // << "," << setw(4) << tag.GetElement() << ")"; + //o.flags(state); + Json::Value val; + dicomMap.Serialize(val); + o << val; + //return o; + } +#endif + + + class DicomStructureSetLoader::AddReferencedInstance : public LoaderStateMachine::State + { + private: + std::string instanceId_; + + public: + AddReferencedInstance(DicomStructureSetLoader& that, + const std::string& instanceId) : + State(that), + instanceId_(instanceId) + { + } + + virtual void Handle(const OrthancStone::OrthancRestApiCommand::SuccessMessage& message) + { + Json::Value tags; + message.ParseJsonBody(tags); + + Orthanc::DicomMap dicom; + dicom.FromDicomAsJson(tags); + + DicomStructureSetLoader& loader = GetLoader<DicomStructureSetLoader>(); + + loader.content_->AddReferencedSlice(dicom); + + loader.countProcessedInstances_ ++; + assert(loader.countProcessedInstances_ <= loader.countReferencedInstances_); + + if (loader.countProcessedInstances_ == loader.countReferencedInstances_) + { + // All the referenced instances have been loaded, finalize the RT-STRUCT + loader.content_->CheckReferencedSlices(); + loader.revision_++; + loader.SetStructuresReady(); + } + } + }; + + + // State that converts a "SOP Instance UID" to an Orthanc identifier + class DicomStructureSetLoader::LookupInstance : public LoaderStateMachine::State + { + private: + std::string sopInstanceUid_; + + public: + LookupInstance(DicomStructureSetLoader& that, + const std::string& sopInstanceUid) : + State(that), + sopInstanceUid_(sopInstanceUid) + { + } + + virtual void Handle(const OrthancStone::OrthancRestApiCommand::SuccessMessage& message) + { +#if 0 + LOG(TRACE) << "DicomStructureSetLoader::LookupInstance::Handle() (SUCCESS)"; +#endif + DicomStructureSetLoader& loader = GetLoader<DicomStructureSetLoader>(); + + Json::Value lookup; + message.ParseJsonBody(lookup); + + if (lookup.type() != Json::arrayValue || + lookup.size() != 1 || + !lookup[0].isMember("Type") || + !lookup[0].isMember("Path") || + lookup[0]["Type"].type() != Json::stringValue || + lookup[0]["ID"].type() != Json::stringValue || + lookup[0]["Type"].asString() != "Instance") + { + std::stringstream msg; + msg << "Unknown resource! message.GetAnswer() = " << message.GetAnswer() << " message.GetAnswerHeaders() = "; + for (OrthancStone::OrthancRestApiCommand::HttpHeaders::const_iterator it = message.GetAnswerHeaders().begin(); + it != message.GetAnswerHeaders().end(); ++it) + { + msg << "\nkey: \"" << it->first << "\" value: \"" << it->second << "\"\n"; + } + const std::string msgStr = msg.str(); + LOG(ERROR) << msgStr; + throw Orthanc::OrthancException(Orthanc::ErrorCode_UnknownResource); + } + + const std::string instanceId = lookup[0]["ID"].asString(); + + { + std::unique_ptr<OrthancStone::OrthancRestApiCommand> command(new OrthancStone::OrthancRestApiCommand); + command->SetHttpHeader("Accept-Encoding", "gzip"); + std::string uri = "/instances/" + instanceId + "/tags"; + command->SetUri(uri); + command->AcquirePayload(new AddReferencedInstance(loader, instanceId)); + Schedule(command.release()); + } + } + }; + + + class DicomStructureSetLoader::LoadStructure : public LoaderStateMachine::State + { + public: + LoadStructure(DicomStructureSetLoader& that) : + State(that) + { + } + + virtual void Handle(const OrthancStone::OrthancRestApiCommand::SuccessMessage& message) + { +#if 0 + if (logbgo115) + LOG(TRACE) << "DicomStructureSetLoader::LoadStructure::Handle() (SUCCESS)"; +#endif + DicomStructureSetLoader& loader = GetLoader<DicomStructureSetLoader>(); + + { + OrthancPlugins::FullOrthancDataset dicom(message.GetAnswer()); + loader.content_.reset(new OrthancStone::DicomStructureSet(dicom)); + size_t structureCount = loader.content_->GetStructuresCount(); + loader.structureVisibility_.resize(structureCount); + bool everythingVisible = false; + if ((loader.initiallyVisibleStructures_.size() == 1) + && (loader.initiallyVisibleStructures_[0].size() == 1) + && (loader.initiallyVisibleStructures_[0][0] == '*')) + { + everythingVisible = true; + } + + for (size_t i = 0; i < structureCount; ++i) + { + // if a single "*" string is supplied, this means we want everything + // to be visible... + if(everythingVisible) + { + loader.structureVisibility_.at(i) = true; + } + else + { + // otherwise, we only enable visibility for those structures whose + // names are mentioned in the initiallyVisibleStructures_ array + const std::string& structureName = loader.content_->GetStructureName(i); + + std::vector<std::string>::iterator foundIt = + std::find( + loader.initiallyVisibleStructures_.begin(), + loader.initiallyVisibleStructures_.end(), + structureName); + std::vector<std::string>::iterator endIt = loader.initiallyVisibleStructures_.end(); + if (foundIt != endIt) + loader.structureVisibility_.at(i) = true; + else + loader.structureVisibility_.at(i) = false; + } + } + } + + // Some (admittedly invalid) Dicom files have empty values in the + // 0008,1155 tag. We try our best to cope with this. + std::set<std::string> instances; + std::set<std::string> nonEmptyInstances; + loader.content_->GetReferencedInstances(instances); + for (std::set<std::string>::const_iterator + it = instances.begin(); it != instances.end(); ++it) + { + std::string instance = Orthanc::Toolbox::StripSpaces(*it); + if(instance != "") + nonEmptyInstances.insert(instance); + } + + loader.countReferencedInstances_ = + static_cast<unsigned int>(nonEmptyInstances.size()); + + for (std::set<std::string>::const_iterator + it = nonEmptyInstances.begin(); it != nonEmptyInstances.end(); ++it) + { + std::unique_ptr<OrthancStone::OrthancRestApiCommand> command(new OrthancStone::OrthancRestApiCommand); + command->SetUri("/tools/lookup"); + command->SetMethod(Orthanc::HttpMethod_Post); + command->SetBody(*it); + command->AcquirePayload(new LookupInstance(loader, *it)); + Schedule(command.release()); + } + } + }; + + + class DicomStructureSetLoader::Slice : public IExtractedSlice + { + private: + const OrthancStone::DicomStructureSet& content_; + uint64_t revision_; + bool isValid_; + std::vector<bool> visibility_; + + public: + /** + The visibility vector must either: + - be empty + or + - contain the same number of items as the number of structures in the + structure set. + In the first case (empty vector), all the structures are displayed. + In the second case, the visibility of each structure is defined by the + content of the vector at the corresponding index. + */ + Slice(const OrthancStone::DicomStructureSet& content, + uint64_t revision, + const OrthancStone::CoordinateSystem3D& cuttingPlane, + std::vector<bool> visibility = std::vector<bool>()) + : content_(content) + , revision_(revision) + , visibility_(visibility) + { + ORTHANC_ASSERT((visibility_.size() == content_.GetStructuresCount()) + || (visibility_.size() == 0u)); + + bool opposite; + + const OrthancStone::Vector normal = content.GetNormal(); + isValid_ = ( + OrthancStone::GeometryToolbox::IsParallelOrOpposite(opposite, normal, cuttingPlane.GetNormal()) || + OrthancStone::GeometryToolbox::IsParallelOrOpposite(opposite, normal, cuttingPlane.GetAxisX()) || + OrthancStone::GeometryToolbox::IsParallelOrOpposite(opposite, normal, cuttingPlane.GetAxisY())); + } + + virtual bool IsValid() + { + return isValid_; + } + + virtual uint64_t GetRevision() + { + return revision_; + } + + virtual OrthancStone::ISceneLayer* CreateSceneLayer( + const OrthancStone::ILayerStyleConfigurator* configurator, + const OrthancStone::CoordinateSystem3D& cuttingPlane) + { + assert(isValid_); + + std::unique_ptr<OrthancStone::PolylineSceneLayer> layer(new OrthancStone::PolylineSceneLayer); + layer->SetThickness(2); + + for (size_t i = 0; i < content_.GetStructuresCount(); i++) + { + if ((visibility_.size() == 0) || visibility_.at(i)) + { + const OrthancStone::Color& color = content_.GetStructureColor(i); + +#ifdef USE_BOOST_UNION_FOR_POLYGONS + std::vector< std::vector<OrthancStone::Point2D> > polygons; + + if (content_.ProjectStructure(polygons, i, cuttingPlane)) + { + for (size_t j = 0; j < polygons.size(); j++) + { + PolylineSceneLayer::Chain chain; + chain.resize(polygons[j].size()); + + for (size_t k = 0; k < polygons[j].size(); k++) + { + chain[k] = ScenePoint2D(polygons[j][k].x, polygons[j][k].y); + } + + layer->AddChain(chain, true /* closed */, color); + } + } +#else + std::vector< std::pair<OrthancStone::Point2D, OrthancStone::Point2D> > segments; + + if (content_.ProjectStructure(segments, i, cuttingPlane)) + { + for (size_t j = 0; j < segments.size(); j++) + { + OrthancStone::PolylineSceneLayer::Chain chain; + chain.resize(2); + + chain[0] = OrthancStone::ScenePoint2D(segments[j].first.x, segments[j].first.y); + chain[1] = OrthancStone::ScenePoint2D(segments[j].second.x, segments[j].second.y); + + layer->AddChain(chain, false /* NOT closed */, color); + } + } +#endif + } + } + + return layer.release(); + } + }; + + + DicomStructureSetLoader::DicomStructureSetLoader( + OrthancStone::ILoadersContext& loadersContext) + : LoaderStateMachine(loadersContext) + , loadersContext_(loadersContext) + , revision_(0) + , countProcessedInstances_(0) + , countReferencedInstances_(0) + , structuresReady_(false) + { + } + + + boost::shared_ptr<Deprecated::DicomStructureSetLoader> DicomStructureSetLoader::Create(OrthancStone::ILoadersContext& loadersContext) + { + boost::shared_ptr<DicomStructureSetLoader> obj( + new DicomStructureSetLoader( + loadersContext)); + obj->LoaderStateMachine::PostConstructor(); + return obj; + + } + + void DicomStructureSetLoader::SetStructureDisplayState(size_t structureIndex, bool display) + { + structureVisibility_.at(structureIndex) = display; + revision_++; + } + + DicomStructureSetLoader::~DicomStructureSetLoader() + { + LOG(TRACE) << "DicomStructureSetLoader::~DicomStructureSetLoader()"; + } + + void DicomStructureSetLoader::LoadInstance( + const std::string& instanceId, + const std::vector<std::string>& initiallyVisibleStructures) + { + Start(); + + instanceId_ = instanceId; + initiallyVisibleStructures_ = initiallyVisibleStructures; + + { + std::unique_ptr<OrthancStone::OrthancRestApiCommand> command(new OrthancStone::OrthancRestApiCommand); + command->SetHttpHeader("Accept-Encoding", "gzip"); + + std::string uri = "/instances/" + instanceId + "/tags?ignore-length=3006-0050"; + + command->SetUri(uri); + command->AcquirePayload(new LoadStructure(*this)); + Schedule(command.release()); + } + } + + + OrthancStone::IVolumeSlicer::IExtractedSlice* DicomStructureSetLoader::ExtractSlice(const OrthancStone::CoordinateSystem3D& cuttingPlane) + { + if (content_.get() == NULL) + { + // Geometry is not available yet + return new OrthancStone::IVolumeSlicer::InvalidSlice; + } + else + { + return new Slice(*content_, revision_, cuttingPlane, structureVisibility_); + } + } + + void DicomStructureSetLoader::SetStructuresReady() + { + ORTHANC_ASSERT(!structuresReady_); + structuresReady_ = true; + BroadcastMessage(DicomStructureSetLoader::StructuresReady(*this)); + } + + bool DicomStructureSetLoader::AreStructuresReady() const + { + return structuresReady_; + } + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Deprecated/Loaders/DicomStructureSetLoader.h Tue Mar 31 15:51:02 2020 +0200 @@ -0,0 +1,104 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 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 "../../Toolbox/DicomStructureSet.h" +#include "../../Volumes/IVolumeSlicer.h" +#include "LoaderStateMachine.h" + +#include <vector> + +namespace Deprecated +{ + class DicomStructureSetLoader : + public LoaderStateMachine, + public OrthancStone::IVolumeSlicer, + public OrthancStone::IObservable + { + private: + class Slice; + + // States of LoaderStateMachine + class AddReferencedInstance; // 3rd state + class LookupInstance; // 2nd state + class LoadStructure; // 1st state + + OrthancStone::ILoadersContext& loadersContext_; + std::unique_ptr<OrthancStone::DicomStructureSet> content_; + uint64_t revision_; + std::string instanceId_; + unsigned int countProcessedInstances_; + unsigned int countReferencedInstances_; + + // will be set to true once the loading is finished + bool structuresReady_; + + /** + At load time, these strings are used to initialize the structureVisibility_ + vector. + + As a special case, if initiallyVisibleStructures_ contains a single string + that is '*', ALL structures will be made visible. + */ + std::vector<std::string> initiallyVisibleStructures_; + + /** + Contains the "Should this structure be displayed?" flag for all structures. + Only filled when structures are loaded. + + Changing this value directly affects the rendering + */ + std::vector<bool> structureVisibility_; + + protected: + DicomStructureSetLoader(OrthancStone::ILoadersContext& loadersContext); + + public: + ORTHANC_STONE_DEFINE_ORIGIN_MESSAGE(__FILE__, __LINE__, StructuresReady, DicomStructureSetLoader); + + static boost::shared_ptr<DicomStructureSetLoader> Create( + OrthancStone::ILoadersContext& loadersContext); + + OrthancStone::DicomStructureSet* GetContent() + { + return content_.get(); + } + + void SetStructureDisplayState(size_t structureIndex, bool display); + + bool GetStructureDisplayState(size_t structureIndex) const + { + return structureVisibility_.at(structureIndex); + } + + ~DicomStructureSetLoader(); + + void LoadInstance(const std::string& instanceId, + const std::vector<std::string>& initiallyVisibleStructures = std::vector<std::string>()); + + virtual IExtractedSlice* ExtractSlice(const OrthancStone::CoordinateSystem3D& cuttingPlane) ORTHANC_OVERRIDE; + + void SetStructuresReady(); + + bool AreStructuresReady() const; + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Deprecated/Loaders/DicomStructureSetLoader2.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -0,0 +1,125 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 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/>. + **/ + +#ifdef BGO_ENABLE_DICOMSTRUCTURESETLOADER2 + +#include "DicomStructureSetLoader2.h" + +#include "../Messages/IObservable.h" +#include "../Oracle/IOracle.h" +#include "../Oracle/OracleCommandExceptionMessage.h" + +namespace Deprecated +{ + + DicomStructureSetLoader2::DicomStructureSetLoader2( + DicomStructureSet2& structureSet + , IOracle& oracle + , IObservable& oracleObservable) + : IObserver(oracleObservable.GetBroker()) + , IObservable(oracleObservable.GetBroker()) + , structureSet_(structureSet) + , oracle_(oracle) + , oracleObservable_(oracleObservable) + , structuresReady_(false) + { + LOG(TRACE) << "DicomStructureSetLoader2(" << std::hex << this << std::dec << ")::DicomStructureSetLoader2()"; + + oracleObservable.RegisterObserverCallback( + new Callable<DicomStructureSetLoader2, OrthancRestApiCommand::SuccessMessage> + (*this, &DicomStructureSetLoader2::HandleSuccessMessage)); + + oracleObservable.RegisterObserverCallback( + new Callable<DicomStructureSetLoader2, OracleCommandExceptionMessage> + (*this, &DicomStructureSetLoader2::HandleExceptionMessage)); + } + + DicomStructureSetLoader2::~DicomStructureSetLoader2() + { + LOG(TRACE) << "DicomStructureSetLoader2(" << std::hex << this << std::dec << ")::~DicomStructureSetLoader2()"; + oracleObservable_.Unregister(this); + } + + void DicomStructureSetLoader2::LoadInstanceFromString(const std::string& body) + { + OrthancPlugins::FullOrthancDataset dicom(body); + //loader.content_.reset(new DicomStructureSet(dicom)); + structureSet_.Clear(); + structureSet_.SetContents(dicom); + SetStructuresReady(); + } + + void DicomStructureSetLoader2::HandleSuccessMessage(const OrthancRestApiCommand::SuccessMessage& message) + { + const std::string& body = message.GetAnswer(); + LoadInstanceFromString(body); + } + + void DicomStructureSetLoader2::HandleExceptionMessage(const OracleCommandExceptionMessage& message) + { + LOG(ERROR) << "DicomStructureSetLoader2::HandleExceptionMessage: error when trying to load data. " + << "Error: " << message.GetException().What() << " Details: " + << message.GetException().GetDetails(); + } + + void DicomStructureSetLoader2::LoadInstance(const std::string& instanceId) + { + std::unique_ptr<OrthancRestApiCommand> command(new OrthancRestApiCommand); + command->SetHttpHeader("Accept-Encoding", "gzip"); + + std::string uri = "/instances/" + instanceId + "/tags?ignore-length=3006-0050"; + + command->SetUri(uri); + oracle_.Schedule(*this, command.release()); + } + + void DicomStructureSetLoader2::SetStructuresReady() + { + structuresReady_ = true; + } + + bool DicomStructureSetLoader2::AreStructuresReady() const + { + return structuresReady_; + } + + /* + + void LoaderStateMachine::HandleExceptionMessage(const OracleCommandExceptionMessage& message) + { + LOG(ERROR) << "LoaderStateMachine::HandleExceptionMessage: error in the state machine, stopping all processing"; + LOG(ERROR) << "Error: " << message.GetException().What() << " Details: " << + message.GetException().GetDetails(); + Clear(); + } + + LoaderStateMachine::~LoaderStateMachine() + { + Clear(); + } + + + */ + +} + +#endif +// BGO_ENABLE_DICOMSTRUCTURESETLOADER2 +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Deprecated/Loaders/DicomStructureSetLoader2.h Tue Mar 31 15:51:02 2020 +0200 @@ -0,0 +1,87 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 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 + +#ifdef BGO_ENABLE_DICOMSTRUCTURESETLOADER2 + +#include "../Toolbox/DicomStructureSet2.h" +#include "../Messages/IMessage.h" +#include "../Messages/IObserver.h" +#include "../Messages/IObservable.h" +#include "../Oracle/OrthancRestApiCommand.h" + +#include <boost/noncopyable.hpp> + +namespace Deprecated +{ + class IOracle; + class IObservable; + class OrthancRestApiCommand; + class OracleCommandExceptionMessage; + + class DicomStructureSetLoader2 : public IObserver, public IObservable + { + public: + ORTHANC_STONE_DEFINE_ORIGIN_MESSAGE(__FILE__, __LINE__, StructuresReady, DicomStructureSetLoader2); + + /** + Warning: the structureSet, oracle and oracleObservable objects must live + at least as long as this object (TODO: shared_ptr?) + */ + DicomStructureSetLoader2(DicomStructureSet2& structureSet, IOracle& oracle, IObservable& oracleObservable); + + ~DicomStructureSetLoader2(); + + void LoadInstance(const std::string& instanceId); + + /** Internal use */ + void LoadInstanceFromString(const std::string& body); + + void SetStructuresReady(); + bool AreStructuresReady() const; + + private: + /** + Called back by the oracle when data is ready! + */ + void HandleSuccessMessage(const OrthancRestApiCommand::SuccessMessage& message); + + /** + Called back by the oracle when shit hits the fan + */ + void HandleExceptionMessage(const OracleCommandExceptionMessage& message); + + /** + The structure set that will be (cleared and) filled with data from the + loader + */ + DicomStructureSet2& structureSet_; + + IOracle& oracle_; + IObservable& oracleObservable_; + bool structuresReady_; + }; +} + +#endif +// BGO_ENABLE_DICOMSTRUCTURESETLOADER2 + +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Deprecated/Loaders/LoaderCache.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -0,0 +1,384 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 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 "LoaderCache.h" + +#include "../../StoneException.h" +#include "OrthancSeriesVolumeProgressiveLoader.h" +#include "OrthancMultiframeVolumeLoader.h" +#include "DicomStructureSetLoader.h" + +#include "../../Loaders/ILoadersContext.h" + + +#ifdef BGO_ENABLE_DICOMSTRUCTURESETLOADER2 +#include "DicomStructureSetLoader2.h" +#endif + //BGO_ENABLE_DICOMSTRUCTURESETLOADER2 + + +#if ORTHANC_ENABLE_WASM == 1 +# include <unistd.h> +# include "../../Oracle/WebAssemblyOracle.h" +#else +# include "../../Oracle/ThreadedOracle.h" +#endif + +#ifdef BGO_ENABLE_DICOMSTRUCTURESETLOADER2 +#include "../../Toolbox/DicomStructureSet2.h" +#endif +//BGO_ENABLE_DICOMSTRUCTURESETLOADER2 + +#include "../../Volumes/DicomVolumeImage.h" +#include "../../Volumes/DicomVolumeImageMPRSlicer.h" + +#ifdef BGO_ENABLE_DICOMSTRUCTURESETLOADER2 +#include "../../Volumes/DicomStructureSetSlicer2.h" +#endif +//BGO_ENABLE_DICOMSTRUCTURESETLOADER2 + +#include <Core/OrthancException.h> +#include <Core/Toolbox.h> + +namespace Deprecated +{ + LoaderCache::LoaderCache(OrthancStone::ILoadersContext& loadersContext) + : loadersContext_(loadersContext) + { + + } + + boost::shared_ptr<OrthancSeriesVolumeProgressiveLoader> + LoaderCache::GetSeriesVolumeProgressiveLoader(std::string seriesUuid) + { + try + { + + // normalize keys a little + seriesUuid = Orthanc::Toolbox::StripSpaces(seriesUuid); + Orthanc::Toolbox::ToLowerCase(seriesUuid); + + // find in cache + if (seriesVolumeProgressiveLoaders_.find(seriesUuid) == seriesVolumeProgressiveLoaders_.end()) + { + std::unique_ptr<OrthancStone::ILoadersContext::ILock> lock(loadersContext_.Lock()); + + boost::shared_ptr<OrthancStone::DicomVolumeImage> volumeImage(new OrthancStone::DicomVolumeImage); + boost::shared_ptr<OrthancSeriesVolumeProgressiveLoader> loader; + + // true means "use progressive quality" + // false means "load high quality slices only" + loader = OrthancSeriesVolumeProgressiveLoader::Create(loadersContext_, volumeImage, false); + loader->LoadSeries(seriesUuid); + seriesVolumeProgressiveLoaders_[seriesUuid] = loader; + } + else + { +// LOG(TRACE) << "LoaderCache::GetSeriesVolumeProgressiveLoader : returning cached loader for seriesUUid = " << seriesUuid; + } + return seriesVolumeProgressiveLoaders_[seriesUuid]; + } + catch (const Orthanc::OrthancException& e) + { + if (e.HasDetails()) + { + LOG(ERROR) << "OrthancException in LoaderCache: " << e.What() << " Details: " << e.GetDetails(); + } + else + { + LOG(ERROR) << "OrthancException in LoaderCache: " << e.What(); + } + throw; + } + catch (const std::exception& e) + { + LOG(ERROR) << "std::exception in LoaderCache: " << e.what(); + throw; + } + catch (...) + { + LOG(ERROR) << "Unknown exception in LoaderCache"; + throw; + } + } + + boost::shared_ptr<OrthancMultiframeVolumeLoader> LoaderCache::GetMultiframeVolumeLoader(std::string instanceUuid) + { + // if the loader is not available, let's trigger its creation + if(multiframeVolumeLoaders_.find(instanceUuid) == multiframeVolumeLoaders_.end()) + { + GetMultiframeDicomVolumeImageMPRSlicer(instanceUuid); + } + ORTHANC_ASSERT(multiframeVolumeLoaders_.find(instanceUuid) != multiframeVolumeLoaders_.end()); + + return multiframeVolumeLoaders_[instanceUuid]; + } + + boost::shared_ptr<OrthancStone::DicomVolumeImageMPRSlicer> LoaderCache::GetMultiframeDicomVolumeImageMPRSlicer(std::string instanceUuid) + { + try + { + // normalize keys a little + instanceUuid = Orthanc::Toolbox::StripSpaces(instanceUuid); + Orthanc::Toolbox::ToLowerCase(instanceUuid); + + // find in cache + if (dicomVolumeImageMPRSlicers_.find(instanceUuid) == dicomVolumeImageMPRSlicers_.end()) + { + std::unique_ptr<OrthancStone::ILoadersContext::ILock> lock(loadersContext_.Lock()); + boost::shared_ptr<OrthancStone::DicomVolumeImage> volumeImage(new OrthancStone::DicomVolumeImage); + boost::shared_ptr<OrthancMultiframeVolumeLoader> loader; + { + loader = OrthancMultiframeVolumeLoader::Create(loadersContext_, volumeImage); + loader->LoadInstance(instanceUuid); + } + multiframeVolumeLoaders_[instanceUuid] = loader; + boost::shared_ptr<OrthancStone::DicomVolumeImageMPRSlicer> mprSlicer(new OrthancStone::DicomVolumeImageMPRSlicer(volumeImage)); + dicomVolumeImageMPRSlicers_[instanceUuid] = mprSlicer; + } + return dicomVolumeImageMPRSlicers_[instanceUuid]; + } + catch (const Orthanc::OrthancException& e) + { + if (e.HasDetails()) + { + LOG(ERROR) << "OrthancException in LoaderCache: " << e.What() << " Details: " << e.GetDetails(); + } + else + { + LOG(ERROR) << "OrthancException in LoaderCache: " << e.What(); + } + throw; + } + catch (const std::exception& e) + { + LOG(ERROR) << "std::exception in LoaderCache: " << e.what(); + throw; + } + catch (...) + { + LOG(ERROR) << "Unknown exception in LoaderCache"; + throw; + } + } + +#ifdef BGO_ENABLE_DICOMSTRUCTURESETLOADER2 + + boost::shared_ptr<DicomStructureSetSlicer2> LoaderCache::GetDicomStructureSetSlicer2(std::string instanceUuid) + { + // if the loader is not available, let's trigger its creation + if (dicomStructureSetSlicers2_.find(instanceUuid) == dicomStructureSetSlicers2_.end()) + { + GetDicomStructureSetLoader2(instanceUuid); + } + ORTHANC_ASSERT(dicomStructureSetSlicers2_.find(instanceUuid) != dicomStructureSetSlicers2_.end()); + + return dicomStructureSetSlicers2_[instanceUuid]; + } +#endif +//BGO_ENABLE_DICOMSTRUCTURESETLOADER2 + + + /** + This method allows to convert a list of string into a string by + sorting the strings then joining them + */ + static std::string SortAndJoin(const std::vector<std::string>& stringList) + { + if (stringList.size() == 0) + { + return ""; + } + else + { + std::vector<std::string> sortedStringList = stringList; + std::sort(sortedStringList.begin(), sortedStringList.end()); + std::stringstream s; + s << sortedStringList[0]; + for (size_t i = 1; i < sortedStringList.size(); ++i) + { + s << "-" << sortedStringList[i]; + } + return s.str(); + } + } + + boost::shared_ptr<DicomStructureSetLoader> + LoaderCache::GetDicomStructureSetLoader( + std::string inInstanceUuid, + const std::vector<std::string>& initiallyVisibleStructures) + { + try + { + // normalize keys a little + inInstanceUuid = Orthanc::Toolbox::StripSpaces(inInstanceUuid); + Orthanc::Toolbox::ToLowerCase(inInstanceUuid); + + std::string initiallyVisibleStructuresKey = + SortAndJoin(initiallyVisibleStructures); + + std::string entryKey = inInstanceUuid + "_" + initiallyVisibleStructuresKey; + + // find in cache + if (dicomStructureSetLoaders_.find(entryKey) == dicomStructureSetLoaders_.end()) + { + std::unique_ptr<OrthancStone::ILoadersContext::ILock> lock(loadersContext_.Lock()); + + boost::shared_ptr<DicomStructureSetLoader> loader; + { + loader = DicomStructureSetLoader::Create(loadersContext_); + loader->LoadInstance(inInstanceUuid, initiallyVisibleStructures); + } + dicomStructureSetLoaders_[entryKey] = loader; + } + return dicomStructureSetLoaders_[entryKey]; + } + catch (const Orthanc::OrthancException& e) + { + if (e.HasDetails()) + { + LOG(ERROR) << "OrthancException in LoaderCache: " << e.What() << " Details: " << e.GetDetails(); + } + else + { + LOG(ERROR) << "OrthancException in LoaderCache: " << e.What(); + } + throw; + } + catch (const std::exception& e) + { + LOG(ERROR) << "std::exception in LoaderCache: " << e.what(); + throw; + } + catch (...) + { + LOG(ERROR) << "Unknown exception in LoaderCache"; + throw; + } + } + +#ifdef BGO_ENABLE_DICOMSTRUCTURESETLOADER2 + + boost::shared_ptr<DicomStructureSetLoader2> LoaderCache::GetDicomStructureSetLoader2(std::string instanceUuid) + { + try + { + // normalize keys a little + instanceUuid = Orthanc::Toolbox::StripSpaces(instanceUuid); + Orthanc::Toolbox::ToLowerCase(instanceUuid); + + // find in cache + if (dicomStructureSetLoaders2_.find(instanceUuid) == dicomStructureSetLoaders2_.end()) + { + boost::shared_ptr<DicomStructureSetLoader2> loader; + boost::shared_ptr<DicomStructureSet2> structureSet(new DicomStructureSet2()); + boost::shared_ptr<DicomStructureSetSlicer2> rtSlicer(new DicomStructureSetSlicer2(structureSet)); + dicomStructureSetSlicers2_[instanceUuid] = rtSlicer; + dicomStructureSets2_[instanceUuid] = structureSet; // to prevent it from being deleted + { +#if ORTHANC_ENABLE_WASM == 1 + loader.reset(new DicomStructureSetLoader2(*(structureSet.get()), oracle_, oracle_)); +#else + LockingEmitter::WriterLock lock(lockingEmitter_); + // TODO: clarify lifetimes... this is DANGEROUS! + loader.reset(new DicomStructureSetLoader2(*(structureSet.get()), oracle_, lock.GetOracleObservable())); +#endif + loader->LoadInstance(instanceUuid); + } + dicomStructureSetLoaders2_[instanceUuid] = loader; + } + return dicomStructureSetLoaders2_[instanceUuid]; + } + catch (const Orthanc::OrthancException& e) + { + if (e.HasDetails()) + { + LOG(ERROR) << "OrthancException in GetDicomStructureSetLoader2: " << e.What() << " Details: " << e.GetDetails(); + } + else + { + LOG(ERROR) << "OrthancException in GetDicomStructureSetLoader2: " << e.What(); + } + throw; + } + catch (const std::exception& e) + { + LOG(ERROR) << "std::exception in GetDicomStructureSetLoader2: " << e.what(); + throw; + } + catch (...) + { + LOG(ERROR) << "Unknown exception in GetDicomStructureSetLoader2"; + throw; + } + } + +#endif +// BGO_ENABLE_DICOMSTRUCTURESETLOADER2 + + + void LoaderCache::ClearCache() + { + std::unique_ptr<OrthancStone::ILoadersContext::ILock> lock(loadersContext_.Lock()); + +#ifndef NDEBUG + // ISO way of checking for debug builds + DebugDisplayObjRefCounts(); +#endif + seriesVolumeProgressiveLoaders_.clear(); + multiframeVolumeLoaders_.clear(); + dicomVolumeImageMPRSlicers_.clear(); + dicomStructureSetLoaders_.clear(); + +#ifdef BGO_ENABLE_DICOMSTRUCTURESETLOADER2 + // order is important! + dicomStructureSetLoaders2_.clear(); + dicomStructureSetSlicers2_.clear(); + dicomStructureSets2_.clear(); +#endif +// BGO_ENABLE_DICOMSTRUCTURESETLOADER2 + } + + template<typename T> void DebugDisplayObjRefCountsInMap( + const std::string& name, const std::map<std::string, boost::shared_ptr<T> >& myMap) + { + LOG(TRACE) << "Map \"" << name << "\" ref counts:"; + size_t i = 0; + for (typename std::map<std::string, boost::shared_ptr<T> >::const_iterator + it = myMap.begin(); it != myMap.end(); ++it) + { + LOG(TRACE) << " element #" << i << ": ref count = " << it->second.use_count(); + i++; + } + } + + void LoaderCache::DebugDisplayObjRefCounts() + { + DebugDisplayObjRefCountsInMap("seriesVolumeProgressiveLoaders_", seriesVolumeProgressiveLoaders_); + DebugDisplayObjRefCountsInMap("multiframeVolumeLoaders_", multiframeVolumeLoaders_); + DebugDisplayObjRefCountsInMap("dicomVolumeImageMPRSlicers_", dicomVolumeImageMPRSlicers_); + DebugDisplayObjRefCountsInMap("dicomStructureSetLoaders_", dicomStructureSetLoaders_); +#ifdef BGO_ENABLE_DICOMSTRUCTURESETLOADER2 + DebugDisplayObjRefCountsInMap("dicomStructureSetLoaders2_", dicomStructureSetLoaders2_); + DebugDisplayObjRefCountsInMap("dicomStructureSetSlicers2_", dicomStructureSetSlicers2_); +#endif +//BGO_ENABLE_DICOMSTRUCTURESETLOADER2 + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Deprecated/Loaders/LoaderCache.h Tue Mar 31 15:51:02 2020 +0200 @@ -0,0 +1,97 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 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 "../Messages/LockingEmitter.h" +#include "../../Volumes/DicomVolumeImageMPRSlicer.h" +#include "OrthancSeriesVolumeProgressiveLoader.h" +#include "OrthancMultiframeVolumeLoader.h" +#include "DicomStructureSetLoader.h" + +#include <boost/shared_ptr.hpp> + +#include <map> +#include <string> +#include <vector> + +namespace OrthancStone +{ + class ILoadersContext; +} + +namespace Deprecated +{ + class LoaderCache + { + public: + LoaderCache(OrthancStone::ILoadersContext& loadersContext); + + boost::shared_ptr<OrthancSeriesVolumeProgressiveLoader> + GetSeriesVolumeProgressiveLoader (std::string seriesUuid); + + boost::shared_ptr<OrthancStone::DicomVolumeImageMPRSlicer> + GetMultiframeDicomVolumeImageMPRSlicer(std::string instanceUuid); + + boost::shared_ptr<OrthancMultiframeVolumeLoader> + GetMultiframeVolumeLoader(std::string instanceUuid); + + boost::shared_ptr<DicomStructureSetLoader> + GetDicomStructureSetLoader( + std::string instanceUuid, + const std::vector<std::string>& initiallyVisibleStructures); + +#ifdef BGO_ENABLE_DICOMSTRUCTURESETLOADER2 + boost::shared_ptr<DicomStructureSetLoader2> + GetDicomStructureSetLoader2(std::string instanceUuid); + + boost::shared_ptr<DicomStructureSetSlicer2> + GetDicomStructureSetSlicer2(std::string instanceUuid); +#endif + //BGO_ENABLE_DICOMSTRUCTURESETLOADER2 + + void ClearCache(); + + private: + + void DebugDisplayObjRefCounts(); + + OrthancStone::ILoadersContext& loadersContext_; + + std::map<std::string, boost::shared_ptr<OrthancSeriesVolumeProgressiveLoader> > + seriesVolumeProgressiveLoaders_; + std::map<std::string, boost::shared_ptr<OrthancMultiframeVolumeLoader> > + multiframeVolumeLoaders_; + std::map<std::string, boost::shared_ptr<OrthancStone::DicomVolumeImageMPRSlicer> > + dicomVolumeImageMPRSlicers_; + std::map<std::string, boost::shared_ptr<DicomStructureSetLoader> > + dicomStructureSetLoaders_; +#ifdef BGO_ENABLE_DICOMSTRUCTURESETLOADER2 + std::map<std::string, boost::shared_ptr<DicomStructureSetLoader2> > + dicomStructureSetLoaders2_; + std::map<std::string, boost::shared_ptr<DicomStructureSet2> > + dicomStructureSets2_; + std::map<std::string, boost::shared_ptr<DicomStructureSetSlicer2> > + dicomStructureSetSlicers2_; +#endif + //BGO_ENABLE_DICOMSTRUCTURESETLOADER2 + }; +} +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Deprecated/Loaders/LoaderStateMachine.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -0,0 +1,219 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 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 "LoaderStateMachine.h" + +#include "../../Loaders/ILoadersContext.h" + +#include <Core/OrthancException.h> + +namespace Deprecated +{ + void LoaderStateMachine::State::Handle(const OrthancStone::OrthancRestApiCommand::SuccessMessage& message) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); + } + + + void LoaderStateMachine::State::Handle(const OrthancStone::GetOrthancImageCommand::SuccessMessage& message) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); + } + + + void LoaderStateMachine::State::Handle(const OrthancStone::GetOrthancWebViewerJpegCommand::SuccessMessage& message) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); + } + + + void LoaderStateMachine::Schedule(OrthancStone::OracleCommandBase* command) + { + LOG(TRACE) << "LoaderStateMachine(" << std::hex << this << std::dec << ")::Schedule()"; + + std::unique_ptr<OrthancStone::OracleCommandBase> protection(command); + + if (command == NULL) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); + } + + if (!command->HasPayload()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange, + "The payload must contain the next state"); + } + pendingCommands_.push_back(protection.release()); + + Step(); + } + + + void LoaderStateMachine::Start() + { + LOG(TRACE) << "LoaderStateMachine(" << std::hex << this << std::dec << ")::Start()"; + + if (active_) + { + LOG(TRACE) << "LoaderStateMachine::Start() called while active_ is true"; + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + + active_ = true; + + for (size_t i = 0; i < simultaneousDownloads_; i++) + { + Step(); + } + } + + + void LoaderStateMachine::Step() + { + if (!pendingCommands_.empty() && + activeCommands_ < simultaneousDownloads_) + { + + OrthancStone::IOracleCommand* nextCommand = pendingCommands_.front(); + + LOG(TRACE) << " LoaderStateMachine(" << std::hex << this << std::dec << + ")::Step(): activeCommands_ (" << activeCommands_ << + ") < simultaneousDownloads_ (" << simultaneousDownloads_ << + ") --> will Schedule command addr " << std::hex << nextCommand << std::dec; + + { + std::unique_ptr<OrthancStone::ILoadersContext::ILock> lock(loadersContext_.Lock()); + boost::shared_ptr<IObserver> observer(GetSharedObserver()); + lock->Schedule(observer, 0, nextCommand); // TODO: priority! + } + pendingCommands_.pop_front(); + + activeCommands_++; + } + else + { + LOG(TRACE) << " LoaderStateMachine(" << std::hex << this << std::dec << + ")::Step(): activeCommands_ (" << activeCommands_ << + ") >= simultaneousDownloads_ (" << simultaneousDownloads_ << + ") --> will NOT Schedule command"; + } + } + + + void LoaderStateMachine::Clear() + { + LOG(TRACE) << "LoaderStateMachine(" << std::hex << this << std::dec << ")::Clear()"; + for (PendingCommands::iterator it = pendingCommands_.begin(); + it != pendingCommands_.end(); ++it) + { + delete *it; + } + + pendingCommands_.clear(); + } + + + void LoaderStateMachine::HandleExceptionMessage(const OrthancStone::OracleCommandExceptionMessage& message) + { + LOG(ERROR) << "LoaderStateMachine::HandleExceptionMessage: error in the state machine, stopping all processing"; + LOG(ERROR) << "Error: " << message.GetException().What() << " Details: " << + message.GetException().GetDetails(); + Clear(); + } + + template <typename T> + void LoaderStateMachine::HandleSuccessMessage(const T& message) + { + if (activeCommands_ <= 0) { + LOG(ERROR) << "LoaderStateMachine(" << std::hex << this << std::dec << ")::HandleSuccessMessage : activeCommands_ should be > 0 but is: " << activeCommands_; + } + else { + activeCommands_--; + try + { + dynamic_cast<State&>(message.GetOrigin().GetPayload()).Handle(message); + Step(); + } + catch (Orthanc::OrthancException& e) + { + LOG(ERROR) << "Error in the state machine, stopping all processing: " << + e.What() << " Details: " << e.GetDetails(); + Clear(); + } + } + } + + + LoaderStateMachine::LoaderStateMachine( + OrthancStone::ILoadersContext& loadersContext) + : loadersContext_(loadersContext) + , active_(false) + , simultaneousDownloads_(4) + , activeCommands_(0) + { + using OrthancStone::ILoadersContext; + + LOG(TRACE) + << "LoaderStateMachine(" << std::hex << this + << std::dec << ")::LoaderStateMachine()"; + } + + void LoaderStateMachine::PostConstructor() + { + std::unique_ptr<OrthancStone::ILoadersContext::ILock> + lock(loadersContext_.Lock()); + + OrthancStone::IObservable& observable = lock->GetOracleObservable(); + + // TODO => Move this out of constructor + Register<OrthancStone::OrthancRestApiCommand::SuccessMessage>( + observable, &LoaderStateMachine::HandleSuccessMessage); + Register<OrthancStone::GetOrthancImageCommand::SuccessMessage>( + observable, &LoaderStateMachine::HandleSuccessMessage); + Register<OrthancStone::GetOrthancWebViewerJpegCommand::SuccessMessage>( + observable, &LoaderStateMachine::HandleSuccessMessage); + Register<OrthancStone::OracleCommandExceptionMessage>( + observable, &LoaderStateMachine::HandleExceptionMessage); + } + + LoaderStateMachine::~LoaderStateMachine() + { + LOG(TRACE) << "LoaderStateMachine(" << std::hex << this << std::dec << ")::~LoaderStateMachine()"; + Clear(); + } + + void LoaderStateMachine::SetSimultaneousDownloads(unsigned int count) + { + if (active_) + { + LOG(ERROR) << "LoaderStateMachine::SetSimultaneousDownloads called while active_ is true"; + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + else if (count == 0) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + else + { + simultaneousDownloads_ = count; + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Deprecated/Loaders/LoaderStateMachine.h Tue Mar 31 15:51:02 2020 +0200 @@ -0,0 +1,129 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 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 "../../Messages/IObservable.h" +#include "../../Messages/ObserverBase.h" +#include "../../Oracle/GetOrthancImageCommand.h" +#include "../../Oracle/GetOrthancWebViewerJpegCommand.h" +#include "../../Oracle/IOracle.h" +#include "../../Oracle/OracleCommandExceptionMessage.h" +#include "../../Oracle/OrthancRestApiCommand.h" + +#include <Core/IDynamicObject.h> + +#include <list> + +namespace OrthancStone +{ + class ILoadersContext; +} + +namespace Deprecated +{ + /** + This class is supplied with Oracle commands and will schedule up to + simultaneousDownloads_ of them at the same time, then will schedule the + rest once slots become available. It is used, a.o., by the + OrtancMultiframeVolumeLoader class. + + To use it, you need to create commands that derive from State. + + You need to initialize them with the object that must be called when + an answer is received. + */ + + class LoaderStateMachine : public OrthancStone::ObserverBase<LoaderStateMachine> + { + public: + class State : public Orthanc::IDynamicObject + { + private: + LoaderStateMachine& that_; + + public: + State(LoaderStateMachine& that) : + that_(that) + { + } + + State(const State& currentState) : + that_(currentState.that_) + { + } + + void Schedule(OrthancStone::OracleCommandBase* command) const + { + that_.Schedule(command); + } + + template <typename T> + T& GetLoader() const + { + return dynamic_cast<T&>(that_); + } + + virtual void Handle(const OrthancStone::OrthancRestApiCommand::SuccessMessage& message); + + virtual void Handle(const OrthancStone::GetOrthancImageCommand::SuccessMessage& message); + + virtual void Handle(const OrthancStone::GetOrthancWebViewerJpegCommand::SuccessMessage& message); + }; + + void Schedule(OrthancStone::OracleCommandBase* command); + + void Start(); + + private: + void Step(); + + void Clear(); + + void HandleExceptionMessage(const OrthancStone::OracleCommandExceptionMessage& message); + + template <typename T> + void HandleSuccessMessage(const T& message); + + typedef std::list<OrthancStone::IOracleCommand*> PendingCommands; + + OrthancStone::ILoadersContext& loadersContext_; + bool active_; + unsigned int simultaneousDownloads_; + PendingCommands pendingCommands_; + unsigned int activeCommands_; + + + public: + LoaderStateMachine(OrthancStone::ILoadersContext& loadersContext); + + void PostConstructor(); + + virtual ~LoaderStateMachine(); + + bool IsActive() const + { + return active_; + } + + void SetSimultaneousDownloads(unsigned int count); + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Deprecated/Loaders/OrthancMultiframeVolumeLoader.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -0,0 +1,593 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 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 "OrthancMultiframeVolumeLoader.h" + +#include <Core/Endianness.h> +#include <Core/Toolbox.h> + +namespace Deprecated +{ + class OrthancMultiframeVolumeLoader::LoadRTDoseGeometry : public LoaderStateMachine::State + { + private: + std::unique_ptr<Orthanc::DicomMap> dicom_; + + public: + LoadRTDoseGeometry(OrthancMultiframeVolumeLoader& that, + Orthanc::DicomMap* dicom) : + State(that), + dicom_(dicom) + { + if (dicom == NULL) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); + } + + } + + virtual void Handle(const OrthancStone::OrthancRestApiCommand::SuccessMessage& message) + { + // Complete the DICOM tags with just-received "Grid Frame Offset Vector" + std::string s = Orthanc::Toolbox::StripSpaces(message.GetAnswer()); + dicom_->SetValue(Orthanc::DICOM_TAG_GRID_FRAME_OFFSET_VECTOR, s, false); + + GetLoader<OrthancMultiframeVolumeLoader>().SetGeometry(*dicom_); + } + }; + + + static std::string GetSopClassUid(const Orthanc::DicomMap& dicom) + { + std::string s; + if (!dicom.LookupStringValue(s, Orthanc::DICOM_TAG_SOP_CLASS_UID, false)) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, + "DICOM file without SOP class UID"); + } + else + { + return s; + } + } + + + class OrthancMultiframeVolumeLoader::LoadGeometry : public State + { + public: + LoadGeometry(OrthancMultiframeVolumeLoader& that) : + State(that) + { + } + + virtual void Handle(const OrthancStone::OrthancRestApiCommand::SuccessMessage& message) + { + OrthancMultiframeVolumeLoader& loader = GetLoader<OrthancMultiframeVolumeLoader>(); + + Json::Value body; + message.ParseJsonBody(body); + + if (body.type() != Json::objectValue) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol); + } + + std::unique_ptr<Orthanc::DicomMap> dicom(new Orthanc::DicomMap); + dicom->FromDicomAsJson(body); + + if (OrthancStone::StringToSopClassUid(GetSopClassUid(*dicom)) == OrthancStone::SopClassUid_RTDose) + { + // Download the "Grid Frame Offset Vector" DICOM tag, that is + // mandatory for RT-DOSE, but is too long to be returned by default + + std::unique_ptr<OrthancStone::OrthancRestApiCommand> command(new OrthancStone::OrthancRestApiCommand); + command->SetUri("/instances/" + loader.GetInstanceId() + "/content/" + + Orthanc::DICOM_TAG_GRID_FRAME_OFFSET_VECTOR.Format()); + command->AcquirePayload(new LoadRTDoseGeometry(loader, dicom.release())); + + Schedule(command.release()); + } + else + { + loader.SetGeometry(*dicom); + } + } + }; + + class OrthancMultiframeVolumeLoader::LoadTransferSyntax : public State + { + public: + LoadTransferSyntax(OrthancMultiframeVolumeLoader& that) : + State(that) + { + } + + virtual void Handle(const OrthancStone::OrthancRestApiCommand::SuccessMessage& message) + { + GetLoader<OrthancMultiframeVolumeLoader>().SetTransferSyntax(message.GetAnswer()); + } + }; + + class OrthancMultiframeVolumeLoader::LoadUncompressedPixelData : public State + { + public: + LoadUncompressedPixelData(OrthancMultiframeVolumeLoader& that) : + State(that) + { + } + + virtual void Handle(const OrthancStone::OrthancRestApiCommand::SuccessMessage& message) + { + GetLoader<OrthancMultiframeVolumeLoader>().SetUncompressedPixelData(message.GetAnswer()); + } + }; + + const std::string& OrthancMultiframeVolumeLoader::GetInstanceId() const + { + if (IsActive()) + { + return instanceId_; + } + else + { + LOG(ERROR) << "OrthancMultiframeVolumeLoader::GetInstanceId(): (!IsActive())"; + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + } + + void OrthancMultiframeVolumeLoader::ScheduleFrameDownloads() + { + if (transferSyntaxUid_.empty() || + !volume_->HasGeometry()) + { + return; + } + /* + 1.2.840.10008.1.2 Implicit VR Endian: Default Transfer Syntax for DICOM + 1.2.840.10008.1.2.1 Explicit VR Little Endian + 1.2.840.10008.1.2.2 Explicit VR Big Endian + + See https://www.dicomlibrary.com/dicom/transfer-syntax/ + */ + if (transferSyntaxUid_ == "1.2.840.10008.1.2" || + transferSyntaxUid_ == "1.2.840.10008.1.2.1" || + transferSyntaxUid_ == "1.2.840.10008.1.2.2") + { + std::unique_ptr<OrthancStone::OrthancRestApiCommand> command(new OrthancStone::OrthancRestApiCommand); + command->SetHttpHeader("Accept-Encoding", "gzip"); + command->SetUri("/instances/" + instanceId_ + "/content/" + + Orthanc::DICOM_TAG_PIXEL_DATA.Format() + "/0"); + command->AcquirePayload(new LoadUncompressedPixelData(*this)); + Schedule(command.release()); + } + else + { + throw Orthanc::OrthancException( + Orthanc::ErrorCode_NotImplemented, + "No support for multiframe instances with transfer syntax: " + transferSyntaxUid_); + } + } + + void OrthancMultiframeVolumeLoader::SetTransferSyntax(const std::string& transferSyntax) + { + transferSyntaxUid_ = Orthanc::Toolbox::StripSpaces(transferSyntax); + ScheduleFrameDownloads(); + } + + void OrthancMultiframeVolumeLoader::SetGeometry(const Orthanc::DicomMap& dicom) + { + OrthancStone::DicomInstanceParameters parameters(dicom); + volume_->SetDicomParameters(parameters); + + Orthanc::PixelFormat format; + if (!parameters.GetImageInformation().ExtractPixelFormat(format, true)) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); + } + + double spacingZ; + switch (parameters.GetSopClassUid()) + { + case OrthancStone::SopClassUid_RTDose: + spacingZ = parameters.GetThickness(); + break; + + default: + throw Orthanc::OrthancException( + Orthanc::ErrorCode_NotImplemented, + "No support for multiframe instances with SOP class UID: " + GetSopClassUid(dicom)); + } + + const unsigned int width = parameters.GetImageInformation().GetWidth(); + const unsigned int height = parameters.GetImageInformation().GetHeight(); + const unsigned int depth = parameters.GetImageInformation().GetNumberOfFrames(); + + { + OrthancStone::VolumeImageGeometry geometry; + geometry.SetSizeInVoxels(width, height, depth); + geometry.SetAxialGeometry(parameters.GetGeometry()); + geometry.SetVoxelDimensions(parameters.GetPixelSpacingX(), + parameters.GetPixelSpacingY(), spacingZ); + volume_->Initialize(geometry, format, true /* Do compute range */); + } + + volume_->GetPixelData().Clear(); + + ScheduleFrameDownloads(); + + + + BroadcastMessage(OrthancStone::DicomVolumeImage::GeometryReadyMessage(*volume_)); + } + + + ORTHANC_FORCE_INLINE + static void CopyPixel(uint32_t& target, const void* source) + { + // TODO - check alignement? + target = le32toh(*reinterpret_cast<const uint32_t*>(source)); + } + + ORTHANC_FORCE_INLINE + static void CopyPixel(uint16_t& target, const void* source) + { + // TODO - check alignement? + target = le16toh(*reinterpret_cast<const uint16_t*>(source)); + } + + ORTHANC_FORCE_INLINE + static void CopyPixel(int16_t& target, const void* source) + { + // byte swapping is the same for unsigned and signed integers + // (the sign bit is always stored with the MSByte) + uint16_t* targetUp = reinterpret_cast<uint16_t*>(&target); + CopyPixel(*targetUp, source); + } + + template <typename T> + void OrthancMultiframeVolumeLoader::CopyPixelDataAndComputeDistribution( + const std::string& pixelData, std::map<T,uint64_t>& distribution) + { + OrthancStone::ImageBuffer3D& target = volume_->GetPixelData(); + + const unsigned int bpp = target.GetBytesPerPixel(); + const unsigned int width = target.GetWidth(); + const unsigned int height = target.GetHeight(); + const unsigned int depth = target.GetDepth(); + + if (pixelData.size() != bpp * width * height * depth) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, + "The pixel data has not the proper size"); + } + + if (pixelData.empty()) + { + return; + } + + // first pass to initialize map + { + const uint8_t* source = reinterpret_cast<const uint8_t*>(pixelData.c_str()); + + for (unsigned int z = 0; z < depth; z++) + { + for (unsigned int y = 0; y < height; y++) + { + for (unsigned int x = 0; x < width; x++) + { + T value; + CopyPixel(value, source); + distribution[value] = 0; + source += bpp; + } + } + } + } + + { + const uint8_t* source = reinterpret_cast<const uint8_t*>(pixelData.c_str()); + + for (unsigned int z = 0; z < depth; z++) + { + OrthancStone::ImageBuffer3D::SliceWriter writer(target, OrthancStone::VolumeProjection_Axial, z); + + assert(writer.GetAccessor().GetWidth() == width && + writer.GetAccessor().GetHeight() == height); + + for (unsigned int y = 0; y < height; y++) + { + assert(sizeof(T) == Orthanc::GetBytesPerPixel(target.GetFormat())); + + T* target = reinterpret_cast<T*>(writer.GetAccessor().GetRow(y)); + + for (unsigned int x = 0; x < width; x++) + { + CopyPixel(*target, source); + + distribution[*target] += 1; + + target++; + source += bpp; + } + } + } + } + } + + template <typename T> + void OrthancMultiframeVolumeLoader::ComputeMinMaxWithOutlierRejection( + const std::map<T, uint64_t>& distribution) + { + if (distribution.size() == 0) + { + LOG(ERROR) << "ComputeMinMaxWithOutlierRejection -- Volume image empty."; + } + else + { + OrthancStone::ImageBuffer3D& target = volume_->GetPixelData(); + + const uint64_t width = target.GetWidth(); + const uint64_t height = target.GetHeight(); + const uint64_t depth = target.GetDepth(); + const uint64_t voxelCount = width * height * depth; + + // now that we have distribution[pixelValue] == numberOfPixelsWithValue + // compute number of values and check (assertion) that it is equal to + // width * height * depth + { + typename std::map<T, uint64_t>::const_iterator it = distribution.begin(); + uint64_t totalCount = 0; + distributionRawMin_ = static_cast<float>(it->first); + + while (it != distribution.end()) + { + T pixelValue = it->first; + uint64_t count = it->second; + totalCount += count; + it++; + if (it == distribution.end()) + distributionRawMax_ = static_cast<float>(pixelValue); + } + LOG(INFO) << "Volume image. First distribution value = " + << static_cast<float>(distributionRawMin_) + << " | Last distribution value = " + << static_cast<float>(distributionRawMax_); + + if (totalCount != voxelCount) + { + LOG(ERROR) << "Internal error in dose distribution computation. TC (" + << totalCount << ") != VoxC (" << voxelCount; + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + } + + // compute the number of voxels to reject at each end of the distribution + uint64_t endRejectionCount = static_cast<uint64_t>( + outliersHalfRejectionRate_ * voxelCount); + + if (endRejectionCount > voxelCount) + { + LOG(ERROR) << "Internal error in dose distribution computation." + << " endRejectionCount = " << endRejectionCount + << " | voxelCount = " << voxelCount; + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + + // this will contain the actual distribution minimum after outlier + // rejection + T resultMin = 0; + + // then start from start and remove pixel values up to + // endRejectionCount voxels rejected + { + typename std::map<T, uint64_t>::const_iterator it = distribution.begin(); + + uint64_t currentCount = 0; + + while (it != distribution.end()) + { + T pixelValue = it->first; + uint64_t count = it->second; + + // if this pixelValue crosses the rejection threshold, let's set it + // and exit the loop + if ((currentCount <= endRejectionCount) && + (currentCount + count > endRejectionCount)) + { + resultMin = pixelValue; + break; + } + else + { + currentCount += count; + } + // and continue walking along the distribution + it++; + } + } + + // this will contain the actual distribution maximum after outlier + // rejection + T resultMax = 0; + // now start from END and remove pixel values up to + // endRejectionCount voxels rejected + { + typename std::map<T, uint64_t>::const_reverse_iterator it = distribution.rbegin(); + + uint64_t currentCount = 0; + + while (it != distribution.rend()) + { + T pixelValue = it->first; + uint64_t count = it->second; + + if ((currentCount <= endRejectionCount) && + (currentCount + count > endRejectionCount)) + { + resultMax = pixelValue; + break; + } + else + { + currentCount += count; + } + // and continue walking along the distribution + it++; + } + } + if (resultMin > resultMax) + { + LOG(ERROR) << "Internal error in dose distribution computation! " << + "resultMin (" << resultMin << ") > resultMax (" << resultMax << ")"; + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + computedDistributionMin_ = static_cast<float>(resultMin); + computedDistributionMax_ = static_cast<float>(resultMax); + } + } + + template <typename T> + void OrthancMultiframeVolumeLoader::CopyPixelDataAndComputeMinMax( + const std::string& pixelData) + { + std::map<T, uint64_t> distribution; + CopyPixelDataAndComputeDistribution(pixelData, distribution); + ComputeMinMaxWithOutlierRejection(distribution); + } + + void OrthancMultiframeVolumeLoader::SetUncompressedPixelData(const std::string& pixelData) + { + switch (volume_->GetPixelData().GetFormat()) + { + case Orthanc::PixelFormat_Grayscale32: + CopyPixelDataAndComputeMinMax<uint32_t>(pixelData); + break; + case Orthanc::PixelFormat_Grayscale16: + CopyPixelDataAndComputeMinMax<uint16_t>(pixelData); + break; + case Orthanc::PixelFormat_SignedGrayscale16: + CopyPixelDataAndComputeMinMax<int16_t>(pixelData); + break; + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); + } + + volume_->IncrementRevision(); + + pixelDataLoaded_ = true; + BroadcastMessage(OrthancStone::DicomVolumeImage::ContentUpdatedMessage(*volume_)); + } + + bool OrthancMultiframeVolumeLoader::HasGeometry() const + { + return volume_->HasGeometry(); + } + + const OrthancStone::VolumeImageGeometry& OrthancMultiframeVolumeLoader::GetImageGeometry() const + { + return volume_->GetGeometry(); + } + + OrthancMultiframeVolumeLoader::OrthancMultiframeVolumeLoader( + OrthancStone::ILoadersContext& loadersContext, + boost::shared_ptr<OrthancStone::DicomVolumeImage> volume, + float outliersHalfRejectionRate) + : LoaderStateMachine(loadersContext) + , volume_(volume) + , pixelDataLoaded_(false) + , outliersHalfRejectionRate_(outliersHalfRejectionRate) + , distributionRawMin_(0) + , distributionRawMax_(0) + , computedDistributionMin_(0) + , computedDistributionMax_(0) + { + if (volume.get() == NULL) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); + } + } + + + boost::shared_ptr<OrthancMultiframeVolumeLoader> + OrthancMultiframeVolumeLoader::Create( + OrthancStone::ILoadersContext& loadersContext, + boost::shared_ptr<OrthancStone::DicomVolumeImage> volume, + float outliersHalfRejectionRate /*= 0.0005*/) + { + boost::shared_ptr<OrthancMultiframeVolumeLoader> obj( + new OrthancMultiframeVolumeLoader( + loadersContext, + volume, + outliersHalfRejectionRate)); + obj->LoaderStateMachine::PostConstructor(); + return obj; + } + + OrthancMultiframeVolumeLoader::~OrthancMultiframeVolumeLoader() + { + LOG(TRACE) << "OrthancMultiframeVolumeLoader::~OrthancMultiframeVolumeLoader()"; + } + + void OrthancMultiframeVolumeLoader::GetDistributionMinMax + (float& minValue, float& maxValue) const + { + if (distributionRawMin_ == 0 && distributionRawMax_ == 0) + { + LOG(WARNING) << "GetDistributionMinMaxWithOutliersRejection called before computation!"; + } + minValue = distributionRawMin_; + maxValue = distributionRawMax_; + } + + void OrthancMultiframeVolumeLoader::GetDistributionMinMaxWithOutliersRejection + (float& minValue, float& maxValue) const + { + if (computedDistributionMin_ == 0 && computedDistributionMax_ == 0) + { + LOG(WARNING) << "GetDistributionMinMaxWithOutliersRejection called before computation!"; + } + minValue = computedDistributionMin_; + maxValue = computedDistributionMax_; + } + + void OrthancMultiframeVolumeLoader::LoadInstance(const std::string& instanceId) + { + Start(); + + instanceId_ = instanceId; + + { + std::unique_ptr<OrthancStone::OrthancRestApiCommand> command(new OrthancStone::OrthancRestApiCommand); + command->SetHttpHeader("Accept-Encoding", "gzip"); + command->SetUri("/instances/" + instanceId + "/tags"); + command->AcquirePayload(new LoadGeometry(*this)); + Schedule(command.release()); + } + + { + std::unique_ptr<OrthancStone::OrthancRestApiCommand> command(new OrthancStone::OrthancRestApiCommand); + command->SetUri("/instances/" + instanceId + "/metadata/TransferSyntax"); + command->AcquirePayload(new LoadTransferSyntax(*this)); + Schedule(command.release()); + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Deprecated/Loaders/OrthancMultiframeVolumeLoader.h Tue Mar 31 15:51:02 2020 +0200 @@ -0,0 +1,123 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 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 "LoaderStateMachine.h" +#include "../../Volumes/DicomVolumeImage.h" +#include "../Volumes/IGeometryProvider.h" + +#include <boost/shared_ptr.hpp> + +namespace Deprecated +{ + class OrthancMultiframeVolumeLoader : + public LoaderStateMachine, + public OrthancStone::IObservable, + public IGeometryProvider + { + private: + class LoadRTDoseGeometry; + class LoadGeometry; + class LoadTransferSyntax; + class LoadUncompressedPixelData; + + boost::shared_ptr<OrthancStone::DicomVolumeImage> volume_; + std::string instanceId_; + std::string transferSyntaxUid_; + bool pixelDataLoaded_; + float outliersHalfRejectionRate_; + float distributionRawMin_; + float distributionRawMax_; + float computedDistributionMin_; + float computedDistributionMax_; + + const std::string& GetInstanceId() const; + + void ScheduleFrameDownloads(); + + void SetTransferSyntax(const std::string& transferSyntax); + + void SetGeometry(const Orthanc::DicomMap& dicom); + + + /** + This method will : + + - copy the pixel values from the response to the volume image + - compute the maximum and minimum value while discarding the + outliersHalfRejectionRate_ fraction of the outliers from both the start + and the end of the distribution. + + In English, this means that, if the volume dataset contains a few extreme + values very different from the rest (outliers) that we want to get rid of, + this method allows to do so. + + If you supply 0.005, for instance, it means 1% of the extreme values will + be rejected (0.5% on each side of the distribution) + */ + template <typename T> + void CopyPixelDataAndComputeMinMax(const std::string& pixelData); + + /** Service method for CopyPixelDataAndComputeMinMax*/ + template <typename T> + void CopyPixelDataAndComputeDistribution( + const std::string& pixelData, + std::map<T, uint64_t>& distribution); + + /** Service method for CopyPixelDataAndComputeMinMax*/ + template <typename T> + void ComputeMinMaxWithOutlierRejection( + const std::map<T, uint64_t>& distribution); + + void SetUncompressedPixelData(const std::string& pixelData); + + bool HasGeometry() const; + const OrthancStone::VolumeImageGeometry& GetImageGeometry() const; + + protected: + OrthancMultiframeVolumeLoader( + OrthancStone::ILoadersContext& loadersContext, + boost::shared_ptr<OrthancStone::DicomVolumeImage> volume, + float outliersHalfRejectionRate); + public: + + static boost::shared_ptr<OrthancMultiframeVolumeLoader> Create( + OrthancStone::ILoadersContext& loadersContext, + boost::shared_ptr<OrthancStone::DicomVolumeImage> volume, + float outliersHalfRejectionRate = 0.0005); + + virtual ~OrthancMultiframeVolumeLoader(); + + bool IsPixelDataLoaded() const + { + return pixelDataLoaded_; + } + + void GetDistributionMinMax + (float& minValue, float& maxValue) const; + + void GetDistributionMinMaxWithOutliersRejection + (float& minValue, float& maxValue) const; + + void LoadInstance(const std::string& instanceId); + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Deprecated/Loaders/OrthancSeriesVolumeProgressiveLoader.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -0,0 +1,577 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 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 "OrthancSeriesVolumeProgressiveLoader.h" + +#include "../../StoneException.h" +#include "../../Loaders/ILoadersContext.h" +#include "../../Loaders/BasicFetchingItemsSorter.h" +#include "../../Loaders/BasicFetchingStrategy.h" +#include "../../Toolbox/GeometryToolbox.h" +#include "../../Volumes/DicomVolumeImageMPRSlicer.h" + +#include <Core/Images/ImageProcessing.h> +#include <Core/OrthancException.h> + +namespace Deprecated +{ + using OrthancStone::ILoadersContext; + + class OrthancSeriesVolumeProgressiveLoader::ExtractedSlice : public OrthancStone::DicomVolumeImageMPRSlicer::Slice + { + private: + const OrthancSeriesVolumeProgressiveLoader& that_; + + public: + ExtractedSlice(const OrthancSeriesVolumeProgressiveLoader& that, + const OrthancStone::CoordinateSystem3D& plane) : + OrthancStone::DicomVolumeImageMPRSlicer::Slice(*that.volume_, plane), + that_(that) + { + if (IsValid()) + { + if (GetProjection() == OrthancStone::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 && + GetProjection() == OrthancStone::VolumeProjection_Axial) + { + that_.strategy_->SetCurrent(GetSliceIndex()); + } + } + } + }; + + void OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::CheckSlice( + size_t index, const OrthancStone::DicomInstanceParameters& reference) const + { + const OrthancStone::DicomInstanceParameters& slice = *slices_[index]; + + if (!OrthancStone::GeometryToolbox::IsParallel( + reference.GetGeometry().GetNormal(), + slice.GetGeometry().GetNormal())) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadGeometry, + "A slice in the volume image is not parallel to the others"); + } + + if (reference.GetExpectedPixelFormat() != slice.GetExpectedPixelFormat()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageFormat, + "The pixel format changes across the slices of the volume image"); + } + + if (reference.GetImageInformation().GetWidth() != slice.GetImageInformation().GetWidth() || + reference.GetImageInformation().GetHeight() != slice.GetImageInformation().GetHeight()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageSize, + "The width/height of slices are not constant in the volume image"); + } + + if (!OrthancStone::LinearAlgebra::IsNear(reference.GetPixelSpacingX(), slice.GetPixelSpacingX()) || + !OrthancStone::LinearAlgebra::IsNear(reference.GetPixelSpacingY(), slice.GetPixelSpacingY())) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadGeometry, + "The pixel spacing of the slices change across the volume image"); + } + } + + + void OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::CheckVolume() const + { + for (size_t i = 0; i < slices_.size(); i++) + { + assert(slices_[i] != NULL); + if (slices_[i]->GetImageInformation().GetNumberOfFrames() != 1) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadGeometry, + "This class does not support multi-frame images"); + } + } + + if (slices_.size() != 0) + { + const OrthancStone::DicomInstanceParameters& reference = *slices_[0]; + + for (size_t i = 1; i < slices_.size(); i++) + { + CheckSlice(i, reference); + } + } + } + + + void OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::Clear() + { + for (size_t i = 0; i < slices_.size(); i++) + { + assert(slices_[i] != NULL); + delete slices_[i]; + } + + slices_.clear(); + slicesRevision_.clear(); + } + + + void OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::CheckSliceIndex(size_t index) const + { + if (!HasGeometry()) + { + LOG(ERROR) << "OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::CheckSliceIndex(size_t index): (!HasGeometry())"; + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + else if (index >= slices_.size()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + else + { + assert(slices_.size() == GetImageGeometry().GetDepth() && + slices_.size() == slicesRevision_.size()); + } + } + + + // WARNING: The payload of "slices" must be of class "DicomInstanceParameters" + // (called with the slices created in LoadGeometry) + void OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::ComputeGeometry(OrthancStone::SlicesSorter& slices) + { + Clear(); + + if (!slices.Sort()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange, + "Cannot sort the 3D slices of a DICOM series"); + } + + if (slices.GetSlicesCount() == 0) + { + geometry_.reset(new OrthancStone::VolumeImageGeometry); + } + else + { + slices_.reserve(slices.GetSlicesCount()); + slicesRevision_.resize(slices.GetSlicesCount(), 0); + + for (size_t i = 0; i < slices.GetSlicesCount(); i++) + { + const OrthancStone::DicomInstanceParameters& slice = + dynamic_cast<const OrthancStone::DicomInstanceParameters&>(slices.GetSlicePayload(i)); + slices_.push_back(new OrthancStone::DicomInstanceParameters(slice)); + } + + CheckVolume(); + + double spacingZ; + + if (slices.ComputeSpacingBetweenSlices(spacingZ)) + { + LOG(TRACE) << "Computed spacing between slices: " << spacingZ << "mm"; + + const OrthancStone::DicomInstanceParameters& parameters = *slices_[0]; + + geometry_.reset(new OrthancStone::VolumeImageGeometry); + geometry_->SetSizeInVoxels(parameters.GetImageInformation().GetWidth(), + parameters.GetImageInformation().GetHeight(), + static_cast<unsigned int>(slices.GetSlicesCount())); + geometry_->SetAxialGeometry(slices.GetSliceGeometry(0)); + geometry_->SetVoxelDimensions(parameters.GetPixelSpacingX(), + parameters.GetPixelSpacingY(), spacingZ); + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadGeometry, + "The origins of the slices of a volume image are not regularly spaced"); + } + } + } + + + const OrthancStone::VolumeImageGeometry& OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::GetImageGeometry() const + { + if (!HasGeometry()) + { + LOG(ERROR) << "OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::GetImageGeometry(): (!HasGeometry())"; + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + else + { + assert(slices_.size() == geometry_->GetDepth()); + return *geometry_; + } + } + + + const OrthancStone::DicomInstanceParameters& OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::GetSliceParameters(size_t index) const + { + CheckSliceIndex(index); + return *slices_[index]; + } + + + uint64_t OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::GetSliceRevision(size_t index) const + { + CheckSliceIndex(index); + return slicesRevision_[index]; + } + + + void OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::IncrementSliceRevision(size_t index) + { + CheckSliceIndex(index); + slicesRevision_[index] ++; + } + + + static unsigned int GetSliceIndexPayload(const OrthancStone::OracleCommandBase& command) + { + assert(command.HasPayload()); + return dynamic_cast< const Orthanc::SingleValueObject<unsigned int>& >(command.GetPayload()).GetValue(); + } + + + void OrthancSeriesVolumeProgressiveLoader::ScheduleNextSliceDownload() + { + assert(strategy_.get() != NULL); + + unsigned int sliceIndex = 0, quality = 0; + + if (strategy_->GetNext(sliceIndex, quality)) + { + if (!progressiveQuality_) + { + ORTHANC_ASSERT(quality == QUALITY_00, "INTERNAL ERROR. quality != QUALITY_00 in " + << "OrthancSeriesVolumeProgressiveLoader::ScheduleNextSliceDownload"); + } + + const OrthancStone::DicomInstanceParameters& slice = seriesGeometry_.GetSliceParameters(sliceIndex); + + const std::string& instance = slice.GetOrthancInstanceIdentifier(); + if (instance.empty()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + + std::unique_ptr<OrthancStone::OracleCommandBase> command; + + if (!progressiveQuality_ || quality == QUALITY_02) + { + std::unique_ptr<OrthancStone::GetOrthancImageCommand> tmp(new OrthancStone::GetOrthancImageCommand); + // TODO: review the following comment. + // - Commented out by bgo on 2019-07-19 | reason: Alain has seen cases + // where gzipping the uint16 image took 11 sec to produce 5mb. + // The unzipped request was much much faster. + // - Re-enabled on 2019-07-30. Reason: in Web Assembly, the browser + // does not use the Accept-Encoding header and always requests + // compression. Furthermore, NOT + tmp->SetHttpHeader("Accept-Encoding", "gzip"); + tmp->SetHttpHeader("Accept", std::string(Orthanc::EnumerationToString(Orthanc::MimeType_Pam))); + tmp->SetInstanceUri(instance, slice.GetExpectedPixelFormat()); + tmp->SetExpectedPixelFormat(slice.GetExpectedPixelFormat()); + //LOG(INFO) + // << "OrthancSeriesVolumeProgressiveLoader.ScheduleNextSliceDownload()" + // << " sliceIndex = " << sliceIndex << " slice quality = " << quality + // << " URI = " << tmp->GetUri(); + command.reset(tmp.release()); + } + else // progressive mode is true AND quality is not final (different from QUALITY_02 + { + std::unique_ptr<OrthancStone::GetOrthancWebViewerJpegCommand> tmp( + new OrthancStone::GetOrthancWebViewerJpegCommand); + + // TODO: review the following comment. Commented out by bgo on 2019-07-19 + // (gzip for jpeg seems overkill) + //tmp->SetHttpHeader("Accept-Encoding", "gzip"); + tmp->SetInstance(instance); + tmp->SetQuality((quality == 0 ? 50 : 90)); // QUALITY_00 is Jpeg50 while QUALITY_01 is Jpeg90 + tmp->SetExpectedPixelFormat(slice.GetExpectedPixelFormat()); + LOG(TRACE) + << "OrthancSeriesVolumeProgressiveLoader.ScheduleNextSliceDownload()" + << " sliceIndex = " << sliceIndex << " slice quality = " << quality; + command.reset(tmp.release()); + } + + command->AcquirePayload(new Orthanc::SingleValueObject<unsigned int>(sliceIndex)); + + { + std::unique_ptr<OrthancStone::ILoadersContext::ILock> lock(loadersContext_.Lock()); + boost::shared_ptr<IObserver> observer(GetSharedObserver()); + lock->Schedule(observer, 0, command.release()); // TODO: priority! + } + } + else + { + // loading is finished! + volumeImageReadyInHighQuality_ = true; + BroadcastMessage(OrthancSeriesVolumeProgressiveLoader::VolumeImageReadyInHighQuality(*this)); + } + } + +/** + This is called in response to GET "/series/XXXXXXXXXXXXX/instances-tags" +*/ + void OrthancSeriesVolumeProgressiveLoader::LoadGeometry(const OrthancStone::OrthancRestApiCommand::SuccessMessage& message) + { + Json::Value body; + message.ParseJsonBody(body); + + if (body.type() != Json::objectValue) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol); + } + + { + Json::Value::Members instances = body.getMemberNames(); + + OrthancStone::SlicesSorter slices; + + for (size_t i = 0; i < instances.size(); i++) + { + Orthanc::DicomMap dicom; + dicom.FromDicomAsJson(body[instances[i]]); + + std::unique_ptr<OrthancStone::DicomInstanceParameters> instance(new OrthancStone::DicomInstanceParameters(dicom)); + instance->SetOrthancInstanceIdentifier(instances[i]); + + // the 3D plane corresponding to the slice + OrthancStone::CoordinateSystem3D geometry = instance->GetGeometry(); + slices.AddSlice(geometry, instance.release()); + } + + seriesGeometry_.ComputeGeometry(slices); + } + + size_t slicesCount = seriesGeometry_.GetImageGeometry().GetDepth(); + + if (slicesCount == 0) + { + volume_->Initialize(seriesGeometry_.GetImageGeometry(), Orthanc::PixelFormat_Grayscale8); + } + else + { + const OrthancStone::DicomInstanceParameters& parameters = seriesGeometry_.GetSliceParameters(0); + + volume_->Initialize(seriesGeometry_.GetImageGeometry(), parameters.GetExpectedPixelFormat()); + volume_->SetDicomParameters(parameters); + volume_->GetPixelData().Clear(); + + // If we are in progressive mode, the Fetching strategy will first request QUALITY_00, then QUALITY_01, then + // QUALITY_02... Otherwise, it's only QUALITY_00 + unsigned int maxQuality = QUALITY_00; + if (progressiveQuality_) + maxQuality = QUALITY_02; + + strategy_.reset(new OrthancStone::BasicFetchingStrategy( + sorter_->CreateSorter(static_cast<unsigned int>(slicesCount)), + maxQuality)); + + assert(simultaneousDownloads_ != 0); + for (unsigned int i = 0; i < simultaneousDownloads_; i++) + { + ScheduleNextSliceDownload(); + } + } + + slicesQuality_.resize(slicesCount, 0); + + BroadcastMessage(OrthancStone::DicomVolumeImage::GeometryReadyMessage(*volume_)); + } + + + void OrthancSeriesVolumeProgressiveLoader::SetSliceContent(unsigned int sliceIndex, + const Orthanc::ImageAccessor& image, + unsigned int quality) + { + ORTHANC_ASSERT(sliceIndex < slicesQuality_.size() && + slicesQuality_.size() == volume_->GetPixelData().GetDepth()); + + if (!progressiveQuality_) + { + ORTHANC_ASSERT(quality == QUALITY_00); + ORTHANC_ASSERT(slicesQuality_[sliceIndex] == QUALITY_00); + } + + if (quality >= slicesQuality_[sliceIndex]) + { + { + OrthancStone::ImageBuffer3D::SliceWriter writer(volume_->GetPixelData(), + OrthancStone::VolumeProjection_Axial, + sliceIndex); + + Orthanc::ImageProcessing::Copy(writer.GetAccessor(), image); + } + + volume_->IncrementRevision(); + seriesGeometry_.IncrementSliceRevision(sliceIndex); + slicesQuality_[sliceIndex] = quality; + + BroadcastMessage(OrthancStone::DicomVolumeImage::ContentUpdatedMessage(*volume_)); + } + LOG(TRACE) << "SetSliceContent sliceIndex = " << sliceIndex << " -- will " + << " now call ScheduleNextSliceDownload()"; + ScheduleNextSliceDownload(); + } + + void OrthancSeriesVolumeProgressiveLoader::LoadBestQualitySliceContent( + const OrthancStone::GetOrthancImageCommand::SuccessMessage& message) + { + unsigned int quality = QUALITY_00; + if (progressiveQuality_) + quality = QUALITY_02; + + SetSliceContent(GetSliceIndexPayload(message.GetOrigin()), + message.GetImage(), + quality); + } + + void OrthancSeriesVolumeProgressiveLoader::LoadJpegSliceContent( + const OrthancStone::GetOrthancWebViewerJpegCommand::SuccessMessage& message) + { + ORTHANC_ASSERT(progressiveQuality_, "INTERNAL ERROR: OrthancSeriesVolumeProgressiveLoader::LoadJpegSliceContent" + << " called while progressiveQuality_ is false!"); + + LOG(TRACE) << "OrthancSeriesVolumeProgressiveLoader::LoadJpegSliceContent"; + unsigned int quality; + + switch (dynamic_cast<const OrthancStone::GetOrthancWebViewerJpegCommand&>(message.GetOrigin()).GetQuality()) + { + case 50: + quality = QUALITY_00; + break; + + case 90: + quality = QUALITY_01; + break; + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + + SetSliceContent(GetSliceIndexPayload(message.GetOrigin()), message.GetImage(), quality); + } + + OrthancSeriesVolumeProgressiveLoader::OrthancSeriesVolumeProgressiveLoader( + OrthancStone::ILoadersContext& loadersContext, + boost::shared_ptr<OrthancStone::DicomVolumeImage> volume, + bool progressiveQuality) + : loadersContext_(loadersContext) + , active_(false) + , progressiveQuality_(progressiveQuality) + , simultaneousDownloads_(4) + , volume_(volume) + , sorter_(new OrthancStone::BasicFetchingItemsSorter::Factory) + , volumeImageReadyInHighQuality_(false) + { + } + + boost::shared_ptr<OrthancSeriesVolumeProgressiveLoader> + OrthancSeriesVolumeProgressiveLoader::Create( + OrthancStone::ILoadersContext& loadersContext, + boost::shared_ptr<OrthancStone::DicomVolumeImage> volume, + bool progressiveQuality) + { + std::auto_ptr<OrthancStone::ILoadersContext::ILock> lock(loadersContext.Lock()); + + boost::shared_ptr<OrthancSeriesVolumeProgressiveLoader> obj( + new OrthancSeriesVolumeProgressiveLoader( + loadersContext, volume, progressiveQuality)); + + obj->Register<OrthancStone::OrthancRestApiCommand::SuccessMessage>( + lock->GetOracleObservable(), + &OrthancSeriesVolumeProgressiveLoader::LoadGeometry); + + obj->Register<OrthancStone::GetOrthancImageCommand::SuccessMessage>( + lock->GetOracleObservable(), + &OrthancSeriesVolumeProgressiveLoader::LoadBestQualitySliceContent); + + obj->Register<OrthancStone::GetOrthancWebViewerJpegCommand::SuccessMessage>( + lock->GetOracleObservable(), + &OrthancSeriesVolumeProgressiveLoader::LoadJpegSliceContent); + + return obj; + } + + + OrthancSeriesVolumeProgressiveLoader::~OrthancSeriesVolumeProgressiveLoader() + { + LOG(TRACE) << "OrthancSeriesVolumeProgressiveLoader::~OrthancSeriesVolumeProgressiveLoader()"; + } + + void OrthancSeriesVolumeProgressiveLoader::SetSimultaneousDownloads(unsigned int count) + { + if (active_) + { + LOG(ERROR) << "OrthancSeriesVolumeProgressiveLoader::SetSimultaneousDownloads(): (active_)"; + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + else if (count == 0) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + else + { + simultaneousDownloads_ = count; + } + } + + + void OrthancSeriesVolumeProgressiveLoader::LoadSeries(const std::string& seriesId) + { + if (active_) + { + LOG(ERROR) << "OrthancSeriesVolumeProgressiveLoader::LoadSeries(const std::string& seriesId): (active_)"; + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + else + { + active_ = true; + + std::unique_ptr<OrthancStone::OrthancRestApiCommand> command(new OrthancStone::OrthancRestApiCommand); + command->SetUri("/series/" + seriesId + "/instances-tags"); + { + std::unique_ptr<OrthancStone::ILoadersContext::ILock> lock(loadersContext_.Lock()); + boost::shared_ptr<IObserver> observer(GetSharedObserver()); + lock->Schedule(observer, 0, command.release()); //TODO: priority! + } + } + } + + + OrthancStone::IVolumeSlicer::IExtractedSlice* + OrthancSeriesVolumeProgressiveLoader::ExtractSlice(const OrthancStone::CoordinateSystem3D& cuttingPlane) + { + if (volume_->HasGeometry()) + { + return new ExtractedSlice(*this, cuttingPlane); + } + else + { + return new IVolumeSlicer::InvalidSlice; + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Deprecated/Loaders/OrthancSeriesVolumeProgressiveLoader.h Tue Mar 31 15:51:02 2020 +0200 @@ -0,0 +1,180 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 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 "../../Loaders/IFetchingItemsSorter.h" +#include "../../Loaders/IFetchingStrategy.h" +#include "../../Messages/IObservable.h" +#include "../../Messages/ObserverBase.h" +#include "../../Oracle/GetOrthancImageCommand.h" +#include "../../Oracle/GetOrthancWebViewerJpegCommand.h" +#include "../../Oracle/IOracle.h" +#include "../../Oracle/OrthancRestApiCommand.h" +#include "../../Toolbox/SlicesSorter.h" +#include "../../Volumes/DicomVolumeImage.h" +#include "../../Volumes/IVolumeSlicer.h" + +#include "../Volumes/IGeometryProvider.h" + + +#include <boost/shared_ptr.hpp> + +namespace OrthancStone +{ + class ILoadersContext; +} + +namespace Deprecated +{ + /** + This class is used to manage the progressive loading of a volume that + is stored in a Dicom series. + */ + class OrthancSeriesVolumeProgressiveLoader : + public OrthancStone::ObserverBase<OrthancSeriesVolumeProgressiveLoader>, + public OrthancStone::IObservable, + public OrthancStone::IVolumeSlicer, + public IGeometryProvider + { + private: + static const unsigned int QUALITY_00 = 0; + static const unsigned int QUALITY_01 = 1; + static const unsigned int QUALITY_02 = 2; + + class ExtractedSlice; + + /** Helper class internal to OrthancSeriesVolumeProgressiveLoader */ + class SeriesGeometry : public boost::noncopyable + { + private: + void CheckSlice(size_t index, + const OrthancStone::DicomInstanceParameters& reference) const; + + void CheckVolume() const; + + void Clear(); + + void CheckSliceIndex(size_t index) const; + + std::unique_ptr<OrthancStone::VolumeImageGeometry> geometry_; + std::vector<OrthancStone::DicomInstanceParameters*> slices_; + std::vector<uint64_t> slicesRevision_; + + public: + ~SeriesGeometry() + { + Clear(); + } + + void ComputeGeometry(OrthancStone::SlicesSorter& slices); + + virtual bool HasGeometry() const + { + return geometry_.get() != NULL; + } + + virtual const OrthancStone::VolumeImageGeometry& GetImageGeometry() const; + + const OrthancStone::DicomInstanceParameters& GetSliceParameters(size_t index) const; + + uint64_t GetSliceRevision(size_t index) const; + + void IncrementSliceRevision(size_t index); + }; + + void ScheduleNextSliceDownload(); + + void LoadGeometry(const OrthancStone::OrthancRestApiCommand::SuccessMessage& message); + + void SetSliceContent(unsigned int sliceIndex, + const Orthanc::ImageAccessor& image, + unsigned int quality); + + void LoadBestQualitySliceContent(const OrthancStone::GetOrthancImageCommand::SuccessMessage& message); + + void LoadJpegSliceContent(const OrthancStone::GetOrthancWebViewerJpegCommand::SuccessMessage& message); + + OrthancStone::ILoadersContext& loadersContext_; + bool active_; + bool progressiveQuality_; + unsigned int simultaneousDownloads_; + SeriesGeometry seriesGeometry_; + boost::shared_ptr<OrthancStone::DicomVolumeImage> volume_; + std::unique_ptr<OrthancStone::IFetchingItemsSorter::IFactory> sorter_; + std::unique_ptr<OrthancStone::IFetchingStrategy> strategy_; + std::vector<unsigned int> slicesQuality_; + bool volumeImageReadyInHighQuality_; + + OrthancSeriesVolumeProgressiveLoader( + OrthancStone::ILoadersContext& loadersContext, + boost::shared_ptr<OrthancStone::DicomVolumeImage> volume, + bool progressiveQuality); + + public: + ORTHANC_STONE_DEFINE_ORIGIN_MESSAGE(__FILE__, __LINE__, VolumeImageReadyInHighQuality, OrthancSeriesVolumeProgressiveLoader); + + /** + See doc for the progressiveQuality_ field + */ + static boost::shared_ptr<OrthancSeriesVolumeProgressiveLoader> Create( + OrthancStone::ILoadersContext& context, + boost::shared_ptr<OrthancStone::DicomVolumeImage> volume, + bool progressiveQuality = false); + + virtual ~OrthancSeriesVolumeProgressiveLoader(); + + void SetSimultaneousDownloads(unsigned int count); + + bool IsVolumeImageReadyInHighQuality() const + { + return volumeImageReadyInHighQuality_; + } + + void LoadSeries(const std::string& seriesId); + + /** + This getter is used by clients that do not receive the geometry through + subscribing, for instance if they are created or listening only AFTER the + "geometry loaded" message is broadcast + */ + bool HasGeometry() const ORTHANC_OVERRIDE + { + return seriesGeometry_.HasGeometry(); + } + + /** + Same remark as HasGeometry + */ + const OrthancStone::VolumeImageGeometry& GetImageGeometry() const ORTHANC_OVERRIDE + { + return seriesGeometry_.GetImageGeometry(); + } + + /** + When a slice is requested, the strategy algorithm (that defines the + sequence of resources to be loaded from the server) is modified to + take into account this request (this is done in the ExtractedSlice ctor) + */ + virtual IExtractedSlice* + ExtractSlice(const OrthancStone::CoordinateSystem3D& cuttingPlane) ORTHANC_OVERRIDE; + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Deprecated/Messages/LockingEmitter.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -0,0 +1,40 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 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 "LockingEmitter.h" + +#include <Core/OrthancException.h> + +namespace Deprecated +{ + void LockingEmitter::EmitMessage(boost::weak_ptr<OrthancStone::IObserver> observer, + const OrthancStone::IMessage& message) + { + try + { + boost::unique_lock<boost::shared_mutex> lock(mutex_); + oracleObservable_.EmitMessage(observer, message); + } + catch (Orthanc::OrthancException& e) + { + LOG(ERROR) << "Exception while emitting a message: " << e.What(); + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Deprecated/Messages/LockingEmitter.h Tue Mar 31 15:51:02 2020 +0200 @@ -0,0 +1,89 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 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 <Core/Enumerations.h> +#include <Core/OrthancException.h> + +#include "../../Messages/IMessageEmitter.h" +#include "../../Messages/IObservable.h" + +#include <Core/Enumerations.h> // For ORTHANC_OVERRIDE + +#include <boost/thread/shared_mutex.hpp> + +namespace Deprecated +{ + /** + * This class is used when using the ThreadedOracle : since messages + * can be sent from multiple Oracle threads, this IMessageEmitter + * implementation serializes the callbacks. + * + * The internal mutex used in Oracle messaging can also be used to + * protect the application data. Thus, this class can be used as a single + * application-wide mutex. + */ + class LockingEmitter : public OrthancStone::IMessageEmitter + { + private: + boost::shared_mutex mutex_; + OrthancStone::IObservable oracleObservable_; + + public: + virtual void EmitMessage(boost::weak_ptr<OrthancStone::IObserver> observer, + const OrthancStone::IMessage& message) ORTHANC_OVERRIDE; + + + class ReaderLock : public boost::noncopyable + { + private: + LockingEmitter& that_; + boost::shared_lock<boost::shared_mutex> lock_; + + public: + ReaderLock(LockingEmitter& that) : + that_(that), + lock_(that.mutex_) + { + } + }; + + + class WriterLock : public boost::noncopyable + { + private: + LockingEmitter& that_; + boost::unique_lock<boost::shared_mutex> lock_; + + public: + WriterLock(LockingEmitter& that) : + that_(that), + lock_(that.mutex_) + { + } + + OrthancStone::IObservable& GetOracleObservable() + { + return that_.oracleObservable_; + } + }; + }; +}
--- a/Framework/Deprecated/SmartLoader.cpp Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Deprecated/SmartLoader.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -21,7 +21,6 @@ #include "SmartLoader.h" -#include "../Messages/MessageForwarder.h" #include "../StoneException.h" #include "Core/Images/Image.h" #include "Core/Logging.h" @@ -68,11 +67,6 @@ CachedSliceStatus status_; public: - CachedSlice(OrthancStone::MessageBroker& broker) : - IVolumeSlicer(broker) - { - } - virtual ~CachedSlice() { } @@ -106,7 +100,7 @@ CachedSlice* Clone() const { - CachedSlice* output = new CachedSlice(GetBroker()); + CachedSlice* output = new CachedSlice; output->sliceIndex_ = sliceIndex_; output->slice_.reset(slice_->Clone()); output->image_ = image_; @@ -119,10 +113,7 @@ }; - SmartLoader::SmartLoader(OrthancStone::MessageBroker& broker, - OrthancApiClient& orthancApiClient) : - IObservable(broker), - IObserver(broker), + SmartLoader::SmartLoader(boost::shared_ptr<OrthancApiClient> orthancApiClient) : imageQuality_(SliceImageQuality_FullPam), orthancApiClient_(orthancApiClient) { @@ -140,7 +131,7 @@ // the messages to its observables // in both cases, we must be carefull about objects lifecycle !!! - std::unique_ptr<IVolumeSlicer> layerSource; + boost::shared_ptr<IVolumeSlicer> layerSource; std::string sliceKeyId = instanceId + ":" + boost::lexical_cast<std::string>(frame); SmartLoader::CachedSlice* cachedSlice = NULL; @@ -151,22 +142,23 @@ } else { - layerSource.reset(new DicomSeriesVolumeSlicer(IObserver::GetBroker(), orthancApiClient_)); + layerSource.reset(new DicomSeriesVolumeSlicer); + dynamic_cast<DicomSeriesVolumeSlicer*>(layerSource.get())->Connect(orthancApiClient_); dynamic_cast<DicomSeriesVolumeSlicer*>(layerSource.get())->SetImageQuality(imageQuality_); - layerSource->RegisterObserverCallback(new OrthancStone::Callable<SmartLoader, IVolumeSlicer::GeometryReadyMessage>(*this, &SmartLoader::OnLayerGeometryReady)); - layerSource->RegisterObserverCallback(new OrthancStone::Callable<SmartLoader, DicomSeriesVolumeSlicer::FrameReadyMessage>(*this, &SmartLoader::OnFrameReady)); - layerSource->RegisterObserverCallback(new OrthancStone::Callable<SmartLoader, IVolumeSlicer::LayerReadyMessage>(*this, &SmartLoader::OnLayerReady)); + Register<IVolumeSlicer::GeometryReadyMessage>(*layerSource, &SmartLoader::OnLayerGeometryReady); + Register<DicomSeriesVolumeSlicer::FrameReadyMessage>(*layerSource, &SmartLoader::OnFrameReady); + Register<IVolumeSlicer::LayerReadyMessage>(*layerSource, &SmartLoader::OnLayerReady); dynamic_cast<DicomSeriesVolumeSlicer*>(layerSource.get())->LoadFrame(instanceId, frame); } // make sure that the widget registers the events before we trigger them if (sliceViewer.GetLayerCount() == layerIndex) { - sliceViewer.AddLayer(layerSource.release()); + sliceViewer.AddLayer(layerSource); } else if (sliceViewer.GetLayerCount() > layerIndex) { - sliceViewer.ReplaceLayer(layerIndex, layerSource.release()); + sliceViewer.ReplaceLayer(layerIndex, layerSource); } else { @@ -190,7 +182,7 @@ // create the slice in the cache with "empty" data - boost::shared_ptr<CachedSlice> cachedSlice(new CachedSlice(IObserver::GetBroker())); + boost::shared_ptr<CachedSlice> cachedSlice(new CachedSlice); cachedSlice->slice_.reset(new Slice(instanceId, frame)); cachedSlice->status_ = CachedSliceStatus_ScheduledToLoad; std::string sliceKeyId = instanceId + ":" + boost::lexical_cast<std::string>(frame); @@ -199,12 +191,12 @@ cachedSlices_[sliceKeyId] = boost::shared_ptr<CachedSlice>(cachedSlice); - std::unique_ptr<IVolumeSlicer> layerSource(new DicomSeriesVolumeSlicer(IObserver::GetBroker(), orthancApiClient_)); - + std::unique_ptr<IVolumeSlicer> layerSource(new DicomSeriesVolumeSlicer); + dynamic_cast<DicomSeriesVolumeSlicer*>(layerSource.get())->Connect(orthancApiClient_); dynamic_cast<DicomSeriesVolumeSlicer*>(layerSource.get())->SetImageQuality(imageQuality_); - layerSource->RegisterObserverCallback(new OrthancStone::Callable<SmartLoader, IVolumeSlicer::GeometryReadyMessage>(*this, &SmartLoader::OnLayerGeometryReady)); - layerSource->RegisterObserverCallback(new OrthancStone::Callable<SmartLoader, DicomSeriesVolumeSlicer::FrameReadyMessage>(*this, &SmartLoader::OnFrameReady)); - layerSource->RegisterObserverCallback(new OrthancStone::Callable<SmartLoader, IVolumeSlicer::LayerReadyMessage>(*this, &SmartLoader::OnLayerReady)); + Register<IVolumeSlicer::GeometryReadyMessage>(*layerSource, &SmartLoader::OnLayerGeometryReady); + Register<DicomSeriesVolumeSlicer::FrameReadyMessage>(*layerSource, &SmartLoader::OnFrameReady); + Register<IVolumeSlicer::LayerReadyMessage>(*layerSource, &SmartLoader::OnLayerReady); dynamic_cast<DicomSeriesVolumeSlicer*>(layerSource.get())->LoadFrame(instanceId, frame); // keep a ref to the VolumeSlicer until the slice is fully loaded and saved to cache @@ -235,7 +227,7 @@ LOG(WARNING) << "Geometry ready: " << sliceKeyId; - boost::shared_ptr<CachedSlice> cachedSlice(new CachedSlice(IObserver::GetBroker())); + boost::shared_ptr<CachedSlice> cachedSlice(new CachedSlice); cachedSlice->slice_.reset(slice.Clone()); cachedSlice->effectiveQuality_ = source.GetImageQuality(); cachedSlice->status_ = CachedSliceStatus_GeometryLoaded; @@ -256,7 +248,7 @@ LOG(WARNING) << "Image ready: " << sliceKeyId; - boost::shared_ptr<CachedSlice> cachedSlice(new CachedSlice(IObserver::GetBroker())); + boost::shared_ptr<CachedSlice> cachedSlice(new CachedSlice); cachedSlice->image_.reset(Orthanc::Image::Clone(message.GetFrame())); cachedSlice->effectiveQuality_ = message.GetImageQuality(); cachedSlice->slice_.reset(message.GetSlice().Clone());
--- a/Framework/Deprecated/SmartLoader.h Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Deprecated/SmartLoader.h Tue Mar 31 15:51:02 2020 +0200 @@ -30,7 +30,7 @@ { class SliceViewerWidget; - class SmartLoader : public OrthancStone::IObservable, public OrthancStone::IObserver + class SmartLoader : public OrthancStone::IObservable, public OrthancStone::ObserverBase<SmartLoader> { class CachedSlice; @@ -42,10 +42,10 @@ PreloadingInstances preloadingInstances_; SliceImageQuality imageQuality_; - OrthancApiClient& orthancApiClient_; + boost::shared_ptr<OrthancApiClient> orthancApiClient_; public: - SmartLoader(OrthancStone::MessageBroker& broker, OrthancApiClient& orthancApiClient); // TODO: add maxPreloadStorageSizeInBytes + SmartLoader(boost::shared_ptr<OrthancApiClient> orthancApiClient); // TODO: add maxPreloadStorageSizeInBytes // void PreloadStudy(const std::string studyId); // void PreloadSeries(const std::string seriesId);
--- a/Framework/Deprecated/Toolbox/BaseWebService.cpp Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Deprecated/Toolbox/BaseWebService.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -37,13 +37,13 @@ class BaseWebService::BaseWebServicePayload : public Orthanc::IDynamicObject { private: - std::unique_ptr< OrthancStone::MessageHandler<IWebService::HttpRequestSuccessMessage> > userSuccessHandler_; - std::unique_ptr< OrthancStone::MessageHandler<IWebService::HttpRequestErrorMessage> > userFailureHandler_; + std::unique_ptr< MessageHandler<IWebService::HttpRequestSuccessMessage> > userSuccessHandler_; + std::unique_ptr< MessageHandler<IWebService::HttpRequestErrorMessage> > userFailureHandler_; std::unique_ptr< Orthanc::IDynamicObject> userPayload_; public: - BaseWebServicePayload(OrthancStone::MessageHandler<IWebService::HttpRequestSuccessMessage>* userSuccessHandler, - OrthancStone::MessageHandler<IWebService::HttpRequestErrorMessage>* userFailureHandler, + BaseWebServicePayload(MessageHandler<IWebService::HttpRequestSuccessMessage>* userSuccessHandler, + MessageHandler<IWebService::HttpRequestErrorMessage>* userFailureHandler, Orthanc::IDynamicObject* userPayload) : userSuccessHandler_(userSuccessHandler), userFailureHandler_(userFailureHandler), @@ -88,18 +88,18 @@ void BaseWebService::GetAsync(const std::string& uri, const HttpHeaders& headers, Orthanc::IDynamicObject* payload /* takes ownership */, - OrthancStone::MessageHandler<IWebService::HttpRequestSuccessMessage>* successCallback, - OrthancStone::MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback, + MessageHandler<IWebService::HttpRequestSuccessMessage>* successCallback, + MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback, unsigned int timeoutInSeconds) { if (!cacheEnabled_ || cache_.find(uri) == cache_.end()) { GetAsyncInternal(uri, headers, new BaseWebService::BaseWebServicePayload(successCallback, failureCallback, payload), // ownership is transfered - new OrthancStone::Callable<BaseWebService, IWebService::HttpRequestSuccessMessage> - (*this, &BaseWebService::CacheAndNotifyHttpSuccess), - new OrthancStone::Callable<BaseWebService, IWebService::HttpRequestErrorMessage> - (*this, &BaseWebService::NotifyHttpError), + new DeprecatedCallable<BaseWebService, IWebService::HttpRequestSuccessMessage> + (GetSharedObserver(), &BaseWebService::CacheAndNotifyHttpSuccess), + new DeprecatedCallable<BaseWebService, IWebService::HttpRequestErrorMessage> + (GetSharedObserver(), &BaseWebService::NotifyHttpError), timeoutInSeconds); } else
--- a/Framework/Deprecated/Toolbox/BaseWebService.h Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Deprecated/Toolbox/BaseWebService.h Tue Mar 31 15:51:02 2020 +0200 @@ -22,6 +22,7 @@ #pragma once #include "IWebService.h" +#include "../../Messages/ObserverBase.h" #include <string> #include <map> @@ -31,7 +32,7 @@ { // This is an intermediate of IWebService that implements some caching on // the HTTP GET requests - class BaseWebService : public IWebService, public OrthancStone::IObserver + class BaseWebService : public IWebService, public OrthancStone::ObserverBase<BaseWebService> { public: class CachedHttpRequestSuccessMessage @@ -90,10 +91,7 @@ std::deque<std::string> orderedCacheKeys_; public: - - BaseWebService(OrthancStone::MessageBroker& broker) : - IWebService(broker), - IObserver(broker), + BaseWebService() : cacheEnabled_(false), cacheCurrentSize_(0), cacheMaxSize_(100*1024*1024) @@ -112,21 +110,21 @@ virtual void GetAsync(const std::string& uri, const HttpHeaders& headers, Orthanc::IDynamicObject* payload /* takes ownership */, - OrthancStone::MessageHandler<IWebService::HttpRequestSuccessMessage>* successCallback, - OrthancStone::MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback = NULL, + MessageHandler<IWebService::HttpRequestSuccessMessage>* successCallback, + MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback = NULL, unsigned int timeoutInSeconds = 60); protected: virtual void GetAsyncInternal(const std::string& uri, const HttpHeaders& headers, Orthanc::IDynamicObject* payload /* takes ownership */, - OrthancStone::MessageHandler<IWebService::HttpRequestSuccessMessage>* successCallback, - OrthancStone::MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback = NULL, + MessageHandler<IWebService::HttpRequestSuccessMessage>* successCallback, + MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback = NULL, unsigned int timeoutInSeconds = 60) = 0; virtual void NotifyHttpSuccessLater(boost::shared_ptr<BaseWebService::CachedHttpRequestSuccessMessage> cachedHttpMessage, Orthanc::IDynamicObject* payload, // takes ownership - OrthancStone::MessageHandler<IWebService::HttpRequestSuccessMessage>* successCallback) = 0; + MessageHandler<IWebService::HttpRequestSuccessMessage>* successCallback) = 0; private: void NotifyHttpSuccess(const IWebService::HttpRequestSuccessMessage& message);
--- a/Framework/Deprecated/Toolbox/IDelayedCallExecutor.h Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Deprecated/Toolbox/IDelayedCallExecutor.h Tue Mar 31 15:51:02 2020 +0200 @@ -21,6 +21,7 @@ #pragma once +#include "IWebService.h" #include "../../Messages/IObserver.h" #include "../../Messages/ICallable.h" @@ -35,24 +36,14 @@ // The IDelayedCall executes a callback after a delay (equivalent to timeout() function in javascript). class IDelayedCallExecutor : public boost::noncopyable { - protected: - OrthancStone::MessageBroker& broker_; - public: ORTHANC_STONE_DEFINE_EMPTY_MESSAGE(__FILE__, __LINE__, TimeoutMessage); - IDelayedCallExecutor(OrthancStone::MessageBroker& broker) : - broker_(broker) - { - } - - virtual ~IDelayedCallExecutor() { } - - virtual void Schedule(OrthancStone::MessageHandler<IDelayedCallExecutor::TimeoutMessage>* callback, + virtual void Schedule(MessageHandler<IDelayedCallExecutor::TimeoutMessage>* callback, unsigned int timeoutInMs = 1000) = 0; }; }
--- a/Framework/Deprecated/Toolbox/IWebService.h Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Deprecated/Toolbox/IWebService.h Tue Mar 31 15:51:02 2020 +0200 @@ -33,6 +33,53 @@ namespace Deprecated { + template <typename TMessage> + class MessageHandler : public OrthancStone::ICallable + { + }; + + + template <typename TObserver, + typename TMessage> + class DeprecatedCallable : public MessageHandler<TMessage> + { + private: + typedef void (TObserver::* MemberMethod) (const TMessage&); + + boost::weak_ptr<OrthancStone::IObserver> observer_; + MemberMethod function_; + + public: + DeprecatedCallable(boost::shared_ptr<TObserver> observer, + MemberMethod function) : + observer_(observer), + function_(function) + { + } + + virtual void Apply(const OrthancStone::IMessage& message) + { + boost::shared_ptr<OrthancStone::IObserver> lock(observer_); + if (lock) + { + TObserver& observer = dynamic_cast<TObserver&>(*lock); + const TMessage& typedMessage = dynamic_cast<const TMessage&>(message); + (observer.*function_) (typedMessage); + } + } + + virtual const OrthancStone::MessageIdentifier& GetMessageIdentifier() + { + return TMessage::GetStaticIdentifier(); + } + + virtual boost::weak_ptr<OrthancStone::IObserver> GetObserver() const + { + return observer_; + } + }; + + // The IWebService performs HTTP requests. // Since applications can run in native or WASM environment and, since // in a WASM environment, the WebService is asynchronous, the IWebservice @@ -40,9 +87,6 @@ // and you'll be notified when the response/error is ready. class IWebService : public boost::noncopyable { - protected: - OrthancStone::MessageBroker& broker_; - public: typedef std::map<std::string, std::string> HttpHeaders; @@ -138,12 +182,6 @@ }; - IWebService(OrthancStone::MessageBroker& broker) : - broker_(broker) - { - } - - virtual ~IWebService() { } @@ -153,23 +191,23 @@ virtual void GetAsync(const std::string& uri, const HttpHeaders& headers, Orthanc::IDynamicObject* payload /* takes ownership */, - OrthancStone::MessageHandler<IWebService::HttpRequestSuccessMessage>* successCallback, - OrthancStone::MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback = NULL, + MessageHandler<IWebService::HttpRequestSuccessMessage>* successCallback, + MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback = NULL, unsigned int timeoutInSeconds = 60) = 0; virtual void PostAsync(const std::string& uri, const HttpHeaders& headers, const std::string& body, Orthanc::IDynamicObject* payload /* takes ownership */, - OrthancStone::MessageHandler<IWebService::HttpRequestSuccessMessage>* successCallback, - OrthancStone::MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback = NULL, + MessageHandler<IWebService::HttpRequestSuccessMessage>* successCallback, + MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback = NULL, unsigned int timeoutInSeconds = 60) = 0; virtual void DeleteAsync(const std::string& uri, const HttpHeaders& headers, Orthanc::IDynamicObject* payload /* takes ownership */, - OrthancStone::MessageHandler<IWebService::HttpRequestSuccessMessage>* successCallback, - OrthancStone::MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback = NULL, + MessageHandler<IWebService::HttpRequestSuccessMessage>* successCallback, + MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback = NULL, unsigned int timeoutInSeconds = 60) = 0; }; }
--- a/Framework/Deprecated/Toolbox/MessagingToolbox.cpp Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Deprecated/Toolbox/MessagingToolbox.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -18,7 +18,6 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. **/ - #include "MessagingToolbox.h" #include <Core/Images/Image.h> @@ -30,9 +29,16 @@ #include <Core/Logging.h> #include <boost/lexical_cast.hpp> + +#ifdef _MSC_VER +// 'Json::Reader': Use CharReader and CharReaderBuilder instead +#pragma warning(disable:4996) +#endif + #include <json/reader.h> #include <json/writer.h> + namespace Deprecated { namespace MessagingToolbox
--- a/Framework/Deprecated/Toolbox/OrthancApiClient.cpp Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Deprecated/Toolbox/OrthancApiClient.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -68,12 +68,12 @@ class OrthancApiClient::WebServicePayload : public Orthanc::IDynamicObject { private: - std::unique_ptr< OrthancStone::MessageHandler<EmptyResponseReadyMessage> > emptyHandler_; - std::unique_ptr< OrthancStone::MessageHandler<JsonResponseReadyMessage> > jsonHandler_; - std::unique_ptr< OrthancStone::MessageHandler<BinaryResponseReadyMessage> > binaryHandler_; - std::unique_ptr< OrthancStone::MessageHandler<IWebService::HttpRequestErrorMessage> > failureHandler_; + std::unique_ptr< MessageHandler<EmptyResponseReadyMessage> > emptyHandler_; + std::unique_ptr< MessageHandler<JsonResponseReadyMessage> > jsonHandler_; + std::unique_ptr< MessageHandler<BinaryResponseReadyMessage> > binaryHandler_; + std::unique_ptr< MessageHandler<IWebService::HttpRequestErrorMessage> > failureHandler_; std::unique_ptr< Orthanc::IDynamicObject > userPayload_; - OrthancStone::MessageBroker& broker_; + void NotifyConversionError(const IWebService::HttpRequestSuccessMessage& message) const { if (failureHandler_.get() != NULL) @@ -84,14 +84,12 @@ } public: - WebServicePayload(OrthancStone::MessageBroker& broker, - OrthancStone::MessageHandler<EmptyResponseReadyMessage>* handler, - OrthancStone::MessageHandler<IWebService::HttpRequestErrorMessage>* failureHandler, + WebServicePayload(MessageHandler<EmptyResponseReadyMessage>* handler, + MessageHandler<IWebService::HttpRequestErrorMessage>* failureHandler, Orthanc::IDynamicObject* userPayload) : emptyHandler_(handler), failureHandler_(failureHandler), - userPayload_(userPayload), - broker_(broker) + userPayload_(userPayload) { if (handler == NULL) @@ -100,14 +98,12 @@ } } - WebServicePayload(OrthancStone::MessageBroker& broker, - OrthancStone::MessageHandler<BinaryResponseReadyMessage>* handler, - OrthancStone::MessageHandler<IWebService::HttpRequestErrorMessage>* failureHandler, + WebServicePayload(MessageHandler<BinaryResponseReadyMessage>* handler, + MessageHandler<IWebService::HttpRequestErrorMessage>* failureHandler, Orthanc::IDynamicObject* userPayload) : binaryHandler_(handler), failureHandler_(failureHandler), - userPayload_(userPayload), - broker_(broker) + userPayload_(userPayload) { if (handler == NULL) { @@ -115,14 +111,12 @@ } } - WebServicePayload(OrthancStone::MessageBroker& broker, - OrthancStone::MessageHandler<JsonResponseReadyMessage>* handler, - OrthancStone::MessageHandler<IWebService::HttpRequestErrorMessage>* failureHandler, + WebServicePayload(MessageHandler<JsonResponseReadyMessage>* handler, + MessageHandler<IWebService::HttpRequestErrorMessage>* failureHandler, Orthanc::IDynamicObject* userPayload) : jsonHandler_(handler), failureHandler_(failureHandler), - userPayload_(userPayload), - broker_(broker) + userPayload_(userPayload) { if (handler == NULL) { @@ -134,35 +128,26 @@ { if (emptyHandler_.get() != NULL) { - if (broker_.IsActive(*(emptyHandler_->GetObserver()))) - { - emptyHandler_->Apply(OrthancApiClient::EmptyResponseReadyMessage - (message.GetUri(), userPayload_.get())); - } + emptyHandler_->Apply(OrthancApiClient::EmptyResponseReadyMessage + (message.GetUri(), userPayload_.get())); } else if (binaryHandler_.get() != NULL) { - if (broker_.IsActive(*(binaryHandler_->GetObserver()))) - { - binaryHandler_->Apply(OrthancApiClient::BinaryResponseReadyMessage - (message.GetUri(), message.GetAnswer(), - message.GetAnswerSize(), userPayload_.get())); - } + binaryHandler_->Apply(OrthancApiClient::BinaryResponseReadyMessage + (message.GetUri(), message.GetAnswer(), + message.GetAnswerSize(), userPayload_.get())); } else if (jsonHandler_.get() != NULL) { - if (broker_.IsActive(*(jsonHandler_->GetObserver()))) + Json::Value response; + if (MessagingToolbox::ParseJson(response, message.GetAnswer(), message.GetAnswerSize())) { - Json::Value response; - if (MessagingToolbox::ParseJson(response, message.GetAnswer(), message.GetAnswerSize())) - { - jsonHandler_->Apply(OrthancApiClient::JsonResponseReadyMessage - (message.GetUri(), response, userPayload_.get())); - } - else - { - NotifyConversionError(message); - } + jsonHandler_->Apply(OrthancApiClient::JsonResponseReadyMessage + (message.GetUri(), response, userPayload_.get())); + } + else + { + NotifyConversionError(message); } } else @@ -182,11 +167,8 @@ }; - OrthancApiClient::OrthancApiClient(OrthancStone::MessageBroker& broker, - IWebService& web, + OrthancApiClient::OrthancApiClient(IWebService& web, const std::string& baseUrl) : - IObservable(broker), - IObserver(broker), web_(web), baseUrl_(baseUrl) { @@ -195,26 +177,26 @@ void OrthancApiClient::GetJsonAsync( const std::string& uri, - OrthancStone::MessageHandler<JsonResponseReadyMessage>* successCallback, - OrthancStone::MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback, + MessageHandler<JsonResponseReadyMessage>* successCallback, + MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback, Orthanc::IDynamicObject* payload) { IWebService::HttpHeaders emptyHeaders; web_.GetAsync(baseUrl_ + uri, emptyHeaders, - new WebServicePayload(IObservable::GetBroker(), successCallback, failureCallback, payload), - new OrthancStone::Callable<OrthancApiClient, IWebService::HttpRequestSuccessMessage> - (*this, &OrthancApiClient::NotifyHttpSuccess), - new OrthancStone::Callable<OrthancApiClient, IWebService::HttpRequestErrorMessage> - (*this, &OrthancApiClient::NotifyHttpError)); + new WebServicePayload(successCallback, failureCallback, payload), + new DeprecatedCallable<OrthancApiClient, IWebService::HttpRequestSuccessMessage> + (GetSharedObserver(), &OrthancApiClient::NotifyHttpSuccess), + new DeprecatedCallable<OrthancApiClient, IWebService::HttpRequestErrorMessage> + (GetSharedObserver(), &OrthancApiClient::NotifyHttpError)); } void OrthancApiClient::GetBinaryAsync( const std::string& uri, const std::string& contentType, - OrthancStone::MessageHandler<BinaryResponseReadyMessage>* successCallback, - OrthancStone::MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback, + MessageHandler<BinaryResponseReadyMessage>* successCallback, + MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback, Orthanc::IDynamicObject* payload) { IWebService::HttpHeaders headers; @@ -225,34 +207,34 @@ void OrthancApiClient::GetBinaryAsync( const std::string& uri, const IWebService::HttpHeaders& headers, - OrthancStone::MessageHandler<BinaryResponseReadyMessage>* successCallback, - OrthancStone::MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback, + MessageHandler<BinaryResponseReadyMessage>* successCallback, + MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback, Orthanc::IDynamicObject* payload) { // printf("GET [%s] [%s]\n", baseUrl_.c_str(), uri.c_str()); web_.GetAsync(baseUrl_ + uri, headers, - new WebServicePayload(IObservable::GetBroker(), successCallback, failureCallback, payload), - new OrthancStone::Callable<OrthancApiClient, IWebService::HttpRequestSuccessMessage> - (*this, &OrthancApiClient::NotifyHttpSuccess), - new OrthancStone::Callable<OrthancApiClient, IWebService::HttpRequestErrorMessage> - (*this, &OrthancApiClient::NotifyHttpError)); + new WebServicePayload(successCallback, failureCallback, payload), + new DeprecatedCallable<OrthancApiClient, IWebService::HttpRequestSuccessMessage> + (GetSharedObserver(), &OrthancApiClient::NotifyHttpSuccess), + new DeprecatedCallable<OrthancApiClient, IWebService::HttpRequestErrorMessage> + (GetSharedObserver(), &OrthancApiClient::NotifyHttpError)); } void OrthancApiClient::PostBinaryAsyncExpectJson( const std::string& uri, const std::string& body, - OrthancStone::MessageHandler<JsonResponseReadyMessage>* successCallback, - OrthancStone::MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback, + MessageHandler<JsonResponseReadyMessage>* successCallback, + MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback, Orthanc::IDynamicObject* payload) { web_.PostAsync(baseUrl_ + uri, IWebService::HttpHeaders(), body, - new WebServicePayload(IObservable::GetBroker(), successCallback, failureCallback, payload), - new OrthancStone::Callable<OrthancApiClient, IWebService::HttpRequestSuccessMessage> - (*this, &OrthancApiClient::NotifyHttpSuccess), - new OrthancStone::Callable<OrthancApiClient, IWebService::HttpRequestErrorMessage> - (*this, &OrthancApiClient::NotifyHttpError)); + new WebServicePayload(successCallback, failureCallback, payload), + new DeprecatedCallable<OrthancApiClient, IWebService::HttpRequestSuccessMessage> + (GetSharedObserver(), &OrthancApiClient::NotifyHttpSuccess), + new DeprecatedCallable<OrthancApiClient, IWebService::HttpRequestErrorMessage> + (GetSharedObserver(), &OrthancApiClient::NotifyHttpError)); } @@ -266,23 +248,23 @@ void OrthancApiClient::PostBinaryAsync( const std::string& uri, const std::string& body, - OrthancStone::MessageHandler<EmptyResponseReadyMessage>* successCallback, - OrthancStone::MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback, + MessageHandler<EmptyResponseReadyMessage>* successCallback, + MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback, Orthanc::IDynamicObject* payload /* takes ownership */) { web_.PostAsync(baseUrl_ + uri, IWebService::HttpHeaders(), body, - new WebServicePayload(IObservable::GetBroker(), successCallback, failureCallback, payload), - new OrthancStone::Callable<OrthancApiClient, IWebService::HttpRequestSuccessMessage> - (*this, &OrthancApiClient::NotifyHttpSuccess), - new OrthancStone::Callable<OrthancApiClient, IWebService::HttpRequestErrorMessage> - (*this, &OrthancApiClient::NotifyHttpError)); + new WebServicePayload(successCallback, failureCallback, payload), + new DeprecatedCallable<OrthancApiClient, IWebService::HttpRequestSuccessMessage> + (GetSharedObserver(), &OrthancApiClient::NotifyHttpSuccess), + new DeprecatedCallable<OrthancApiClient, IWebService::HttpRequestErrorMessage> + (GetSharedObserver(), &OrthancApiClient::NotifyHttpError)); } void OrthancApiClient::PostJsonAsyncExpectJson( const std::string& uri, const Json::Value& data, - OrthancStone::MessageHandler<JsonResponseReadyMessage>* successCallback, - OrthancStone::MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback, + MessageHandler<JsonResponseReadyMessage>* successCallback, + MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback, Orthanc::IDynamicObject* payload) { std::string body; @@ -302,8 +284,8 @@ void OrthancApiClient::PostJsonAsync( const std::string& uri, const Json::Value& data, - OrthancStone::MessageHandler<EmptyResponseReadyMessage>* successCallback, - OrthancStone::MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback, + MessageHandler<EmptyResponseReadyMessage>* successCallback, + MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback, Orthanc::IDynamicObject* payload /* takes ownership */) { std::string body; @@ -313,16 +295,16 @@ void OrthancApiClient::DeleteAsync( const std::string& uri, - OrthancStone::MessageHandler<EmptyResponseReadyMessage>* successCallback, - OrthancStone::MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback, + MessageHandler<EmptyResponseReadyMessage>* successCallback, + MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback, Orthanc::IDynamicObject* payload) { web_.DeleteAsync(baseUrl_ + uri, IWebService::HttpHeaders(), - new WebServicePayload(IObservable::GetBroker(), successCallback, failureCallback, payload), - new OrthancStone::Callable<OrthancApiClient, IWebService::HttpRequestSuccessMessage> - (*this, &OrthancApiClient::NotifyHttpSuccess), - new OrthancStone::Callable<OrthancApiClient, IWebService::HttpRequestErrorMessage> - (*this, &OrthancApiClient::NotifyHttpError)); + new WebServicePayload(successCallback, failureCallback, payload), + new DeprecatedCallable<OrthancApiClient, IWebService::HttpRequestSuccessMessage> + (GetSharedObserver(), &OrthancApiClient::NotifyHttpSuccess), + new DeprecatedCallable<OrthancApiClient, IWebService::HttpRequestErrorMessage> + (GetSharedObserver(), &OrthancApiClient::NotifyHttpError)); }
--- a/Framework/Deprecated/Toolbox/OrthancApiClient.h Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Deprecated/Toolbox/OrthancApiClient.h Tue Mar 31 15:51:02 2020 +0200 @@ -25,13 +25,24 @@ #include <json/json.h> #include "IWebService.h" -#include "../../Messages/IObservable.h" +#include "../../Messages/ObserverBase.h" namespace Deprecated { + enum SliceImageQuality + { + SliceImageQuality_FullPng, // smaller to transmit but longer to generate on Orthanc side (better choice when on low bandwidth) + SliceImageQuality_FullPam, // bigger to transmit but faster to generate on Orthanc side (better choice when on localhost or LAN) + SliceImageQuality_Jpeg50, + SliceImageQuality_Jpeg90, + SliceImageQuality_Jpeg95, + + SliceImageQuality_InternalRaw // downloads the raw pixels data as they are stored in the DICOM file (internal use only) + }; + class OrthancApiClient : public OrthancStone::IObservable, - public OrthancStone::IObserver + public OrthancStone::ObserverBase<OrthancApiClient> { public: class JsonResponseReadyMessage : public OrthancStone::IMessage @@ -157,8 +168,7 @@ std::string baseUrl_; public: - OrthancApiClient(OrthancStone::MessageBroker& broker, - IWebService& web, + OrthancApiClient(IWebService& web, const std::string& baseUrl); virtual ~OrthancApiClient() @@ -169,36 +179,36 @@ // schedule a GET request expecting a JSON response. void GetJsonAsync(const std::string& uri, - OrthancStone::MessageHandler<JsonResponseReadyMessage>* successCallback, - OrthancStone::MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback = NULL, + MessageHandler<JsonResponseReadyMessage>* successCallback, + MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback = NULL, Orthanc::IDynamicObject* payload = NULL /* takes ownership */); // schedule a GET request expecting a binary response. void GetBinaryAsync(const std::string& uri, const std::string& contentType, - OrthancStone::MessageHandler<BinaryResponseReadyMessage>* successCallback, - OrthancStone::MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback = NULL, + MessageHandler<BinaryResponseReadyMessage>* successCallback, + MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback = NULL, Orthanc::IDynamicObject* payload = NULL /* takes ownership */); // schedule a GET request expecting a binary response. void GetBinaryAsync(const std::string& uri, const IWebService::HttpHeaders& headers, - OrthancStone::MessageHandler<BinaryResponseReadyMessage>* successCallback, - OrthancStone::MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback = NULL, + MessageHandler<BinaryResponseReadyMessage>* successCallback, + MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback = NULL, Orthanc::IDynamicObject* payload = NULL /* takes ownership */); // schedule a POST request expecting a JSON response. void PostBinaryAsyncExpectJson(const std::string& uri, const std::string& body, - OrthancStone::MessageHandler<JsonResponseReadyMessage>* successCallback, - OrthancStone::MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback = NULL, + MessageHandler<JsonResponseReadyMessage>* successCallback, + MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback = NULL, Orthanc::IDynamicObject* payload = NULL /* takes ownership */); // schedule a POST request expecting a JSON response. void PostJsonAsyncExpectJson(const std::string& uri, const Json::Value& data, - OrthancStone::MessageHandler<JsonResponseReadyMessage>* successCallback, - OrthancStone::MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback = NULL, + MessageHandler<JsonResponseReadyMessage>* successCallback, + MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback = NULL, Orthanc::IDynamicObject* payload = NULL /* takes ownership */); // schedule a POST request and don't mind the response. @@ -208,8 +218,8 @@ // schedule a POST request and don't expect any response. void PostJsonAsync(const std::string& uri, const Json::Value& data, - OrthancStone::MessageHandler<EmptyResponseReadyMessage>* successCallback, - OrthancStone::MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback = NULL, + MessageHandler<EmptyResponseReadyMessage>* successCallback, + MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback = NULL, Orthanc::IDynamicObject* payload = NULL /* takes ownership */); @@ -220,14 +230,14 @@ // schedule a POST request and don't expect any response. void PostBinaryAsync(const std::string& uri, const std::string& body, - OrthancStone::MessageHandler<EmptyResponseReadyMessage>* successCallback, - OrthancStone::MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback = NULL, + MessageHandler<EmptyResponseReadyMessage>* successCallback, + MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback = NULL, Orthanc::IDynamicObject* payload = NULL /* takes ownership */); // schedule a DELETE request expecting an empty response. void DeleteAsync(const std::string& uri, - OrthancStone::MessageHandler<EmptyResponseReadyMessage>* successCallback, - OrthancStone::MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback = NULL, + MessageHandler<EmptyResponseReadyMessage>* successCallback, + MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback = NULL, Orthanc::IDynamicObject* payload = NULL /* takes ownership */); void NotifyHttpSuccess(const IWebService::HttpRequestSuccessMessage& message);
--- a/Framework/Deprecated/Toolbox/OrthancSlicesLoader.cpp Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Deprecated/Toolbox/OrthancSlicesLoader.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -639,10 +639,7 @@ } - OrthancSlicesLoader::OrthancSlicesLoader(OrthancStone::MessageBroker& broker, - OrthancApiClient& orthanc) : - OrthancStone::IObservable(broker), - OrthancStone::IObserver(broker), + OrthancSlicesLoader::OrthancSlicesLoader(boost::shared_ptr<OrthancApiClient> orthanc) : orthanc_(orthanc), state_(State_Initialization) { @@ -658,10 +655,10 @@ else { state_ = State_LoadingGeometry; - orthanc_.GetJsonAsync("/series/" + seriesId + "/instances-tags", - new OrthancStone::Callable<OrthancSlicesLoader, OrthancApiClient::JsonResponseReadyMessage>(*this, &OrthancSlicesLoader::ParseSeriesGeometry), - new OrthancStone::Callable<OrthancSlicesLoader, IWebService::HttpRequestErrorMessage>(*this, &OrthancSlicesLoader::OnGeometryError), - NULL); + orthanc_->GetJsonAsync("/series/" + seriesId + "/instances-tags", + new DeprecatedCallable<OrthancSlicesLoader, OrthancApiClient::JsonResponseReadyMessage>(GetSharedObserver(), &OrthancSlicesLoader::ParseSeriesGeometry), + new DeprecatedCallable<OrthancSlicesLoader, IWebService::HttpRequestErrorMessage>(GetSharedObserver(), &OrthancSlicesLoader::OnGeometryError), + NULL); } } @@ -677,10 +674,10 @@ // Tag "3004-000c" is "Grid Frame Offset Vector", which is // mandatory to read RT DOSE, but is too long to be returned by default - orthanc_.GetJsonAsync("/instances/" + instanceId + "/tags?ignore-length=3004-000c", - new OrthancStone::Callable<OrthancSlicesLoader, OrthancApiClient::JsonResponseReadyMessage>(*this, &OrthancSlicesLoader::ParseInstanceGeometry), - new OrthancStone::Callable<OrthancSlicesLoader, IWebService::HttpRequestErrorMessage>(*this, &OrthancSlicesLoader::OnGeometryError), - Operation::DownloadInstanceGeometry(instanceId)); + orthanc_->GetJsonAsync("/instances/" + instanceId + "/tags?ignore-length=3004-000c", + new DeprecatedCallable<OrthancSlicesLoader, OrthancApiClient::JsonResponseReadyMessage>(GetSharedObserver(), &OrthancSlicesLoader::ParseInstanceGeometry), + new DeprecatedCallable<OrthancSlicesLoader, IWebService::HttpRequestErrorMessage>(GetSharedObserver(), &OrthancSlicesLoader::OnGeometryError), + Operation::DownloadInstanceGeometry(instanceId)); } } @@ -696,10 +693,10 @@ { state_ = State_LoadingGeometry; - orthanc_.GetJsonAsync("/instances/" + instanceId + "/tags", - new OrthancStone::Callable<OrthancSlicesLoader, OrthancApiClient::JsonResponseReadyMessage>(*this, &OrthancSlicesLoader::ParseFrameGeometry), - new OrthancStone::Callable<OrthancSlicesLoader, IWebService::HttpRequestErrorMessage>(*this, &OrthancSlicesLoader::OnGeometryError), - Operation::DownloadFrameGeometry(instanceId, frame)); + orthanc_->GetJsonAsync("/instances/" + instanceId + "/tags", + new DeprecatedCallable<OrthancSlicesLoader, OrthancApiClient::JsonResponseReadyMessage>(GetSharedObserver(), &OrthancSlicesLoader::ParseFrameGeometry), + new DeprecatedCallable<OrthancSlicesLoader, IWebService::HttpRequestErrorMessage>(GetSharedObserver(), &OrthancSlicesLoader::OnGeometryError), + Operation::DownloadFrameGeometry(instanceId, frame)); } } @@ -770,23 +767,23 @@ throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); } - orthanc_.GetBinaryAsync(uri, "image/png", - new OrthancStone::Callable<OrthancSlicesLoader, - OrthancApiClient::BinaryResponseReadyMessage> - (*this, &OrthancSlicesLoader::ParseSliceImagePng), - new OrthancStone::Callable<OrthancSlicesLoader, - IWebService::HttpRequestErrorMessage> - (*this, &OrthancSlicesLoader::OnSliceImageError), - Operation::DownloadSliceImage( - static_cast<unsigned int>(index), slice, SliceImageQuality_FullPng)); -} + orthanc_->GetBinaryAsync(uri, "image/png", + new DeprecatedCallable<OrthancSlicesLoader, + OrthancApiClient::BinaryResponseReadyMessage> + (GetSharedObserver(), &OrthancSlicesLoader::ParseSliceImagePng), + new DeprecatedCallable<OrthancSlicesLoader, + IWebService::HttpRequestErrorMessage> + (GetSharedObserver(), &OrthancSlicesLoader::OnSliceImageError), + Operation::DownloadSliceImage( + static_cast<unsigned int>(index), slice, SliceImageQuality_FullPng)); + } void OrthancSlicesLoader::ScheduleSliceImagePam(const Slice& slice, size_t index) { std::string uri = ("/instances/" + slice.GetOrthancInstanceId() + "/frames/" + - boost::lexical_cast<std::string>(slice.GetFrame())); + boost::lexical_cast<std::string>(slice.GetFrame())); switch (slice.GetConverter().GetExpectedPixelFormat()) { @@ -806,15 +803,15 @@ throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); } - orthanc_.GetBinaryAsync(uri, "image/x-portable-arbitrarymap", - new OrthancStone::Callable<OrthancSlicesLoader, - OrthancApiClient::BinaryResponseReadyMessage> - (*this, &OrthancSlicesLoader::ParseSliceImagePam), - new OrthancStone::Callable<OrthancSlicesLoader, - IWebService::HttpRequestErrorMessage> - (*this, &OrthancSlicesLoader::OnSliceImageError), - Operation::DownloadSliceImage(static_cast<unsigned int>(index), - slice, SliceImageQuality_FullPam)); + orthanc_->GetBinaryAsync(uri, "image/x-portable-arbitrarymap", + new DeprecatedCallable<OrthancSlicesLoader, + OrthancApiClient::BinaryResponseReadyMessage> + (GetSharedObserver(), &OrthancSlicesLoader::ParseSliceImagePam), + new DeprecatedCallable<OrthancSlicesLoader, + IWebService::HttpRequestErrorMessage> + (GetSharedObserver(), &OrthancSlicesLoader::OnSliceImageError), + Operation::DownloadSliceImage(static_cast<unsigned int>(index), + slice, SliceImageQuality_FullPam)); } @@ -849,15 +846,15 @@ "-" + slice.GetOrthancInstanceId() + "_" + boost::lexical_cast<std::string>(slice.GetFrame())); - orthanc_.GetJsonAsync(uri, - new OrthancStone::Callable<OrthancSlicesLoader, - OrthancApiClient::JsonResponseReadyMessage> - (*this, &OrthancSlicesLoader::ParseSliceImageJpeg), - new OrthancStone::Callable<OrthancSlicesLoader, - IWebService::HttpRequestErrorMessage> - (*this, &OrthancSlicesLoader::OnSliceImageError), - Operation::DownloadSliceImage( - static_cast<unsigned int>(index), slice, quality)); + orthanc_->GetJsonAsync(uri, + new DeprecatedCallable<OrthancSlicesLoader, + OrthancApiClient::JsonResponseReadyMessage> + (GetSharedObserver(), &OrthancSlicesLoader::ParseSliceImageJpeg), + new DeprecatedCallable<OrthancSlicesLoader, + IWebService::HttpRequestErrorMessage> + (GetSharedObserver(), &OrthancSlicesLoader::OnSliceImageError), + Operation::DownloadSliceImage( + static_cast<unsigned int>(index), slice, quality)); } @@ -890,15 +887,15 @@ { std::string uri = ("/instances/" + slice.GetOrthancInstanceId() + "/frames/" + boost::lexical_cast<std::string>(slice.GetFrame()) + "/raw.gz"); - orthanc_.GetBinaryAsync(uri, IWebService::HttpHeaders(), - new OrthancStone::Callable<OrthancSlicesLoader, - OrthancApiClient::BinaryResponseReadyMessage> - (*this, &OrthancSlicesLoader::ParseSliceRawImage), - new OrthancStone::Callable<OrthancSlicesLoader, - IWebService::HttpRequestErrorMessage> - (*this, &OrthancSlicesLoader::OnSliceImageError), - Operation::DownloadSliceRawImage( - static_cast<unsigned int>(index), slice)); + orthanc_->GetBinaryAsync(uri, IWebService::HttpHeaders(), + new DeprecatedCallable<OrthancSlicesLoader, + OrthancApiClient::BinaryResponseReadyMessage> + (GetSharedObserver(), &OrthancSlicesLoader::ParseSliceRawImage), + new DeprecatedCallable<OrthancSlicesLoader, + IWebService::HttpRequestErrorMessage> + (GetSharedObserver(), &OrthancSlicesLoader::OnSliceImageError), + Operation::DownloadSliceRawImage( + static_cast<unsigned int>(index), slice)); } } }
--- a/Framework/Deprecated/Toolbox/OrthancSlicesLoader.h Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Deprecated/Toolbox/OrthancSlicesLoader.h Tue Mar 31 15:51:02 2020 +0200 @@ -22,6 +22,7 @@ #pragma once #include "../../Messages/IObservable.h" +#include "../../Messages/ObserverBase.h" #include "../../StoneEnumerations.h" #include "../../Toolbox/SlicesSorter.h" #include "IWebService.h" @@ -33,7 +34,9 @@ namespace Deprecated { - class OrthancSlicesLoader : public OrthancStone::IObservable, public OrthancStone::IObserver + class OrthancSlicesLoader : + public OrthancStone::IObservable, + public OrthancStone::ObserverBase<OrthancSlicesLoader> { public: ORTHANC_STONE_DEFINE_ORIGIN_MESSAGE(__FILE__, __LINE__, SliceGeometryReadyMessage, OrthancSlicesLoader); @@ -143,7 +146,7 @@ class Operation; - OrthancApiClient& orthanc_; + boost::shared_ptr<OrthancApiClient> orthanc_; State state_; OrthancStone::SlicesSorter slices_; @@ -183,9 +186,8 @@ void SortAndFinalizeSlices(); public: - OrthancSlicesLoader(OrthancStone::MessageBroker& broker, - //ISliceLoaderObserver& callback, - OrthancApiClient& orthancApi); + OrthancSlicesLoader(//ISliceLoaderObserver& callback, + boost::shared_ptr<OrthancApiClient> orthancApi); void ScheduleLoadSeries(const std::string& seriesId);
--- a/Framework/Deprecated/Viewport/IViewport.h Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Deprecated/Viewport/IViewport.h Tue Mar 31 15:51:02 2020 +0200 @@ -37,15 +37,6 @@ public: ORTHANC_STONE_DEFINE_ORIGIN_MESSAGE(__FILE__, __LINE__, ViewportChangedMessage, IViewport); - IViewport(OrthancStone::MessageBroker& broker) : - IObservable(broker) - { - } - - virtual ~IViewport() - { - } - virtual void FitContent() = 0; virtual void SetStatusBar(IStatusBar& statusBar) = 0;
--- a/Framework/Deprecated/Viewport/WidgetViewport.cpp Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Deprecated/Viewport/WidgetViewport.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -26,8 +26,7 @@ namespace Deprecated { - WidgetViewport::WidgetViewport(OrthancStone::MessageBroker& broker) : - IViewport(broker), + WidgetViewport::WidgetViewport() : statusBar_(NULL), isMouseOver_(false), lastMouseX_(0), @@ -57,7 +56,7 @@ } - IWidget& WidgetViewport::SetCentralWidget(IWidget* widget) + void WidgetViewport::SetCentralWidget(boost::shared_ptr<IWidget> widget) { if (widget == NULL) { @@ -66,7 +65,7 @@ mouseTracker_.reset(NULL); - centralWidget_.reset(widget); + centralWidget_ = widget; centralWidget_->SetViewport(*this); if (statusBar_ != NULL) @@ -75,8 +74,6 @@ } NotifyBackgroundChanged(); - - return *widget; }
--- a/Framework/Deprecated/Viewport/WidgetViewport.h Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Deprecated/Viewport/WidgetViewport.h Tue Mar 31 15:51:02 2020 +0200 @@ -33,7 +33,7 @@ class WidgetViewport : public IViewport { private: - std::unique_ptr<IWidget> centralWidget_; + boost::shared_ptr<IWidget> centralWidget_; IStatusBar* statusBar_; std::unique_ptr<IMouseTracker> mouseTracker_; bool isMouseOver_; @@ -43,13 +43,13 @@ bool backgroundChanged_; public: - WidgetViewport(OrthancStone::MessageBroker& broker); + WidgetViewport(); virtual void FitContent(); virtual void SetStatusBar(IStatusBar& statusBar); - IWidget& SetCentralWidget(IWidget* widget); // Takes ownership + void SetCentralWidget(boost::shared_ptr<IWidget> widget); virtual void NotifyBackgroundChanged();
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Deprecated/Volumes/IGeometryProvider.h Tue Mar 31 15:51:02 2020 +0200 @@ -0,0 +1,35 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 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 "../../Volumes/VolumeImageGeometry.h" + +namespace Deprecated +{ + class IGeometryProvider + { + public: + virtual ~IGeometryProvider() {} + virtual bool HasGeometry() const = 0; + virtual const OrthancStone::VolumeImageGeometry& GetImageGeometry() const = 0; + }; +}
--- a/Framework/Deprecated/Volumes/ISlicedVolume.h Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Deprecated/Volumes/ISlicedVolume.h Tue Mar 31 15:51:02 2020 +0200 @@ -65,11 +65,6 @@ }; - ISlicedVolume(OrthancStone::MessageBroker& broker) : - IObservable(broker) - { - } - virtual size_t GetSliceCount() const = 0; virtual const Slice& GetSlice(size_t slice) const = 0;
--- a/Framework/Deprecated/Volumes/IVolumeLoader.h Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Deprecated/Volumes/IVolumeLoader.h Tue Mar 31 15:51:02 2020 +0200 @@ -31,10 +31,5 @@ ORTHANC_STONE_DEFINE_ORIGIN_MESSAGE(__FILE__, __LINE__, GeometryReadyMessage, IVolumeLoader); ORTHANC_STONE_DEFINE_ORIGIN_MESSAGE(__FILE__, __LINE__, GeometryErrorMessage, IVolumeLoader); ORTHANC_STONE_DEFINE_ORIGIN_MESSAGE(__FILE__, __LINE__, ContentChangedMessage, IVolumeLoader); - - IVolumeLoader(OrthancStone::MessageBroker& broker) : - IObservable(broker) - { - } }; }
--- a/Framework/Deprecated/Volumes/StructureSetLoader.cpp Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Deprecated/Volumes/StructureSetLoader.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -27,10 +27,7 @@ namespace Deprecated { - StructureSetLoader::StructureSetLoader(OrthancStone::MessageBroker& broker, - OrthancApiClient& orthanc) : - IVolumeLoader(broker), - IObserver(broker), + StructureSetLoader::StructureSetLoader(OrthancApiClient& orthanc) : orthanc_(orthanc) { } @@ -60,7 +57,7 @@ it != instances.end(); ++it) { orthanc_.PostBinaryAsyncExpectJson("/tools/lookup", *it, - new OrthancStone::Callable<StructureSetLoader, OrthancApiClient::JsonResponseReadyMessage>(*this, &StructureSetLoader::OnLookupCompleted)); + new DeprecatedCallable<StructureSetLoader, OrthancApiClient::JsonResponseReadyMessage>(GetSharedObserver(), &StructureSetLoader::OnLookupCompleted)); } BroadcastMessage(GeometryReadyMessage(*this)); @@ -84,7 +81,7 @@ const std::string& instance = lookup[0]["ID"].asString(); orthanc_.GetJsonAsync("/instances/" + instance + "/tags", - new OrthancStone::Callable<StructureSetLoader, OrthancApiClient::JsonResponseReadyMessage>(*this, &StructureSetLoader::OnReferencedSliceLoaded)); + new DeprecatedCallable<StructureSetLoader, OrthancApiClient::JsonResponseReadyMessage>(GetSharedObserver(), &StructureSetLoader::OnReferencedSliceLoaded)); } @@ -97,7 +94,7 @@ else { orthanc_.GetJsonAsync("/instances/" + instance + "/tags?ignore-length=3006-0050", - new OrthancStone::Callable<StructureSetLoader, OrthancApiClient::JsonResponseReadyMessage>(*this, &StructureSetLoader::OnStructureSetLoaded)); + new DeprecatedCallable<StructureSetLoader, OrthancApiClient::JsonResponseReadyMessage>(GetSharedObserver(), &StructureSetLoader::OnStructureSetLoaded)); } }
--- a/Framework/Deprecated/Volumes/StructureSetLoader.h Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Deprecated/Volumes/StructureSetLoader.h Tue Mar 31 15:51:02 2020 +0200 @@ -21,6 +21,7 @@ #pragma once +#include "../../Messages/ObserverBase.h" #include "../../Toolbox/DicomStructureSet.h" #include "../Toolbox/OrthancApiClient.h" #include "IVolumeLoader.h" @@ -31,7 +32,7 @@ { class StructureSetLoader : public IVolumeLoader, - public OrthancStone::IObserver + public OrthancStone::ObserverBase<StructureSetLoader> { private: OrthancApiClient& orthanc_; @@ -44,8 +45,7 @@ void OnLookupCompleted(const OrthancApiClient::JsonResponseReadyMessage& message); public: - StructureSetLoader(OrthancStone::MessageBroker& broker, - OrthancApiClient& orthanc); + StructureSetLoader(OrthancApiClient& orthanc); void ScheduleLoadInstance(const std::string& instance);
--- a/Framework/Deprecated/Widgets/LayoutWidget.cpp Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Deprecated/Widgets/LayoutWidget.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -85,14 +85,14 @@ class LayoutWidget::ChildWidget : public boost::noncopyable { private: - std::unique_ptr<IWidget> widget_; + boost::shared_ptr<IWidget> widget_; int left_; int top_; unsigned int width_; unsigned int height_; public: - ChildWidget(IWidget* widget) : + ChildWidget(boost::shared_ptr<IWidget> widget) : widget_(widget) { assert(widget != NULL); @@ -354,7 +354,7 @@ } - IWidget& LayoutWidget::AddWidget(IWidget* widget) // Takes ownership + void LayoutWidget::AddWidget(boost::shared_ptr<IWidget> widget) // Takes ownership { if (widget == NULL) { @@ -375,8 +375,6 @@ { hasAnimation_ = true; } - - return *widget; }
--- a/Framework/Deprecated/Widgets/LayoutWidget.h Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Deprecated/Widgets/LayoutWidget.h Tue Mar 31 15:51:02 2020 +0200 @@ -94,7 +94,7 @@ return paddingInternal_; } - IWidget& AddWidget(IWidget* widget); // Takes ownership + void AddWidget(boost::shared_ptr<IWidget> widget); virtual void SetStatusBar(IStatusBar& statusBar);
--- a/Framework/Deprecated/Widgets/SliceViewerWidget.cpp Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Deprecated/Widgets/SliceViewerWidget.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -35,206 +35,174 @@ namespace Deprecated { - class SliceViewerWidget::Scene : public boost::noncopyable + void SliceViewerWidget::Scene::DeleteLayer(size_t index) + { + if (index >= renderers_.size()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + + assert(countMissing_ <= renderers_.size()); + + if (renderers_[index] != NULL) + { + assert(countMissing_ < renderers_.size()); + delete renderers_[index]; + renderers_[index] = NULL; + countMissing_++; + } + } + + + SliceViewerWidget::Scene::Scene(const OrthancStone::CoordinateSystem3D& plane, + double thickness, + size_t countLayers) : + plane_(plane), + thickness_(thickness), + countMissing_(countLayers), + renderers_(countLayers, NULL) + { + if (thickness <= 0) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + } + + + SliceViewerWidget::Scene::~Scene() + { + for (size_t i = 0; i < renderers_.size(); i++) + { + DeleteLayer(i); + } + } + + void SliceViewerWidget::Scene::SetLayer(size_t index, + ILayerRenderer* renderer) // Takes ownership { - private: - OrthancStone::CoordinateSystem3D plane_; - double thickness_; - size_t countMissing_; - std::vector<ILayerRenderer*> renderers_; + if (renderer == NULL) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); + } + + DeleteLayer(index); + + renderers_[index] = renderer; + countMissing_--; + } + + + bool SliceViewerWidget::Scene::RenderScene(OrthancStone::CairoContext& context, + const ViewportGeometry& view, + const OrthancStone::CoordinateSystem3D& viewportPlane) + { + bool fullQuality = true; + cairo_t *cr = context.GetObject(); - public: - void DeleteLayer(size_t index) + for (size_t i = 0; i < renderers_.size(); i++) { - if (index >= renderers_.size()) + if (renderers_[i] != NULL) { - throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + const OrthancStone::CoordinateSystem3D& framePlane = renderers_[i]->GetLayerPlane(); + + double x0, y0, x1, y1, x2, y2; + viewportPlane.ProjectPoint(x0, y0, framePlane.GetOrigin()); + viewportPlane.ProjectPoint(x1, y1, framePlane.GetOrigin() + framePlane.GetAxisX()); + viewportPlane.ProjectPoint(x2, y2, framePlane.GetOrigin() + framePlane.GetAxisY()); + + /** + * Now we solve the system of linear equations Ax + b = x', given: + * A [0 ; 0] + b = [x0 ; y0] + * A [1 ; 0] + b = [x1 ; y1] + * A [0 ; 1] + b = [x2 ; y2] + * <=> + * b = [x0 ; y0] + * A [1 ; 0] = [x1 ; y1] - b = [x1 - x0 ; y1 - y0] + * A [0 ; 1] = [x2 ; y2] - b = [x2 - x0 ; y2 - y0] + * <=> + * b = [x0 ; y0] + * [a11 ; a21] = [x1 - x0 ; y1 - y0] + * [a12 ; a22] = [x2 - x0 ; y2 - y0] + **/ + + cairo_matrix_t transform; + cairo_matrix_init(&transform, x1 - x0, y1 - y0, x2 - x0, y2 - y0, x0, y0); + + cairo_save(cr); + cairo_transform(cr, &transform); + + if (!renderers_[i]->RenderLayer(context, view)) + { + cairo_restore(cr); + return false; + } + + cairo_restore(cr); } - assert(countMissing_ <= renderers_.size()); - - if (renderers_[index] != NULL) + if (renderers_[i] != NULL && + !renderers_[i]->IsFullQuality()) { - assert(countMissing_ < renderers_.size()); - delete renderers_[index]; - renderers_[index] = NULL; - countMissing_++; - } - } - - Scene(const OrthancStone::CoordinateSystem3D& plane, - double thickness, - size_t countLayers) : - plane_(plane), - thickness_(thickness), - countMissing_(countLayers), - renderers_(countLayers, NULL) - { - if (thickness <= 0) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); - } - } - - ~Scene() - { - for (size_t i = 0; i < renderers_.size(); i++) - { - DeleteLayer(i); + fullQuality = false; } } - void SetLayer(size_t index, - ILayerRenderer* renderer) // Takes ownership - { - if (renderer == NULL) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); - } - - DeleteLayer(index); - - renderers_[index] = renderer; - countMissing_--; - } - - const OrthancStone::CoordinateSystem3D& GetPlane() const + if (!fullQuality) { - return plane_; - } + double x, y; + view.MapDisplayToScene(x, y, static_cast<double>(view.GetDisplayWidth()) / 2.0, 10); - bool HasRenderer(size_t index) - { - return renderers_[index] != NULL; - } + cairo_translate(cr, x, y); - bool IsComplete() const - { - return countMissing_ == 0; - } - - unsigned int GetCountMissing() const - { - return static_cast<unsigned int>(countMissing_); +#if 1 + double s = 5.0 / view.GetZoom(); + cairo_rectangle(cr, -s, -s, 2.0 * s, 2.0 * s); +#else + // TODO Drawing filled circles makes WebAssembly crash! + cairo_arc(cr, 0, 0, 5.0 / view.GetZoom(), 0, 2.0 * boost::math::constants::pi<double>()); +#endif + + cairo_set_line_width(cr, 2.0 / view.GetZoom()); + cairo_set_source_rgb(cr, 1, 1, 1); + cairo_stroke_preserve(cr); + cairo_set_source_rgb(cr, 1, 0, 0); + cairo_fill(cr); } - bool RenderScene(OrthancStone::CairoContext& context, - const ViewportGeometry& view, - const OrthancStone::CoordinateSystem3D& viewportPlane) - { - bool fullQuality = true; - cairo_t *cr = context.GetObject(); + return true; + } - for (size_t i = 0; i < renderers_.size(); i++) - { - if (renderers_[i] != NULL) - { - const OrthancStone::CoordinateSystem3D& framePlane = renderers_[i]->GetLayerPlane(); - - double x0, y0, x1, y1, x2, y2; - viewportPlane.ProjectPoint(x0, y0, framePlane.GetOrigin()); - viewportPlane.ProjectPoint(x1, y1, framePlane.GetOrigin() + framePlane.GetAxisX()); - viewportPlane.ProjectPoint(x2, y2, framePlane.GetOrigin() + framePlane.GetAxisY()); + void SliceViewerWidget::Scene::SetLayerStyle(size_t index, + const RenderStyle& style) + { + if (renderers_[index] != NULL) + { + renderers_[index]->SetLayerStyle(style); + } + } - /** - * Now we solve the system of linear equations Ax + b = x', given: - * A [0 ; 0] + b = [x0 ; y0] - * A [1 ; 0] + b = [x1 ; y1] - * A [0 ; 1] + b = [x2 ; y2] - * <=> - * b = [x0 ; y0] - * A [1 ; 0] = [x1 ; y1] - b = [x1 - x0 ; y1 - y0] - * A [0 ; 1] = [x2 ; y2] - b = [x2 - x0 ; y2 - y0] - * <=> - * b = [x0 ; y0] - * [a11 ; a21] = [x1 - x0 ; y1 - y0] - * [a12 ; a22] = [x2 - x0 ; y2 - y0] - **/ - - cairo_matrix_t transform; - cairo_matrix_init(&transform, x1 - x0, y1 - y0, x2 - x0, y2 - y0, x0, y0); + bool SliceViewerWidget::Scene::ContainsPlane(const OrthancStone::CoordinateSystem3D& plane) const + { + bool isOpposite; + if (!OrthancStone::GeometryToolbox::IsParallelOrOpposite(isOpposite, + plane.GetNormal(), + plane_.GetNormal())) + { + return false; + } + else + { + double z = (plane_.ProjectAlongNormal(plane.GetOrigin()) - + plane_.ProjectAlongNormal(plane_.GetOrigin())); - cairo_save(cr); - cairo_transform(cr, &transform); - - if (!renderers_[i]->RenderLayer(context, view)) - { - cairo_restore(cr); - return false; - } - - cairo_restore(cr); - } - - if (renderers_[i] != NULL && - !renderers_[i]->IsFullQuality()) - { - fullQuality = false; - } + if (z < 0) + { + z = -z; } - if (!fullQuality) - { - double x, y; - view.MapDisplayToScene(x, y, static_cast<double>(view.GetDisplayWidth()) / 2.0, 10); - - cairo_translate(cr, x, y); - -#if 1 - double s = 5.0 / view.GetZoom(); - cairo_rectangle(cr, -s, -s, 2.0 * s, 2.0 * s); -#else - // TODO Drawing filled circles makes WebAssembly crash! - cairo_arc(cr, 0, 0, 5.0 / view.GetZoom(), 0, 2.0 * boost::math::constants::pi<double>()); -#endif - - cairo_set_line_width(cr, 2.0 / view.GetZoom()); - cairo_set_source_rgb(cr, 1, 1, 1); - cairo_stroke_preserve(cr); - cairo_set_source_rgb(cr, 1, 0, 0); - cairo_fill(cr); - } - - return true; + return z <= thickness_; } - - void SetLayerStyle(size_t index, - const RenderStyle& style) - { - if (renderers_[index] != NULL) - { - renderers_[index]->SetLayerStyle(style); - } - } - - bool ContainsPlane(const OrthancStone::CoordinateSystem3D& plane) const - { - bool isOpposite; - if (!OrthancStone::GeometryToolbox::IsParallelOrOpposite(isOpposite, - plane.GetNormal(), - plane_.GetNormal())) - { - return false; - } - else - { - double z = (plane_.ProjectAlongNormal(plane.GetOrigin()) - - plane_.ProjectAlongNormal(plane_.GetOrigin())); - - if (z < 0) - { - z = -z; - } - - return z <= thickness_; - } - } - - double GetThickness() const - { - return thickness_; - } - }; + } bool SliceViewerWidget::LookupLayer(size_t& index /* out */, @@ -250,7 +218,7 @@ { index = found->second; assert(index < layers_.size() && - layers_[index] == &layer); + layers_[index].get() == &layer); return true; } } @@ -369,42 +337,27 @@ } - SliceViewerWidget::SliceViewerWidget(OrthancStone::MessageBroker& broker, - const std::string& name) : + SliceViewerWidget::SliceViewerWidget(const std::string& name) : WorldSceneWidget(name), - IObserver(broker), - IObservable(broker), started_(false) { SetBackgroundCleared(true); } - SliceViewerWidget::~SliceViewerWidget() - { - for (size_t i = 0; i < layers_.size(); i++) - { - delete layers_[i]; - } - } - void SliceViewerWidget::ObserveLayer(IVolumeSlicer& layer) { - layer.RegisterObserverCallback(new OrthancStone::Callable<SliceViewerWidget, IVolumeSlicer::GeometryReadyMessage> - (*this, &SliceViewerWidget::OnGeometryReady)); - // currently ignore errors layer->RegisterObserverCallback(new Callable<SliceViewerWidget, IVolumeSlicer::GeometryErrorMessage>(*this, &SliceViewerWidget::...)); - layer.RegisterObserverCallback(new OrthancStone::Callable<SliceViewerWidget, IVolumeSlicer::SliceContentChangedMessage> - (*this, &SliceViewerWidget::OnSliceChanged)); - layer.RegisterObserverCallback(new OrthancStone::Callable<SliceViewerWidget, IVolumeSlicer::ContentChangedMessage> - (*this, &SliceViewerWidget::OnContentChanged)); - layer.RegisterObserverCallback(new OrthancStone::Callable<SliceViewerWidget, IVolumeSlicer::LayerReadyMessage> - (*this, &SliceViewerWidget::OnLayerReady)); - layer.RegisterObserverCallback(new OrthancStone::Callable<SliceViewerWidget, IVolumeSlicer::LayerErrorMessage> - (*this, &SliceViewerWidget::OnLayerError)); + // currently ignoring errors of type IVolumeSlicer::GeometryErrorMessage + + Register<IVolumeSlicer::GeometryReadyMessage>(layer, &SliceViewerWidget::OnGeometryReady); + Register<IVolumeSlicer::SliceContentChangedMessage>(layer, &SliceViewerWidget::OnSliceChanged); + Register<IVolumeSlicer::ContentChangedMessage>(layer, &SliceViewerWidget::OnContentChanged); + Register<IVolumeSlicer::LayerReadyMessage>(layer, &SliceViewerWidget::OnLayerReady); + Register<IVolumeSlicer::LayerErrorMessage>(layer, &SliceViewerWidget::OnLayerError); } - size_t SliceViewerWidget::AddLayer(IVolumeSlicer* layer) // Takes ownership + size_t SliceViewerWidget::AddLayer(boost::shared_ptr<IVolumeSlicer> layer) { if (layer == NULL) { @@ -414,7 +367,7 @@ size_t index = layers_.size(); layers_.push_back(layer); styles_.push_back(RenderStyle()); - layersIndex_[layer] = index; + layersIndex_[layer.get()] = index; ResetPendingScene(); @@ -426,7 +379,8 @@ } - void SliceViewerWidget::ReplaceLayer(size_t index, IVolumeSlicer* layer) // Takes ownership + void SliceViewerWidget::ReplaceLayer(size_t index, + boost::shared_ptr<IVolumeSlicer> layer) { if (layer == NULL) { @@ -438,9 +392,8 @@ throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); } - delete layers_[index]; layers_[index] = layer; - layersIndex_[layer] = index; + layersIndex_[layer.get()] = index; ResetPendingScene(); @@ -457,13 +410,13 @@ throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); } - IVolumeSlicer* previousLayer = layers_[index]; + IVolumeSlicer* previousLayer = layers_[index].get(); layersIndex_.erase(layersIndex_.find(previousLayer)); layers_.erase(layers_.begin() + index); changedLayers_.erase(changedLayers_.begin() + index); styles_.erase(styles_.begin() + index); - delete layers_[index]; + layers_[index].reset(); currentScene_->DeleteLayer(index); ResetPendingScene();
--- a/Framework/Deprecated/Widgets/SliceViewerWidget.h Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Deprecated/Widgets/SliceViewerWidget.h Tue Mar 31 15:51:02 2020 +0200 @@ -24,7 +24,7 @@ #include "WorldSceneWidget.h" #include "../Layers/IVolumeSlicer.h" #include "../../Toolbox/Extent2D.h" -#include "../../Messages/IObserver.h" +#include "../../Messages/ObserverBase.h" #include <map> @@ -32,7 +32,7 @@ { class SliceViewerWidget : public WorldSceneWidget, - public OrthancStone::IObserver, + public OrthancStone::ObserverBase<SliceViewerWidget>, public OrthancStone::IObservable { public: @@ -66,13 +66,67 @@ SliceViewerWidget(const SliceViewerWidget&); SliceViewerWidget& operator=(const SliceViewerWidget&); - class Scene; + class Scene : public boost::noncopyable + { + private: + OrthancStone::CoordinateSystem3D plane_; + double thickness_; + size_t countMissing_; + std::vector<ILayerRenderer*> renderers_; + + public: + void DeleteLayer(size_t index); + + Scene(const OrthancStone::CoordinateSystem3D& plane, + double thickness, + size_t countLayers); + + ~Scene(); + + void SetLayer(size_t index, + ILayerRenderer* renderer); // Takes ownership + + const OrthancStone::CoordinateSystem3D& GetPlane() const + { + return plane_; + } + + bool HasRenderer(size_t index) + { + return renderers_[index] != NULL; + } + + bool IsComplete() const + { + return countMissing_ == 0; + } + + unsigned int GetCountMissing() const + { + return static_cast<unsigned int>(countMissing_); + } + + bool RenderScene(OrthancStone::CairoContext& context, + const ViewportGeometry& view, + const OrthancStone::CoordinateSystem3D& viewportPlane); + + void SetLayerStyle(size_t index, + const RenderStyle& style); + + bool ContainsPlane(const OrthancStone::CoordinateSystem3D& plane) const; + + double GetThickness() const + { + return thickness_; + } + }; + typedef std::map<const IVolumeSlicer*, size_t> LayersIndex; bool started_; LayersIndex layersIndex_; - std::vector<IVolumeSlicer*> layers_; + std::vector<boost::shared_ptr<IVolumeSlicer> > layers_; std::vector<RenderStyle> styles_; OrthancStone::CoordinateSystem3D plane_; std::unique_ptr<Scene> currentScene_; @@ -100,8 +154,7 @@ void ResetChangedLayers(); public: - SliceViewerWidget(OrthancStone::MessageBroker& broker, - const std::string& name); + SliceViewerWidget(const std::string& name); virtual OrthancStone::Extent2D GetSceneExtent(); @@ -120,11 +173,13 @@ void InvalidateLayer(size_t layer); public: - virtual ~SliceViewerWidget(); + virtual ~SliceViewerWidget() + { + } - size_t AddLayer(IVolumeSlicer* layer); // Takes ownership + size_t AddLayer(boost::shared_ptr<IVolumeSlicer> layer); - void ReplaceLayer(size_t layerIndex, IVolumeSlicer* layer); // Takes ownership + void ReplaceLayer(size_t layerIndex, boost::shared_ptr<IVolumeSlicer> layer); // Takes ownership void RemoveLayer(size_t layerIndex);
--- a/Framework/Fonts/GlyphTextureAlphabet.cpp Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Fonts/GlyphTextureAlphabet.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -28,7 +28,7 @@ #include <Core/Images/ImageProcessing.h> #include <Core/OrthancException.h> -#ifdef __EMSCRIPTEN__ +#if defined(__EMSCRIPTEN__) /* Avoid this error: .../boost/math/special_functions/round.hpp:86:12: warning: implicit conversion from 'std::__2::numeric_limits<int>::type' (aka 'int') to 'float' changes value from 2147483647 to 2147483648 [-Wimplicit-int-float-conversion]
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Loaders/DicomResourcesLoader.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -0,0 +1,909 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 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 "DicomResourcesLoader.h" + +#if !defined(ORTHANC_ENABLE_DCMTK) +# error The macro ORTHANC_ENABLE_DCMTK must be defined +#endif + +#if ORTHANC_ENABLE_DCMTK == 1 +# include "../Oracle/ParseDicomFromFileCommand.h" +# include <Core/DicomParsing/ParsedDicomFile.h> +#endif + +#include <boost/filesystem/path.hpp> + +namespace OrthancStone +{ + static std::string GetUri(Orthanc::ResourceType level) + { + switch (level) + { + case Orthanc::ResourceType_Patient: + return "patients"; + + case Orthanc::ResourceType_Study: + return "studies"; + + case Orthanc::ResourceType_Series: + return "series"; + + case Orthanc::ResourceType_Instance: + return "instances"; + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + } + + + class DicomResourcesLoader::Handler : public Orthanc::IDynamicObject + { + private: + boost::shared_ptr<DicomResourcesLoader> loader_; + boost::shared_ptr<LoadedDicomResources> target_; + int priority_; + DicomSource source_; + boost::shared_ptr<Orthanc::IDynamicObject> userPayload_; + + public: + Handler(boost::shared_ptr<DicomResourcesLoader> loader, + boost::shared_ptr<LoadedDicomResources> target, + int priority, + const DicomSource& source, + boost::shared_ptr<Orthanc::IDynamicObject> userPayload) : + loader_(loader), + target_(target), + priority_(priority), + source_(source), + userPayload_(userPayload) + { + if (!loader || + !target) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); + } + } + + virtual ~Handler() + { + } + + void BroadcastSuccess() + { + SuccessMessage message(*loader_, target_, priority_, source_, userPayload_.get()); + loader_->BroadcastMessage(message); + } + + boost::shared_ptr<DicomResourcesLoader> GetLoader() + { + assert(loader_); + return loader_; + } + + boost::shared_ptr<LoadedDicomResources> GetTarget() + { + assert(target_); + return target_; + } + + int GetPriority() const + { + return priority_; + } + + const DicomSource& GetSource() const + { + return source_; + } + + const boost::shared_ptr<Orthanc::IDynamicObject> GetUserPayload() const + { + return userPayload_; + } + }; + + + class DicomResourcesLoader::StringHandler : public DicomResourcesLoader::Handler + { + public: + StringHandler(boost::shared_ptr<DicomResourcesLoader> loader, + boost::shared_ptr<LoadedDicomResources> target, + int priority, + const DicomSource& source, + boost::shared_ptr<Orthanc::IDynamicObject> userPayload) : + Handler(loader, target, priority, source, userPayload) + { + } + + virtual void HandleJson(const Json::Value& body) = 0; + + virtual void HandleString(const std::string& body) + { + Json::Reader reader; + Json::Value value; + if (reader.parse(body, value)) + { + HandleJson(value); + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol); + } + } + }; + + + class DicomResourcesLoader::DicomWebHandler : public StringHandler + { + public: + DicomWebHandler(boost::shared_ptr<DicomResourcesLoader> loader, + boost::shared_ptr<LoadedDicomResources> target, + int priority, + const DicomSource& source, + boost::shared_ptr<Orthanc::IDynamicObject> userPayload) : + StringHandler(loader, target, priority, source, userPayload) + { + } + + virtual void HandleJson(const Json::Value& body) + { + GetTarget()->AddFromDicomWeb(body); + BroadcastSuccess(); + } + }; + + + class DicomResourcesLoader::OrthancHandler : public StringHandler + { + private: + boost::shared_ptr<unsigned int> remainingCommands_; + + protected: + void CloseCommand() + { + assert(remainingCommands_); + + if (*remainingCommands_ == 0) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + + (*remainingCommands_) --; + + if (*remainingCommands_ == 0) + { + BroadcastSuccess(); + } + } + + public: + OrthancHandler(boost::shared_ptr<DicomResourcesLoader> loader, + boost::shared_ptr<LoadedDicomResources> target, + int priority, + const DicomSource& source, + boost::shared_ptr<unsigned int> remainingCommands, + boost::shared_ptr<Orthanc::IDynamicObject> userPayload) : + StringHandler(loader, target, priority, source, userPayload), + remainingCommands_(remainingCommands) + { + if (!remainingCommands) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); + } + + (*remainingCommands) ++; + } + + boost::shared_ptr<unsigned int> GetRemainingCommands() + { + assert(remainingCommands_); + return remainingCommands_; + } + }; + + + class DicomResourcesLoader::OrthancInstanceTagsHandler : public OrthancHandler + { + public: + OrthancInstanceTagsHandler(boost::shared_ptr<DicomResourcesLoader> loader, + boost::shared_ptr<LoadedDicomResources> target, + int priority, + const DicomSource& source, + boost::shared_ptr<unsigned int> remainingCommands, + boost::shared_ptr<Orthanc::IDynamicObject> userPayload) : + OrthancHandler(loader, target, priority, source, remainingCommands, userPayload) + { + } + + virtual void HandleJson(const Json::Value& body) + { + GetTarget()->AddFromOrthanc(body); + CloseCommand(); + } + }; + + + class DicomResourcesLoader::OrthancOneChildInstanceHandler : public OrthancHandler + { + public: + OrthancOneChildInstanceHandler(boost::shared_ptr<DicomResourcesLoader> loader, + boost::shared_ptr<LoadedDicomResources> target, + int priority, + const DicomSource& source, + boost::shared_ptr<unsigned int> remainingCommands, + boost::shared_ptr<Orthanc::IDynamicObject> userPayload) : + OrthancHandler(loader, target, priority, source, remainingCommands, userPayload) + { + } + + virtual void HandleJson(const Json::Value& body) + { + static const char* const ID = "ID"; + + if (body.type() == Json::arrayValue) + { + if (body.size() > 0) + { + if (body[0].type() == Json::objectValue && + body[0].isMember(ID) && + body[0][ID].type() == Json::stringValue) + { + GetLoader()->ScheduleLoadOrthancInstanceTags + (GetTarget(), GetPriority(), GetSource(), body[0][ID].asString(), GetRemainingCommands(), GetUserPayload()); + CloseCommand(); + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol); + } + } + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol); + } + } + }; + + + class DicomResourcesLoader::OrthancAllChildrenInstancesHandler : public OrthancHandler + { + private: + Orthanc::ResourceType bottomLevel_; + + public: + OrthancAllChildrenInstancesHandler(boost::shared_ptr<DicomResourcesLoader> loader, + boost::shared_ptr<LoadedDicomResources> target, + int priority, + const DicomSource& source, + boost::shared_ptr<unsigned int> remainingCommands, + Orthanc::ResourceType bottomLevel, + boost::shared_ptr<Orthanc::IDynamicObject> userPayload) : + OrthancHandler(loader, target, priority, source, remainingCommands, userPayload), + bottomLevel_(bottomLevel) + { + } + + virtual void HandleJson(const Json::Value& body) + { + static const char* const ID = "ID"; + static const char* const INSTANCES = "Instances"; + + if (body.type() == Json::arrayValue) + { + for (Json::Value::ArrayIndex i = 0; i < body.size(); i++) + { + switch (bottomLevel_) + { + case Orthanc::ResourceType_Patient: + case Orthanc::ResourceType_Study: + if (body[i].type() == Json::objectValue && + body[i].isMember(ID) && + body[i][ID].type() == Json::stringValue) + { + GetLoader()->ScheduleLoadOrthancOneChildInstance + (GetTarget(), GetPriority(), GetSource(), bottomLevel_, + body[i][ID].asString(), GetRemainingCommands(), GetUserPayload()); + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol); + } + + break; + + case Orthanc::ResourceType_Series: + // At the series level, avoid a call to + // "/series/.../instances", as we already have this + // information in the JSON + if (body[i].type() == Json::objectValue && + body[i].isMember(INSTANCES) && + body[i][INSTANCES].type() == Json::arrayValue) + { + if (body[i][INSTANCES].size() > 0) + { + if (body[i][INSTANCES][0].type() == Json::stringValue) + { + GetLoader()->ScheduleLoadOrthancInstanceTags + (GetTarget(), GetPriority(), GetSource(), + body[i][INSTANCES][0].asString(), GetRemainingCommands(), GetUserPayload()); + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol); + } + } + } + + break; + + case Orthanc::ResourceType_Instance: + if (body[i].type() == Json::objectValue && + body[i].isMember(ID) && + body[i][ID].type() == Json::stringValue) + { + GetLoader()->ScheduleLoadOrthancInstanceTags + (GetTarget(), GetPriority(), GetSource(), + body[i][ID].asString(), GetRemainingCommands(), GetUserPayload()); + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol); + } + + break; + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + } + } + + CloseCommand(); + } + }; + + +#if ORTHANC_ENABLE_DCMTK == 1 + static void ExploreDicomDir(OrthancStone::LoadedDicomResources& instances, + const Orthanc::ParsedDicomDir& dicomDir, + Orthanc::ResourceType level, + size_t index, + const Orthanc::DicomMap& parent) + { + std::string expectedType; + + switch (level) + { + case Orthanc::ResourceType_Patient: + expectedType = "PATIENT"; + break; + + case Orthanc::ResourceType_Study: + expectedType = "STUDY"; + break; + + case Orthanc::ResourceType_Series: + expectedType = "SERIES"; + break; + + case Orthanc::ResourceType_Instance: + expectedType = "IMAGE"; + break; + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + + for (;;) + { + std::unique_ptr<Orthanc::DicomMap> current(dicomDir.GetItem(index).Clone()); + current->RemoveBinaryTags(); + current->Merge(parent); + + std::string type; + if (!current->LookupStringValue(type, Orthanc::DICOM_TAG_DIRECTORY_RECORD_TYPE, false)) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat); + } + + if (type == expectedType) + { + if (level == Orthanc::ResourceType_Instance) + { + instances.AddResource(*current); + } + else + { + size_t lower; + if (dicomDir.LookupLower(lower, index)) + { + ExploreDicomDir(instances, dicomDir, Orthanc::GetChildResourceType(level), lower, *current); + } + } + } + + size_t next; + if (dicomDir.LookupNext(next, index)) + { + index = next; + } + else + { + return; + } + } + } +#endif + + +#if ORTHANC_ENABLE_DCMTK == 1 + void DicomResourcesLoader::GetDicomDirInstances(LoadedDicomResources& target, + const Orthanc::ParsedDicomDir& dicomDir) + { + Orthanc::DicomMap parent; + ExploreDicomDir(target, dicomDir, Orthanc::ResourceType_Patient, 0, parent); + } +#endif + + +#if ORTHANC_ENABLE_DCMTK == 1 + class DicomResourcesLoader::DicomDirHandler : public StringHandler + { + public: + DicomDirHandler(boost::shared_ptr<DicomResourcesLoader> loader, + boost::shared_ptr<LoadedDicomResources> target, + int priority, + const DicomSource& source, + boost::shared_ptr<Orthanc::IDynamicObject> userPayload) : + StringHandler(loader, target, priority, source, userPayload) + { + } + + virtual void HandleJson(const Json::Value& body) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + + virtual void HandleString(const std::string& body) + { + Orthanc::ParsedDicomDir dicomDir(body); + GetDicomDirInstances(*GetTarget(), dicomDir); + BroadcastSuccess(); + } + }; +#endif + + + void DicomResourcesLoader::Handle(const HttpCommand::SuccessMessage& message) + { + if (message.GetOrigin().HasPayload()) + { + dynamic_cast<StringHandler&>(message.GetOrigin().GetPayload()).HandleString(message.GetAnswer()); + } + } + + + void DicomResourcesLoader::Handle(const OrthancRestApiCommand::SuccessMessage& message) + { + if (message.GetOrigin().HasPayload()) + { + dynamic_cast<StringHandler&>(message.GetOrigin().GetPayload()).HandleString(message.GetAnswer()); + } + } + + + void DicomResourcesLoader::Handle(const ReadFileCommand::SuccessMessage& message) + { + if (message.GetOrigin().HasPayload()) + { + dynamic_cast<StringHandler&>(message.GetOrigin().GetPayload()).HandleString(message.GetContent()); + } + } + + +#if ORTHANC_ENABLE_DCMTK == 1 + void DicomResourcesLoader::Handle(const ParseDicomSuccessMessage& message) + { + if (message.GetOrigin().HasPayload()) + { + Handler& handler = dynamic_cast<Handler&>(message.GetOrigin().GetPayload()); + + std::set<Orthanc::DicomTag> ignoreTagLength; + ignoreTagLength.insert(Orthanc::DICOM_TAG_GRID_FRAME_OFFSET_VECTOR); // Needed for RT-DOSE + + Orthanc::DicomMap summary; + message.GetDicom().ExtractDicomSummary(summary, ignoreTagLength); + handler.GetTarget()->AddResource(summary); + + handler.BroadcastSuccess(); + } + } +#endif + + + void DicomResourcesLoader::Handle(const OracleCommandExceptionMessage& message) + { + // TODO + LOG(ERROR) << "Exception: " << message.GetException().What(); + } + + + void DicomResourcesLoader::ScheduleLoadOrthancInstanceTags(boost::shared_ptr<LoadedDicomResources> target, + int priority, + const DicomSource& source, + const std::string& instanceId, + boost::shared_ptr<unsigned int> remainingCommands, + boost::shared_ptr<Orthanc::IDynamicObject> userPayload) + { + std::unique_ptr<OrthancRestApiCommand> command(new OrthancRestApiCommand); + command->SetUri("/instances/" + instanceId + "/tags"); + command->AcquirePayload(new OrthancInstanceTagsHandler(shared_from_this(), target, priority, + source, remainingCommands, userPayload)); + + { + std::unique_ptr<ILoadersContext::ILock> lock(context_.Lock()); + lock->Schedule(GetSharedObserver(), priority, command.release()); + } + } + + + void DicomResourcesLoader::ScheduleLoadOrthancOneChildInstance(boost::shared_ptr<LoadedDicomResources> target, + int priority, + const DicomSource& source, + Orthanc::ResourceType level, + const std::string& id, + boost::shared_ptr<unsigned int> remainingCommands, + boost::shared_ptr<Orthanc::IDynamicObject> userPayload) + { + std::unique_ptr<OrthancRestApiCommand> command(new OrthancRestApiCommand); + command->SetUri("/" + GetUri(level) + "/" + id + "/instances"); + command->AcquirePayload(new OrthancOneChildInstanceHandler(shared_from_this(), target, priority, + source, remainingCommands, userPayload)); + + { + std::unique_ptr<ILoadersContext::ILock> lock(context_.Lock()); + lock->Schedule(GetSharedObserver(), priority, command.release()); + } + } + + + + const Orthanc::IDynamicObject& DicomResourcesLoader::SuccessMessage::GetUserPayload() const + { + if (userPayload_ == NULL) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + else + { + return *userPayload_; + } + } + + + boost::shared_ptr<IObserver> DicomResourcesLoader::Factory::Create(ILoadersContext::ILock& stone) + { + boost::shared_ptr<DicomResourcesLoader> result(new DicomResourcesLoader(stone.GetContext())); + result->Register<HttpCommand::SuccessMessage>(stone.GetOracleObservable(), &DicomResourcesLoader::Handle); + result->Register<OracleCommandExceptionMessage>(stone.GetOracleObservable(), &DicomResourcesLoader::Handle); + result->Register<OrthancRestApiCommand::SuccessMessage>(stone.GetOracleObservable(), &DicomResourcesLoader::Handle); + result->Register<ReadFileCommand::SuccessMessage>(stone.GetOracleObservable(), &DicomResourcesLoader::Handle); + +#if ORTHANC_ENABLE_DCMTK == 1 + result->Register<ParseDicomSuccessMessage>(stone.GetOracleObservable(), &DicomResourcesLoader::Handle); +#endif + + return boost::shared_ptr<IObserver>(result); + } + + + static void SetIncludeTags(std::map<std::string, std::string>& arguments, + const std::set<Orthanc::DicomTag>& includeTags) + { + if (!includeTags.empty()) + { + std::string s; + bool first = true; + + for (std::set<Orthanc::DicomTag>::const_iterator + it = includeTags.begin(); it != includeTags.end(); ++it) + { + if (first) + { + first = false; + } + else + { + s += ","; + } + + char buf[16]; + sprintf(buf, "%04X%04X", it->GetGroup(), it->GetElement()); + s += std::string(buf); + } + + arguments["includefield"] = s; + } + } + + + void DicomResourcesLoader::ScheduleGetDicomWeb(boost::shared_ptr<LoadedDicomResources> target, + int priority, + const DicomSource& source, + const std::string& uri, + const std::set<Orthanc::DicomTag>& includeTags, + Orthanc::IDynamicObject* userPayload) + { + boost::shared_ptr<Orthanc::IDynamicObject> protection(userPayload); + + if (!source.IsDicomWeb()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls, "Not a DICOMweb source"); + } + + std::map<std::string, std::string> arguments, headers; + SetIncludeTags(arguments, includeTags); + + std::unique_ptr<IOracleCommand> command( + source.CreateDicomWebCommand(uri, arguments, headers, + new DicomWebHandler(shared_from_this(), target, priority, source, protection))); + + { + std::unique_ptr<ILoadersContext::ILock> lock(context_.Lock()); + lock->Schedule(GetSharedObserver(), priority, command.release()); + } + } + + + void DicomResourcesLoader::ScheduleQido(boost::shared_ptr<LoadedDicomResources> target, + int priority, + const DicomSource& source, + Orthanc::ResourceType level, + const Orthanc::DicomMap& filter, + const std::set<Orthanc::DicomTag>& includeTags, + Orthanc::IDynamicObject* userPayload) + { + boost::shared_ptr<Orthanc::IDynamicObject> protection(userPayload); + + if (!source.IsDicomWeb()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls, "Not a DICOMweb source"); + } + + std::string uri; + switch (level) + { + case Orthanc::ResourceType_Study: + uri = "/studies"; + break; + + case Orthanc::ResourceType_Series: + uri = "/series"; + break; + + case Orthanc::ResourceType_Instance: + uri = "/instances"; + break; + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + + std::set<Orthanc::DicomTag> tags; + filter.GetTags(tags); + + std::map<std::string, std::string> arguments, headers; + + for (std::set<Orthanc::DicomTag>::const_iterator it = tags.begin(); it != tags.end(); ++it) + { + std::string s; + if (filter.LookupStringValue(s, *it, false /* no binary */)) + { + char buf[16]; + sprintf(buf, "%04X%04X", it->GetGroup(), it->GetElement()); + arguments[buf] = s; + } + } + + SetIncludeTags(arguments, includeTags); + + std::unique_ptr<IOracleCommand> command( + source.CreateDicomWebCommand(uri, arguments, headers, + new DicomWebHandler(shared_from_this(), target, priority, source, protection))); + + + { + std::unique_ptr<ILoadersContext::ILock> lock(context_.Lock()); + lock->Schedule(GetSharedObserver(), priority, command.release()); + } + } + + + void DicomResourcesLoader::ScheduleLoadOrthancResources(boost::shared_ptr<LoadedDicomResources> target, + int priority, + const DicomSource& source, + Orthanc::ResourceType topLevel, + const std::string& topId, + Orthanc::ResourceType bottomLevel, + Orthanc::IDynamicObject* userPayload) + { + boost::shared_ptr<Orthanc::IDynamicObject> protection(userPayload); + + if (!source.IsOrthanc()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls, "Not an Orthanc source"); + } + + bool ok = false; + + switch (topLevel) + { + case Orthanc::ResourceType_Patient: + ok = (bottomLevel == Orthanc::ResourceType_Patient || + bottomLevel == Orthanc::ResourceType_Study || + bottomLevel == Orthanc::ResourceType_Series || + bottomLevel == Orthanc::ResourceType_Instance); + break; + + case Orthanc::ResourceType_Study: + ok = (bottomLevel == Orthanc::ResourceType_Study || + bottomLevel == Orthanc::ResourceType_Series || + bottomLevel == Orthanc::ResourceType_Instance); + break; + + case Orthanc::ResourceType_Series: + ok = (bottomLevel == Orthanc::ResourceType_Series || + bottomLevel == Orthanc::ResourceType_Instance); + break; + + case Orthanc::ResourceType_Instance: + ok = (bottomLevel == Orthanc::ResourceType_Instance); + break; + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + + if (!ok) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + + boost::shared_ptr<unsigned int> remainingCommands(new unsigned int(0)); + + if (topLevel == Orthanc::ResourceType_Instance) + { + ScheduleLoadOrthancInstanceTags(target, priority, source, topId, remainingCommands, protection); + } + else if (topLevel == bottomLevel) + { + ScheduleLoadOrthancOneChildInstance(target, priority, source, topLevel, topId, remainingCommands, protection); + } + else + { + std::unique_ptr<OrthancRestApiCommand> command(new OrthancRestApiCommand); + command->SetUri("/" + GetUri(topLevel) + "/" + topId + "/" + GetUri(bottomLevel)); + command->AcquirePayload(new OrthancAllChildrenInstancesHandler + (shared_from_this(), target, priority, source, + remainingCommands, bottomLevel, protection)); + + { + std::unique_ptr<ILoadersContext::ILock> lock(context_.Lock()); + + // GetSharedObserver() means "this" (for use as an IObserver), as a + // shared_ptr + // The oracle will thus call "this" + lock->Schedule(GetSharedObserver(), priority, command.release()); + } + } + } + + + void DicomResourcesLoader::ScheduleLoadDicomDir(boost::shared_ptr<LoadedDicomResources> target, + int priority, + const DicomSource& source, + const std::string& path, + Orthanc::IDynamicObject* userPayload) + { + boost::shared_ptr<Orthanc::IDynamicObject> protection(userPayload); + + if (!source.IsDicomDir()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls, "Not a DICOMDIR source"); + } + + if (target->GetIndexedTag() == Orthanc::DICOM_TAG_SOP_INSTANCE_UID) + { + LOG(WARNING) << "If loading DICOMDIR, it is advised to index tag " + << "ReferencedSopInstanceUidInFile (0004,1511)"; + } + +#if ORTHANC_ENABLE_DCMTK == 1 + std::unique_ptr<ReadFileCommand> command(new ReadFileCommand(path)); + command->AcquirePayload(new DicomDirHandler(shared_from_this(), target, priority, source, protection)); + + { + std::unique_ptr<ILoadersContext::ILock> lock(context_.Lock()); + lock->Schedule(GetSharedObserver(), priority, command.release()); + } +#else + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError, + "DCMTK is disabled, cannot load DICOMDIR"); +#endif + } + + + void DicomResourcesLoader::ScheduleLoadDicomFile(boost::shared_ptr<LoadedDicomResources> target, + int priority, + const DicomSource& source, + const std::string& path, + bool includePixelData, + Orthanc::IDynamicObject* userPayload) + { + boost::shared_ptr<Orthanc::IDynamicObject> protection(userPayload); + +#if ORTHANC_ENABLE_DCMTK == 1 + std::unique_ptr<ParseDicomFromFileCommand> command(new ParseDicomFromFileCommand(path)); + command->SetPixelDataIncluded(includePixelData); + command->AcquirePayload(new Handler(shared_from_this(), target, priority, source, protection)); + + { + std::unique_ptr<ILoadersContext::ILock> lock(context_.Lock()); + lock->Schedule(GetSharedObserver(), priority, command.release()); + } +#else + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError, + "DCMTK is disabled, cannot load DICOM files"); +#endif + } + + + bool DicomResourcesLoader::ScheduleLoadDicomFile(boost::shared_ptr<LoadedDicomResources> target, + int priority, + const DicomSource& source, + const std::string& dicomDirPath, + const Orthanc::DicomMap& dicomDirEntry, + bool includePixelData, + Orthanc::IDynamicObject* userPayload) + { + std::unique_ptr<Orthanc::IDynamicObject> protection(userPayload); + +#if ORTHANC_ENABLE_DCMTK == 1 + std::string file; + if (dicomDirEntry.LookupStringValue(file, Orthanc::DICOM_TAG_REFERENCED_FILE_ID, false)) + { + ScheduleLoadDicomFile(target, priority, source, ParseDicomFromFileCommand::GetDicomDirPath(dicomDirPath, file), + includePixelData, protection.release()); + return true; + } + else + { + return false; + } +#else + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError, + "DCMTK is disabled, cannot load DICOM files"); +#endif + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Loaders/DicomResourcesLoader.h Tue Mar 31 15:51:02 2020 +0200 @@ -0,0 +1,220 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 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 + +#if !defined(ORTHANC_ENABLE_DCMTK) +# error The macro ORTHANC_ENABLE_DCMTK must be defined +#endif + +#if ORTHANC_ENABLE_DCMTK == 1 +# include "../Oracle/ParseDicomFromFileCommand.h" +# include <Core/DicomParsing/ParsedDicomDir.h> +#endif + +#include "../Oracle/HttpCommand.h" +#include "../Oracle/OracleCommandExceptionMessage.h" +#include "../Oracle/OrthancRestApiCommand.h" +#include "../Oracle/ReadFileCommand.h" +#include "DicomSource.h" +#include "ILoaderFactory.h" +#include "LoadedDicomResources.h" +#include "OracleScheduler.h" + +namespace OrthancStone +{ + class DicomResourcesLoader : + public ObserverBase<DicomResourcesLoader>, + public IObservable + { + private: + class Handler; + class StringHandler; + class DicomWebHandler; + class OrthancHandler; + class OrthancInstanceTagsHandler; + class OrthancOneChildInstanceHandler; + class OrthancAllChildrenInstancesHandler; + +#if ORTHANC_ENABLE_DCMTK == 1 + class DicomDirHandler; +#endif + + void Handle(const HttpCommand::SuccessMessage& message); + + void Handle(const OrthancRestApiCommand::SuccessMessage& message); + + void Handle(const ReadFileCommand::SuccessMessage& message); + + void Handle(const OracleCommandExceptionMessage& message); + +#if ORTHANC_ENABLE_DCMTK == 1 + void Handle(const ParseDicomSuccessMessage& message); +#endif + + void ScheduleLoadOrthancInstanceTags(boost::shared_ptr<LoadedDicomResources> target, + int priority, + const DicomSource& source, + const std::string& instanceId, + boost::shared_ptr<unsigned int> remainingCommands, + boost::shared_ptr<Orthanc::IDynamicObject> userPayload); + + void ScheduleLoadOrthancOneChildInstance(boost::shared_ptr<LoadedDicomResources> target, + int priority, + const DicomSource& source, + Orthanc::ResourceType level, + const std::string& id, + boost::shared_ptr<unsigned int> remainingCommands, + boost::shared_ptr<Orthanc::IDynamicObject> userPayload); + + DicomResourcesLoader(ILoadersContext& context) : + context_(context) + { + } + + ILoadersContext& context_; + + + public: + class SuccessMessage : public OrthancStone::OriginMessage<DicomResourcesLoader> + { + ORTHANC_STONE_MESSAGE(__FILE__, __LINE__); + + private: + boost::shared_ptr<LoadedDicomResources> resources_; + int priority_; + const DicomSource& source_; + const Orthanc::IDynamicObject* userPayload_; + + public: + SuccessMessage(const DicomResourcesLoader& origin, + boost::shared_ptr<LoadedDicomResources> resources, + int priority, + const DicomSource& source, + const Orthanc::IDynamicObject* userPayload) : + OriginMessage(origin), + resources_(resources), + priority_(priority), + source_(source), + userPayload_(userPayload) + { + } + + int GetPriority() const + { + return priority_; + } + + const boost::shared_ptr<LoadedDicomResources> GetResources() const + { + return resources_; + } + + const DicomSource& GetDicomSource() const + { + return source_; + } + + bool HasUserPayload() const + { + return userPayload_ != NULL; + } + + const Orthanc::IDynamicObject& GetUserPayload() const; + }; + + + class Factory : public ILoaderFactory + { + public: + virtual boost::shared_ptr<IObserver> Create(ILoadersContext::ILock& stone); + }; + + void ScheduleGetDicomWeb(boost::shared_ptr<LoadedDicomResources> target, + int priority, + const DicomSource& source, + const std::string& uri, + const std::set<Orthanc::DicomTag>& includeTags, + Orthanc::IDynamicObject* userPayload); + + void ScheduleGetDicomWeb(boost::shared_ptr<LoadedDicomResources> target, + int priority, + const DicomSource& source, + const std::string& uri, + Orthanc::IDynamicObject* userPayload) + { + std::set<Orthanc::DicomTag> includeTags; + ScheduleGetDicomWeb(target, priority, source, uri, includeTags, userPayload); + } + + void ScheduleQido(boost::shared_ptr<LoadedDicomResources> target, + int priority, + const DicomSource& source, + Orthanc::ResourceType level, + const Orthanc::DicomMap& filter, + const std::set<Orthanc::DicomTag>& includeTags, + Orthanc::IDynamicObject* userPayload); + + void ScheduleLoadOrthancResources(boost::shared_ptr<LoadedDicomResources> target, + int priority, + const DicomSource& source, + Orthanc::ResourceType topLevel, + const std::string& topId, + Orthanc::ResourceType bottomLevel, + Orthanc::IDynamicObject* userPayload); + + void ScheduleLoadOrthancResource(boost::shared_ptr<LoadedDicomResources> target, + int priority, + const DicomSource& source, + Orthanc::ResourceType level, + const std::string& id, + Orthanc::IDynamicObject* userPayload) + { + ScheduleLoadOrthancResources(target, priority, source, level, id, level, userPayload); + } + +#if ORTHANC_ENABLE_DCMTK == 1 + static void GetDicomDirInstances(LoadedDicomResources& target, + const Orthanc::ParsedDicomDir& dicomDir); +#endif + + void ScheduleLoadDicomDir(boost::shared_ptr<LoadedDicomResources> target, + int priority, + const DicomSource& source, + const std::string& path, + Orthanc::IDynamicObject* userPayload); + + void ScheduleLoadDicomFile(boost::shared_ptr<LoadedDicomResources> target, + int priority, + const DicomSource& source, + const std::string& path, + bool includePixelData, + Orthanc::IDynamicObject* userPayload); + + bool ScheduleLoadDicomFile(boost::shared_ptr<LoadedDicomResources> target, + int priority, + const DicomSource& source, + const std::string& dicomDirPath, + const Orthanc::DicomMap& dicomDirEntry, + bool includePixelData, + Orthanc::IDynamicObject* userPayload); + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Loaders/DicomSource.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -0,0 +1,356 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 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 "DicomSource.h" + +#include "../Oracle/HttpCommand.h" +#include "../Oracle/OrthancRestApiCommand.h" + +#include <Core/OrthancException.h> + +#include <boost/algorithm/string/predicate.hpp> + +namespace OrthancStone +{ + static std::string EncodeGetArguments(const std::string& uri, + const std::map<std::string, std::string>& arguments) + { + std::string s = uri; + bool first = true; + + for (std::map<std::string, std::string>::const_iterator + it = arguments.begin(); it != arguments.end(); ++it) + { + if (first) + { + s += "?"; + first = false; + } + else + { + s += "&"; + } + + s += it->first + "=" + it->second; + } + + // TODO: Call Orthanc::Toolbox::UriEncode() ? + + return s; + } + + + void DicomSource::SetOrthancSource(const Orthanc::WebServiceParameters& parameters) + { + type_ = DicomSourceType_Orthanc; + webService_ = parameters; + hasOrthancWebViewer1_ = false; + hasOrthancAdvancedPreview_ = false; + } + + + void DicomSource::SetOrthancSource() + { + Orthanc::WebServiceParameters parameters; + parameters.SetUrl("http://localhost:8042/"); + SetOrthancSource(parameters); + } + + + const Orthanc::WebServiceParameters& DicomSource::GetOrthancParameters() const + { + if (type_ == DicomSourceType_Orthanc || + type_ == DicomSourceType_DicomWebThroughOrthanc) + { + return webService_; + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + } + + + void DicomSource::SetDicomDirSource() + { + type_ = DicomSourceType_DicomDir; + } + + + void DicomSource::SetDicomWebSource(const std::string& baseUrl) + { + type_ = DicomSourceType_DicomWeb; + webService_.SetUrl(baseUrl); + webService_.ClearCredentials(); + } + + + void DicomSource::SetDicomWebSource(const std::string& baseUrl, + const std::string& username, + const std::string& password) + { + type_ = DicomSourceType_DicomWeb; + webService_.SetUrl(baseUrl); + webService_.SetCredentials(username, password); + } + + + void DicomSource::SetDicomWebThroughOrthancSource(const Orthanc::WebServiceParameters& orthancParameters, + const std::string& dicomWebRoot, + const std::string& serverName) + { + type_ = DicomSourceType_DicomWebThroughOrthanc; + webService_ = orthancParameters; + orthancDicomWebRoot_ = dicomWebRoot; + serverName_ = serverName; + } + + + void DicomSource::SetDicomWebThroughOrthancSource(const std::string& serverName) + { + Orthanc::WebServiceParameters orthanc; + orthanc.SetUrl("http://localhost:8042/"); + SetDicomWebThroughOrthancSource(orthanc, "/dicom-web/", serverName); + } + + + bool DicomSource::IsDicomWeb() const + { + return (type_ == DicomSourceType_DicomWeb || + type_ == DicomSourceType_DicomWebThroughOrthanc); + } + + + IOracleCommand* DicomSource::CreateDicomWebCommand(const std::string& uri, + const std::map<std::string, std::string>& arguments, + const std::map<std::string, std::string>& headers, + Orthanc::IDynamicObject* payload) const + { + std::unique_ptr<Orthanc::IDynamicObject> protection(payload); + + switch (type_) + { + case DicomSourceType_DicomWeb: + { + std::unique_ptr<HttpCommand> command(new HttpCommand); + + command->SetMethod(Orthanc::HttpMethod_Get); + command->SetUrl(webService_.GetUrl() + "/" + EncodeGetArguments(uri, arguments)); + command->SetHttpHeaders(webService_.GetHttpHeaders()); + + for (std::map<std::string, std::string>::const_iterator + it = headers.begin(); it != headers.end(); ++it) + { + command->SetHttpHeader(it->first, it->second); + } + + if (!webService_.GetUsername().empty()) + { + command->SetCredentials(webService_.GetUsername(), webService_.GetPassword()); + } + + if (protection.get()) + { + command->AcquirePayload(protection.release()); + } + + return command.release(); + } + + case DicomSourceType_DicomWebThroughOrthanc: + { + Json::Value args = Json::objectValue; + for (std::map<std::string, std::string>::const_iterator + it = arguments.begin(); it != arguments.end(); ++it) + { + args[it->first] = it->second; + } + + Json::Value h = Json::objectValue; + for (std::map<std::string, std::string>::const_iterator + it = headers.begin(); it != headers.end(); ++it) + { + h[it->first] = it->second; + } + + Json::Value body = Json::objectValue; + body["Uri"] = uri; + body["Arguments"] = args; + body["Headers"] = h; + + std::unique_ptr<OrthancRestApiCommand> command(new OrthancRestApiCommand); + command->SetMethod(Orthanc::HttpMethod_Post); + command->SetUri(orthancDicomWebRoot_ + "/servers/" + serverName_ + "/get"); + command->SetBody(body); + + if (protection.get()) + { + command->AcquirePayload(protection.release()); + } + + return command.release(); + } + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + } + + + void DicomSource::AutodetectOrthancFeatures(const std::string& system, + const std::string& plugins) + { + static const char* const REST_API_VERSION = "ApiVersion"; + + if (IsDicomWeb()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + + Json::Value a, b; + Json::Reader reader; + if (reader.parse(system, a) && + reader.parse(plugins, b) && + a.type() == Json::objectValue && + b.type() == Json::arrayValue && + a.isMember(REST_API_VERSION) && + a[REST_API_VERSION].type() == Json::intValue) + { + SetOrthancAdvancedPreview(a[REST_API_VERSION].asInt() >= 5); + + hasOrthancWebViewer1_ = false; + + for (Json::Value::ArrayIndex i = 0; i < b.size(); i++) + { + if (b[i].type() != Json::stringValue) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat); + } + + if (boost::iequals(b[i].asString(), "web-viewer")) + { + hasOrthancWebViewer1_ = true; + } + } + } + else + { + printf("[%s] [%s]\n", system.c_str(), plugins.c_str()); + + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat); + } + } + + + void DicomSource::SetOrthancWebViewer1(bool hasPlugin) + { + if (IsOrthanc()) + { + hasOrthancWebViewer1_ = hasPlugin; + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + } + + bool DicomSource::HasOrthancWebViewer1() const + { + if (IsOrthanc()) + { + return hasOrthancWebViewer1_; + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + } + + void DicomSource::SetOrthancAdvancedPreview(bool hasFeature) + { + if (IsOrthanc()) + { + hasOrthancAdvancedPreview_ = hasFeature; + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + } + + bool DicomSource::HasOrthancAdvancedPreview() const + { + if (IsOrthanc()) + { + return hasOrthancAdvancedPreview_; + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + } + + void DicomSource::SetDicomWebRendered(bool hasFeature) + { + if (IsDicomWeb()) + { + hasDicomWebRendered_ = hasFeature; + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + } + + bool DicomSource::HasDicomWebRendered() const + { + if (IsDicomWeb()) + { + return hasDicomWebRendered_; + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + } + + + unsigned int DicomSource::GetQualityCount() const + { + if (IsDicomWeb()) + { + return (HasDicomWebRendered() ? 2 : 1); + } + else if (IsOrthanc()) + { + return (HasOrthancWebViewer1() || + HasOrthancAdvancedPreview() ? 2 : 1); + } + else if (IsDicomDir()) + { + return 1; + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Loaders/DicomSource.h Tue Mar 31 15:51:02 2020 +0200 @@ -0,0 +1,118 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 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 "../Oracle/IOracleCommand.h" + +#include <Core/WebServiceParameters.h> + +namespace OrthancStone +{ + enum DicomSourceType + { + DicomSourceType_Orthanc, + DicomSourceType_DicomWeb, + DicomSourceType_DicomWebThroughOrthanc, + DicomSourceType_DicomDir + }; + + + class DicomSource + { + private: + DicomSourceType type_; + Orthanc::WebServiceParameters webService_; + std::string orthancDicomWebRoot_; + std::string serverName_; + bool hasOrthancWebViewer1_; + bool hasOrthancAdvancedPreview_; + bool hasDicomWebRendered_; + + public: + DicomSource() : + hasOrthancWebViewer1_(false), + hasOrthancAdvancedPreview_(false), + hasDicomWebRendered_(false) + { + SetOrthancSource(); + } + + DicomSourceType GetType() const + { + return type_; + } + + void SetOrthancSource(); + + void SetOrthancSource(const Orthanc::WebServiceParameters& parameters); + + const Orthanc::WebServiceParameters& GetOrthancParameters() const; + + void SetDicomDirSource(); + + void SetDicomWebSource(const std::string& baseUrl); + + void SetDicomWebSource(const std::string& baseUrl, + const std::string& username, + const std::string& password); + + void SetDicomWebThroughOrthancSource(const Orthanc::WebServiceParameters& orthancParameters, + const std::string& dicomWebRoot, + const std::string& serverName); + + void SetDicomWebThroughOrthancSource(const std::string& serverName); + + bool IsDicomWeb() const; + + bool IsOrthanc() const + { + return type_ == DicomSourceType_Orthanc; + } + + bool IsDicomDir() const + { + return type_ == DicomSourceType_DicomDir; + } + + IOracleCommand* CreateDicomWebCommand(const std::string& uri, + const std::map<std::string, std::string>& arguments, + const std::map<std::string, std::string>& headers, + Orthanc::IDynamicObject* payload /* takes ownership */) const; + + void AutodetectOrthancFeatures(const std::string& system, + const std::string& plugins); + + void SetOrthancWebViewer1(bool hasPlugin); + + bool HasOrthancWebViewer1() const; + + void SetOrthancAdvancedPreview(bool hasFeature); + + bool HasOrthancAdvancedPreview() const; + + void SetDicomWebRendered(bool hasFeature); + + bool HasDicomWebRendered() const; + + unsigned int GetQualityCount() const; + }; +}
--- a/Framework/Loaders/DicomStructureSetLoader.cpp Tue Mar 31 15:47:29 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,417 +0,0 @@ -/** - * Stone of Orthanc - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2020 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 "DicomStructureSetLoader.h" - -#include "../Scene2D/PolylineSceneLayer.h" -#include "../StoneException.h" -#include "../Toolbox/GeometryToolbox.h" - -#include <Core/Toolbox.h> - -#include <algorithm> - -#if 0 -bool logbgo233 = false; -bool logbgo115 = false; -#endif - -namespace OrthancStone -{ - -#if 0 - void DumpDicomMap(std::ostream& o, const Orthanc::DicomMap& dicomMap) - { - using namespace std; - //ios_base::fmtflags state = o.flags(); - //o.flags(ios::right | ios::hex); - //o << "(" << setfill('0') << setw(4) << tag.GetGroup() - // << "," << setw(4) << tag.GetElement() << ")"; - //o.flags(state); - Json::Value val; - dicomMap.Serialize(val); - o << val; - //return o; - } -#endif - - - class DicomStructureSetLoader::AddReferencedInstance : public LoaderStateMachine::State - { - private: - std::string instanceId_; - - public: - AddReferencedInstance(DicomStructureSetLoader& that, - const std::string& instanceId) : - State(that), - instanceId_(instanceId) - { - } - - virtual void Handle(const OrthancRestApiCommand::SuccessMessage& message) - { - Json::Value tags; - message.ParseJsonBody(tags); - - Orthanc::DicomMap dicom; - dicom.FromDicomAsJson(tags); - - DicomStructureSetLoader& loader = GetLoader<DicomStructureSetLoader>(); - - loader.content_->AddReferencedSlice(dicom); - - loader.countProcessedInstances_ ++; - assert(loader.countProcessedInstances_ <= loader.countReferencedInstances_); - - if (loader.countProcessedInstances_ == loader.countReferencedInstances_) - { - // All the referenced instances have been loaded, finalize the RT-STRUCT - loader.content_->CheckReferencedSlices(); - loader.revision_++; - loader.SetStructuresReady(); - } - } - }; - - - // State that converts a "SOP Instance UID" to an Orthanc identifier - class DicomStructureSetLoader::LookupInstance : public LoaderStateMachine::State - { - private: - std::string sopInstanceUid_; - - public: - LookupInstance(DicomStructureSetLoader& that, - const std::string& sopInstanceUid) : - State(that), - sopInstanceUid_(sopInstanceUid) - { - } - - virtual void Handle(const OrthancRestApiCommand::SuccessMessage& message) - { -#if 0 - LOG(TRACE) << "DicomStructureSetLoader::LookupInstance::Handle() (SUCCESS)"; -#endif - DicomStructureSetLoader& loader = GetLoader<DicomStructureSetLoader>(); - - Json::Value lookup; - message.ParseJsonBody(lookup); - - if (lookup.type() != Json::arrayValue || - lookup.size() != 1 || - !lookup[0].isMember("Type") || - !lookup[0].isMember("Path") || - lookup[0]["Type"].type() != Json::stringValue || - lookup[0]["ID"].type() != Json::stringValue || - lookup[0]["Type"].asString() != "Instance") - { - std::stringstream msg; - msg << "Unknown resource! message.GetAnswer() = " << message.GetAnswer() << " message.GetAnswerHeaders() = "; - for (OrthancRestApiCommand::HttpHeaders::const_iterator it = message.GetAnswerHeaders().begin(); - it != message.GetAnswerHeaders().end(); ++it) - { - msg << "\nkey: \"" << it->first << "\" value: \"" << it->second << "\"\n"; - } - const std::string msgStr = msg.str(); - LOG(ERROR) << msgStr; - throw Orthanc::OrthancException(Orthanc::ErrorCode_UnknownResource); - } - - const std::string instanceId = lookup[0]["ID"].asString(); - - { - std::unique_ptr<OrthancRestApiCommand> command(new OrthancRestApiCommand); - command->SetHttpHeader("Accept-Encoding", "gzip"); - std::string uri = "/instances/" + instanceId + "/tags"; - command->SetUri(uri); - command->SetPayload(new AddReferencedInstance(loader, instanceId)); - Schedule(command.release()); - } - } - }; - - - class DicomStructureSetLoader::LoadStructure : public LoaderStateMachine::State - { - public: - LoadStructure(DicomStructureSetLoader& that) : - State(that) - { - } - - virtual void Handle(const OrthancRestApiCommand::SuccessMessage& message) - { -#if 0 - if (logbgo115) - LOG(TRACE) << "DicomStructureSetLoader::LoadStructure::Handle() (SUCCESS)"; -#endif - DicomStructureSetLoader& loader = GetLoader<DicomStructureSetLoader>(); - - { - OrthancPlugins::FullOrthancDataset dicom(message.GetAnswer()); - loader.content_.reset(new DicomStructureSet(dicom)); - size_t structureCount = loader.content_->GetStructuresCount(); - loader.structureVisibility_.resize(structureCount); - bool everythingVisible = false; - if ((loader.initiallyVisibleStructures_.size() == 1) - && (loader.initiallyVisibleStructures_[0].size() == 1) - && (loader.initiallyVisibleStructures_[0][0] == '*')) - { - everythingVisible = true; - } - - for (size_t i = 0; i < structureCount; ++i) - { - // if a single "*" string is supplied, this means we want everything - // to be visible... - if(everythingVisible) - { - loader.structureVisibility_.at(i) = true; - } - else - { - // otherwise, we only enable visibility for those structures whose - // names are mentioned in the initiallyVisibleStructures_ array - const std::string& structureName = loader.content_->GetStructureName(i); - - std::vector<std::string>::iterator foundIt = - std::find( - loader.initiallyVisibleStructures_.begin(), - loader.initiallyVisibleStructures_.end(), - structureName); - std::vector<std::string>::iterator endIt = loader.initiallyVisibleStructures_.end(); - if (foundIt != endIt) - loader.structureVisibility_.at(i) = true; - else - loader.structureVisibility_.at(i) = false; - } - } - } - - // Some (admittedly invalid) Dicom files have empty values in the - // 0008,1155 tag. We try our best to cope with this. - std::set<std::string> instances; - std::set<std::string> nonEmptyInstances; - loader.content_->GetReferencedInstances(instances); - for (std::set<std::string>::const_iterator - it = instances.begin(); it != instances.end(); ++it) - { - std::string instance = Orthanc::Toolbox::StripSpaces(*it); - if(instance != "") - nonEmptyInstances.insert(instance); - } - - loader.countReferencedInstances_ = - static_cast<unsigned int>(nonEmptyInstances.size()); - - for (std::set<std::string>::const_iterator - it = nonEmptyInstances.begin(); it != nonEmptyInstances.end(); ++it) - { - std::unique_ptr<OrthancRestApiCommand> command(new OrthancRestApiCommand); - command->SetUri("/tools/lookup"); - command->SetMethod(Orthanc::HttpMethod_Post); - command->SetBody(*it); - command->SetPayload(new LookupInstance(loader, *it)); - Schedule(command.release()); - } - } - }; - - - class DicomStructureSetLoader::Slice : public IExtractedSlice - { - private: - const DicomStructureSet& content_; - uint64_t revision_; - bool isValid_; - std::vector<bool> visibility_; - - public: - /** - The visibility vector must either: - - be empty - or - - contain the same number of items as the number of structures in the - structure set. - In the first case (empty vector), all the structures are displayed. - In the second case, the visibility of each structure is defined by the - content of the vector at the corresponding index. - */ - Slice(const DicomStructureSet& content, - uint64_t revision, - const CoordinateSystem3D& cuttingPlane, - std::vector<bool> visibility = std::vector<bool>()) - : content_(content) - , revision_(revision) - , visibility_(visibility) - { - ORTHANC_ASSERT((visibility_.size() == content_.GetStructuresCount()) - || (visibility_.size() == 0u)); - - bool opposite; - - const Vector normal = content.GetNormal(); - isValid_ = ( - GeometryToolbox::IsParallelOrOpposite(opposite, normal, cuttingPlane.GetNormal()) || - GeometryToolbox::IsParallelOrOpposite(opposite, normal, cuttingPlane.GetAxisX()) || - GeometryToolbox::IsParallelOrOpposite(opposite, normal, cuttingPlane.GetAxisY())); - } - - virtual bool IsValid() - { - return isValid_; - } - - virtual uint64_t GetRevision() - { - return revision_; - } - - virtual ISceneLayer* CreateSceneLayer(const ILayerStyleConfigurator* configurator, - const CoordinateSystem3D& cuttingPlane) - { - assert(isValid_); - - std::unique_ptr<PolylineSceneLayer> layer(new PolylineSceneLayer); - layer->SetThickness(2); - - for (size_t i = 0; i < content_.GetStructuresCount(); i++) - { - if ((visibility_.size() == 0) || visibility_.at(i)) - { - const Color& color = content_.GetStructureColor(i); - -#ifdef USE_BOOST_UNION_FOR_POLYGONS - std::vector< std::vector<Point2D> > polygons; - - if (content_.ProjectStructure(polygons, i, cuttingPlane)) - { - for (size_t j = 0; j < polygons.size(); j++) - { - PolylineSceneLayer::Chain chain; - chain.resize(polygons[j].size()); - - for (size_t k = 0; k < polygons[j].size(); k++) - { - chain[k] = ScenePoint2D(polygons[j][k].x, polygons[j][k].y); - } - - layer->AddChain(chain, true /* closed */, color); - } - } -#else - std::vector< std::pair<Point2D, Point2D> > segments; - - if (content_.ProjectStructure(segments, i, cuttingPlane)) - { - for (size_t j = 0; j < segments.size(); j++) - { - PolylineSceneLayer::Chain chain; - chain.resize(2); - - chain[0] = ScenePoint2D(segments[j].first.x, segments[j].first.y); - chain[1] = ScenePoint2D(segments[j].second.x, segments[j].second.y); - - layer->AddChain(chain, false /* NOT closed */, color); - } - } -#endif - } - } - - return layer.release(); - } - }; - - - DicomStructureSetLoader::DicomStructureSetLoader(IOracle& oracle, - IObservable& oracleObservable) : - LoaderStateMachine(oracle, oracleObservable), - IObservable(oracleObservable.GetBroker()), - revision_(0), - countProcessedInstances_(0), - countReferencedInstances_(0), - structuresReady_(false) - { - } - - - void DicomStructureSetLoader::SetStructureDisplayState(size_t structureIndex, bool display) - { - structureVisibility_.at(structureIndex) = display; - revision_++; - } - - DicomStructureSetLoader::~DicomStructureSetLoader() - { - LOG(TRACE) << "DicomStructureSetLoader::~DicomStructureSetLoader()"; - } - - void DicomStructureSetLoader::LoadInstance( - const std::string& instanceId, - const std::vector<std::string>& initiallyVisibleStructures) - { - Start(); - - instanceId_ = instanceId; - initiallyVisibleStructures_ = initiallyVisibleStructures; - - { - std::unique_ptr<OrthancRestApiCommand> command(new OrthancRestApiCommand); - command->SetHttpHeader("Accept-Encoding", "gzip"); - - std::string uri = "/instances/" + instanceId + "/tags?ignore-length=3006-0050"; - - command->SetUri(uri); - command->SetPayload(new LoadStructure(*this)); - Schedule(command.release()); - } - } - - - IVolumeSlicer::IExtractedSlice* DicomStructureSetLoader::ExtractSlice(const CoordinateSystem3D& cuttingPlane) - { - if (content_.get() == NULL) - { - // Geometry is not available yet - return new IVolumeSlicer::InvalidSlice; - } - else - { - return new Slice(*content_, revision_, cuttingPlane, structureVisibility_); - } - } - - void DicomStructureSetLoader::SetStructuresReady() - { - ORTHANC_ASSERT(!structuresReady_); - structuresReady_ = true; - BroadcastMessage(DicomStructureSetLoader::StructuresReady(*this)); - } - - bool DicomStructureSetLoader::AreStructuresReady() const - { - return structuresReady_; - } - -}
--- a/Framework/Loaders/DicomStructureSetLoader.h Tue Mar 31 15:47:29 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,100 +0,0 @@ -/** - * Stone of Orthanc - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2020 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 "../Toolbox/DicomStructureSet.h" -#include "../Volumes/IVolumeSlicer.h" -#include "LoaderStateMachine.h" - -#include <vector> - -namespace OrthancStone -{ - class DicomStructureSetLoader : - public LoaderStateMachine, - public IVolumeSlicer, - public IObservable - { - private: - class Slice; - - // States of LoaderStateMachine - class AddReferencedInstance; // 3rd state - class LookupInstance; // 2nd state - class LoadStructure; // 1st state - - std::unique_ptr<DicomStructureSet> content_; - uint64_t revision_; - std::string instanceId_; - unsigned int countProcessedInstances_; - unsigned int countReferencedInstances_; - - // will be set to true once the loading is finished - bool structuresReady_; - - /** - At load time, these strings are used to initialize the structureVisibility_ - vector. - - As a special case, if initiallyVisibleStructures_ contains a single string - that is '*', ALL structures will be made visible. - */ - std::vector<std::string> initiallyVisibleStructures_; - - /** - Contains the "Should this structure be displayed?" flag for all structures. - Only filled when structures are loaded. - - Changing this value directly affects the rendering - */ - std::vector<bool> structureVisibility_; - - public: - ORTHANC_STONE_DEFINE_ORIGIN_MESSAGE(__FILE__, __LINE__, StructuresReady, DicomStructureSetLoader); - - DicomStructureSetLoader(IOracle& oracle, - IObservable& oracleObservable); - - DicomStructureSet* GetContent() - { - return content_.get(); - } - - void SetStructureDisplayState(size_t structureIndex, bool display); - - bool GetStructureDisplayState(size_t structureIndex) const - { - return structureVisibility_.at(structureIndex); - } - - ~DicomStructureSetLoader(); - - void LoadInstance(const std::string& instanceId, - const std::vector<std::string>& initiallyVisibleStructures = std::vector<std::string>()); - - virtual IExtractedSlice* ExtractSlice(const CoordinateSystem3D& cuttingPlane) ORTHANC_OVERRIDE; - - void SetStructuresReady(); - - bool AreStructuresReady() const; - }; -}
--- a/Framework/Loaders/DicomStructureSetLoader2.cpp Tue Mar 31 15:47:29 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,125 +0,0 @@ -/** - * Stone of Orthanc - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2020 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/>. - **/ - -#ifdef BGO_ENABLE_DICOMSTRUCTURESETLOADER2 - -#include "DicomStructureSetLoader2.h" - -#include "../Messages/IObservable.h" -#include "../Oracle/IOracle.h" -#include "../Oracle/OracleCommandExceptionMessage.h" - -namespace OrthancStone -{ - - DicomStructureSetLoader2::DicomStructureSetLoader2( - DicomStructureSet2& structureSet - , IOracle& oracle - , IObservable& oracleObservable) - : IObserver(oracleObservable.GetBroker()) - , IObservable(oracleObservable.GetBroker()) - , structureSet_(structureSet) - , oracle_(oracle) - , oracleObservable_(oracleObservable) - , structuresReady_(false) - { - LOG(TRACE) << "DicomStructureSetLoader2(" << std::hex << this << std::dec << ")::DicomStructureSetLoader2()"; - - oracleObservable.RegisterObserverCallback( - new Callable<DicomStructureSetLoader2, OrthancRestApiCommand::SuccessMessage> - (*this, &DicomStructureSetLoader2::HandleSuccessMessage)); - - oracleObservable.RegisterObserverCallback( - new Callable<DicomStructureSetLoader2, OracleCommandExceptionMessage> - (*this, &DicomStructureSetLoader2::HandleExceptionMessage)); - } - - DicomStructureSetLoader2::~DicomStructureSetLoader2() - { - LOG(TRACE) << "DicomStructureSetLoader2(" << std::hex << this << std::dec << ")::~DicomStructureSetLoader2()"; - oracleObservable_.Unregister(this); - } - - void DicomStructureSetLoader2::LoadInstanceFromString(const std::string& body) - { - OrthancPlugins::FullOrthancDataset dicom(body); - //loader.content_.reset(new DicomStructureSet(dicom)); - structureSet_.Clear(); - structureSet_.SetContents(dicom); - SetStructuresReady(); - } - - void DicomStructureSetLoader2::HandleSuccessMessage(const OrthancRestApiCommand::SuccessMessage& message) - { - const std::string& body = message.GetAnswer(); - LoadInstanceFromString(body); - } - - void DicomStructureSetLoader2::HandleExceptionMessage(const OracleCommandExceptionMessage& message) - { - LOG(ERROR) << "DicomStructureSetLoader2::HandleExceptionMessage: error when trying to load data. " - << "Error: " << message.GetException().What() << " Details: " - << message.GetException().GetDetails(); - } - - void DicomStructureSetLoader2::LoadInstance(const std::string& instanceId) - { - std::unique_ptr<OrthancRestApiCommand> command(new OrthancRestApiCommand); - command->SetHttpHeader("Accept-Encoding", "gzip"); - - std::string uri = "/instances/" + instanceId + "/tags?ignore-length=3006-0050"; - - command->SetUri(uri); - oracle_.Schedule(*this, command.release()); - } - - void DicomStructureSetLoader2::SetStructuresReady() - { - structuresReady_ = true; - } - - bool DicomStructureSetLoader2::AreStructuresReady() const - { - return structuresReady_; - } - - /* - - void LoaderStateMachine::HandleExceptionMessage(const OracleCommandExceptionMessage& message) - { - LOG(ERROR) << "LoaderStateMachine::HandleExceptionMessage: error in the state machine, stopping all processing"; - LOG(ERROR) << "Error: " << message.GetException().What() << " Details: " << - message.GetException().GetDetails(); - Clear(); - } - - LoaderStateMachine::~LoaderStateMachine() - { - Clear(); - } - - - */ - -} - -#endif -// BGO_ENABLE_DICOMSTRUCTURESETLOADER2 -
--- a/Framework/Loaders/DicomStructureSetLoader2.h Tue Mar 31 15:47:29 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,87 +0,0 @@ -/** - * Stone of Orthanc - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2020 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 - -#ifdef BGO_ENABLE_DICOMSTRUCTURESETLOADER2 - -#include "../Toolbox/DicomStructureSet2.h" -#include "../Messages/IMessage.h" -#include "../Messages/IObserver.h" -#include "../Messages/IObservable.h" -#include "../Oracle/OrthancRestApiCommand.h" - -#include <boost/noncopyable.hpp> - -namespace OrthancStone -{ - class IOracle; - class IObservable; - class OrthancRestApiCommand; - class OracleCommandExceptionMessage; - - class DicomStructureSetLoader2 : public IObserver, public IObservable - { - public: - ORTHANC_STONE_DEFINE_ORIGIN_MESSAGE(__FILE__, __LINE__, StructuresReady, DicomStructureSetLoader2); - - /** - Warning: the structureSet, oracle and oracleObservable objects must live - at least as long as this object (TODO: shared_ptr?) - */ - DicomStructureSetLoader2(DicomStructureSet2& structureSet, IOracle& oracle, IObservable& oracleObservable); - - ~DicomStructureSetLoader2(); - - void LoadInstance(const std::string& instanceId); - - /** Internal use */ - void LoadInstanceFromString(const std::string& body); - - void SetStructuresReady(); - bool AreStructuresReady() const; - - private: - /** - Called back by the oracle when data is ready! - */ - void HandleSuccessMessage(const OrthancRestApiCommand::SuccessMessage& message); - - /** - Called back by the oracle when shit hits the fan - */ - void HandleExceptionMessage(const OracleCommandExceptionMessage& message); - - /** - The structure set that will be (cleared and) filled with data from the - loader - */ - DicomStructureSet2& structureSet_; - - IOracle& oracle_; - IObservable& oracleObservable_; - bool structuresReady_; - }; -} - -#endif -// BGO_ENABLE_DICOMSTRUCTURESETLOADER2 - -
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Loaders/DicomVolumeLoader.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -0,0 +1,188 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 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 "DicomVolumeLoader.h" + +#include <Core/Images/ImageProcessing.h> + +namespace OrthancStone +{ + DicomVolumeLoader::DicomVolumeLoader(boost::shared_ptr<SeriesFramesLoader>& framesLoader, + bool computeRange) : + framesLoader_(framesLoader), + isValid_(false), + started_(false), + remaining_(0) + { + volume_.reset(new OrthancStone::DicomVolumeImage); + + const SeriesOrderedFrames& frames = framesLoader_->GetOrderedFrames(); + + if (frames.IsRegular3DVolume() && + frames.GetFramesCount() > 0) + { + // TODO - Is "0" the good choice for the reference frame? + // Shouldn't we use "count - 1" depending on the direction + // of the normal? + const OrthancStone::DicomInstanceParameters& parameters = frames.GetInstanceParameters(0); + + OrthancStone::CoordinateSystem3D plane(frames.GetInstance(0)); + + OrthancStone::VolumeImageGeometry geometry; + geometry.SetSizeInVoxels(parameters.GetImageInformation().GetWidth(), + parameters.GetImageInformation().GetHeight(), + static_cast<unsigned int>(frames.GetFramesCount())); + geometry.SetAxialGeometry(plane); + + double spacing; + if (parameters.GetSopClassUid() == SopClassUid_RTDose) + { + if (!parameters.ComputeRegularSpacing(spacing)) + { + LOG(WARNING) << "Unable to compute the spacing in a RT-DOSE instance"; + spacing = frames.GetSpacingBetweenSlices(); + } + } + else + { + spacing = frames.GetSpacingBetweenSlices(); + } + + geometry.SetVoxelDimensions(parameters.GetPixelSpacingX(), + parameters.GetPixelSpacingY(), spacing); + volume_->Initialize(geometry, parameters.GetExpectedPixelFormat(), computeRange); + volume_->GetPixelData().Clear(); + volume_->SetDicomParameters(parameters); + + remaining_ = frames.GetFramesCount(); + isValid_ = true; + } + else + { + LOG(WARNING) << "Not a regular 3D volume"; + } + } + + + void DicomVolumeLoader::Handle(const OrthancStone::SeriesFramesLoader::FrameLoadedMessage& message) + { + if (remaining_ == 0 || + !message.HasUserPayload()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + + if (message.GetImage().GetWidth() != volume_->GetPixelData().GetWidth() || + message.GetImage().GetHeight() != volume_->GetPixelData().GetHeight()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageSize); + } + + if (message.GetImage().GetFormat() != volume_->GetPixelData().GetFormat()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageFormat); + } + + if (message.GetFrameIndex() >= volume_->GetPixelData().GetDepth()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + + size_t frameIndex = dynamic_cast<const Orthanc::SingleValueObject<size_t>&> + (message.GetUserPayload()).GetValue(); + + { + ImageBuffer3D::SliceWriter writer(volume_->GetPixelData(), + VolumeProjection_Axial, + static_cast<unsigned int>(frameIndex)); + + Orthanc::ImageProcessing::Copy(writer.GetAccessor(), message.GetImage()); + } + + volume_->IncrementRevision(); + + { + VolumeUpdatedMessage updated(*this, + static_cast<unsigned int>(frameIndex)); + + BroadcastMessage(updated); + } + + remaining_--; + + if (remaining_ == 0) + { + VolumeReadyMessage ready(*this); + BroadcastMessage(ready); + } + } + + + DicomVolumeLoader::Factory::Factory(LoadedDicomResources& instances) : + framesFactory_(instances), + computeRange_(false) + { + } + + DicomVolumeLoader::Factory::Factory(const SeriesMetadataLoader::SeriesLoadedMessage& metadata) : + framesFactory_(metadata.GetInstances()), + computeRange_(false) + { + SetDicomDir(metadata.GetDicomDirPath(), metadata.GetDicomDir()); // Only useful for DICOMDIR sources + } + + + boost::shared_ptr<IObserver> DicomVolumeLoader::Factory::Create(ILoadersContext::ILock& context) + { + boost::shared_ptr<SeriesFramesLoader> frames = + boost::dynamic_pointer_cast<SeriesFramesLoader>(framesFactory_.Create(context)); + + boost::shared_ptr<DicomVolumeLoader> volume(new DicomVolumeLoader(frames, computeRange_)); + volume->Register<SeriesFramesLoader::FrameLoadedMessage>(*frames, &DicomVolumeLoader::Handle); + + return volume; + } + + void DicomVolumeLoader::Start(int priority, + const DicomSource& source) + { + if (started_) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + + started_ = true; + + if (IsValid()) + { + for (size_t i = 0; i < GetOrderedFrames().GetFramesCount(); i++) + { + framesLoader_->ScheduleLoadFrame(priority, source, i, source.GetQualityCount() - 1, + new Orthanc::SingleValueObject<size_t>(i)); + } + } + else + { + VolumeReadyMessage ready(*this); + BroadcastMessage(ready); + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Loaders/DicomVolumeLoader.h Tue Mar 31 15:51:02 2020 +0200 @@ -0,0 +1,141 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 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 "../Volumes/DicomVolumeImage.h" +#include "SeriesFramesLoader.h" +#include "SeriesMetadataLoader.h" + +namespace OrthancStone +{ + class DicomVolumeLoader : + public ObserverBase<DicomVolumeLoader>, + public IObservable + { + private: + boost::shared_ptr<SeriesFramesLoader> framesLoader_; + boost::shared_ptr<DicomVolumeImage> volume_; + bool isValid_; + bool started_; + size_t remaining_; + + DicomVolumeLoader(boost::shared_ptr<SeriesFramesLoader>& framesLoader, + bool computeRange); + + void Handle(const OrthancStone::SeriesFramesLoader::FrameLoadedMessage& message); + + public: + class VolumeReadyMessage : public OriginMessage<DicomVolumeLoader> + { + ORTHANC_STONE_MESSAGE(__FILE__, __LINE__); + + public: + VolumeReadyMessage(const DicomVolumeLoader& loader) : + OriginMessage(loader) + { + } + + const DicomVolumeImage& GetVolume() const + { + assert(GetOrigin().GetVolume()); + return *GetOrigin().GetVolume(); + } + }; + + + class VolumeUpdatedMessage : public OriginMessage<DicomVolumeLoader> + { + ORTHANC_STONE_MESSAGE(__FILE__, __LINE__); + + private: + unsigned int axial_; + + public: + VolumeUpdatedMessage(const DicomVolumeLoader& loader, + unsigned int axial) : + OriginMessage(loader), + axial_(axial) + { + } + + unsigned int GetAxialIndex() const + { + return axial_; + } + + const DicomVolumeImage& GetVolume() const + { + assert(GetOrigin().GetVolume()); + return *GetOrigin().GetVolume(); + } + }; + + + class Factory : public ILoaderFactory + { + private: + SeriesFramesLoader::Factory framesFactory_; + bool computeRange_; + + public: + Factory(LoadedDicomResources& instances); + + Factory(const SeriesMetadataLoader::SeriesLoadedMessage& metadata); + + void SetComputeRange(bool computeRange) + { + computeRange_ = computeRange; + } + + void SetDicomDir(const std::string& dicomDirPath, + boost::shared_ptr<LoadedDicomResources> dicomDir) + { + framesFactory_.SetDicomDir(dicomDirPath, dicomDir); + } + + virtual boost::shared_ptr<IObserver> Create(ILoadersContext::ILock& context) ORTHANC_OVERRIDE; + }; + + bool IsValid() const + { + return isValid_; + } + + bool IsFullyLoaded() const + { + return remaining_ == 0; + } + + boost::shared_ptr<DicomVolumeImage> GetVolume() const + { + return volume_; + } + + const SeriesOrderedFrames& GetOrderedFrames() const + { + return framesLoader_->GetOrderedFrames(); + } + + void Start(int priority, + const DicomSource& source); + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Loaders/GenericLoadersContext.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -0,0 +1,183 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 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 "GenericLoadersContext.h" + +namespace OrthancStone +{ + class GenericLoadersContext::Locker : public ILoadersContext::ILock + { + private: + GenericLoadersContext& that_; + boost::recursive_mutex::scoped_lock lock_; + + public: + Locker(GenericLoadersContext& that) : + that_(that), + lock_(that.mutex_) + { + if (!that_.scheduler_) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + } + + virtual ILoadersContext& GetContext() const ORTHANC_OVERRIDE + { + return that_; + }; + + virtual void AddLoader(boost::shared_ptr<IObserver> loader) ORTHANC_OVERRIDE + { + that_.loaders_.push_back(loader); + } + + virtual IObservable& GetOracleObservable() const ORTHANC_OVERRIDE + { + return that_.oracleObservable_; + } + + virtual void Schedule(boost::shared_ptr<IObserver> receiver, + int priority, + IOracleCommand* command /* Takes ownership */) ORTHANC_OVERRIDE + { + that_.scheduler_->Schedule(receiver, priority, command); + }; + + virtual void CancelRequests(boost::shared_ptr<IObserver> receiver) ORTHANC_OVERRIDE + { + that_.scheduler_->CancelRequests(receiver); + } + + virtual void CancelAllRequests() ORTHANC_OVERRIDE + { + that_.scheduler_->CancelAllRequests(); + } + + virtual void GetStatistics(uint64_t& scheduledCommands, + uint64_t& processedCommands) + { + scheduledCommands = that_.scheduler_->GetTotalScheduled(); + processedCommands = that_.scheduler_->GetTotalProcessed(); + } + }; + + + void GenericLoadersContext::EmitMessage(boost::weak_ptr<IObserver> observer, + const IMessage& message) + { + boost::recursive_mutex::scoped_lock lock(mutex_); + //LOG(INFO) << " inside emit lock: " << message.GetIdentifier().AsString(); + oracleObservable_.EmitMessage(observer, message); + //LOG(INFO) << " outside emit lock"; + } + + + GenericLoadersContext::GenericLoadersContext(unsigned int maxHighPriority, + unsigned int maxStandardPriority, + unsigned int maxLowPriority) + { + oracle_.reset(new ThreadedOracle(*this)); + scheduler_ = OracleScheduler::Create(*oracle_, oracleObservable_, *this, + maxHighPriority, maxStandardPriority, maxLowPriority); + + if (!scheduler_) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + } + + + GenericLoadersContext::~GenericLoadersContext() + { + LOG(WARNING) << "scheduled commands: " << scheduler_->GetTotalScheduled() + << ", processed commands: " << scheduler_->GetTotalProcessed(); + scheduler_.reset(); + //LOG(INFO) << "counter: " << scheduler_.use_count(); + } + + + void GenericLoadersContext::SetOrthancParameters(const Orthanc::WebServiceParameters& parameters) + { + boost::recursive_mutex::scoped_lock lock(mutex_); + oracle_->SetOrthancParameters(parameters); + } + + + void GenericLoadersContext::SetRootDirectory(const std::string& root) + { + boost::recursive_mutex::scoped_lock lock(mutex_); + oracle_->SetRootDirectory(root); + } + + + void GenericLoadersContext::SetDicomCacheSize(size_t size) + { + boost::recursive_mutex::scoped_lock lock(mutex_); + oracle_->SetDicomCacheSize(size); + } + + + void GenericLoadersContext::StartOracle() + { + boost::recursive_mutex::scoped_lock lock(mutex_); + oracle_->Start(); + //LOG(INFO) << "STARTED ORACLE"; + } + + + void GenericLoadersContext::StopOracle() + { + /** + * DON'T lock "mutex_" here, otherwise Stone won't be able to + * stop if one command being executed by the oracle has to emit + * a message (method "EmitMessage()" would have to lock the + * mutex too). + **/ + + //LOG(INFO) << "STOPPING ORACLE"; + oracle_->Stop(); + //LOG(INFO) << "STOPPED ORACLE"; + } + + + void GenericLoadersContext::WaitUntilComplete() + { + for (;;) + { + { + boost::recursive_mutex::scoped_lock lock(mutex_); + if (scheduler_ && + scheduler_->GetTotalScheduled() == scheduler_->GetTotalProcessed()) + { + return; + } + } + + boost::this_thread::sleep(boost::posix_time::milliseconds(100)); + } + } + + ILoadersContext::ILock* GenericLoadersContext::Lock() + { + return new Locker(*this); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Loaders/GenericLoadersContext.h Tue Mar 31 15:51:02 2020 +0200 @@ -0,0 +1,79 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 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 "../Messages/IMessageEmitter.h" +#include "../Oracle/ThreadedOracle.h" +#include "ILoadersContext.h" +#include "DicomSource.h" +#include "OracleScheduler.h" + +#include <boost/thread/recursive_mutex.hpp> + +namespace OrthancStone +{ + class GenericLoadersContext : + public ILoadersContext, + private IMessageEmitter + { + private: + class Locker; + + // "Recursive mutex" is necessary, to be able to run + // "ILoaderFactory" from a message handler triggered by + // "EmitMessage()" + boost::recursive_mutex mutex_; + + IObservable oracleObservable_; + std::unique_ptr<ThreadedOracle> oracle_; + boost::shared_ptr<OracleScheduler> scheduler_; + + // Necessary to keep the loaders persistent (including global + // function promises), after the function that created them is + // left. This avoids creating one global variable for each loader. + std::list< boost::shared_ptr<IObserver> > loaders_; + + virtual void EmitMessage(boost::weak_ptr<IObserver> observer, + const IMessage& message); + + public: + GenericLoadersContext(unsigned int maxHighPriority, + unsigned int maxStandardPriority, + unsigned int maxLowPriority); + + virtual ~GenericLoadersContext(); + + virtual ILock* Lock() ORTHANC_OVERRIDE; + + void SetOrthancParameters(const Orthanc::WebServiceParameters& parameters); + + void SetRootDirectory(const std::string& root); + + void SetDicomCacheSize(size_t size); + + void StartOracle(); + + void StopOracle(); + + void WaitUntilComplete(); + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Loaders/ILoaderFactory.h Tue Mar 31 15:51:02 2020 +0200 @@ -0,0 +1,41 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 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 "ILoadersContext.h" + +namespace OrthancStone +{ + class ILoaderFactory : public boost::noncopyable + { + public: + virtual ~ILoaderFactory() + { + } + + /** + * Factory function that creates a new loader, to be used by the + * Stone loaders context. + **/ + virtual boost::shared_ptr<IObserver> Create(ILoadersContext::ILock& context) = 0; + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Loaders/ILoadersContext.h Tue Mar 31 15:51:02 2020 +0200 @@ -0,0 +1,126 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 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 "../Messages/IObserver.h" +#include "../Messages/IObservable.h" +#include "../Oracle/IOracleCommand.h" + +#include <boost/shared_ptr.hpp> + +namespace OrthancStone +{ + class ILoadersContext : public boost::noncopyable + { + public: + class ILock : public boost::noncopyable + { + public: + virtual ~ILock() + { + } + + /** + * This method is useful for loaders that must be able to + * re-lock the Stone loaders context in the future (for instance + * to schedule new commands once some command is processed). + **/ + virtual ILoadersContext& GetContext() const = 0; + + /** + * Get a reference to the observable against which a loader must + * listen to be informed of messages issued by the oracle once + * some command is processed. + **/ + virtual IObservable& GetOracleObservable() const = 0; + + /** + * Schedule a new command for further processing by the + * oracle. The "receiver" argument indicates to which object the + * notification messages are sent by the oracle upon completion + * of the command. The command is possibly not directly sent to + * the oracle: Instead, an internal "OracleScheduler" object is + * often used as a priority queue to rule the order in which + * commands are actually sent to the oracle. Hence the + * "priority" argument (commands with lower value are executed + * first). + **/ + virtual void Schedule(boost::shared_ptr<IObserver> receiver, + int priority, + IOracleCommand* command /* Takes ownership */) = 0; + + /** + * Cancel all the commands that are waiting in the + * "OracleScheduler" queue and that are linked to the given + * receiver (i.e. the observer that was specified at the time + * method "Schedule()" was called). This is useful for real-time + * processing, as it allows to replace commands that were + * scheduled in the past by more urgent commands. + * + * Note that this call does not affect commands that would have + * already be sent to the oracle. As a consequence, the receiver + * might still receive messages that were sent to the oracle + * before the cancellation (be prepared to handle such + * messages). + **/ + virtual void CancelRequests(boost::shared_ptr<IObserver> receiver) = 0; + + /** + * Same as "CancelRequests()", but targets all the receivers. + **/ + virtual void CancelAllRequests() = 0; + + /** + * Add a reference to the given observer in the Stone loaders + * context. This can be used to match the lifetime of a loader + * with the lifetime of the Stone context: This is useful if + * your Stone application does not keep a reference to the + * loader by itself (typically in global promises), which would + * make the loader disappear as soon as the scope of the + * variable is left. + **/ + virtual void AddLoader(boost::shared_ptr<IObserver> loader) = 0; + + /** + * Returns the number of commands that were scheduled and + * processed using the "Schedule()" method. By "processed" + * commands, we refer to the number of commands that were either + * executed by the oracle, or canceled by the user. So the + * counting sequences are monotonically increasing over time. + **/ + virtual void GetStatistics(uint64_t& scheduledCommands, + uint64_t& processedCommands) = 0; + }; + + virtual ~ILoadersContext() + { + } + + /** + * Locks the Stone loaders context, to give access to its + * underlying features. This is important for Stone applications + * running in a multi-threaded environment, for which a global + * mutex is locked. + **/ + virtual ILock* Lock() = 0; + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Loaders/LoadedDicomResources.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -0,0 +1,233 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 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 "LoadedDicomResources.h" + +#include <Core/OrthancException.h> + +#include <cassert> + + +namespace OrthancStone +{ + void LoadedDicomResources::Flatten() + { + // Lazy generation of a "std::vector" from the "std::map" + if (flattened_.empty()) + { + flattened_.resize(resources_.size()); + + size_t pos = 0; + for (Resources::const_iterator it = resources_.begin(); it != resources_.end(); ++it) + { + assert(it->second != NULL); + flattened_[pos++] = it->second; + } + } + else + { + // No need to flatten + assert(flattened_.size() == resources_.size()); + } + } + + + void LoadedDicomResources::AddFromDicomWebInternal(const Json::Value& dicomweb) + { + assert(dicomweb.type() == Json::objectValue); + Orthanc::DicomMap dicom; + dicom.FromDicomWeb(dicomweb); + AddResource(dicom); + } + + + LoadedDicomResources::LoadedDicomResources(const LoadedDicomResources& other, + const Orthanc::DicomTag& indexedTag) : + indexedTag_(indexedTag) + { + for (Resources::const_iterator it = other.resources_.begin(); + it != other.resources_.end(); ++it) + { + assert(it->second != NULL); + AddResource(*it->second); + } + } + + void LoadedDicomResources::Clear() + { + for (Resources::iterator it = resources_.begin(); it != resources_.end(); ++it) + { + assert(it->second != NULL); + delete it->second; + } + + resources_.clear(); + flattened_.clear(); + } + + + Orthanc::DicomMap& LoadedDicomResources::GetResource(size_t index) + { + Flatten(); + + if (index >= flattened_.size()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + else + { + assert(flattened_[index] != NULL); + return *flattened_[index]; + } + } + + + void LoadedDicomResources::MergeResource(Orthanc::DicomMap& target, + const std::string& id) const + { + Resources::const_iterator it = resources_.find(id); + + if (it == resources_.end()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_InexistentItem); + } + else + { + assert(it->second != NULL); + target.Merge(*it->second); + } + } + + + bool LoadedDicomResources::LookupStringValue(std::string& target, + const std::string& id, + const Orthanc::DicomTag& tag) const + { + Resources::const_iterator found = resources_.find(id); + + if (found == resources_.end()) + { + return false; + } + else + { + assert(found->second != NULL); + return found->second->LookupStringValue(target, tag, false); + } + } + + + void LoadedDicomResources::AddResource(const Orthanc::DicomMap& dicom) + { + std::string id; + + if (dicom.LookupStringValue(id, indexedTag_, false /* no binary value */) && + resources_.find(id) == resources_.end() /* Don't index twice the same resource */) + { + resources_[id] = dicom.Clone(); + flattened_.clear(); // Invalidate the flattened version + } + } + + + void LoadedDicomResources::AddFromOrthanc(const Json::Value& tags) + { + Orthanc::DicomMap dicom; + dicom.FromDicomAsJson(tags); + AddResource(dicom); + } + + + void LoadedDicomResources::AddFromDicomWeb(const Json::Value& dicomweb) + { + if (dicomweb.type() == Json::objectValue) + { + AddFromDicomWebInternal(dicomweb); + } + else if (dicomweb.type() == Json::arrayValue) + { + for (Json::Value::ArrayIndex i = 0; i < dicomweb.size(); i++) + { + if (dicomweb[i].type() == Json::objectValue) + { + AddFromDicomWebInternal(dicomweb[i]); + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol); + } + } + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol); + } + } + + + bool LoadedDicomResources::LookupTagValueConsensus(std::string& target, + const Orthanc::DicomTag& tag) const + { + typedef std::map<std::string, unsigned int> Counter; + + Counter counter; + + for (Resources::const_iterator it = resources_.begin(); it != resources_.end(); ++it) + { + assert(it->second != NULL); + + std::string value; + if (it->second->LookupStringValue(value, tag, false)) + { + Counter::iterator found = counter.find(value); + if (found == counter.end()) + { + counter[value] = 1; + } + else + { + found->second ++; + } + } + } + + Counter::const_iterator best = counter.end(); + + for (Counter::const_iterator it = counter.begin(); it != counter.end(); ++it) + { + if (best == counter.end() || + best->second < it->second) + { + best = it; + } + } + + if (best == counter.end()) + { + return false; + } + else + { + target = best->first; + return true; + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Loaders/LoadedDicomResources.h Tue Mar 31 15:51:02 2020 +0200 @@ -0,0 +1,96 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 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 <Core/DicomFormat/DicomMap.h> + + +namespace OrthancStone +{ + /** + Stores an indexed collection of DicomMap objects. The index is a + user-specified DicomTag. + */ + class LoadedDicomResources : public boost::noncopyable + { + private: + typedef std::map<std::string, Orthanc::DicomMap*> Resources; + + Orthanc::DicomTag indexedTag_; + Resources resources_; + std::vector<Orthanc::DicomMap*> flattened_; + + void Flatten(); + + void AddFromDicomWebInternal(const Json::Value& dicomweb); + + public: + LoadedDicomResources(const Orthanc::DicomTag& indexedTag) : + indexedTag_(indexedTag) + { + } + + // Re-index another set of resources using another tag + LoadedDicomResources(const LoadedDicomResources& other, + const Orthanc::DicomTag& indexedTag); + + ~LoadedDicomResources() + { + Clear(); + } + + const Orthanc::DicomTag& GetIndexedTag() const + { + return indexedTag_; + } + + void Clear(); + + size_t GetSize() const + { + return resources_.size(); + } + + Orthanc::DicomMap& GetResource(size_t index); + + bool HasResource(const std::string& id) const + { + return resources_.find(id) != resources_.end(); + } + + void MergeResource(Orthanc::DicomMap& target, + const std::string& id) const; + + bool LookupStringValue(std::string& target, + const std::string& id, + const Orthanc::DicomTag& tag) const; + + void AddResource(const Orthanc::DicomMap& dicom); + + void AddFromOrthanc(const Json::Value& tags); + + void AddFromDicomWeb(const Json::Value& dicomweb); + + bool LookupTagValueConsensus(std::string& target, + const Orthanc::DicomTag& tag) const; + }; +}
--- a/Framework/Loaders/LoaderCache.cpp Tue Mar 31 15:47:29 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,415 +0,0 @@ -/** - * Stone of Orthanc - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2020 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 "LoaderCache.h" - -#include "../StoneException.h" -#include "OrthancSeriesVolumeProgressiveLoader.h" -#include "OrthancMultiframeVolumeLoader.h" -#include "DicomStructureSetLoader.h" - -#ifdef BGO_ENABLE_DICOMSTRUCTURESETLOADER2 -#include "DicomStructureSetLoader2.h" -#endif - //BGO_ENABLE_DICOMSTRUCTURESETLOADER2 - - -#if ORTHANC_ENABLE_WASM == 1 -# include <unistd.h> -# include "../Oracle/WebAssemblyOracle.h" -#else -# include "../Oracle/ThreadedOracle.h" -#endif - -#include "../Messages/LockingEmitter.h" - -#ifdef BGO_ENABLE_DICOMSTRUCTURESETLOADER2 -#include "../Toolbox/DicomStructureSet2.h" -#endif -//BGO_ENABLE_DICOMSTRUCTURESETLOADER2 - -#include "../Volumes/DicomVolumeImage.h" -#include "../Volumes/DicomVolumeImageMPRSlicer.h" - -#ifdef BGO_ENABLE_DICOMSTRUCTURESETLOADER2 -#include "../Volumes/DicomStructureSetSlicer2.h" -#endif -//BGO_ENABLE_DICOMSTRUCTURESETLOADER2 - -#include <Core/OrthancException.h> -#include <Core/Toolbox.h> - -namespace OrthancStone -{ -#if ORTHANC_ENABLE_WASM == 1 - LoaderCache::LoaderCache(WebAssemblyOracle& oracle) - : oracle_(oracle) - { - - } -#else - LoaderCache::LoaderCache(ThreadedOracle& oracle, LockingEmitter& lockingEmitter) - : oracle_(oracle) - , lockingEmitter_(lockingEmitter) - { - } -#endif - - boost::shared_ptr<OrthancStone::OrthancSeriesVolumeProgressiveLoader> - LoaderCache::GetSeriesVolumeProgressiveLoader(std::string seriesUuid) - { - try - { - - // normalize keys a little - seriesUuid = Orthanc::Toolbox::StripSpaces(seriesUuid); - Orthanc::Toolbox::ToLowerCase(seriesUuid); - - // find in cache - if (seriesVolumeProgressiveLoaders_.find(seriesUuid) == seriesVolumeProgressiveLoaders_.end()) - { -// LOG(TRACE) << "LoaderCache::GetSeriesVolumeProgressiveLoader : CACHEMISS --> need to load seriesUUid = " << seriesUuid; -#if ORTHANC_ENABLE_WASM == 1 -// LOG(TRACE) << "Performing request for series " << seriesUuid << " sbrk(0) = " << sbrk(0); -#else -// LOG(TRACE) << "Performing request for series " << seriesUuid; -#endif - boost::shared_ptr<DicomVolumeImage> volumeImage(new DicomVolumeImage); - boost::shared_ptr<OrthancSeriesVolumeProgressiveLoader> loader; -// LOG(TRACE) << "volumeImage = " << volumeImage.get(); - { -#if ORTHANC_ENABLE_WASM == 1 - loader.reset(new OrthancSeriesVolumeProgressiveLoader(volumeImage, oracle_, oracle_)); -#else - LockingEmitter::WriterLock lock(lockingEmitter_); - loader.reset(new OrthancSeriesVolumeProgressiveLoader(volumeImage, oracle_, lock.GetOracleObservable())); -#endif -// LOG(TRACE) << "LoaderCache::GetSeriesVolumeProgressiveLoader : loader = " << loader.get(); - loader->LoadSeries(seriesUuid); -// LOG(TRACE) << "LoaderCache::GetSeriesVolumeProgressiveLoader : loader->LoadSeries successful"; - } - seriesVolumeProgressiveLoaders_[seriesUuid] = loader; - } - else - { -// LOG(TRACE) << "LoaderCache::GetSeriesVolumeProgressiveLoader : returning cached loader for seriesUUid = " << seriesUuid; - } - return seriesVolumeProgressiveLoaders_[seriesUuid]; - } - catch (const Orthanc::OrthancException& e) - { - if (e.HasDetails()) - { - LOG(ERROR) << "OrthancException in LoaderCache: " << e.What() << " Details: " << e.GetDetails(); - } - else - { - LOG(ERROR) << "OrthancException in LoaderCache: " << e.What(); - } - throw; - } - catch (const std::exception& e) - { - LOG(ERROR) << "std::exception in LoaderCache: " << e.what(); - throw; - } - catch (...) - { - LOG(ERROR) << "Unknown exception in LoaderCache"; - throw; - } - } - - boost::shared_ptr<OrthancMultiframeVolumeLoader> LoaderCache::GetMultiframeVolumeLoader(std::string instanceUuid) - { - // if the loader is not available, let's trigger its creation - if(multiframeVolumeLoaders_.find(instanceUuid) == multiframeVolumeLoaders_.end()) - { - GetMultiframeDicomVolumeImageMPRSlicer(instanceUuid); - } - ORTHANC_ASSERT(multiframeVolumeLoaders_.find(instanceUuid) != multiframeVolumeLoaders_.end()); - - return multiframeVolumeLoaders_[instanceUuid]; - } - - boost::shared_ptr<DicomVolumeImageMPRSlicer> LoaderCache::GetMultiframeDicomVolumeImageMPRSlicer(std::string instanceUuid) - { - try - { - // normalize keys a little - instanceUuid = Orthanc::Toolbox::StripSpaces(instanceUuid); - Orthanc::Toolbox::ToLowerCase(instanceUuid); - - // find in cache - if (dicomVolumeImageMPRSlicers_.find(instanceUuid) == dicomVolumeImageMPRSlicers_.end()) - { - boost::shared_ptr<DicomVolumeImage> volumeImage(new DicomVolumeImage); - boost::shared_ptr<OrthancMultiframeVolumeLoader> loader; - - { -#if ORTHANC_ENABLE_WASM == 1 - loader.reset(new OrthancMultiframeVolumeLoader(volumeImage, oracle_, oracle_)); -#else - LockingEmitter::WriterLock lock(lockingEmitter_); - loader.reset(new OrthancMultiframeVolumeLoader(volumeImage, - oracle_, - lock.GetOracleObservable())); -#endif - loader->LoadInstance(instanceUuid); - } - multiframeVolumeLoaders_[instanceUuid] = loader; - boost::shared_ptr<DicomVolumeImageMPRSlicer> mprSlicer(new DicomVolumeImageMPRSlicer(volumeImage)); - dicomVolumeImageMPRSlicers_[instanceUuid] = mprSlicer; - } - return dicomVolumeImageMPRSlicers_[instanceUuid]; - } - catch (const Orthanc::OrthancException& e) - { - if (e.HasDetails()) - { - LOG(ERROR) << "OrthancException in LoaderCache: " << e.What() << " Details: " << e.GetDetails(); - } - else - { - LOG(ERROR) << "OrthancException in LoaderCache: " << e.What(); - } - throw; - } - catch (const std::exception& e) - { - LOG(ERROR) << "std::exception in LoaderCache: " << e.what(); - throw; - } - catch (...) - { - LOG(ERROR) << "Unknown exception in LoaderCache"; - throw; - } - } - -#ifdef BGO_ENABLE_DICOMSTRUCTURESETLOADER2 - - boost::shared_ptr<DicomStructureSetSlicer2> LoaderCache::GetDicomStructureSetSlicer2(std::string instanceUuid) - { - // if the loader is not available, let's trigger its creation - if (dicomStructureSetSlicers2_.find(instanceUuid) == dicomStructureSetSlicers2_.end()) - { - GetDicomStructureSetLoader2(instanceUuid); - } - ORTHANC_ASSERT(dicomStructureSetSlicers2_.find(instanceUuid) != dicomStructureSetSlicers2_.end()); - - return dicomStructureSetSlicers2_[instanceUuid]; - } -#endif -//BGO_ENABLE_DICOMSTRUCTURESETLOADER2 - - - /** - This method allows to convert a list of string into a string by - sorting the strings then joining them - */ - static std::string SortAndJoin(const std::vector<std::string>& stringList) - { - if (stringList.size() == 0) - { - return ""; - } - else - { - std::vector<std::string> sortedStringList = stringList; - std::sort(sortedStringList.begin(), sortedStringList.end()); - std::stringstream s; - s << sortedStringList[0]; - for (size_t i = 1; i < sortedStringList.size(); ++i) - { - s << "-" << sortedStringList[i]; - } - return s.str(); - } - } - - boost::shared_ptr<DicomStructureSetLoader> - LoaderCache::GetDicomStructureSetLoader( - std::string inInstanceUuid, - const std::vector<std::string>& initiallyVisibleStructures) - { - try - { - // normalize keys a little - inInstanceUuid = Orthanc::Toolbox::StripSpaces(inInstanceUuid); - Orthanc::Toolbox::ToLowerCase(inInstanceUuid); - - std::string initiallyVisibleStructuresKey = - SortAndJoin(initiallyVisibleStructures); - - std::string entryKey = inInstanceUuid + "_" + initiallyVisibleStructuresKey; - - // find in cache - if (dicomStructureSetLoaders_.find(entryKey) == dicomStructureSetLoaders_.end()) - { - boost::shared_ptr<DicomStructureSetLoader> loader; - - { -#if ORTHANC_ENABLE_WASM == 1 - loader.reset(new DicomStructureSetLoader(oracle_, oracle_)); -#else - LockingEmitter::WriterLock lock(lockingEmitter_); - loader.reset(new DicomStructureSetLoader(oracle_, lock.GetOracleObservable())); -#endif - loader->LoadInstance(inInstanceUuid, initiallyVisibleStructures); - } - dicomStructureSetLoaders_[entryKey] = loader; - } - return dicomStructureSetLoaders_[entryKey]; - } - catch (const Orthanc::OrthancException& e) - { - if (e.HasDetails()) - { - LOG(ERROR) << "OrthancException in LoaderCache: " << e.What() << " Details: " << e.GetDetails(); - } - else - { - LOG(ERROR) << "OrthancException in LoaderCache: " << e.What(); - } - throw; - } - catch (const std::exception& e) - { - LOG(ERROR) << "std::exception in LoaderCache: " << e.what(); - throw; - } - catch (...) - { - LOG(ERROR) << "Unknown exception in LoaderCache"; - throw; - } - } - -#ifdef BGO_ENABLE_DICOMSTRUCTURESETLOADER2 - - boost::shared_ptr<DicomStructureSetLoader2> LoaderCache::GetDicomStructureSetLoader2(std::string instanceUuid) - { - try - { - // normalize keys a little - instanceUuid = Orthanc::Toolbox::StripSpaces(instanceUuid); - Orthanc::Toolbox::ToLowerCase(instanceUuid); - - // find in cache - if (dicomStructureSetLoaders2_.find(instanceUuid) == dicomStructureSetLoaders2_.end()) - { - boost::shared_ptr<DicomStructureSetLoader2> loader; - boost::shared_ptr<DicomStructureSet2> structureSet(new DicomStructureSet2()); - boost::shared_ptr<DicomStructureSetSlicer2> rtSlicer(new DicomStructureSetSlicer2(structureSet)); - dicomStructureSetSlicers2_[instanceUuid] = rtSlicer; - dicomStructureSets2_[instanceUuid] = structureSet; // to prevent it from being deleted - { -#if ORTHANC_ENABLE_WASM == 1 - loader.reset(new DicomStructureSetLoader2(*(structureSet.get()), oracle_, oracle_)); -#else - LockingEmitter::WriterLock lock(lockingEmitter_); - // TODO: clarify lifetimes... this is DANGEROUS! - loader.reset(new DicomStructureSetLoader2(*(structureSet.get()), oracle_, lock.GetOracleObservable())); -#endif - loader->LoadInstance(instanceUuid); - } - dicomStructureSetLoaders2_[instanceUuid] = loader; - } - return dicomStructureSetLoaders2_[instanceUuid]; - } - catch (const Orthanc::OrthancException& e) - { - if (e.HasDetails()) - { - LOG(ERROR) << "OrthancException in GetDicomStructureSetLoader2: " << e.What() << " Details: " << e.GetDetails(); - } - else - { - LOG(ERROR) << "OrthancException in GetDicomStructureSetLoader2: " << e.What(); - } - throw; - } - catch (const std::exception& e) - { - LOG(ERROR) << "std::exception in GetDicomStructureSetLoader2: " << e.what(); - throw; - } - catch (...) - { - LOG(ERROR) << "Unknown exception in GetDicomStructureSetLoader2"; - throw; - } - } - -#endif -// BGO_ENABLE_DICOMSTRUCTURESETLOADER2 - - - void LoaderCache::ClearCache() - { -#if ORTHANC_ENABLE_WASM != 1 - LockingEmitter::WriterLock lock(lockingEmitter_); -#endif - -//#ifndef NDEBUG - // ISO way of checking for debug builds - DebugDisplayObjRefCounts(); -//#endif - seriesVolumeProgressiveLoaders_.clear(); - multiframeVolumeLoaders_.clear(); - dicomVolumeImageMPRSlicers_.clear(); - dicomStructureSetLoaders_.clear(); - -#ifdef BGO_ENABLE_DICOMSTRUCTURESETLOADER2 - // order is important! - dicomStructureSetLoaders2_.clear(); - dicomStructureSetSlicers2_.clear(); - dicomStructureSets2_.clear(); -#endif -// BGO_ENABLE_DICOMSTRUCTURESETLOADER2 - } - - template<typename T> void DebugDisplayObjRefCountsInMap( - const std::string& name, const std::map<std::string, boost::shared_ptr<T> >& myMap) - { - LOG(TRACE) << "Map \"" << name << "\" ref counts:"; - size_t i = 0; - for (typename std::map<std::string, boost::shared_ptr<T> >::const_iterator - it = myMap.begin(); it != myMap.end(); ++it) - { - LOG(TRACE) << " element #" << i << ": ref count = " << it->second.use_count(); - i++; - } - } - - void LoaderCache::DebugDisplayObjRefCounts() - { - DebugDisplayObjRefCountsInMap("seriesVolumeProgressiveLoaders_", seriesVolumeProgressiveLoaders_); - DebugDisplayObjRefCountsInMap("multiframeVolumeLoaders_", multiframeVolumeLoaders_); - DebugDisplayObjRefCountsInMap("dicomVolumeImageMPRSlicers_", dicomVolumeImageMPRSlicers_); - DebugDisplayObjRefCountsInMap("dicomStructureSetLoaders_", dicomStructureSetLoaders_); -#ifdef BGO_ENABLE_DICOMSTRUCTURESETLOADER2 - DebugDisplayObjRefCountsInMap("dicomStructureSetLoaders2_", dicomStructureSetLoaders2_); - DebugDisplayObjRefCountsInMap("dicomStructureSetSlicers2_", dicomStructureSetSlicers2_); -#endif -//BGO_ENABLE_DICOMSTRUCTURESETLOADER2 - } -}
--- a/Framework/Loaders/LoaderCache.h Tue Mar 31 15:47:29 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,112 +0,0 @@ -/** - * Stone of Orthanc - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2020 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 <boost/shared_ptr.hpp> - -#include <map> -#include <string> -#include <vector> - -namespace OrthancStone -{ - class OrthancSeriesVolumeProgressiveLoader; - class DicomVolumeImageMPRSlicer; - class DicomStructureSetLoader; -#ifdef BGO_ENABLE_DICOMSTRUCTURESETLOADER2 - class DicomStructureSetLoader2; - class DicomStructureSetSlicer2; - class DicomStructureSet2; -#endif - //BGO_ENABLE_DICOMSTRUCTURESETLOADER2 - class OrthancMultiframeVolumeLoader; - -#if ORTHANC_ENABLE_WASM == 1 - class WebAssemblyOracle; -#else - class ThreadedOracle; - class LockingEmitter; -#endif - - class LoaderCache - { - public: -#if ORTHANC_ENABLE_WASM == 1 - LoaderCache(WebAssemblyOracle& oracle); -#else - LoaderCache(ThreadedOracle& oracle, LockingEmitter& lockingEmitter); -#endif - - boost::shared_ptr<OrthancSeriesVolumeProgressiveLoader> - GetSeriesVolumeProgressiveLoader (std::string seriesUuid); - - boost::shared_ptr<DicomVolumeImageMPRSlicer> - GetMultiframeDicomVolumeImageMPRSlicer(std::string instanceUuid); - - boost::shared_ptr<OrthancMultiframeVolumeLoader> - GetMultiframeVolumeLoader(std::string instanceUuid); - - boost::shared_ptr<DicomStructureSetLoader> - GetDicomStructureSetLoader( - std::string instanceUuid, - const std::vector<std::string>& initiallyVisibleStructures); - -#ifdef BGO_ENABLE_DICOMSTRUCTURESETLOADER2 - boost::shared_ptr<DicomStructureSetLoader2> - GetDicomStructureSetLoader2(std::string instanceUuid); - - boost::shared_ptr<DicomStructureSetSlicer2> - GetDicomStructureSetSlicer2(std::string instanceUuid); -#endif - //BGO_ENABLE_DICOMSTRUCTURESETLOADER2 - - void ClearCache(); - - private: - - void DebugDisplayObjRefCounts(); -#if ORTHANC_ENABLE_WASM == 1 - WebAssemblyOracle& oracle_; -#else - ThreadedOracle& oracle_; - LockingEmitter& lockingEmitter_; -#endif - - std::map<std::string, boost::shared_ptr<OrthancSeriesVolumeProgressiveLoader> > - seriesVolumeProgressiveLoaders_; - std::map<std::string, boost::shared_ptr<OrthancMultiframeVolumeLoader> > - multiframeVolumeLoaders_; - std::map<std::string, boost::shared_ptr<DicomVolumeImageMPRSlicer> > - dicomVolumeImageMPRSlicers_; - std::map<std::string, boost::shared_ptr<DicomStructureSetLoader> > - dicomStructureSetLoaders_; -#ifdef BGO_ENABLE_DICOMSTRUCTURESETLOADER2 - std::map<std::string, boost::shared_ptr<DicomStructureSetLoader2> > - dicomStructureSetLoaders2_; - std::map<std::string, boost::shared_ptr<DicomStructureSet2> > - dicomStructureSets2_; - std::map<std::string, boost::shared_ptr<DicomStructureSetSlicer2> > - dicomStructureSetSlicers2_; -#endif - //BGO_ENABLE_DICOMSTRUCTURESETLOADER2 - }; -} -
--- a/Framework/Loaders/LoaderStateMachine.cpp Tue Mar 31 15:47:29 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,211 +0,0 @@ -/** - * Stone of Orthanc - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2020 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 "LoaderStateMachine.h" - -#include <Core/OrthancException.h> - -namespace OrthancStone -{ - void LoaderStateMachine::State::Handle(const OrthancRestApiCommand::SuccessMessage& message) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); - } - - - void LoaderStateMachine::State::Handle(const GetOrthancImageCommand::SuccessMessage& message) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); - } - - - void LoaderStateMachine::State::Handle(const GetOrthancWebViewerJpegCommand::SuccessMessage& message) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); - } - - - void LoaderStateMachine::Schedule(OracleCommandWithPayload* command) - { - LOG(TRACE) << "LoaderStateMachine(" << std::hex << this << std::dec << ")::Schedule()"; - - std::unique_ptr<OracleCommandWithPayload> protection(command); - - if (command == NULL) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); - } - - if (!command->HasPayload()) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange, - "The payload must contain the next state"); - } - pendingCommands_.push_back(protection.release()); - - Step(); - } - - - void LoaderStateMachine::Start() - { - LOG(TRACE) << "LoaderStateMachine(" << std::hex << this << std::dec << ")::Start()"; - - if (active_) - { - LOG(TRACE) << "LoaderStateMachine::Start() called while active_ is true"; - throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); - } - - active_ = true; - - for (size_t i = 0; i < simultaneousDownloads_; i++) - { - Step(); - } - } - - - void LoaderStateMachine::Step() - { - if (!pendingCommands_.empty() && - activeCommands_ < simultaneousDownloads_) - { - - IOracleCommand* nextCommand = pendingCommands_.front(); - - LOG(TRACE) << " LoaderStateMachine(" << std::hex << this << std::dec << - ")::Step(): activeCommands_ (" << activeCommands_ << - ") < simultaneousDownloads_ (" << simultaneousDownloads_ << - ") --> will Schedule command addr " << std::hex << nextCommand << std::dec; - - oracle_.Schedule(*this, nextCommand); - pendingCommands_.pop_front(); - - activeCommands_++; - } - else - { - LOG(TRACE) << " LoaderStateMachine(" << std::hex << this << std::dec << - ")::Step(): activeCommands_ (" << activeCommands_ << - ") >= simultaneousDownloads_ (" << simultaneousDownloads_ << - ") --> will NOT Schedule command"; - } - } - - - void LoaderStateMachine::Clear() - { - LOG(TRACE) << "LoaderStateMachine(" << std::hex << this << std::dec << ")::Clear()"; - for (PendingCommands::iterator it = pendingCommands_.begin(); - it != pendingCommands_.end(); ++it) - { - delete *it; - } - - pendingCommands_.clear(); - } - - - void LoaderStateMachine::HandleExceptionMessage(const OracleCommandExceptionMessage& message) - { - LOG(ERROR) << "LoaderStateMachine::HandleExceptionMessage: error in the state machine, stopping all processing"; - LOG(ERROR) << "Error: " << message.GetException().What() << " Details: " << - message.GetException().GetDetails(); - Clear(); - } - - template <typename T> - void LoaderStateMachine::HandleSuccessMessage(const T& message) - { - LOG(TRACE) << "LoaderStateMachine(" << std::hex << this << std::dec << ")::HandleSuccessMessage(). Receiver fingerprint = " << GetFingerprint(); - if (activeCommands_ <= 0) { - LOG(ERROR) << "LoaderStateMachine(" << std::hex << this << std::dec << ")::HandleSuccessMessage : activeCommands_ should be > 0 but is: " << activeCommands_; - } - else { - activeCommands_--; - try - { - dynamic_cast<State&>(message.GetOrigin().GetPayload()).Handle(message); - Step(); - } - catch (Orthanc::OrthancException& e) - { - LOG(ERROR) << "Error in the state machine, stopping all processing: " << - e.What() << " Details: " << e.GetDetails(); - Clear(); - } - } - } - - - LoaderStateMachine::LoaderStateMachine(IOracle& oracle, - IObservable& oracleObservable) : - IObserver(oracleObservable.GetBroker()), - oracle_(oracle), - oracleObservable_(oracleObservable), - active_(false), - simultaneousDownloads_(4), - activeCommands_(0) - { - LOG(TRACE) << "LoaderStateMachine(" << std::hex << this << std::dec << ")::LoaderStateMachine()"; - - oracleObservable.RegisterObserverCallback( - new Callable<LoaderStateMachine, OrthancRestApiCommand::SuccessMessage> - (*this, &LoaderStateMachine::HandleSuccessMessage)); - - oracleObservable.RegisterObserverCallback( - new Callable<LoaderStateMachine, GetOrthancImageCommand::SuccessMessage> - (*this, &LoaderStateMachine::HandleSuccessMessage)); - - oracleObservable.RegisterObserverCallback( - new Callable<LoaderStateMachine, GetOrthancWebViewerJpegCommand::SuccessMessage> - (*this, &LoaderStateMachine::HandleSuccessMessage)); - - oracleObservable.RegisterObserverCallback( - new Callable<LoaderStateMachine, OracleCommandExceptionMessage> - (*this, &LoaderStateMachine::HandleExceptionMessage)); - } - - LoaderStateMachine::~LoaderStateMachine() - { - oracleObservable_.Unregister(this); - LOG(TRACE) << "LoaderStateMachine(" << std::hex << this << std::dec << ")::~LoaderStateMachine()"; - Clear(); - } - - void LoaderStateMachine::SetSimultaneousDownloads(unsigned int count) - { - if (active_) - { - LOG(ERROR) << "LoaderStateMachine::SetSimultaneousDownloads called while active_ is true"; - throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); - } - else if (count == 0) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); - } - else - { - simultaneousDownloads_ = count; - } - } -}
--- a/Framework/Loaders/LoaderStateMachine.h Tue Mar 31 15:47:29 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,117 +0,0 @@ -/** - * Stone of Orthanc - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2020 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 "../Messages/IObservable.h" -#include "../Messages/IObserver.h" -#include "../Oracle/GetOrthancImageCommand.h" -#include "../Oracle/GetOrthancWebViewerJpegCommand.h" -#include "../Oracle/IOracle.h" -#include "../Oracle/OracleCommandExceptionMessage.h" -#include "../Oracle/OrthancRestApiCommand.h" - -#include <Core/IDynamicObject.h> - -#include <list> - -namespace OrthancStone -{ - /** - This class is supplied with Oracle commands and will schedule up to - simultaneousDownloads_ of them at the same time, then will schedule the - rest once slots become available. It is used, a.o., by the - OrtancMultiframeVolumeLoader class. - */ - class LoaderStateMachine : public IObserver - { - protected: - class State : public Orthanc::IDynamicObject - { - private: - LoaderStateMachine& that_; - - public: - State(LoaderStateMachine& that) : - that_(that) - { - } - - State(const State& currentState) : - that_(currentState.that_) - { - } - - void Schedule(OracleCommandWithPayload* command) const - { - that_.Schedule(command); - } - - template <typename T> - T& GetLoader() const - { - return dynamic_cast<T&>(that_); - } - - virtual void Handle(const OrthancRestApiCommand::SuccessMessage& message); - - virtual void Handle(const GetOrthancImageCommand::SuccessMessage& message); - - virtual void Handle(const GetOrthancWebViewerJpegCommand::SuccessMessage& message); - }; - - void Schedule(OracleCommandWithPayload* command); - - void Start(); - - private: - void Step(); - - void Clear(); - - void HandleExceptionMessage(const OracleCommandExceptionMessage& message); - - template <typename T> - void HandleSuccessMessage(const T& message); - - typedef std::list<IOracleCommand*> PendingCommands; - - IOracle& oracle_; - IObservable& oracleObservable_; - bool active_; - unsigned int simultaneousDownloads_; - PendingCommands pendingCommands_; - unsigned int activeCommands_; - - public: - LoaderStateMachine(IOracle& oracle, - IObservable& oracleObservable); - - virtual ~LoaderStateMachine(); - - bool IsActive() const - { - return active_; - } - - void SetSimultaneousDownloads(unsigned int count); - }; -}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Loaders/OracleScheduler.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -0,0 +1,557 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 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 "OracleScheduler.h" + +#include "../Oracle/ParseDicomFromFileCommand.h" + +namespace OrthancStone +{ + class OracleScheduler::ReceiverPayload : public Orthanc::IDynamicObject + { + private: + Priority priority_; + boost::weak_ptr<IObserver> receiver_; + std::unique_ptr<IOracleCommand> command_; + + public: + ReceiverPayload(Priority priority, + boost::weak_ptr<IObserver> receiver, + IOracleCommand* command) : + priority_(priority), + receiver_(receiver), + command_(command) + { + if (command == NULL) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); + } + } + + Priority GetActivePriority() const + { + return priority_; + } + + boost::weak_ptr<IObserver> GetOriginalReceiver() const + { + return receiver_; + } + + const IOracleCommand& GetOriginalCommand() const + { + assert(command_.get() != NULL); + return *command_; + } + }; + + + class OracleScheduler::ScheduledCommand : public boost::noncopyable + { + private: + boost::weak_ptr<IObserver> receiver_; + std::unique_ptr<IOracleCommand> command_; + + public: + ScheduledCommand(boost::shared_ptr<IObserver> receiver, + IOracleCommand* command) : + receiver_(receiver), + command_(command) + { + if (command == NULL) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); + } + } + + boost::weak_ptr<IObserver> GetReceiver() + { + return receiver_; + } + + bool IsSameReceiver(boost::shared_ptr<OrthancStone::IObserver> receiver) const + { + boost::shared_ptr<IObserver> lock(receiver_.lock()); + + return (lock && + lock.get() == receiver.get()); + } + + IOracleCommand* WrapCommand(Priority priority) + { + if (command_.get() == NULL) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + else + { + std::unique_ptr<IOracleCommand> wrapped(command_->Clone()); + dynamic_cast<OracleCommandBase&>(*wrapped).AcquirePayload(new ReceiverPayload(priority, receiver_, command_.release())); + return wrapped.release(); + } + } + }; + + + + void OracleScheduler::ClearQueue(Queue& queue) + { + for (Queue::iterator it = queue.begin(); it != queue.end(); ++it) + { + assert(it->second != NULL); + delete it->second; + + totalProcessed_ ++; + } + + queue.clear(); + } + + + void OracleScheduler::RemoveReceiverFromQueue(Queue& queue, + boost::shared_ptr<IObserver> receiver) + { + if (!receiver) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); + } + + Queue tmp; + + for (Queue::iterator it = queue.begin(); it != queue.end(); ++it) + { + assert(it->second != NULL); + + if (!(it->second->IsSameReceiver(receiver))) + { + // This promise is still active + tmp.insert(std::make_pair(it->first, it->second)); + } + else + { + delete it->second; + + totalProcessed_ ++; + } + } + + queue = tmp; + } + + + void OracleScheduler::CheckInvariants() const + { +#ifndef NDEBUG + /*char buf[1024]; + sprintf(buf, "active: %d %d %d ; pending: %lu %lu %lu", + activeHighPriorityCommands_, activeStandardPriorityCommands_, activeLowPriorityCommands_, + highPriorityQueue_.size(), standardPriorityQueue_.size(), lowPriorityQueue_.size()); + LOG(INFO) << buf;*/ + + assert(activeHighPriorityCommands_ <= maxHighPriorityCommands_); + assert(activeStandardPriorityCommands_ <= maxStandardPriorityCommands_); + assert(activeLowPriorityCommands_ <= maxLowPriorityCommands_); + assert(totalProcessed_ <= totalScheduled_); + + for (Queue::const_iterator it = standardPriorityQueue_.begin(); it != standardPriorityQueue_.end(); ++it) + { + assert(it->first > PRIORITY_HIGH && + it->first < PRIORITY_LOW); + } + + for (Queue::const_iterator it = highPriorityQueue_.begin(); it != highPriorityQueue_.end(); ++it) + { + assert(it->first <= PRIORITY_HIGH); + } + + for (Queue::const_iterator it = lowPriorityQueue_.begin(); it != lowPriorityQueue_.end(); ++it) + { + assert(it->first >= PRIORITY_LOW); + } +#endif + } + + + void OracleScheduler::SpawnFromQueue(Queue& queue, + Priority priority) + { + CheckInvariants(); + + Queue::iterator item = queue.begin(); + assert(item != queue.end()); + + std::unique_ptr<ScheduledCommand> command(dynamic_cast<ScheduledCommand*>(item->second)); + queue.erase(item); + + if (command.get() != NULL) + { + /** + * Only schedule the command for execution in the oracle, if its + * receiver has not been destroyed yet. + **/ + boost::shared_ptr<IObserver> observer(command->GetReceiver().lock()); + if (observer) + { + if (oracle_.Schedule(GetSharedObserver(), command->WrapCommand(priority))) + { + /** + * Executing this code if "Schedule()" returned "false" + * above, will result in a memory leak within + * "OracleScheduler", as the scheduler believes that some + * command is still active (i.e. pending to be executed by + * the oracle), hereby stalling the scheduler during its + * destruction, and not freeing the + * "shared_ptr<OracleScheduler>" of the Stone context (check + * out "sjo-playground/WebViewer/Backend/Leak") + **/ + + switch (priority) + { + case Priority_High: + activeHighPriorityCommands_ ++; + break; + + case Priority_Standard: + activeStandardPriorityCommands_ ++; + break; + + case Priority_Low: + activeLowPriorityCommands_ ++; + break; + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + } + else + { + totalProcessed_ ++; + } + } + } + else + { + LOG(ERROR) << "NULL command, should never happen"; + } + + CheckInvariants(); + } + + + void OracleScheduler::SpawnCommands() + { + // Send as many commands as possible to the oracle + while (!highPriorityQueue_.empty()) + { + if (activeHighPriorityCommands_ < maxHighPriorityCommands_) + { + // First fill the high-priority lane + SpawnFromQueue(highPriorityQueue_, Priority_High); + } + else if (activeStandardPriorityCommands_ < maxStandardPriorityCommands_) + { + // There remain too many high-priority commands for the + // high-priority lane, schedule them to the standard-priority lanes + SpawnFromQueue(highPriorityQueue_, Priority_Standard); + } + else if (activeLowPriorityCommands_ < maxLowPriorityCommands_) + { + SpawnFromQueue(highPriorityQueue_, Priority_Low); + } + else + { + return; // No slot available + } + } + + while (!standardPriorityQueue_.empty()) + { + if (activeStandardPriorityCommands_ < maxStandardPriorityCommands_) + { + SpawnFromQueue(standardPriorityQueue_, Priority_Standard); + } + else if (activeLowPriorityCommands_ < maxLowPriorityCommands_) + { + SpawnFromQueue(standardPriorityQueue_, Priority_Low); + } + else + { + return; + } + } + + while (!lowPriorityQueue_.empty()) + { + if (activeLowPriorityCommands_ < maxLowPriorityCommands_) + { + SpawnFromQueue(lowPriorityQueue_, Priority_Low); + } + else + { + return; + } + } + } + + + void OracleScheduler::RemoveActiveCommand(const ReceiverPayload& payload) + { + CheckInvariants(); + + totalProcessed_ ++; + + switch (payload.GetActivePriority()) + { + case Priority_High: + assert(activeHighPriorityCommands_ > 0); + activeHighPriorityCommands_ --; + break; + + case Priority_Standard: + assert(activeStandardPriorityCommands_ > 0); + activeStandardPriorityCommands_ --; + break; + + case Priority_Low: + assert(activeLowPriorityCommands_ > 0); + activeLowPriorityCommands_ --; + break; + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + + SpawnCommands(); + + CheckInvariants(); + } + + + void OracleScheduler::Handle(const GetOrthancImageCommand::SuccessMessage& message) + { + assert(message.GetOrigin().HasPayload()); + const ReceiverPayload& payload = dynamic_cast<const ReceiverPayload&>(message.GetOrigin().GetPayload()); + + RemoveActiveCommand(payload); + + GetOrthancImageCommand::SuccessMessage bis( + dynamic_cast<const GetOrthancImageCommand&>(payload.GetOriginalCommand()), + message.GetImage(), message.GetMimeType()); + emitter_.EmitMessage(payload.GetOriginalReceiver(), bis); + } + + + void OracleScheduler::Handle(const GetOrthancWebViewerJpegCommand::SuccessMessage& message) + { + assert(message.GetOrigin().HasPayload()); + const ReceiverPayload& payload = dynamic_cast<const ReceiverPayload&>(message.GetOrigin().GetPayload()); + + RemoveActiveCommand(payload); + + GetOrthancWebViewerJpegCommand::SuccessMessage bis( + dynamic_cast<const GetOrthancWebViewerJpegCommand&>(payload.GetOriginalCommand()), + message.GetImage()); + emitter_.EmitMessage(payload.GetOriginalReceiver(), bis); + } + + + void OracleScheduler::Handle(const HttpCommand::SuccessMessage& message) + { + assert(message.GetOrigin().HasPayload()); + const ReceiverPayload& payload = dynamic_cast<const ReceiverPayload&>(message.GetOrigin().GetPayload()); + + RemoveActiveCommand(payload); + + HttpCommand::SuccessMessage bis( + dynamic_cast<const HttpCommand&>(payload.GetOriginalCommand()), + message.GetAnswerHeaders(), message.GetAnswer()); + emitter_.EmitMessage(payload.GetOriginalReceiver(), bis); + } + + + void OracleScheduler::Handle(const OrthancRestApiCommand::SuccessMessage& message) + { + assert(message.GetOrigin().HasPayload()); + const ReceiverPayload& payload = dynamic_cast<const ReceiverPayload&>(message.GetOrigin().GetPayload()); + + RemoveActiveCommand(payload); + + OrthancRestApiCommand::SuccessMessage bis( + dynamic_cast<const OrthancRestApiCommand&>(payload.GetOriginalCommand()), + message.GetAnswerHeaders(), message.GetAnswer()); + emitter_.EmitMessage(payload.GetOriginalReceiver(), bis); + } + + +#if ORTHANC_ENABLE_DCMTK == 1 + void OracleScheduler::Handle(const ParseDicomSuccessMessage& message) + { + assert(message.GetOrigin().HasPayload()); + const ReceiverPayload& payload = dynamic_cast<const ReceiverPayload&>(message.GetOrigin().GetPayload()); + + RemoveActiveCommand(payload); + + ParseDicomSuccessMessage bis( + dynamic_cast<const OracleCommandBase&>(payload.GetOriginalCommand()), + message.GetDicom(), message.GetFileSize(), message.HasPixelData()); + emitter_.EmitMessage(payload.GetOriginalReceiver(), bis); + } +#endif + + + void OracleScheduler::Handle(const ReadFileCommand::SuccessMessage& message) + { + assert(message.GetOrigin().HasPayload()); + const ReceiverPayload& payload = dynamic_cast<const ReceiverPayload&>(message.GetOrigin().GetPayload()); + + RemoveActiveCommand(payload); + + ReadFileCommand::SuccessMessage bis( + dynamic_cast<const ReadFileCommand&>(payload.GetOriginalCommand()), + message.GetContent()); + emitter_.EmitMessage(payload.GetOriginalReceiver(), bis); + } + + + void OracleScheduler::Handle(const OracleCommandExceptionMessage& message) + { + const OracleCommandBase& command = dynamic_cast<const OracleCommandBase&>(message.GetOrigin()); + + assert(command.HasPayload()); + const ReceiverPayload& payload = dynamic_cast<const ReceiverPayload&>(command.GetPayload()); + + RemoveActiveCommand(payload); + + OracleCommandExceptionMessage bis(payload.GetOriginalCommand(), message.GetException()); + emitter_.EmitMessage(payload.GetOriginalReceiver(), bis); + } + + + OracleScheduler::OracleScheduler(IOracle& oracle, + IMessageEmitter& emitter, + unsigned int maxHighPriority, + unsigned int maxStandardPriority, + unsigned int maxLowPriority) : + oracle_(oracle), + emitter_(emitter), + maxHighPriorityCommands_(maxHighPriority), + maxStandardPriorityCommands_(maxStandardPriority), + maxLowPriorityCommands_(maxLowPriority), + activeHighPriorityCommands_(0), + activeStandardPriorityCommands_(0), + activeLowPriorityCommands_(0), + totalScheduled_(0), + totalProcessed_(0) + { + assert(PRIORITY_HIGH < 0 && + PRIORITY_LOW > 0); + + if (maxLowPriority <= 0) + { + // There must be at least 1 lane available to deal with low-priority commands + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + } + + + boost::shared_ptr<OracleScheduler> OracleScheduler::Create(IOracle& oracle, + IObservable& oracleObservable, + IMessageEmitter& emitter, + unsigned int maxHighPriority, + unsigned int maxStandardPriority, + unsigned int maxLowPriority) + { + boost::shared_ptr<OracleScheduler> scheduler + (new OracleScheduler(oracle, emitter, maxHighPriority, maxStandardPriority, maxLowPriority)); + scheduler->Register<GetOrthancImageCommand::SuccessMessage>(oracleObservable, &OracleScheduler::Handle); + scheduler->Register<GetOrthancWebViewerJpegCommand::SuccessMessage>(oracleObservable, &OracleScheduler::Handle); + scheduler->Register<HttpCommand::SuccessMessage>(oracleObservable, &OracleScheduler::Handle); + scheduler->Register<OrthancRestApiCommand::SuccessMessage>(oracleObservable, &OracleScheduler::Handle); + scheduler->Register<ReadFileCommand::SuccessMessage>(oracleObservable, &OracleScheduler::Handle); + scheduler->Register<OracleCommandExceptionMessage>(oracleObservable, &OracleScheduler::Handle); + +#if ORTHANC_ENABLE_DCMTK == 1 + scheduler->Register<ParseDicomSuccessMessage>(oracleObservable, &OracleScheduler::Handle); +#endif + + return scheduler; + } + + + OracleScheduler::~OracleScheduler() + { + CancelAllRequests(); + } + + + void OracleScheduler::CancelRequests(boost::shared_ptr<IObserver> receiver) + { + RemoveReceiverFromQueue(standardPriorityQueue_, receiver); + RemoveReceiverFromQueue(highPriorityQueue_, receiver); + RemoveReceiverFromQueue(lowPriorityQueue_, receiver); + } + + + void OracleScheduler::CancelAllRequests() + { + ClearQueue(standardPriorityQueue_); + ClearQueue(highPriorityQueue_); + ClearQueue(lowPriorityQueue_); + } + + + void OracleScheduler::Schedule(boost::shared_ptr<IObserver> receiver, + int priority, + IOracleCommand* command /* Takes ownership */) + { + std::unique_ptr<ScheduledCommand> pending(new ScheduledCommand(receiver, dynamic_cast<IOracleCommand*>(command))); + + /** + * Safeguard to remember that a new "Handle()" method and a call + * to "scheduler->Register()" must be implemented for each + * possible oracle command. + **/ + assert(command->GetType() == IOracleCommand::Type_GetOrthancImage || + command->GetType() == IOracleCommand::Type_GetOrthancWebViewerJpeg || + command->GetType() == IOracleCommand::Type_Http || + command->GetType() == IOracleCommand::Type_OrthancRestApi || + command->GetType() == IOracleCommand::Type_ParseDicomFromFile || + command->GetType() == IOracleCommand::Type_ParseDicomFromWado || + command->GetType() == IOracleCommand::Type_ReadFile); + + if (priority <= PRIORITY_HIGH) + { + highPriorityQueue_.insert(std::make_pair(priority, pending.release())); + } + else if (priority >= PRIORITY_LOW) + { + lowPriorityQueue_.insert(std::make_pair(priority, pending.release())); + } + else + { + standardPriorityQueue_.insert(std::make_pair(priority, pending.release())); + } + + totalScheduled_ ++; + + SpawnCommands(); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Loaders/OracleScheduler.h Tue Mar 31 15:51:02 2020 +0200 @@ -0,0 +1,167 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 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 + +#if !defined(ORTHANC_ENABLE_DCMTK) +# error The macro ORTHANC_ENABLE_DCMTK must be defined +#endif + +#include "../Messages/IMessageEmitter.h" +#include "../Messages/ObserverBase.h" +#include "../Oracle/GetOrthancImageCommand.h" +#include "../Oracle/GetOrthancWebViewerJpegCommand.h" +#include "../Oracle/HttpCommand.h" +#include "../Oracle/IOracle.h" +#include "../Oracle/OracleCommandExceptionMessage.h" +#include "../Oracle/OrthancRestApiCommand.h" +#include "../Oracle/ReadFileCommand.h" + +#if ORTHANC_ENABLE_DCMTK == 1 +# include "../Oracle/ParseDicomSuccessMessage.h" +#endif + +namespace OrthancStone +{ + class OracleScheduler : public ObserverBase<OracleScheduler> + { + public: + static const int PRIORITY_HIGH = -1; + static const int PRIORITY_LOW = 100; + + private: + enum Priority + { + Priority_Low, + Priority_Standard, + Priority_High + }; + + class ReceiverPayload; + class ScheduledCommand; + + typedef std::multimap<int, ScheduledCommand*> Queue; + + IOracle& oracle_; + IMessageEmitter& emitter_; + Queue standardPriorityQueue_; + Queue highPriorityQueue_; + Queue lowPriorityQueue_; + unsigned int maxHighPriorityCommands_; // Used if priority <= PRIORITY_HIGH + unsigned int maxStandardPriorityCommands_; + unsigned int maxLowPriorityCommands_; // Used if priority >= PRIORITY_LOW + unsigned int activeHighPriorityCommands_; + unsigned int activeStandardPriorityCommands_; + unsigned int activeLowPriorityCommands_; + uint64_t totalScheduled_; + uint64_t totalProcessed_; + + void ClearQueue(Queue& queue); + + void RemoveReceiverFromQueue(Queue& queue, + boost::shared_ptr<IObserver> receiver); + + void CheckInvariants() const; + + void SpawnFromQueue(Queue& queue, + Priority priority); + + void SpawnCommands(); + + void RemoveActiveCommand(const ReceiverPayload& payload); + + void Handle(const GetOrthancImageCommand::SuccessMessage& message); + + void Handle(const GetOrthancWebViewerJpegCommand::SuccessMessage& message); + + void Handle(const HttpCommand::SuccessMessage& message); + + void Handle(const OrthancRestApiCommand::SuccessMessage& message); + +#if ORTHANC_ENABLE_DCMTK == 1 + void Handle(const ParseDicomSuccessMessage& message); +#endif + + void Handle(const ReadFileCommand::SuccessMessage& message); + + void Handle(const OracleCommandExceptionMessage& message); + + OracleScheduler(IOracle& oracle, + IMessageEmitter& emitter, + unsigned int maxHighPriority, + unsigned int maxStandardPriority, + unsigned int maxLowPriority); + + public: + static boost::shared_ptr<OracleScheduler> Create(IOracle& oracle, + IObservable& oracleObservable, + IMessageEmitter& emitter) + { + return Create(oracle, oracleObservable, emitter, 1, 4, 1); + } + + static boost::shared_ptr<OracleScheduler> Create(IOracle& oracle, + IObservable& oracleObservable, + IMessageEmitter& emitter, + unsigned int maxHighPriority, + unsigned int maxStandardPriority, + unsigned int maxLowPriority); + + ~OracleScheduler(); + + unsigned int GetMaxHighPriorityCommands() const + { + return maxHighPriorityCommands_; + } + + unsigned int GetMaxStandardPriorityCommands() const + { + return maxStandardPriorityCommands_; + } + + unsigned int GetMaxLowPriorityCommands() const + { + return maxLowPriorityCommands_; + } + + uint64_t GetTotalScheduled() const + { + return totalScheduled_; + } + + uint64_t GetTotalProcessed() const + { + return totalProcessed_; + } + + // Cancel the HTTP requests that are still pending in the queues, + // and that are associated with the given receiver. Note that the + // receiver might still receive answers to HTTP requests that were + // already submitted to the oracle. + void CancelRequests(boost::shared_ptr<IObserver> receiver); + + void CancelAllRequests(); + + void Schedule(boost::shared_ptr<IObserver> receiver, + int priority, + IOracleCommand* command /* Takes ownership */); + }; +}
--- a/Framework/Loaders/OrthancMultiframeVolumeLoader.cpp Tue Mar 31 15:47:29 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,580 +0,0 @@ -/** - * Stone of Orthanc - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2020 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 "OrthancMultiframeVolumeLoader.h" - -#include <Core/Endianness.h> -#include <Core/Toolbox.h> - -namespace OrthancStone -{ - class OrthancMultiframeVolumeLoader::LoadRTDoseGeometry : public LoaderStateMachine::State - { - private: - std::unique_ptr<Orthanc::DicomMap> dicom_; - - public: - LoadRTDoseGeometry(OrthancMultiframeVolumeLoader& that, - Orthanc::DicomMap* dicom) : - State(that), - dicom_(dicom) - { - if (dicom == NULL) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); - } - - } - - virtual void Handle(const OrthancRestApiCommand::SuccessMessage& message) - { - // Complete the DICOM tags with just-received "Grid Frame Offset Vector" - std::string s = Orthanc::Toolbox::StripSpaces(message.GetAnswer()); - dicom_->SetValue(Orthanc::DICOM_TAG_GRID_FRAME_OFFSET_VECTOR, s, false); - - GetLoader<OrthancMultiframeVolumeLoader>().SetGeometry(*dicom_); - } - }; - - - static std::string GetSopClassUid(const Orthanc::DicomMap& dicom) - { - std::string s; - if (!dicom.LookupStringValue(s, Orthanc::DICOM_TAG_SOP_CLASS_UID, false)) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, - "DICOM file without SOP class UID"); - } - else - { - return s; - } - } - - - class OrthancMultiframeVolumeLoader::LoadGeometry : public State - { - public: - LoadGeometry(OrthancMultiframeVolumeLoader& that) : - State(that) - { - } - - virtual void Handle(const OrthancRestApiCommand::SuccessMessage& message) - { - OrthancMultiframeVolumeLoader& loader = GetLoader<OrthancMultiframeVolumeLoader>(); - - Json::Value body; - message.ParseJsonBody(body); - - if (body.type() != Json::objectValue) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol); - } - - std::unique_ptr<Orthanc::DicomMap> dicom(new Orthanc::DicomMap); - dicom->FromDicomAsJson(body); - - if (StringToSopClassUid(GetSopClassUid(*dicom)) == SopClassUid_RTDose) - { - // Download the "Grid Frame Offset Vector" DICOM tag, that is - // mandatory for RT-DOSE, but is too long to be returned by default - - std::unique_ptr<OrthancRestApiCommand> command(new OrthancRestApiCommand); - command->SetUri("/instances/" + loader.GetInstanceId() + "/content/" + - Orthanc::DICOM_TAG_GRID_FRAME_OFFSET_VECTOR.Format()); - command->SetPayload(new LoadRTDoseGeometry(loader, dicom.release())); - - Schedule(command.release()); - } - else - { - loader.SetGeometry(*dicom); - } - } - }; - - class OrthancMultiframeVolumeLoader::LoadTransferSyntax : public State - { - public: - LoadTransferSyntax(OrthancMultiframeVolumeLoader& that) : - State(that) - { - } - - virtual void Handle(const OrthancRestApiCommand::SuccessMessage& message) - { - GetLoader<OrthancMultiframeVolumeLoader>().SetTransferSyntax(message.GetAnswer()); - } - }; - - class OrthancMultiframeVolumeLoader::LoadUncompressedPixelData : public State - { - public: - LoadUncompressedPixelData(OrthancMultiframeVolumeLoader& that) : - State(that) - { - } - - virtual void Handle(const OrthancRestApiCommand::SuccessMessage& message) - { - GetLoader<OrthancMultiframeVolumeLoader>().SetUncompressedPixelData(message.GetAnswer()); - } - }; - - const std::string& OrthancMultiframeVolumeLoader::GetInstanceId() const - { - if (IsActive()) - { - return instanceId_; - } - else - { - LOG(ERROR) << "OrthancMultiframeVolumeLoader::GetInstanceId(): (!IsActive())"; - throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); - } - } - - void OrthancMultiframeVolumeLoader::ScheduleFrameDownloads() - { - if (transferSyntaxUid_.empty() || - !volume_->HasGeometry()) - { - return; - } - /* - 1.2.840.10008.1.2 Implicit VR Endian: Default Transfer Syntax for DICOM - 1.2.840.10008.1.2.1 Explicit VR Little Endian - 1.2.840.10008.1.2.2 Explicit VR Big Endian - - See https://www.dicomlibrary.com/dicom/transfer-syntax/ - */ - if (transferSyntaxUid_ == "1.2.840.10008.1.2" || - transferSyntaxUid_ == "1.2.840.10008.1.2.1" || - transferSyntaxUid_ == "1.2.840.10008.1.2.2") - { - std::unique_ptr<OrthancRestApiCommand> command(new OrthancRestApiCommand); - command->SetHttpHeader("Accept-Encoding", "gzip"); - command->SetUri("/instances/" + instanceId_ + "/content/" + - Orthanc::DICOM_TAG_PIXEL_DATA.Format() + "/0"); - command->SetPayload(new LoadUncompressedPixelData(*this)); - Schedule(command.release()); - } - else - { - throw Orthanc::OrthancException( - Orthanc::ErrorCode_NotImplemented, - "No support for multiframe instances with transfer syntax: " + transferSyntaxUid_); - } - } - - void OrthancMultiframeVolumeLoader::SetTransferSyntax(const std::string& transferSyntax) - { - transferSyntaxUid_ = Orthanc::Toolbox::StripSpaces(transferSyntax); - ScheduleFrameDownloads(); - } - - void OrthancMultiframeVolumeLoader::SetGeometry(const Orthanc::DicomMap& dicom) - { - DicomInstanceParameters parameters(dicom); - volume_->SetDicomParameters(parameters); - - Orthanc::PixelFormat format; - if (!parameters.GetImageInformation().ExtractPixelFormat(format, true)) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); - } - - double spacingZ; - switch (parameters.GetSopClassUid()) - { - case SopClassUid_RTDose: - spacingZ = parameters.GetThickness(); - break; - - default: - throw Orthanc::OrthancException( - Orthanc::ErrorCode_NotImplemented, - "No support for multiframe instances with SOP class UID: " + GetSopClassUid(dicom)); - } - - const unsigned int width = parameters.GetImageInformation().GetWidth(); - const unsigned int height = parameters.GetImageInformation().GetHeight(); - const unsigned int depth = parameters.GetImageInformation().GetNumberOfFrames(); - - { - VolumeImageGeometry geometry; - geometry.SetSizeInVoxels(width, height, depth); - geometry.SetAxialGeometry(parameters.GetGeometry()); - geometry.SetVoxelDimensions(parameters.GetPixelSpacingX(), - parameters.GetPixelSpacingY(), spacingZ); - volume_->Initialize(geometry, format, true /* Do compute range */); - } - - volume_->GetPixelData().Clear(); - - ScheduleFrameDownloads(); - - - - BroadcastMessage(DicomVolumeImage::GeometryReadyMessage(*volume_)); - } - - - ORTHANC_FORCE_INLINE - static void CopyPixel(uint32_t& target, const void* source) - { - // TODO - check alignement? - target = le32toh(*reinterpret_cast<const uint32_t*>(source)); - } - - ORTHANC_FORCE_INLINE - static void CopyPixel(uint16_t& target, const void* source) - { - // TODO - check alignement? - target = le16toh(*reinterpret_cast<const uint16_t*>(source)); - } - - ORTHANC_FORCE_INLINE - static void CopyPixel(int16_t& target, const void* source) - { - // byte swapping is the same for unsigned and signed integers - // (the sign bit is always stored with the MSByte) - uint16_t* targetUp = reinterpret_cast<uint16_t*>(&target); - CopyPixel(*targetUp, source); - } - - template <typename T> - void OrthancMultiframeVolumeLoader::CopyPixelDataAndComputeDistribution( - const std::string& pixelData, std::map<T,uint64_t>& distribution) - { - ImageBuffer3D& target = volume_->GetPixelData(); - - const unsigned int bpp = target.GetBytesPerPixel(); - const unsigned int width = target.GetWidth(); - const unsigned int height = target.GetHeight(); - const unsigned int depth = target.GetDepth(); - - if (pixelData.size() != bpp * width * height * depth) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, - "The pixel data has not the proper size"); - } - - if (pixelData.empty()) - { - return; - } - - // first pass to initialize map - { - const uint8_t* source = reinterpret_cast<const uint8_t*>(pixelData.c_str()); - - for (unsigned int z = 0; z < depth; z++) - { - for (unsigned int y = 0; y < height; y++) - { - for (unsigned int x = 0; x < width; x++) - { - T value; - CopyPixel(value, source); - distribution[value] = 0; - source += bpp; - } - } - } - } - - { - const uint8_t* source = reinterpret_cast<const uint8_t*>(pixelData.c_str()); - - for (unsigned int z = 0; z < depth; z++) - { - ImageBuffer3D::SliceWriter writer(target, VolumeProjection_Axial, z); - - assert(writer.GetAccessor().GetWidth() == width && - writer.GetAccessor().GetHeight() == height); - - for (unsigned int y = 0; y < height; y++) - { - assert(sizeof(T) == Orthanc::GetBytesPerPixel(target.GetFormat())); - - T* target = reinterpret_cast<T*>(writer.GetAccessor().GetRow(y)); - - for (unsigned int x = 0; x < width; x++) - { - CopyPixel(*target, source); - - distribution[*target] += 1; - - target++; - source += bpp; - } - } - } - } - } - - template <typename T> - void OrthancMultiframeVolumeLoader::ComputeMinMaxWithOutlierRejection( - const std::map<T, uint64_t>& distribution) - { - if (distribution.size() == 0) - { - LOG(ERROR) << "ComputeMinMaxWithOutlierRejection -- Volume image empty."; - } - else - { - ImageBuffer3D& target = volume_->GetPixelData(); - - const uint64_t width = target.GetWidth(); - const uint64_t height = target.GetHeight(); - const uint64_t depth = target.GetDepth(); - const uint64_t voxelCount = width * height * depth; - - // now that we have distribution[pixelValue] == numberOfPixelsWithValue - // compute number of values and check (assertion) that it is equal to - // width * height * depth - { - typename std::map<T, uint64_t>::const_iterator it = distribution.begin(); - uint64_t totalCount = 0; - distributionRawMin_ = static_cast<float>(it->first); - - while (it != distribution.end()) - { - T pixelValue = it->first; - uint64_t count = it->second; - totalCount += count; - it++; - if (it == distribution.end()) - distributionRawMax_ = static_cast<float>(pixelValue); - } - LOG(INFO) << "Volume image. First distribution value = " - << static_cast<float>(distributionRawMin_) - << " | Last distribution value = " - << static_cast<float>(distributionRawMax_); - - if (totalCount != voxelCount) - { - LOG(ERROR) << "Internal error in dose distribution computation. TC (" - << totalCount << ") != VoxC (" << voxelCount; - throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); - } - } - - // compute the number of voxels to reject at each end of the distribution - uint64_t endRejectionCount = static_cast<uint64_t>( - outliersHalfRejectionRate_ * voxelCount); - - if (endRejectionCount > voxelCount) - { - LOG(ERROR) << "Internal error in dose distribution computation." - << " endRejectionCount = " << endRejectionCount - << " | voxelCount = " << voxelCount; - throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); - } - - // this will contain the actual distribution minimum after outlier - // rejection - T resultMin = 0; - - // then start from start and remove pixel values up to - // endRejectionCount voxels rejected - { - typename std::map<T, uint64_t>::const_iterator it = distribution.begin(); - - uint64_t currentCount = 0; - - while (it != distribution.end()) - { - T pixelValue = it->first; - uint64_t count = it->second; - - // if this pixelValue crosses the rejection threshold, let's set it - // and exit the loop - if ((currentCount <= endRejectionCount) && - (currentCount + count > endRejectionCount)) - { - resultMin = pixelValue; - break; - } - else - { - currentCount += count; - } - // and continue walking along the distribution - it++; - } - } - - // this will contain the actual distribution maximum after outlier - // rejection - T resultMax = 0; - // now start from END and remove pixel values up to - // endRejectionCount voxels rejected - { - typename std::map<T, uint64_t>::const_reverse_iterator it = distribution.rbegin(); - - uint64_t currentCount = 0; - - while (it != distribution.rend()) - { - T pixelValue = it->first; - uint64_t count = it->second; - - if ((currentCount <= endRejectionCount) && - (currentCount + count > endRejectionCount)) - { - resultMax = pixelValue; - break; - } - else - { - currentCount += count; - } - // and continue walking along the distribution - it++; - } - } - if (resultMin > resultMax) - { - LOG(ERROR) << "Internal error in dose distribution computation! " << - "resultMin (" << resultMin << ") > resultMax (" << resultMax << ")"; - throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); - } - computedDistributionMin_ = static_cast<float>(resultMin); - computedDistributionMax_ = static_cast<float>(resultMax); - } - } - - template <typename T> - void OrthancMultiframeVolumeLoader::CopyPixelDataAndComputeMinMax( - const std::string& pixelData) - { - std::map<T, uint64_t> distribution; - CopyPixelDataAndComputeDistribution(pixelData, distribution); - ComputeMinMaxWithOutlierRejection(distribution); - } - - void OrthancMultiframeVolumeLoader::SetUncompressedPixelData(const std::string& pixelData) - { - switch (volume_->GetPixelData().GetFormat()) - { - case Orthanc::PixelFormat_Grayscale32: - CopyPixelDataAndComputeMinMax<uint32_t>(pixelData); - break; - case Orthanc::PixelFormat_Grayscale16: - CopyPixelDataAndComputeMinMax<uint16_t>(pixelData); - break; - case Orthanc::PixelFormat_SignedGrayscale16: - CopyPixelDataAndComputeMinMax<int16_t>(pixelData); - break; - default: - throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); - } - - volume_->IncrementRevision(); - - pixelDataLoaded_ = true; - BroadcastMessage(DicomVolumeImage::ContentUpdatedMessage(*volume_)); - } - - bool OrthancMultiframeVolumeLoader::HasGeometry() const - { - return volume_->HasGeometry(); - } - - const OrthancStone::VolumeImageGeometry& OrthancMultiframeVolumeLoader::GetImageGeometry() const - { - return volume_->GetGeometry(); - } - - OrthancMultiframeVolumeLoader::OrthancMultiframeVolumeLoader( - boost::shared_ptr<DicomVolumeImage> volume, - IOracle& oracle, - IObservable& oracleObservable, - float outliersHalfRejectionRate) : - LoaderStateMachine(oracle, oracleObservable), - IObservable(oracleObservable.GetBroker()), - volume_(volume), - pixelDataLoaded_(false), - outliersHalfRejectionRate_(outliersHalfRejectionRate), - distributionRawMin_(0), - distributionRawMax_(0), - computedDistributionMin_(0), - computedDistributionMax_(0) - { - if (volume.get() == NULL) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); - } - } - - OrthancMultiframeVolumeLoader::~OrthancMultiframeVolumeLoader() - { - LOG(TRACE) << "OrthancMultiframeVolumeLoader::~OrthancMultiframeVolumeLoader()"; - } - - - void OrthancMultiframeVolumeLoader::GetDistributionMinMax - (float& minValue, float& maxValue) const - { - if (distributionRawMin_ == 0 && distributionRawMax_ == 0) - { - LOG(WARNING) << "GetDistributionMinMaxWithOutliersRejection called before computation!"; - } - minValue = distributionRawMin_; - maxValue = distributionRawMax_; - } - - void OrthancMultiframeVolumeLoader::GetDistributionMinMaxWithOutliersRejection - (float& minValue, float& maxValue) const - { - if (computedDistributionMin_ == 0 && computedDistributionMax_ == 0) - { - LOG(WARNING) << "GetDistributionMinMaxWithOutliersRejection called before computation!"; - } - minValue = computedDistributionMin_; - maxValue = computedDistributionMax_; - } - - void OrthancMultiframeVolumeLoader::LoadInstance(const std::string& instanceId) - { - Start(); - - instanceId_ = instanceId; - - { - std::unique_ptr<OrthancRestApiCommand> command(new OrthancRestApiCommand); - command->SetHttpHeader("Accept-Encoding", "gzip"); - command->SetUri("/instances/" + instanceId + "/tags"); - command->SetPayload(new LoadGeometry(*this)); - Schedule(command.release()); - } - - { - std::unique_ptr<OrthancRestApiCommand> command(new OrthancRestApiCommand); - command->SetUri("/instances/" + instanceId + "/metadata/TransferSyntax"); - command->SetPayload(new LoadTransferSyntax(*this)); - Schedule(command.release()); - } - } -}
--- a/Framework/Loaders/OrthancMultiframeVolumeLoader.h Tue Mar 31 15:47:29 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,116 +0,0 @@ -/** - * Stone of Orthanc - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2020 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 "LoaderStateMachine.h" -#include "../Volumes/DicomVolumeImage.h" - -#include <boost/shared_ptr.hpp> - -namespace OrthancStone -{ - class OrthancMultiframeVolumeLoader : - public LoaderStateMachine, - public IObservable, - public IGeometryProvider - { - private: - class LoadRTDoseGeometry; - class LoadGeometry; - class LoadTransferSyntax; - class LoadUncompressedPixelData; - - boost::shared_ptr<DicomVolumeImage> volume_; - std::string instanceId_; - std::string transferSyntaxUid_; - bool pixelDataLoaded_; - float outliersHalfRejectionRate_; - float distributionRawMin_; - float distributionRawMax_; - float computedDistributionMin_; - float computedDistributionMax_; - - const std::string& GetInstanceId() const; - - void ScheduleFrameDownloads(); - - void SetTransferSyntax(const std::string& transferSyntax); - - void SetGeometry(const Orthanc::DicomMap& dicom); - - - /** - This method will : - - - copy the pixel values from the response to the volume image - - compute the maximum and minimum value while discarding the - outliersHalfRejectionRate_ fraction of the outliers from both the start - and the end of the distribution. - - In English, this means that, if the volume dataset contains a few extreme - values very different from the rest (outliers) that we want to get rid of, - this method allows to do so. - - If you supply 0.005, for instance, it means 1% of the extreme values will - be rejected (0.5% on each side of the distribution) - */ - template <typename T> - void CopyPixelDataAndComputeMinMax(const std::string& pixelData); - - /** Service method for CopyPixelDataAndComputeMinMax*/ - template <typename T> - void CopyPixelDataAndComputeDistribution( - const std::string& pixelData, - std::map<T, uint64_t>& distribution); - - /** Service method for CopyPixelDataAndComputeMinMax*/ - template <typename T> - void ComputeMinMaxWithOutlierRejection( - const std::map<T, uint64_t>& distribution); - - void SetUncompressedPixelData(const std::string& pixelData); - - virtual bool HasGeometry() const ORTHANC_OVERRIDE; - virtual const VolumeImageGeometry& GetImageGeometry() const ORTHANC_OVERRIDE; - - public: - OrthancMultiframeVolumeLoader(boost::shared_ptr<DicomVolumeImage> volume, - IOracle& oracle, - IObservable& oracleObservable, - float outliersHalfRejectionRate = 0.0005); - - virtual ~OrthancMultiframeVolumeLoader(); - - bool IsPixelDataLoaded() const - { - return pixelDataLoaded_; - } - - void GetDistributionMinMax - (float& minValue, float& maxValue) const; - - void GetDistributionMinMaxWithOutliersRejection - (float& minValue, float& maxValue) const; - - void LoadInstance(const std::string& instanceId); - }; -}
--- a/Framework/Loaders/OrthancSeriesVolumeProgressiveLoader.cpp Tue Mar 31 15:47:29 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,506 +0,0 @@ -/** - * Stone of Orthanc - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2020 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 "OrthancSeriesVolumeProgressiveLoader.h" - -#include "../Toolbox/GeometryToolbox.h" -#include "../Volumes/DicomVolumeImageMPRSlicer.h" -#include "BasicFetchingItemsSorter.h" -#include "BasicFetchingStrategy.h" - -#include <Core/Images/ImageProcessing.h> -#include <Core/OrthancException.h> - -namespace OrthancStone -{ - class OrthancSeriesVolumeProgressiveLoader::ExtractedSlice : public DicomVolumeImageMPRSlicer::Slice - { - private: - const OrthancSeriesVolumeProgressiveLoader& that_; - - public: - ExtractedSlice(const OrthancSeriesVolumeProgressiveLoader& that, - const CoordinateSystem3D& plane) : - DicomVolumeImageMPRSlicer::Slice(*that.volume_, plane), - that_(that) - { - if (IsValid()) - { - 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 && - GetProjection() == VolumeProjection_Axial) - { - that_.strategy_->SetCurrent(GetSliceIndex()); - } - } - } - }; - - - - void OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::CheckSlice(size_t index, - const DicomInstanceParameters& reference) const - { - const DicomInstanceParameters& slice = *slices_[index]; - - if (!GeometryToolbox::IsParallel( - reference.GetGeometry().GetNormal(), - slice.GetGeometry().GetNormal())) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_BadGeometry, - "A slice in the volume image is not parallel to the others"); - } - - if (reference.GetExpectedPixelFormat() != slice.GetExpectedPixelFormat()) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageFormat, - "The pixel format changes across the slices of the volume image"); - } - - if (reference.GetImageInformation().GetWidth() != slice.GetImageInformation().GetWidth() || - reference.GetImageInformation().GetHeight() != slice.GetImageInformation().GetHeight()) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageSize, - "The width/height of slices are not constant in the volume image"); - } - - if (!LinearAlgebra::IsNear(reference.GetPixelSpacingX(), slice.GetPixelSpacingX()) || - !LinearAlgebra::IsNear(reference.GetPixelSpacingY(), slice.GetPixelSpacingY())) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_BadGeometry, - "The pixel spacing of the slices change across the volume image"); - } - } - - - void OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::CheckVolume() const - { - for (size_t i = 0; i < slices_.size(); i++) - { - assert(slices_[i] != NULL); - if (slices_[i]->GetImageInformation().GetNumberOfFrames() != 1) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_BadGeometry, - "This class does not support multi-frame images"); - } - } - - if (slices_.size() != 0) - { - const DicomInstanceParameters& reference = *slices_[0]; - - for (size_t i = 1; i < slices_.size(); i++) - { - CheckSlice(i, reference); - } - } - } - - - void OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::Clear() - { - for (size_t i = 0; i < slices_.size(); i++) - { - assert(slices_[i] != NULL); - delete slices_[i]; - } - - slices_.clear(); - slicesRevision_.clear(); - } - - - void OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::CheckSliceIndex(size_t index) const - { - if (!HasGeometry()) - { - LOG(ERROR) << "OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::CheckSliceIndex(size_t index): (!HasGeometry())"; - throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); - } - else if (index >= slices_.size()) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); - } - else - { - assert(slices_.size() == GetImageGeometry().GetDepth() && - slices_.size() == slicesRevision_.size()); - } - } - - - // WARNING: The payload of "slices" must be of class "DicomInstanceParameters" - // (called with the slices created in LoadGeometry) - void OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::ComputeGeometry(SlicesSorter& slices) - { - Clear(); - - if (!slices.Sort()) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange, - "Cannot sort the 3D slices of a DICOM series"); - } - - if (slices.GetSlicesCount() == 0) - { - geometry_.reset(new VolumeImageGeometry); - } - else - { - slices_.reserve(slices.GetSlicesCount()); - slicesRevision_.resize(slices.GetSlicesCount(), 0); - - for (size_t i = 0; i < slices.GetSlicesCount(); i++) - { - const DicomInstanceParameters& slice = - dynamic_cast<const DicomInstanceParameters&>(slices.GetSlicePayload(i)); - slices_.push_back(new DicomInstanceParameters(slice)); - } - - CheckVolume(); - - const double spacingZ = slices.ComputeSpacingBetweenSlices(); - LOG(INFO) << "Computed spacing between slices: " << spacingZ << "mm"; - - const DicomInstanceParameters& parameters = *slices_[0]; - - geometry_.reset(new VolumeImageGeometry); - geometry_->SetSizeInVoxels(parameters.GetImageInformation().GetWidth(), - parameters.GetImageInformation().GetHeight(), - static_cast<unsigned int>(slices.GetSlicesCount())); - geometry_->SetAxialGeometry(slices.GetSliceGeometry(0)); - geometry_->SetVoxelDimensions(parameters.GetPixelSpacingX(), - parameters.GetPixelSpacingY(), spacingZ); - } - } - - - const VolumeImageGeometry& OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::GetImageGeometry() const - { - if (!HasGeometry()) - { - LOG(ERROR) << "OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::GetImageGeometry(): (!HasGeometry())"; - throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); - } - else - { - assert(slices_.size() == geometry_->GetDepth()); - return *geometry_; - } - } - - - const DicomInstanceParameters& OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::GetSliceParameters(size_t index) const - { - CheckSliceIndex(index); - return *slices_[index]; - } - - - uint64_t OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::GetSliceRevision(size_t index) const - { - CheckSliceIndex(index); - return slicesRevision_[index]; - } - - - void OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::IncrementSliceRevision(size_t index) - { - CheckSliceIndex(index); - slicesRevision_[index] ++; - } - - - static unsigned int GetSliceIndexPayload(const OracleCommandWithPayload& command) - { - return dynamic_cast< const Orthanc::SingleValueObject<unsigned int>& >(command.GetPayload()).GetValue(); - } - - - void OrthancSeriesVolumeProgressiveLoader::ScheduleNextSliceDownload() - { - assert(strategy_.get() != NULL); - - unsigned int sliceIndex, quality; - - if (strategy_->GetNext(sliceIndex, quality)) - { - assert(quality <= BEST_QUALITY); - - const DicomInstanceParameters& slice = seriesGeometry_.GetSliceParameters(sliceIndex); - - const std::string& instance = slice.GetOrthancInstanceIdentifier(); - if (instance.empty()) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); - } - - std::unique_ptr<OracleCommandWithPayload> command; - - if (quality == BEST_QUALITY) - { - std::unique_ptr<GetOrthancImageCommand> tmp(new GetOrthancImageCommand); - // TODO: review the following comment. - // - Commented out by bgo on 2019-07-19 | reason: Alain has seen cases - // where gzipping the uint16 image took 11 sec to produce 5mb. - // The unzipped request was much much faster. - // - Re-enabled on 2019-07-30. Reason: in Web Assembly, the browser - // does not use the Accept-Encoding header and always requests - // compression. Furthermore, NOT - tmp->SetHttpHeader("Accept-Encoding", "gzip"); - tmp->SetHttpHeader("Accept", std::string(Orthanc::EnumerationToString(Orthanc::MimeType_Pam))); - tmp->SetInstanceUri(instance, slice.GetExpectedPixelFormat()); - tmp->SetExpectedPixelFormat(slice.GetExpectedPixelFormat()); - command.reset(tmp.release()); - } - else - { - std::unique_ptr<GetOrthancWebViewerJpegCommand> tmp(new GetOrthancWebViewerJpegCommand); - // TODO: review the following comment. Commented out by bgo on 2019-07-19 - // (gzip for jpeg seems overkill) - //tmp->SetHttpHeader("Accept-Encoding", "gzip"); - tmp->SetInstance(instance); - tmp->SetQuality((quality == 0 ? 50 : 90)); - tmp->SetExpectedPixelFormat(slice.GetExpectedPixelFormat()); - command.reset(tmp.release()); - } - - command->SetPayload(new Orthanc::SingleValueObject<unsigned int>(sliceIndex)); - oracle_.Schedule(*this, command.release()); - } - else - { - // loading is finished! - volumeImageReadyInHighQuality_ = true; - BroadcastMessage(OrthancSeriesVolumeProgressiveLoader::VolumeImageReadyInHighQuality(*this)); - } - } - -/** - This is called in response to GET "/series/XXXXXXXXXXXXX/instances-tags" -*/ - void OrthancSeriesVolumeProgressiveLoader::LoadGeometry(const OrthancRestApiCommand::SuccessMessage& message) - { - Json::Value body; - message.ParseJsonBody(body); - - if (body.type() != Json::objectValue) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol); - } - - { - Json::Value::Members instances = body.getMemberNames(); - - SlicesSorter slices; - - for (size_t i = 0; i < instances.size(); i++) - { - Orthanc::DicomMap dicom; - dicom.FromDicomAsJson(body[instances[i]]); - - std::unique_ptr<DicomInstanceParameters> instance(new DicomInstanceParameters(dicom)); - instance->SetOrthancInstanceIdentifier(instances[i]); - - // the 3D plane corresponding to the slice - CoordinateSystem3D geometry = instance->GetGeometry(); - slices.AddSlice(geometry, instance.release()); - } - - seriesGeometry_.ComputeGeometry(slices); - } - - size_t slicesCount = seriesGeometry_.GetImageGeometry().GetDepth(); - - if (slicesCount == 0) - { - volume_->Initialize(seriesGeometry_.GetImageGeometry(), Orthanc::PixelFormat_Grayscale8); - } - else - { - const DicomInstanceParameters& parameters = seriesGeometry_.GetSliceParameters(0); - - volume_->Initialize(seriesGeometry_.GetImageGeometry(), parameters.GetExpectedPixelFormat()); - volume_->SetDicomParameters(parameters); - volume_->GetPixelData().Clear(); - - strategy_.reset(new BasicFetchingStrategy(sorter_->CreateSorter(static_cast<unsigned int>(slicesCount)), BEST_QUALITY)); - - assert(simultaneousDownloads_ != 0); - for (unsigned int i = 0; i < simultaneousDownloads_; i++) - { - ScheduleNextSliceDownload(); - } - } - - slicesQuality_.resize(slicesCount, 0); - - BroadcastMessage(DicomVolumeImage::GeometryReadyMessage(*volume_)); - } - - - void OrthancSeriesVolumeProgressiveLoader::SetSliceContent(unsigned int sliceIndex, - const Orthanc::ImageAccessor& image, - unsigned int quality) - { - assert(sliceIndex < slicesQuality_.size() && - slicesQuality_.size() == volume_->GetPixelData().GetDepth()); - - if (quality >= slicesQuality_[sliceIndex]) - { - { - ImageBuffer3D::SliceWriter writer(volume_->GetPixelData(), VolumeProjection_Axial, sliceIndex); - Orthanc::ImageProcessing::Copy(writer.GetAccessor(), image); - } - - volume_->IncrementRevision(); - seriesGeometry_.IncrementSliceRevision(sliceIndex); - slicesQuality_[sliceIndex] = quality; - - BroadcastMessage(DicomVolumeImage::ContentUpdatedMessage(*volume_)); - } - - ScheduleNextSliceDownload(); - } - - - void OrthancSeriesVolumeProgressiveLoader::LoadBestQualitySliceContent(const GetOrthancImageCommand::SuccessMessage& message) - { - SetSliceContent(GetSliceIndexPayload(message.GetOrigin()), message.GetImage(), BEST_QUALITY); - } - - - void OrthancSeriesVolumeProgressiveLoader::LoadJpegSliceContent(const GetOrthancWebViewerJpegCommand::SuccessMessage& message) - { - unsigned int quality; - - switch (message.GetOrigin().GetQuality()) - { - case 50: - quality = LOW_QUALITY; - break; - - case 90: - quality = MIDDLE_QUALITY; - break; - - default: - throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); - } - - SetSliceContent(GetSliceIndexPayload(message.GetOrigin()), message.GetImage(), quality); - } - - - OrthancSeriesVolumeProgressiveLoader::OrthancSeriesVolumeProgressiveLoader(const boost::shared_ptr<DicomVolumeImage>& volume, - IOracle& oracle, - IObservable& oracleObservable) : - IObserver(oracleObservable.GetBroker()), - IObservable(oracleObservable.GetBroker()), - oracle_(oracle), - oracleObservable_(oracleObservable), - active_(false), - simultaneousDownloads_(4), - volume_(volume), - sorter_(new BasicFetchingItemsSorter::Factory), - volumeImageReadyInHighQuality_(false) - { - oracleObservable.RegisterObserverCallback( - new Callable<OrthancSeriesVolumeProgressiveLoader, OrthancRestApiCommand::SuccessMessage> - (*this, &OrthancSeriesVolumeProgressiveLoader::LoadGeometry)); - - oracleObservable.RegisterObserverCallback( - new Callable<OrthancSeriesVolumeProgressiveLoader, GetOrthancImageCommand::SuccessMessage> - (*this, &OrthancSeriesVolumeProgressiveLoader::LoadBestQualitySliceContent)); - - oracleObservable.RegisterObserverCallback( - new Callable<OrthancSeriesVolumeProgressiveLoader, GetOrthancWebViewerJpegCommand::SuccessMessage> - (*this, &OrthancSeriesVolumeProgressiveLoader::LoadJpegSliceContent)); - } - - OrthancSeriesVolumeProgressiveLoader::~OrthancSeriesVolumeProgressiveLoader() - { - oracleObservable_.Unregister(this); - LOG(TRACE) << "OrthancSeriesVolumeProgressiveLoader::~OrthancSeriesVolumeProgressiveLoader()"; - } - - void OrthancSeriesVolumeProgressiveLoader::SetSimultaneousDownloads(unsigned int count) - { - if (active_) - { - LOG(ERROR) << "OrthancSeriesVolumeProgressiveLoader::SetSimultaneousDownloads(): (active_)"; - throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); - } - else if (count == 0) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); - } - else - { - simultaneousDownloads_ = count; - } - } - - - void OrthancSeriesVolumeProgressiveLoader::LoadSeries(const std::string& seriesId) - { -// LOG(TRACE) << "OrthancSeriesVolumeProgressiveLoader::LoadSeries seriesId=" << seriesId; - if (active_) - { -// LOG(TRACE) << "OrthancSeriesVolumeProgressiveLoader::LoadSeries NOT ACTIVE! --> ERROR"; - LOG(ERROR) << "OrthancSeriesVolumeProgressiveLoader::LoadSeries(const std::string& seriesId): (active_)"; - throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); - } - else - { - active_ = true; - - std::unique_ptr<OrthancRestApiCommand> command(new OrthancRestApiCommand); - command->SetUri("/series/" + seriesId + "/instances-tags"); - -// LOG(TRACE) << "OrthancSeriesVolumeProgressiveLoader::LoadSeries about to call oracle_.Schedule"; - oracle_.Schedule(*this, command.release()); -// LOG(TRACE) << "OrthancSeriesVolumeProgressiveLoader::LoadSeries called oracle_.Schedule"; - } - } - - - IVolumeSlicer::IExtractedSlice* - OrthancSeriesVolumeProgressiveLoader::ExtractSlice(const CoordinateSystem3D& cuttingPlane) - { - if (volume_->HasGeometry()) - { - return new ExtractedSlice(*this, cuttingPlane); - } - else - { - return new IVolumeSlicer::InvalidSlice; - } - } -}
--- a/Framework/Loaders/OrthancSeriesVolumeProgressiveLoader.h Tue Mar 31 15:47:29 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,165 +0,0 @@ -/** - * Stone of Orthanc - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2020 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 "../Messages/IObservable.h" -#include "../Messages/IObserver.h" -#include "../Oracle/GetOrthancImageCommand.h" -#include "../Oracle/GetOrthancWebViewerJpegCommand.h" -#include "../Oracle/IOracle.h" -#include "../Oracle/OrthancRestApiCommand.h" -#include "../Toolbox/SlicesSorter.h" -#include "../Volumes/DicomVolumeImage.h" -#include "../Volumes/IVolumeSlicer.h" -#include "IFetchingItemsSorter.h" -#include "IFetchingStrategy.h" - -#include <boost/shared_ptr.hpp> - -namespace OrthancStone -{ - /** - This class is used to manage the progressive loading of a volume that - is stored in a Dicom series. - */ - class OrthancSeriesVolumeProgressiveLoader : - public IObserver, - public IObservable, - public IVolumeSlicer, - public IGeometryProvider - { - private: - static const unsigned int LOW_QUALITY = 0; - static const unsigned int MIDDLE_QUALITY = 1; - static const unsigned int BEST_QUALITY = 2; - - class ExtractedSlice; - - /** Helper class internal to OrthancSeriesVolumeProgressiveLoader */ - class SeriesGeometry : public boost::noncopyable - { - private: - void CheckSlice(size_t index, - const DicomInstanceParameters& reference) const; - - void CheckVolume() const; - - void Clear(); - - void CheckSliceIndex(size_t index) const; - - std::unique_ptr<VolumeImageGeometry> geometry_; - std::vector<DicomInstanceParameters*> slices_; - std::vector<uint64_t> slicesRevision_; - - public: - ~SeriesGeometry() - { - Clear(); - } - - void ComputeGeometry(SlicesSorter& slices); - - virtual bool HasGeometry() const - { - return geometry_.get() != NULL; - } - - virtual const VolumeImageGeometry& GetImageGeometry() const; - - const DicomInstanceParameters& GetSliceParameters(size_t index) const; - - uint64_t GetSliceRevision(size_t index) const; - - void IncrementSliceRevision(size_t index); - }; - - void ScheduleNextSliceDownload(); - - void LoadGeometry(const OrthancRestApiCommand::SuccessMessage& message); - - void SetSliceContent(unsigned int sliceIndex, - const Orthanc::ImageAccessor& image, - unsigned int quality); - - void LoadBestQualitySliceContent(const GetOrthancImageCommand::SuccessMessage& message); - - void LoadJpegSliceContent(const GetOrthancWebViewerJpegCommand::SuccessMessage& message); - - IOracle& oracle_; - IObservable& oracleObservable_; - bool active_; - unsigned int simultaneousDownloads_; - SeriesGeometry seriesGeometry_; - boost::shared_ptr<DicomVolumeImage> volume_; - std::unique_ptr<IFetchingItemsSorter::IFactory> sorter_; - std::unique_ptr<IFetchingStrategy> strategy_; - std::vector<unsigned int> slicesQuality_; - bool volumeImageReadyInHighQuality_; - - - public: - ORTHANC_STONE_DEFINE_ORIGIN_MESSAGE(__FILE__, __LINE__, VolumeImageReadyInHighQuality, OrthancSeriesVolumeProgressiveLoader); - - - OrthancSeriesVolumeProgressiveLoader(const boost::shared_ptr<DicomVolumeImage>& volume, - IOracle& oracle, - IObservable& oracleObservable); - - virtual ~OrthancSeriesVolumeProgressiveLoader(); - - void SetSimultaneousDownloads(unsigned int count); - - bool IsVolumeImageReadyInHighQuality() const - { - return volumeImageReadyInHighQuality_; - } - - void LoadSeries(const std::string& seriesId); - - /** - This getter is used by clients that do not receive the geometry through - subscribing, for instance if they are created or listening only AFTER the - "geometry loaded" message is broadcast - */ - bool HasGeometry() const ORTHANC_OVERRIDE - { - return seriesGeometry_.HasGeometry(); - } - - /** - Same remark as HasGeometry - */ - const VolumeImageGeometry& GetImageGeometry() const ORTHANC_OVERRIDE - { - return seriesGeometry_.GetImageGeometry(); - } - - /** - When a slice is requested, the strategy algorithm (that defines the - sequence of resources to be loaded from the server) is modified to - take into account this request (this is done in the ExtractedSlice ctor) - */ - virtual IExtractedSlice* - ExtractSlice(const CoordinateSystem3D& cuttingPlane) ORTHANC_OVERRIDE; - }; -}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Loaders/SeriesFramesLoader.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -0,0 +1,550 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 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 "SeriesFramesLoader.h" + +#include "../Oracle/ParseDicomFromFileCommand.h" +#include "../Oracle/ParseDicomFromWadoCommand.h" + +#if ORTHANC_ENABLE_DCMTK == 1 +# include <Core/DicomParsing/Internals/DicomImageDecoder.h> +#endif + +#include <Core/DicomFormat/DicomInstanceHasher.h> +#include <Core/Images/Image.h> +#include <Core/Images/ImageProcessing.h> +#include <Core/Images/JpegReader.h> + +#include <boost/algorithm/string/predicate.hpp> + +namespace OrthancStone +{ + class SeriesFramesLoader::Payload : public Orthanc::IDynamicObject + { + private: + DicomSource source_; + size_t seriesIndex_; + std::string sopInstanceUid_; // Only used for debug purpose + unsigned int quality_; + bool hasWindowing_; + float windowingCenter_; + float windowingWidth_; + std::unique_ptr<Orthanc::IDynamicObject> userPayload_; + + public: + Payload(const DicomSource& source, + size_t seriesIndex, + const std::string& sopInstanceUid, + unsigned int quality, + Orthanc::IDynamicObject* userPayload) : + source_(source), + seriesIndex_(seriesIndex), + sopInstanceUid_(sopInstanceUid), + quality_(quality), + hasWindowing_(false), + userPayload_(userPayload) + { + } + + size_t GetSeriesIndex() const + { + return seriesIndex_; + } + + const std::string& GetSopInstanceUid() const + { + return sopInstanceUid_; + } + + unsigned int GetQuality() const + { + return quality_; + } + + void SetWindowing(float center, + float width) + { + hasWindowing_ = true; + windowingCenter_ = center; + windowingWidth_ = width; + } + + bool HasWindowing() const + { + return hasWindowing_; + } + + float GetWindowingCenter() const + { + if (hasWindowing_) + { + return windowingCenter_; + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + } + + float GetWindowingWidth() const + { + if (hasWindowing_) + { + return windowingWidth_; + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + } + + const DicomSource& GetSource() const + { + return source_; + } + + Orthanc::IDynamicObject* GetUserPayload() const + { + return userPayload_.get(); + } + }; + + + SeriesFramesLoader::SeriesFramesLoader(ILoadersContext& context, + LoadedDicomResources& instances, + const std::string& dicomDirPath, + boost::shared_ptr<LoadedDicomResources> dicomDir) : + context_(context), + frames_(instances), + dicomDirPath_(dicomDirPath), + dicomDir_(dicomDir) + { + } + + + void SeriesFramesLoader::EmitMessage(const Payload& payload, + const Orthanc::ImageAccessor& image) + { + const DicomInstanceParameters& parameters = frames_.GetInstanceParameters(payload.GetSeriesIndex()); + const Orthanc::DicomMap& instance = frames_.GetInstance(payload.GetSeriesIndex()); + size_t frameIndex = frames_.GetFrameIndex(payload.GetSeriesIndex()); + + if (frameIndex >= parameters.GetImageInformation().GetNumberOfFrames() || + payload.GetSopInstanceUid() != parameters.GetSopInstanceUid()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + + LOG(TRACE) << "Decoded instance " << payload.GetSopInstanceUid() << ", frame " + << frameIndex << ": " << image.GetWidth() << "x" + << image.GetHeight() << ", " << Orthanc::EnumerationToString(image.GetFormat()) + << ", quality " << payload.GetQuality(); + + FrameLoadedMessage message(*this, frameIndex, payload.GetQuality(), image, instance, parameters, payload.GetUserPayload()); + BroadcastMessage(message); + } + + +#if ORTHANC_ENABLE_DCMTK == 1 + void SeriesFramesLoader::HandleDicom(const Payload& payload, + Orthanc::ParsedDicomFile& dicom) + { + size_t frameIndex = frames_.GetFrameIndex(payload.GetSeriesIndex()); + + std::unique_ptr<Orthanc::ImageAccessor> decoded; + decoded.reset(Orthanc::DicomImageDecoder::Decode( + dicom, + static_cast<unsigned int>(frameIndex))); + + if (decoded.get() == NULL) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); + } + + EmitMessage(payload, *decoded); + } +#endif + + + void SeriesFramesLoader::HandleDicomWebRendered(const Payload& payload, + const std::string& body, + const std::map<std::string, std::string>& headers) + { + assert(payload.GetSource().IsDicomWeb() && + payload.HasWindowing()); + + bool ok = false; + for (std::map<std::string, std::string>::const_iterator it = headers.begin(); + it != headers.end(); ++it) + { + if (boost::iequals("content-type", it->first) && + boost::iequals(Orthanc::MIME_JPEG, it->second)) + { + ok = true; + break; + } + } + + if (!ok) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol, + "The WADO-RS server has not generated a JPEG image on /rendered"); + } + + Orthanc::JpegReader reader; + reader.ReadFromMemory(body); + + switch (reader.GetFormat()) + { + case Orthanc::PixelFormat_RGB24: + EmitMessage(payload, reader); + break; + + case Orthanc::PixelFormat_Grayscale8: + { + const DicomInstanceParameters& parameters = frames_.GetInstanceParameters(payload.GetSeriesIndex()); + + Orthanc::Image scaled(parameters.GetExpectedPixelFormat(), reader.GetWidth(), reader.GetHeight(), false); + Orthanc::ImageProcessing::Convert(scaled, reader); + + float w = payload.GetWindowingWidth(); + if (w <= 0.01f) + { + w = 0.01f; // Prevent division by zero + } + + const float c = payload.GetWindowingCenter(); + const float scaling = w / 255.0f; + const float offset = (c - w / 2.0f) / scaling; + + Orthanc::ImageProcessing::ShiftScale(scaled, offset, scaling, false /* truncation to speed up */); + EmitMessage(payload, scaled); + break; + } + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); + } + } + + +#if ORTHANC_ENABLE_DCMTK == 1 + void SeriesFramesLoader::Handle(const ParseDicomSuccessMessage& message) + { + assert(message.GetOrigin().HasPayload()); + + const Payload& payload = dynamic_cast<const Payload&>(message.GetOrigin().GetPayload()); + if ((payload.GetSource().IsDicomDir() || + payload.GetSource().IsDicomWeb()) && + message.HasPixelData()) + { + HandleDicom(dynamic_cast<const Payload&>(message.GetOrigin().GetPayload()), message.GetDicom()); + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + } +#endif + + + void SeriesFramesLoader::Handle(const GetOrthancImageCommand::SuccessMessage& message) + { + assert(message.GetOrigin().HasPayload()); + + const Payload& payload = dynamic_cast<const Payload&>(message.GetOrigin().GetPayload()); + assert(payload.GetSource().IsOrthanc()); + + EmitMessage(payload, message.GetImage()); + } + + + void SeriesFramesLoader::Handle(const GetOrthancWebViewerJpegCommand::SuccessMessage& message) + { + assert(message.GetOrigin().HasPayload()); + + const Payload& payload = dynamic_cast<const Payload&>(message.GetOrigin().GetPayload()); + assert(payload.GetSource().IsOrthanc()); + + EmitMessage(payload, message.GetImage()); + } + + + void SeriesFramesLoader::Handle(const OrthancRestApiCommand::SuccessMessage& message) + { + // This is to handle "/rendered" in DICOMweb + assert(message.GetOrigin().HasPayload()); + HandleDicomWebRendered(dynamic_cast<const Payload&>(message.GetOrigin().GetPayload()), + message.GetAnswer(), message.GetAnswerHeaders()); + } + + + void SeriesFramesLoader::Handle(const HttpCommand::SuccessMessage& message) + { + // This is to handle "/rendered" in DICOMweb + assert(message.GetOrigin().HasPayload()); + HandleDicomWebRendered(dynamic_cast<const Payload&>(message.GetOrigin().GetPayload()), + message.GetAnswer(), message.GetAnswerHeaders()); + } + + + void SeriesFramesLoader::GetPreviewWindowing(float& center, + float& width, + size_t index) const + { + const Orthanc::DicomMap& instance = frames_.GetInstance(index); + const DicomInstanceParameters& parameters = frames_.GetInstanceParameters(index); + + if (parameters.HasDefaultWindowing()) + { + // TODO - Handle multiple presets (take the largest width) + center = parameters.GetDefaultWindowingCenter(); + width = parameters.GetDefaultWindowingWidth(); + } + else + { + float a, b; + if (instance.ParseFloat(a, Orthanc::DICOM_TAG_SMALLEST_IMAGE_PIXEL_VALUE) && + instance.ParseFloat(b, Orthanc::DICOM_TAG_LARGEST_IMAGE_PIXEL_VALUE) && + a < b) + { + center = (a + b) / 2.0f; + width = (b - a); + } + else + { + // Cannot infer a suitable windowing from the available tags + center = 128.0f; + width = 256.0f; + } + } + } + + + Orthanc::IDynamicObject& SeriesFramesLoader::FrameLoadedMessage::GetUserPayload() const + { + if (userPayload_) + { + return *userPayload_; + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + } + + + void SeriesFramesLoader::Factory::SetDicomDir(const std::string& dicomDirPath, + boost::shared_ptr<LoadedDicomResources> dicomDir) + { + dicomDirPath_ = dicomDirPath; + dicomDir_ = dicomDir; + } + + + boost::shared_ptr<IObserver> SeriesFramesLoader::Factory::Create(ILoadersContext::ILock& stone) + { + boost::shared_ptr<SeriesFramesLoader> loader( + new SeriesFramesLoader(stone.GetContext(), instances_, dicomDirPath_, dicomDir_)); + loader->Register<GetOrthancImageCommand::SuccessMessage>(stone.GetOracleObservable(), &SeriesFramesLoader::Handle); + loader->Register<GetOrthancWebViewerJpegCommand::SuccessMessage>(stone.GetOracleObservable(), &SeriesFramesLoader::Handle); + loader->Register<HttpCommand::SuccessMessage>(stone.GetOracleObservable(), &SeriesFramesLoader::Handle); + loader->Register<OrthancRestApiCommand::SuccessMessage>(stone.GetOracleObservable(), &SeriesFramesLoader::Handle); + +#if ORTHANC_ENABLE_DCMTK == 1 + loader->Register<ParseDicomSuccessMessage>(stone.GetOracleObservable(), &SeriesFramesLoader::Handle); +#endif + + return loader; + } + + + void SeriesFramesLoader::ScheduleLoadFrame(int priority, + const DicomSource& source, + size_t index, + unsigned int quality, + Orthanc::IDynamicObject* userPayload) + { + std::unique_ptr<Orthanc::IDynamicObject> protection(userPayload); + + if (index >= frames_.GetFramesCount() || + quality >= source.GetQualityCount()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + + const Orthanc::DicomMap& instance = frames_.GetInstance(index); + + std::string sopInstanceUid; + if (!instance.LookupStringValue(sopInstanceUid, Orthanc::DICOM_TAG_SOP_INSTANCE_UID, false)) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, + "Missing SOPInstanceUID in a DICOM instance"); + } + + if (source.IsDicomDir()) + { + if (dicomDir_.get() == NULL) + { + // Should have been set in the factory + throw Orthanc::OrthancException( + Orthanc::ErrorCode_BadSequenceOfCalls, + "SeriesFramesLoader::Factory::SetDicomDir() should have been called"); + } + + assert(quality == 0); + + std::string file; + if (dicomDir_->LookupStringValue(file, sopInstanceUid, Orthanc::DICOM_TAG_REFERENCED_FILE_ID)) + { + std::unique_ptr<ParseDicomFromFileCommand> command(new ParseDicomFromFileCommand(dicomDirPath_, file)); + command->SetPixelDataIncluded(true); + command->AcquirePayload(new Payload(source, index, sopInstanceUid, quality, protection.release())); + + { + std::unique_ptr<ILoadersContext::ILock> lock(context_.Lock()); + lock->Schedule(GetSharedObserver(), priority, command.release()); + } + } + else + { + LOG(WARNING) << "Missing tag ReferencedFileID in a DICOMDIR entry"; + } + } + else if (source.IsDicomWeb()) + { + std::string studyInstanceUid, seriesInstanceUid; + if (!instance.LookupStringValue(studyInstanceUid, Orthanc::DICOM_TAG_STUDY_INSTANCE_UID, false) || + !instance.LookupStringValue(seriesInstanceUid, Orthanc::DICOM_TAG_SERIES_INSTANCE_UID, false)) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, + "Missing StudyInstanceUID or SeriesInstanceUID in a DICOM instance"); + } + + const std::string uri = ("/studies/" + studyInstanceUid + + "/series/" + seriesInstanceUid + + "/instances/" + sopInstanceUid); + + if (source.HasDicomWebRendered() && + quality == 0) + { + float c, w; + GetPreviewWindowing(c, w, index); + + std::map<std::string, std::string> arguments, headers; + arguments["window"] = (boost::lexical_cast<std::string>(c) + "," + + boost::lexical_cast<std::string>(w) + ",linear"); + headers["Accept"] = "image/jpeg"; + + std::unique_ptr<Payload> payload(new Payload(source, index, sopInstanceUid, quality, protection.release())); + payload->SetWindowing(c, w); + + { + std::unique_ptr<ILoadersContext::ILock> lock(context_.Lock()); + lock->Schedule(GetSharedObserver(), priority, + source.CreateDicomWebCommand(uri + "/rendered", arguments, headers, payload.release())); + } + } + else + { + assert((source.HasDicomWebRendered() && quality == 1) || + (!source.HasDicomWebRendered() && quality == 0)); + +#if ORTHANC_ENABLE_DCMTK == 1 + std::unique_ptr<Payload> payload(new Payload(source, index, sopInstanceUid, quality, protection.release())); + + const std::map<std::string, std::string> empty; + + std::unique_ptr<ParseDicomFromWadoCommand> command( + new ParseDicomFromWadoCommand(sopInstanceUid, source.CreateDicomWebCommand(uri, empty, empty, NULL))); + command->AcquirePayload(payload.release()); + + { + std::unique_ptr<ILoadersContext::ILock> lock(context_.Lock()); + lock->Schedule(GetSharedObserver(), priority, command.release()); + } +#else + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented, + "DCMTK is not enabled, cannot parse a DICOM instance"); +#endif + } + } + else if (source.IsOrthanc()) + { + std::string orthancId; + + { + std::string patientId, studyInstanceUid, seriesInstanceUid; + if (!instance.LookupStringValue(patientId, Orthanc::DICOM_TAG_PATIENT_ID, false) || + !instance.LookupStringValue(studyInstanceUid, Orthanc::DICOM_TAG_STUDY_INSTANCE_UID, false) || + !instance.LookupStringValue(seriesInstanceUid, Orthanc::DICOM_TAG_SERIES_INSTANCE_UID, false)) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, + "Missing StudyInstanceUID or SeriesInstanceUID in a DICOM instance"); + } + + Orthanc::DicomInstanceHasher hasher(patientId, studyInstanceUid, seriesInstanceUid, sopInstanceUid); + orthancId = hasher.HashInstance(); + } + + const DicomInstanceParameters& parameters = frames_.GetInstanceParameters(index); + + if (quality == 0 && source.HasOrthancWebViewer1()) + { + std::unique_ptr<GetOrthancWebViewerJpegCommand> command(new GetOrthancWebViewerJpegCommand); + command->SetInstance(orthancId); + command->SetExpectedPixelFormat(parameters.GetExpectedPixelFormat()); + command->AcquirePayload(new Payload(source, index, sopInstanceUid, quality, protection.release())); + + { + std::unique_ptr<ILoadersContext::ILock> lock(context_.Lock()); + lock->Schedule(GetSharedObserver(), priority, command.release()); + } + } + else if (quality == 0 && source.HasOrthancAdvancedPreview()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); + } + else + { + assert(quality <= 1); + assert(quality == 0 || + source.HasOrthancWebViewer1() || + source.HasOrthancAdvancedPreview()); + + std::unique_ptr<GetOrthancImageCommand> command(new GetOrthancImageCommand); + command->SetFrameUri(orthancId, frames_.GetFrameIndex(index), parameters.GetExpectedPixelFormat()); + command->SetExpectedPixelFormat(parameters.GetExpectedPixelFormat()); + command->SetHttpHeader("Accept", Orthanc::MIME_PAM); + command->AcquirePayload(new Payload(source, index, sopInstanceUid, quality, protection.release())); + + { + std::unique_ptr<ILoadersContext::ILock> lock(context_.Lock()); + lock->Schedule(GetSharedObserver(), priority, command.release()); + } + } + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Loaders/SeriesFramesLoader.h Tue Mar 31 15:51:02 2020 +0200 @@ -0,0 +1,176 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 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 + +#if !defined(ORTHANC_ENABLE_DCMTK) +# error The macro ORTHANC_ENABLE_DCMTK must be defined +#endif + +#include "OracleScheduler.h" +#include "DicomSource.h" +#include "SeriesOrderedFrames.h" +#include "ILoaderFactory.h" + +namespace OrthancStone +{ + class SeriesFramesLoader : + public ObserverBase<SeriesFramesLoader>, + public IObservable + { + private: + class Payload; + + ILoadersContext& context_; + SeriesOrderedFrames frames_; + std::string dicomDirPath_; + boost::shared_ptr<LoadedDicomResources> dicomDir_; + + SeriesFramesLoader(ILoadersContext& context, + LoadedDicomResources& instances, + const std::string& dicomDirPath, + boost::shared_ptr<LoadedDicomResources> dicomDir); + + void EmitMessage(const Payload& payload, + const Orthanc::ImageAccessor& image); + +#if ORTHANC_ENABLE_DCMTK == 1 + void HandleDicom(const Payload& payload, + Orthanc::ParsedDicomFile& dicom); +#endif + + void HandleDicomWebRendered(const Payload& payload, + const std::string& body, + const std::map<std::string, std::string>& headers); + +#if ORTHANC_ENABLE_DCMTK == 1 + void Handle(const ParseDicomSuccessMessage& message); +#endif + + void Handle(const GetOrthancImageCommand::SuccessMessage& message); + + void Handle(const GetOrthancWebViewerJpegCommand::SuccessMessage& message); + + void Handle(const OrthancRestApiCommand::SuccessMessage& message); + + void Handle(const HttpCommand::SuccessMessage& message); + + void GetPreviewWindowing(float& center, + float& width, + size_t index) const; + + public: + class FrameLoadedMessage : public OriginMessage<SeriesFramesLoader> + { + ORTHANC_STONE_MESSAGE(__FILE__, __LINE__); + + private: + size_t frameIndex_; + unsigned int quality_; + const Orthanc::ImageAccessor& image_; + const Orthanc::DicomMap& instance_; + const DicomInstanceParameters& parameters_; + Orthanc::IDynamicObject* userPayload_; // Ownership is maintained by the caller + + public: + FrameLoadedMessage(const SeriesFramesLoader& loader, + size_t frameIndex, + unsigned int quality, + const Orthanc::ImageAccessor& image, + const Orthanc::DicomMap& instance, + const DicomInstanceParameters& parameters, + Orthanc::IDynamicObject* userPayload) : + OriginMessage(loader), + frameIndex_(frameIndex), + quality_(quality), + image_(image), + instance_(instance), + parameters_(parameters), + userPayload_(userPayload) + { + } + + size_t GetFrameIndex() const + { + return frameIndex_; + } + + unsigned int GetQuality() const + { + return quality_; + } + + const Orthanc::ImageAccessor& GetImage() const + { + return image_; + } + + const Orthanc::DicomMap& GetInstance() const + { + return instance_; + } + + const DicomInstanceParameters& GetInstanceParameters() const + { + return parameters_; + } + + bool HasUserPayload() const + { + return userPayload_ != NULL; + } + + Orthanc::IDynamicObject& GetUserPayload() const; + }; + + + class Factory : public ILoaderFactory + { + private: + LoadedDicomResources& instances_; + std::string dicomDirPath_; + boost::shared_ptr<LoadedDicomResources> dicomDir_; + + public: + // No "const" because "LoadedDicomResources::GetResource()" will call "Flatten()" + Factory(LoadedDicomResources& instances) : + instances_(instances) + { + } + + void SetDicomDir(const std::string& dicomDirPath, + boost::shared_ptr<LoadedDicomResources> dicomDir); + + virtual boost::shared_ptr<IObserver> Create(ILoadersContext::ILock& context); + }; + + const SeriesOrderedFrames& GetOrderedFrames() const + { + return frames_; + } + + void ScheduleLoadFrame(int priority, + const DicomSource& source, + size_t index, + unsigned int quality, + Orthanc::IDynamicObject* userPayload /* transfer ownership */); + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Loaders/SeriesMetadataLoader.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -0,0 +1,348 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 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 "SeriesMetadataLoader.h" + +#include <Core/DicomFormat/DicomInstanceHasher.h> + +namespace OrthancStone +{ + SeriesMetadataLoader::SeriesMetadataLoader(boost::shared_ptr<DicomResourcesLoader>& loader) : + loader_(loader), + state_(State_Setup) + { + } + + + bool SeriesMetadataLoader::IsScheduledWithHigherPriority(const std::string& seriesInstanceUid, + int priority) const + { + if (series_.find(seriesInstanceUid) != series_.end()) + { + // This series is readily available + return true; + } + else + { + std::map<std::string, int>::const_iterator found = scheduled_.find(seriesInstanceUid); + + return (found != scheduled_.end() && + found->second < priority); + } + } + + + void SeriesMetadataLoader::Handle(const DicomResourcesLoader::SuccessMessage& message) + { + assert(message.GetResources()); + + switch (state_) + { + case State_Setup: + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + + case State_Default: + { + std::string studyInstanceUid; + std::string seriesInstanceUid; + + if (message.GetResources()->LookupTagValueConsensus(studyInstanceUid, Orthanc::DICOM_TAG_STUDY_INSTANCE_UID) && + message.GetResources()->LookupTagValueConsensus(seriesInstanceUid, Orthanc::DICOM_TAG_SERIES_INSTANCE_UID)) + { + series_[seriesInstanceUid] = message.GetResources(); + + SeriesLoadedMessage loadedMessage(*this, message.GetDicomSource(), studyInstanceUid, + seriesInstanceUid, *message.GetResources()); + BroadcastMessage(loadedMessage); + } + + break; + } + + case State_DicomDir: + { + assert(!dicomDir_); + assert(seriesSize_.empty()); + + dicomDir_ = message.GetResources(); + + for (size_t i = 0; i < message.GetResources()->GetSize(); i++) + { + std::string seriesInstanceUid; + if (message.GetResources()->GetResource(i).LookupStringValue + (seriesInstanceUid, Orthanc::DICOM_TAG_SERIES_INSTANCE_UID, false)) + { + boost::shared_ptr<OrthancStone::LoadedDicomResources> target + (new OrthancStone::LoadedDicomResources(Orthanc::DICOM_TAG_SOP_INSTANCE_UID)); + + if (loader_->ScheduleLoadDicomFile(target, message.GetPriority(), message.GetDicomSource(), dicomDirPath_, + message.GetResources()->GetResource(i), false /* no need for pixel data */, + NULL /* TODO PAYLOAD */)) + { + std::map<std::string, unsigned int>::iterator found = seriesSize_.find(seriesInstanceUid); + if (found == seriesSize_.end()) + { + series_[seriesInstanceUid].reset + (new OrthancStone::LoadedDicomResources(Orthanc::DICOM_TAG_SOP_INSTANCE_UID)); + seriesSize_[seriesInstanceUid] = 1; + } + else + { + found->second ++; + } + } + } + } + + LOG(INFO) << "Read a DICOMDIR containing " << seriesSize_.size() << " series"; + + state_ = State_DicomFile; + break; + } + + case State_DicomFile: + { + assert(dicomDir_); + assert(message.GetResources()->GetSize() <= 1); // Could be zero if corrupted DICOM instance + + if (message.GetResources()->GetSize() == 1) + { + const Orthanc::DicomMap& instance = message.GetResources()->GetResource(0); + + std::string studyInstanceUid; + std::string seriesInstanceUid; + if (instance.LookupStringValue(studyInstanceUid, Orthanc::DICOM_TAG_STUDY_INSTANCE_UID, false) && + instance.LookupStringValue(seriesInstanceUid, Orthanc::DICOM_TAG_SERIES_INSTANCE_UID, false)) + { + Series::const_iterator series = series_.find(seriesInstanceUid); + std::map<std::string, unsigned int>::const_iterator size = seriesSize_.find(seriesInstanceUid); + + if (series == series_.end() || + size == seriesSize_.end()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + else + { + series->second->AddResource(instance); + + if (series->second->GetSize() > size->second) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + else if (series->second->GetSize() == size->second) + { + // The series is complete + SeriesLoadedMessage loadedMessage( + *this, message.GetDicomSource(), + studyInstanceUid, seriesInstanceUid, *series->second); + loadedMessage.SetDicomDir(dicomDirPath_, dicomDir_); + BroadcastMessage(loadedMessage); + } + } + } + } + + break; + } + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + } + + + SeriesMetadataLoader::SeriesLoadedMessage::SeriesLoadedMessage( + const SeriesMetadataLoader& loader, + const DicomSource& source, + const std::string& studyInstanceUid, + const std::string& seriesInstanceUid, + LoadedDicomResources& instances) : + OriginMessage(loader), + source_(source), + studyInstanceUid_(studyInstanceUid), + seriesInstanceUid_(seriesInstanceUid), + instances_(instances) + { + LOG(INFO) << "Loaded series " << seriesInstanceUid + << ", number of instances: " << instances_.GetSize(); + } + + + boost::shared_ptr<IObserver> SeriesMetadataLoader::Factory::Create(ILoadersContext::ILock& context) + { + DicomResourcesLoader::Factory factory; + boost::shared_ptr<DicomResourcesLoader> loader + (boost::dynamic_pointer_cast<DicomResourcesLoader>(factory.Create(context))); + + boost::shared_ptr<SeriesMetadataLoader> obj(new SeriesMetadataLoader(loader)); + obj->Register<DicomResourcesLoader::SuccessMessage>(*loader, &SeriesMetadataLoader::Handle); + return obj; + } + + + SeriesMetadataLoader::Accessor::Accessor(SeriesMetadataLoader& that, + const std::string& seriesInstanceUid) + { + Series::const_iterator found = that.series_.find(seriesInstanceUid); + if (found != that.series_.end()) + { + assert(found->second != NULL); + series_ = found->second; + } + } + + + size_t SeriesMetadataLoader::Accessor::GetInstancesCount() const + { + if (IsComplete()) + { + return series_->GetSize(); + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + } + + + const Orthanc::DicomMap& SeriesMetadataLoader::Accessor::GetInstance(size_t index) const + { + if (IsComplete()) + { + return series_->GetResource(index); + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + } + + + void SeriesMetadataLoader::ScheduleLoadSeries(int priority, + const DicomSource& source, + const std::string& studyInstanceUid, + const std::string& seriesInstanceUid) + { + if (state_ != State_Setup && + state_ != State_Default) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls, + "The loader is working in DICOMDIR state"); + } + + state_ = State_Default; + + // Only re-schedule the loading if the previous loading was with lower priority + if (!IsScheduledWithHigherPriority(seriesInstanceUid, priority)) + { + if (source.IsDicomWeb()) + { + boost::shared_ptr<LoadedDicomResources> target + (new LoadedDicomResources(Orthanc::DICOM_TAG_SOP_INSTANCE_UID)); + loader_->ScheduleGetDicomWeb( + target, priority, source, + "/studies/" + studyInstanceUid + "/series/" + seriesInstanceUid + "/metadata", + NULL /* TODO PAYLOAD */); + + scheduled_[seriesInstanceUid] = priority; + } + else if (source.IsOrthanc()) + { + // This flavor of the method is only available with DICOMweb, as + // Orthanc requires the "PatientID" to be known + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls, + "The PatientID must be provided on Orthanc sources"); + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); + } + } + } + + + void SeriesMetadataLoader::ScheduleLoadSeries(int priority, + const DicomSource& source, + const std::string& patientId, + const std::string& studyInstanceUid, + const std::string& seriesInstanceUid) + { + if (state_ != State_Setup && + state_ != State_Default) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls, + "The loader is working in DICOMDIR state"); + } + + state_ = State_Default; + + if (source.IsDicomWeb()) + { + ScheduleLoadSeries(priority, source, studyInstanceUid, seriesInstanceUid); + } + else if (!IsScheduledWithHigherPriority(seriesInstanceUid, priority)) + { + if (source.IsOrthanc()) + { + // Dummy SOP Instance UID, as we are working at the "series" level + Orthanc::DicomInstanceHasher hasher(patientId, studyInstanceUid, seriesInstanceUid, "dummy"); + + boost::shared_ptr<LoadedDicomResources> target + (new LoadedDicomResources(Orthanc::DICOM_TAG_SOP_INSTANCE_UID)); + + loader_->ScheduleLoadOrthancResources(target, priority, source, Orthanc::ResourceType_Series, + hasher.HashSeries(), Orthanc::ResourceType_Instance, + NULL /* TODO PAYLOAD */); + + scheduled_[seriesInstanceUid] = priority; + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); + } + } + } + + + void SeriesMetadataLoader::ScheduleLoadDicomDir(int priority, + const DicomSource& source, + const std::string& path) + { + if (!source.IsDicomDir()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + + if (state_ != State_Setup) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls, + "The loader cannot load two different DICOMDIR"); + } + + state_ = State_DicomDir; + dicomDirPath_ = path; + boost::shared_ptr<LoadedDicomResources> dicomDir + (new LoadedDicomResources(Orthanc::DICOM_TAG_REFERENCED_SOP_INSTANCE_UID_IN_FILE)); + loader_->ScheduleLoadDicomDir(dicomDir, priority, source, path, + NULL /* TODO PAYLOAD */); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Loaders/SeriesMetadataLoader.h Tue Mar 31 15:51:02 2020 +0200 @@ -0,0 +1,170 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 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 "DicomResourcesLoader.h" + +namespace OrthancStone +{ + class SeriesMetadataLoader : + public ObserverBase<SeriesMetadataLoader>, + public IObservable + { + private: + enum State + { + State_Setup, + State_Default, + State_DicomDir, + State_DicomFile + }; + + typedef std::map<std::string, boost::shared_ptr<LoadedDicomResources> > Series; + + boost::shared_ptr<DicomResourcesLoader> loader_; + State state_; + std::map<std::string, int> scheduled_; // Maps a "SeriesInstanceUID" to a priority + Series series_; + boost::shared_ptr<LoadedDicomResources> dicomDir_; + std::string dicomDirPath_; + std::map<std::string, unsigned int> seriesSize_; + + SeriesMetadataLoader(boost::shared_ptr<DicomResourcesLoader>& loader); + + bool IsScheduledWithHigherPriority(const std::string& seriesInstanceUid, + int priority) const; + + void Handle(const DicomResourcesLoader::SuccessMessage& message); + + public: + class SeriesLoadedMessage : public OriginMessage<SeriesMetadataLoader> + { + ORTHANC_STONE_MESSAGE(__FILE__, __LINE__); + + private: + const DicomSource& source_; + const std::string& studyInstanceUid_; + const std::string& seriesInstanceUid_; + LoadedDicomResources& instances_; + std::string dicomDirPath_; + boost::shared_ptr<LoadedDicomResources> dicomDir_; + + public: + SeriesLoadedMessage(const SeriesMetadataLoader& loader, + const DicomSource& source, + const std::string& studyInstanceUid, + const std::string& seriesInstanceUid, + LoadedDicomResources& instances); + + const DicomSource& GetDicomSource() const + { + return source_; + } + + const std::string& GetStudyInstanceUid() const + { + return studyInstanceUid_; + } + + const std::string& GetSeriesInstanceUid() const + { + return seriesInstanceUid_; + } + + size_t GetInstancesCount() const + { + return instances_.GetSize(); + } + + const Orthanc::DicomMap& GetInstance(size_t index) const + { + return instances_.GetResource(index); + } + + LoadedDicomResources& GetInstances() const + { + return instances_; + } + + void SetDicomDir(const std::string& dicomDirPath, + boost::shared_ptr<LoadedDicomResources> dicomDir) + { + dicomDirPath_ = dicomDirPath; + dicomDir_ = dicomDir; + } + + const std::string& GetDicomDirPath() const + { + return dicomDirPath_; + } + + // Will be NULL on non-DICOMDIR sources + boost::shared_ptr<LoadedDicomResources> GetDicomDir() const + { + return dicomDir_; + } + }; + + + class Factory : public ILoaderFactory + { + public: + virtual boost::shared_ptr<IObserver> Create(ILoadersContext::ILock& context); + }; + + + class Accessor : public boost::noncopyable + { + private: + boost::shared_ptr<LoadedDicomResources> series_; + + public: + Accessor(SeriesMetadataLoader& that, + const std::string& seriesInstanceUid); + + bool IsComplete() const + { + return series_ != NULL; + } + + size_t GetInstancesCount() const; + + const Orthanc::DicomMap& GetInstance(size_t index) const; + }; + + + void ScheduleLoadSeries(int priority, + const DicomSource& source, + const std::string& studyInstanceUid, + const std::string& seriesInstanceUid); + + void ScheduleLoadSeries(int priority, + const DicomSource& source, + const std::string& patientId, + const std::string& studyInstanceUid, + const std::string& seriesInstanceUid); + + void ScheduleLoadDicomDir(int priority, + const DicomSource& source, + const std::string& path); + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Loaders/SeriesOrderedFrames.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -0,0 +1,345 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 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 "../Toolbox/SlicesSorter.h" +#include "SeriesOrderedFrames.h" + +#include <Core/OrthancException.h> + +namespace OrthancStone +{ + class SeriesOrderedFrames::Instance : public boost::noncopyable + { + private: + std::unique_ptr<Orthanc::DicomMap> dicom_; + DicomInstanceParameters parameters_; + + public: + Instance(const Orthanc::DicomMap& dicom) : + dicom_(dicom.Clone()), + parameters_(dicom) + { + } + + const Orthanc::DicomMap& GetInstance() const + { + return *dicom_; + } + + const DicomInstanceParameters& GetInstanceParameters() const + { + return parameters_; + } + + bool Lookup3DGeometry(CoordinateSystem3D& target) const + { + try + { + std::string imagePositionPatient, imageOrientationPatient; + if (dicom_->LookupStringValue(imagePositionPatient, Orthanc::DICOM_TAG_IMAGE_POSITION_PATIENT, false) && + dicom_->LookupStringValue(imageOrientationPatient, Orthanc::DICOM_TAG_IMAGE_ORIENTATION_PATIENT, false)) + { + target = CoordinateSystem3D(imagePositionPatient, imageOrientationPatient); + return true; + } + } + catch (Orthanc::OrthancException&) + { + } + + return false; + } + + bool LookupIndexInSeries(int& target) const + { + std::string value; + + if (dicom_->LookupStringValue(value, Orthanc::DICOM_TAG_INSTANCE_NUMBER, false) || + dicom_->LookupStringValue(value, Orthanc::DICOM_TAG_IMAGE_INDEX, false)) + { + try + { + target = boost::lexical_cast<int>(value); + return true; + } + catch (boost::bad_lexical_cast&) + { + } + } + + return false; + } + }; + + + class SeriesOrderedFrames::Frame : public boost::noncopyable + { + private: + const Instance* instance_; + unsigned int frameIndex_; + + public: + Frame(const Instance& instance, + unsigned int frameIndex) : + instance_(&instance), + frameIndex_(frameIndex) + { + if (frameIndex_ >= instance.GetInstanceParameters().GetImageInformation().GetNumberOfFrames()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + } + + const Orthanc::DicomMap& GetInstance() const + { + assert(instance_ != NULL); + return instance_->GetInstance(); + } + + const DicomInstanceParameters& GetInstanceParameters() const + { + assert(instance_ != NULL); + return instance_->GetInstanceParameters(); + } + + unsigned int GetFrameIndex() const + { + return frameIndex_; + } + }; + + + class SeriesOrderedFrames::InstanceWithIndexInSeries + { + private: + const Instance* instance_; // Don't use a reference to make "std::sort()" happy + int index_; + + public: + InstanceWithIndexInSeries(const Instance& instance) : + instance_(&instance) + { + if (!instance_->LookupIndexInSeries(index_)) + { + index_ = std::numeric_limits<int>::max(); + } + } + + const Instance& GetInstance() const + { + return *instance_; + } + + int GetIndexInSeries() const + { + return index_; + } + + bool operator< (const InstanceWithIndexInSeries& other) const + { + return (index_ < other.index_); + } + }; + + + void SeriesOrderedFrames::Clear() + { + for (size_t i = 0; i < instances_.size(); i++) + { + assert(instances_[i] != NULL); + delete instances_[i]; + } + + for (size_t i = 0; i < orderedFrames_.size(); i++) + { + assert(orderedFrames_[i] != NULL); + delete orderedFrames_[i]; + } + + instances_.clear(); + orderedFrames_.clear(); + } + + + bool SeriesOrderedFrames::Sort3DVolume() + { + SlicesSorter sorter; + sorter.Reserve(instances_.size()); + + for (size_t i = 0; i < instances_.size(); i++) + { + CoordinateSystem3D geometry; + if (instances_[i]->Lookup3DGeometry(geometry)) + { + sorter.AddSlice(geometry, new Orthanc::SingleValueObject<Instance*>(instances_[i])); + } + else + { + return false; // Not a 3D volume + } + } + + if (!sorter.Sort() || + sorter.GetSlicesCount() != instances_.size() || + !sorter.AreAllSlicesDistinct()) + { + return false; + } + else + { + for (size_t i = 0; i < sorter.GetSlicesCount(); i++) + { + assert(sorter.HasSlicePayload(i)); + + const Orthanc::SingleValueObject<Instance*>& payload = + dynamic_cast<const Orthanc::SingleValueObject<Instance*>&>(sorter.GetSlicePayload(i)); + + assert(payload.GetValue() != NULL); + + for (size_t j = 0; j < payload.GetValue()->GetInstanceParameters().GetImageInformation().GetNumberOfFrames(); j++) + { + orderedFrames_.push_back(new Frame(*payload.GetValue(), + static_cast<unsigned int>(j))); + } + } + + isRegular_ = sorter.ComputeSpacingBetweenSlices(spacingBetweenSlices_); + return true; + } + } + + + void SeriesOrderedFrames::SortIndexInSeries() + { + std::vector<InstanceWithIndexInSeries> tmp; + tmp.reserve(instances_.size()); + + for (size_t i = 0; i < instances_.size(); i++) + { + assert(instances_[i] != NULL); + tmp.push_back(InstanceWithIndexInSeries(*instances_[i])); + } + + std::sort(tmp.begin(), tmp.end()); + + for (size_t i = 0; i < tmp.size(); i++) + { + for (size_t j = 0; j < tmp[i].GetInstance().GetInstanceParameters().GetImageInformation().GetNumberOfFrames(); j++) + { + orderedFrames_.push_back(new Frame(tmp[i].GetInstance(), + static_cast<unsigned int>(j))); + } + } + } + + + const SeriesOrderedFrames::Frame& SeriesOrderedFrames::GetFrame(size_t seriesIndex) const + { + if (seriesIndex >= orderedFrames_.size()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + else + { + assert(orderedFrames_[seriesIndex] != NULL); + return *(orderedFrames_[seriesIndex]); + } + } + + + SeriesOrderedFrames::SeriesOrderedFrames(LoadedDicomResources& instances) : + isVolume_(false), + isRegular_(false), + spacingBetweenSlices_(0) + { + instances_.reserve(instances.GetSize()); + + size_t numberOfFrames = 0; + + for (size_t i = 0; i < instances.GetSize(); i++) + { + try + { + std::unique_ptr<Instance> instance(new Instance(instances.GetResource(i))); + numberOfFrames += instance->GetInstanceParameters().GetImageInformation().GetNumberOfFrames(); + instances_.push_back(instance.release()); + } + catch (Orthanc::OrthancException&) + { + // The instance has not all the required DICOM tags, skip it + } + } + + orderedFrames_.reserve(numberOfFrames); + + if (Sort3DVolume()) + { + isVolume_ = true; + + if (isRegular_) + { + LOG(INFO) << "Regular 3D volume detected"; + } + else + { + LOG(INFO) << "Non-regular 3D volume detected"; + } + } + else + { + LOG(INFO) << "Series is not a 3D volume, sorting by index"; + SortIndexInSeries(); + } + + LOG(INFO) << "Number of frames: " << orderedFrames_.size(); + } + + + unsigned int SeriesOrderedFrames::GetFrameIndex(size_t seriesIndex) const + { + return GetFrame(seriesIndex).GetFrameIndex(); + } + + + const Orthanc::DicomMap& SeriesOrderedFrames::GetInstance(size_t seriesIndex) const + { + return GetFrame(seriesIndex).GetInstance(); + } + + + const DicomInstanceParameters& SeriesOrderedFrames::GetInstanceParameters(size_t seriesIndex) const + { + return GetFrame(seriesIndex).GetInstanceParameters(); + } + + + double SeriesOrderedFrames::GetSpacingBetweenSlices() const + { + if (IsRegular3DVolume()) + { + return spacingBetweenSlices_; + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Loaders/SeriesOrderedFrames.h Tue Mar 31 15:51:02 2020 +0200 @@ -0,0 +1,85 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 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 "LoadedDicomResources.h" + +#include "../Toolbox/DicomInstanceParameters.h" + +namespace OrthancStone +{ + class SeriesOrderedFrames : public boost::noncopyable + { + private: + class Instance; + class Frame; + class InstanceWithIndexInSeries; + + std::vector<Instance*> instances_; + std::vector<Frame*> orderedFrames_; + bool isVolume_; + bool isRegular_; + double spacingBetweenSlices_; + + void Clear(); + + bool Sort3DVolume(); + + void SortIndexInSeries(); + + const Frame& GetFrame(size_t seriesIndex) const; + + public: + SeriesOrderedFrames(LoadedDicomResources& instances); + + ~SeriesOrderedFrames() + { + Clear(); + } + + size_t GetFramesCount() const + { + return orderedFrames_.size(); + } + + unsigned int GetFrameIndex(size_t seriesIndex) const; + + const Orthanc::DicomMap& GetInstance(size_t seriesIndex) const; + + const DicomInstanceParameters& GetInstanceParameters(size_t seriesIndex) const; + + // Are all frames parallel and aligned? + bool Is3DVolume() const + { + return isVolume_; + } + + // Are all frames parallel, aligned and evenly spaced? + bool IsRegular3DVolume() const + { + return isRegular_; + } + + // Only available on regular 3D volumes + double GetSpacingBetweenSlices() const; + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Loaders/SeriesThumbnailsLoader.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -0,0 +1,566 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 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 "SeriesThumbnailsLoader.h" + +#include <Core/DicomFormat/DicomMap.h> +#include <Core/DicomFormat/DicomInstanceHasher.h> +#include <Core/Images/ImageProcessing.h> +#include <Core/Images/JpegWriter.h> +#include <Core/OrthancException.h> + +#include <boost/algorithm/string/predicate.hpp> + +static const unsigned int JPEG_QUALITY = 70; // Only used for Orthanc source + +namespace OrthancStone +{ + static SeriesThumbnailType ExtractSopClassUid(const std::string& sopClassUid) + { + if (sopClassUid == "1.2.840.10008.5.1.4.1.1.104.1") // Encapsulated PDF Storage + { + return SeriesThumbnailType_Pdf; + } + else if (sopClassUid == "1.2.840.10008.5.1.4.1.1.77.1.1.1" || // Video Endoscopic Image Storage + sopClassUid == "1.2.840.10008.5.1.4.1.1.77.1.2.1" || // Video Microscopic Image Storage + sopClassUid == "1.2.840.10008.5.1.4.1.1.77.1.4.1") // Video Photographic Image Storage + { + return SeriesThumbnailType_Video; + } + else + { + return SeriesThumbnailType_Unsupported; + } + } + + + SeriesThumbnailsLoader::Thumbnail::Thumbnail(const std::string& image, + const std::string& mime) : + type_(SeriesThumbnailType_Image), + image_(image), + mime_(mime) + { + } + + + SeriesThumbnailsLoader::Thumbnail::Thumbnail(SeriesThumbnailType type) : + type_(type) + { + if (type == SeriesThumbnailType_Image) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + } + + + void SeriesThumbnailsLoader::AcquireThumbnail(const DicomSource& source, + const std::string& studyInstanceUid, + const std::string& seriesInstanceUid, + SeriesThumbnailsLoader::Thumbnail* thumbnail) + { + assert(thumbnail != NULL); + + std::unique_ptr<Thumbnail> protection(thumbnail); + + Thumbnails::iterator found = thumbnails_.find(seriesInstanceUid); + if (found == thumbnails_.end()) + { + thumbnails_[seriesInstanceUid] = protection.release(); + } + else + { + assert(found->second != NULL); + delete found->second; + found->second = protection.release(); + } + + ThumbnailLoadedMessage message(*this, source, studyInstanceUid, seriesInstanceUid, *thumbnail); + BroadcastMessage(message); + } + + + class SeriesThumbnailsLoader::Handler : public Orthanc::IDynamicObject + { + private: + boost::shared_ptr<SeriesThumbnailsLoader> loader_; + DicomSource source_; + std::string studyInstanceUid_; + std::string seriesInstanceUid_; + + public: + Handler(boost::shared_ptr<SeriesThumbnailsLoader> loader, + const DicomSource& source, + const std::string& studyInstanceUid, + const std::string& seriesInstanceUid) : + loader_(loader), + source_(source), + studyInstanceUid_(studyInstanceUid), + seriesInstanceUid_(seriesInstanceUid) + { + if (!loader) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); + } + } + + boost::shared_ptr<SeriesThumbnailsLoader> GetLoader() + { + return loader_; + } + + const DicomSource& GetSource() const + { + return source_; + } + + const std::string& GetStudyInstanceUid() const + { + return studyInstanceUid_; + } + + const std::string& GetSeriesInstanceUid() const + { + return seriesInstanceUid_; + } + + virtual void HandleSuccess(const std::string& body, + const std::map<std::string, std::string>& headers) = 0; + + virtual void HandleError() + { + LOG(INFO) << "Cannot generate thumbnail for SeriesInstanceUID: " << seriesInstanceUid_; + } + }; + + + class SeriesThumbnailsLoader::DicomWebSopClassHandler : public SeriesThumbnailsLoader::Handler + { + private: + static bool GetSopClassUid(std::string& sopClassUid, + const Json::Value& json) + { + Orthanc::DicomMap dicom; + dicom.FromDicomWeb(json); + + return dicom.LookupStringValue(sopClassUid, Orthanc::DICOM_TAG_SOP_CLASS_UID, false); + } + + public: + DicomWebSopClassHandler(boost::shared_ptr<SeriesThumbnailsLoader> loader, + const DicomSource& source, + const std::string& studyInstanceUid, + const std::string& seriesInstanceUid) : + Handler(loader, source, studyInstanceUid, seriesInstanceUid) + { + } + + virtual void HandleSuccess(const std::string& body, + const std::map<std::string, std::string>& headers) + { + Json::Reader reader; + Json::Value value; + + if (!reader.parse(body, value) || + value.type() != Json::arrayValue) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol); + } + else + { + SeriesThumbnailType type = SeriesThumbnailType_Unsupported; + + std::string sopClassUid; + if (value.size() > 0 && + GetSopClassUid(sopClassUid, value[0])) + { + bool ok = true; + + for (Json::Value::ArrayIndex i = 1; i < value.size() && ok; i++) + { + std::string s; + if (!GetSopClassUid(s, value[i]) || + s != sopClassUid) + { + ok = false; + } + } + + if (ok) + { + type = ExtractSopClassUid(sopClassUid); + } + } + + GetLoader()->AcquireThumbnail(GetSource(), GetStudyInstanceUid(), + GetSeriesInstanceUid(), new Thumbnail(type)); + } + } + }; + + + class SeriesThumbnailsLoader::DicomWebThumbnailHandler : public SeriesThumbnailsLoader::Handler + { + public: + DicomWebThumbnailHandler(boost::shared_ptr<SeriesThumbnailsLoader> loader, + const DicomSource& source, + const std::string& studyInstanceUid, + const std::string& seriesInstanceUid) : + Handler(loader, source, studyInstanceUid, seriesInstanceUid) + { + } + + virtual void HandleSuccess(const std::string& body, + const std::map<std::string, std::string>& headers) + { + std::string mime = Orthanc::MIME_JPEG; + for (std::map<std::string, std::string>::const_iterator + it = headers.begin(); it != headers.end(); ++it) + { + if (boost::iequals(it->first, "content-type")) + { + mime = it->second; + } + } + + GetLoader()->AcquireThumbnail(GetSource(), GetStudyInstanceUid(), + GetSeriesInstanceUid(), new Thumbnail(body, mime)); + } + + virtual void HandleError() + { + // The DICOMweb wasn't able to generate a thumbnail, try to + // retrieve the SopClassUID tag using QIDO-RS + + std::map<std::string, std::string> arguments, headers; + arguments["0020000D"] = GetStudyInstanceUid(); + arguments["0020000E"] = GetSeriesInstanceUid(); + arguments["includefield"] = "00080016"; + + std::unique_ptr<IOracleCommand> command( + GetSource().CreateDicomWebCommand( + "/instances", arguments, headers, new DicomWebSopClassHandler( + GetLoader(), GetSource(), GetStudyInstanceUid(), GetSeriesInstanceUid()))); + GetLoader()->Schedule(command.release()); + } + }; + + + class SeriesThumbnailsLoader::ThumbnailInformation : public Orthanc::IDynamicObject + { + private: + DicomSource source_; + std::string studyInstanceUid_; + std::string seriesInstanceUid_; + + public: + ThumbnailInformation(const DicomSource& source, + const std::string& studyInstanceUid, + const std::string& seriesInstanceUid) : + source_(source), + studyInstanceUid_(studyInstanceUid), + seriesInstanceUid_(seriesInstanceUid) + { + } + + const DicomSource& GetDicomSource() const + { + return source_; + } + + const std::string& GetStudyInstanceUid() const + { + return studyInstanceUid_; + } + + const std::string& GetSeriesInstanceUid() const + { + return seriesInstanceUid_; + } + }; + + + class SeriesThumbnailsLoader::OrthancSopClassHandler : public SeriesThumbnailsLoader::Handler + { + private: + std::string instanceId_; + + public: + OrthancSopClassHandler(boost::shared_ptr<SeriesThumbnailsLoader> loader, + const DicomSource& source, + const std::string& studyInstanceUid, + const std::string& seriesInstanceUid, + const std::string& instanceId) : + Handler(loader, source, studyInstanceUid, seriesInstanceUid), + instanceId_(instanceId) + { + } + + virtual void HandleSuccess(const std::string& body, + const std::map<std::string, std::string>& headers) + { + SeriesThumbnailType type = ExtractSopClassUid(body); + + if (type == SeriesThumbnailType_Pdf || + type == SeriesThumbnailType_Video) + { + GetLoader()->AcquireThumbnail(GetSource(), GetStudyInstanceUid(), + GetSeriesInstanceUid(), new Thumbnail(type)); + } + else + { + std::unique_ptr<GetOrthancImageCommand> command(new GetOrthancImageCommand); + command->SetUri("/instances/" + instanceId_ + "/preview"); + command->SetHttpHeader("Accept", Orthanc::MIME_JPEG); + command->AcquirePayload(new ThumbnailInformation( + GetSource(), GetStudyInstanceUid(), GetSeriesInstanceUid())); + GetLoader()->Schedule(command.release()); + } + } + }; + + + class SeriesThumbnailsLoader::SelectOrthancInstanceHandler : public SeriesThumbnailsLoader::Handler + { + public: + SelectOrthancInstanceHandler(boost::shared_ptr<SeriesThumbnailsLoader> loader, + const DicomSource& source, + const std::string& studyInstanceUid, + const std::string& seriesInstanceUid) : + Handler(loader, source, studyInstanceUid, seriesInstanceUid) + { + } + + virtual void HandleSuccess(const std::string& body, + const std::map<std::string, std::string>& headers) + { + static const char* const INSTANCES = "Instances"; + + Json::Value json; + Json::Reader reader; + if (!reader.parse(body, json) || + json.type() != Json::objectValue) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol); + } + + if (json.isMember(INSTANCES) && + json[INSTANCES].type() == Json::arrayValue && + json[INSTANCES].size() > 0) + { + // Select one instance of the series to generate the thumbnail + Json::Value::ArrayIndex index = json[INSTANCES].size() / 2; + if (json[INSTANCES][index].type() == Json::stringValue) + { + std::map<std::string, std::string> arguments, headers; + arguments["quality"] = boost::lexical_cast<std::string>(JPEG_QUALITY); + headers["Accept"] = Orthanc::MIME_JPEG; + + const std::string instance = json[INSTANCES][index].asString(); + + std::unique_ptr<OrthancRestApiCommand> command(new OrthancRestApiCommand); + command->SetUri("/instances/" + instance + "/metadata/SopClassUid"); + command->AcquirePayload( + new OrthancSopClassHandler( + GetLoader(), GetSource(), GetStudyInstanceUid(), GetSeriesInstanceUid(), instance)); + GetLoader()->Schedule(command.release()); + } + } + } + }; + + + void SeriesThumbnailsLoader::Schedule(IOracleCommand* command) + { + std::unique_ptr<ILoadersContext::ILock> lock(context_.Lock()); + lock->Schedule(GetSharedObserver(), priority_, command); + } + + + void SeriesThumbnailsLoader::Handle(const HttpCommand::SuccessMessage& message) + { + assert(message.GetOrigin().HasPayload()); + dynamic_cast<Handler&>(message.GetOrigin().GetPayload()).HandleSuccess(message.GetAnswer(), message.GetAnswerHeaders()); + } + + + void SeriesThumbnailsLoader::Handle(const OrthancRestApiCommand::SuccessMessage& message) + { + assert(message.GetOrigin().HasPayload()); + dynamic_cast<Handler&>(message.GetOrigin().GetPayload()).HandleSuccess(message.GetAnswer(), message.GetAnswerHeaders()); + } + + + void SeriesThumbnailsLoader::Handle(const GetOrthancImageCommand::SuccessMessage& message) + { + assert(message.GetOrigin().HasPayload()); + + std::unique_ptr<Orthanc::ImageAccessor> resized(Orthanc::ImageProcessing::FitSize(message.GetImage(), width_, height_)); + + std::string jpeg; + Orthanc::JpegWriter writer; + writer.SetQuality(JPEG_QUALITY); + writer.WriteToMemory(jpeg, *resized); + + const ThumbnailInformation& info = dynamic_cast<ThumbnailInformation&>(message.GetOrigin().GetPayload()); + AcquireThumbnail(info.GetDicomSource(), info.GetStudyInstanceUid(), + info.GetSeriesInstanceUid(), new Thumbnail(jpeg, Orthanc::MIME_JPEG)); + } + + + void SeriesThumbnailsLoader::Handle(const OracleCommandExceptionMessage& message) + { + const OracleCommandBase& command = dynamic_cast<const OracleCommandBase&>(message.GetOrigin()); + assert(command.HasPayload()); + + if (command.GetType() == IOracleCommand::Type_GetOrthancImage) + { + // This is presumably a HTTP status 301 (Moved permanently) + // because of an unsupported DICOM file in "/preview" + const ThumbnailInformation& info = dynamic_cast<const ThumbnailInformation&>(command.GetPayload()); + AcquireThumbnail(info.GetDicomSource(), info.GetStudyInstanceUid(), + info.GetSeriesInstanceUid(), new Thumbnail(SeriesThumbnailType_Unsupported)); + } + else + { + dynamic_cast<Handler&>(command.GetPayload()).HandleError(); + } + } + + + SeriesThumbnailsLoader::SeriesThumbnailsLoader(ILoadersContext& context, + int priority) : + context_(context), + priority_(priority), + width_(128), + height_(128) + { + } + + + boost::shared_ptr<IObserver> SeriesThumbnailsLoader::Factory::Create(ILoadersContext::ILock& stone) + { + boost::shared_ptr<SeriesThumbnailsLoader> result(new SeriesThumbnailsLoader(stone.GetContext(), priority_)); + result->Register<GetOrthancImageCommand::SuccessMessage>(stone.GetOracleObservable(), &SeriesThumbnailsLoader::Handle); + result->Register<HttpCommand::SuccessMessage>(stone.GetOracleObservable(), &SeriesThumbnailsLoader::Handle); + result->Register<OracleCommandExceptionMessage>(stone.GetOracleObservable(), &SeriesThumbnailsLoader::Handle); + result->Register<OrthancRestApiCommand::SuccessMessage>(stone.GetOracleObservable(), &SeriesThumbnailsLoader::Handle); + return result; + } + + + void SeriesThumbnailsLoader::SetThumbnailSize(unsigned int width, + unsigned int height) + { + if (width <= 0 || + height <= 0) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + else + { + width_ = width; + height_ = height; + } + } + + + void SeriesThumbnailsLoader::Clear() + { + for (Thumbnails::iterator it = thumbnails_.begin(); it != thumbnails_.end(); ++it) + { + assert(it->second != NULL); + delete it->second; + } + + thumbnails_.clear(); + } + + + SeriesThumbnailType SeriesThumbnailsLoader::GetSeriesThumbnail(std::string& image, + std::string& mime, + const std::string& seriesInstanceUid) const + { + Thumbnails::const_iterator found = thumbnails_.find(seriesInstanceUid); + + if (found == thumbnails_.end()) + { + return SeriesThumbnailType_NotLoaded; + } + else + { + assert(found->second != NULL); + image.assign(found->second->GetImage()); + mime.assign(found->second->GetMime()); + return found->second->GetType(); + } + } + + + void SeriesThumbnailsLoader::ScheduleLoadThumbnail(const DicomSource& source, + const std::string& patientId, + const std::string& studyInstanceUid, + const std::string& seriesInstanceUid) + { + if (source.IsDicomWeb()) + { + if (!source.HasDicomWebRendered()) + { + // TODO - Could use DCMTK here + throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol, + "DICOMweb server is not able to generate renderings of DICOM series"); + } + + const std::string uri = ("/studies/" + studyInstanceUid + + "/series/" + seriesInstanceUid + "/rendered"); + + std::map<std::string, std::string> arguments, headers; + arguments["viewport"] = (boost::lexical_cast<std::string>(width_) + "," + + boost::lexical_cast<std::string>(height_)); + + // Needed to set this header explicitly, as long as emscripten + // does not include macro "EMSCRIPTEN_FETCH_RESPONSE_HEADERS" + // https://github.com/emscripten-core/emscripten/pull/8486 + headers["Accept"] = Orthanc::MIME_JPEG; + + std::unique_ptr<IOracleCommand> command( + source.CreateDicomWebCommand( + uri, arguments, headers, new DicomWebThumbnailHandler( + shared_from_this(), source, studyInstanceUid, seriesInstanceUid))); + Schedule(command.release()); + } + else if (source.IsOrthanc()) + { + // Dummy SOP Instance UID, as we are working at the "series" level + Orthanc::DicomInstanceHasher hasher(patientId, studyInstanceUid, seriesInstanceUid, "dummy"); + + std::unique_ptr<OrthancRestApiCommand> command(new OrthancRestApiCommand); + command->SetUri("/series/" + hasher.HashSeries()); + command->AcquirePayload(new SelectOrthancInstanceHandler( + shared_from_this(), source, studyInstanceUid, seriesInstanceUid)); + Schedule(command.release()); + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented, + "Can only load thumbnails from Orthanc or DICOMweb"); + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Loaders/SeriesThumbnailsLoader.h Tue Mar 31 15:51:02 2020 +0200 @@ -0,0 +1,210 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 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 "../Oracle/GetOrthancImageCommand.h" +#include "../Oracle/HttpCommand.h" +#include "../Oracle/OracleCommandExceptionMessage.h" +#include "../Oracle/OrthancRestApiCommand.h" +#include "DicomSource.h" +#include "ILoaderFactory.h" +#include "OracleScheduler.h" + + +namespace OrthancStone +{ + enum SeriesThumbnailType + { + SeriesThumbnailType_NotLoaded = 1, // The remote server cannot decode this image + SeriesThumbnailType_Unsupported = 2, // The remote server cannot decode this image + SeriesThumbnailType_Pdf = 3, + SeriesThumbnailType_Video = 4, + SeriesThumbnailType_Image = 5 + }; + + + class SeriesThumbnailsLoader : + public IObservable, + public ObserverBase<SeriesThumbnailsLoader> + { + private: + class Thumbnail : public boost::noncopyable + { + private: + SeriesThumbnailType type_; + std::string image_; + std::string mime_; + + public: + Thumbnail(const std::string& image, + const std::string& mime); + + Thumbnail(SeriesThumbnailType type); + + SeriesThumbnailType GetType() const + { + return type_; + } + + const std::string& GetImage() const + { + return image_; + } + + const std::string& GetMime() const + { + return mime_; + } + }; + + public: + class ThumbnailLoadedMessage : public OriginMessage<SeriesThumbnailsLoader> + { + ORTHANC_STONE_MESSAGE(__FILE__, __LINE__); + + private: + const DicomSource& source_; + const std::string& studyInstanceUid_; + const std::string& seriesInstanceUid_; + const Thumbnail& thumbnail_; + + public: + ThumbnailLoadedMessage(const SeriesThumbnailsLoader& origin, + const DicomSource& source, + const std::string& studyInstanceUid, + const std::string& seriesInstanceUid, + const Thumbnail& thumbnail) : + OriginMessage(origin), + source_(source), + studyInstanceUid_(studyInstanceUid), + seriesInstanceUid_(seriesInstanceUid), + thumbnail_(thumbnail) + { + } + + const DicomSource& GetDicomSource() const + { + return source_; + } + + SeriesThumbnailType GetType() const + { + return thumbnail_.GetType(); + } + + const std::string& GetStudyInstanceUid() const + { + return studyInstanceUid_; + } + + const std::string& GetSeriesInstanceUid() const + { + return seriesInstanceUid_; + } + + const std::string& GetEncodedImage() const + { + return thumbnail_.GetImage(); + } + + const std::string& GetMime() const + { + return thumbnail_.GetMime(); + } + }; + + private: + class Handler; + class DicomWebSopClassHandler; + class DicomWebThumbnailHandler; + class ThumbnailInformation; + class OrthancSopClassHandler; + class SelectOrthancInstanceHandler; + + // Maps a "Series Instance UID" to a thumbnail + typedef std::map<std::string, Thumbnail*> Thumbnails; + + ILoadersContext& context_; + Thumbnails thumbnails_; + int priority_; + unsigned int width_; + unsigned int height_; + + void AcquireThumbnail(const DicomSource& source, + const std::string& studyInstanceUid, + const std::string& seriesInstanceUid, + Thumbnail* thumbnail /* takes ownership */); + + void Schedule(IOracleCommand* command); + + void Handle(const HttpCommand::SuccessMessage& message); + + void Handle(const OrthancRestApiCommand::SuccessMessage& message); + + void Handle(const GetOrthancImageCommand::SuccessMessage& message); + + void Handle(const OracleCommandExceptionMessage& message); + + SeriesThumbnailsLoader(ILoadersContext& context, + int priority); + + public: + class Factory : public ILoaderFactory + { + private: + int priority_; + + public: + Factory() : + priority_(0) + { + } + + void SetPriority(int priority) + { + priority_ = priority; + } + + virtual boost::shared_ptr<IObserver> Create(ILoadersContext::ILock& context); + }; + + + virtual ~SeriesThumbnailsLoader() + { + Clear(); + } + + void SetThumbnailSize(unsigned int width, + unsigned int height); + + void Clear(); + + SeriesThumbnailType GetSeriesThumbnail(std::string& image, + std::string& mime, + const std::string& seriesInstanceUid) const; + + void ScheduleLoadThumbnail(const DicomSource& source, + const std::string& patientId, + const std::string& studyInstanceUid, + const std::string& seriesInstanceUid); + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Loaders/WebAssemblyLoadersContext.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -0,0 +1,96 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 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 "WebAssemblyLoadersContext.h" + +namespace OrthancStone +{ + class WebAssemblyLoadersContext::Locker : public ILoadersContext::ILock + { + private: + WebAssemblyLoadersContext& that_; + + public: + Locker(WebAssemblyLoadersContext& that) : + that_(that) + { + } + + virtual ILoadersContext& GetContext() const ORTHANC_OVERRIDE + { + return that_; + } + + virtual IObservable& GetOracleObservable() const ORTHANC_OVERRIDE + { + return that_.oracle_.GetOracleObservable(); + } + + virtual void Schedule(boost::shared_ptr<IObserver> receiver, + int priority, + IOracleCommand* command /* Takes ownership */) ORTHANC_OVERRIDE + { + that_.scheduler_->Schedule(receiver, priority, command); + } + + virtual void CancelRequests(boost::shared_ptr<IObserver> receiver) ORTHANC_OVERRIDE + { + that_.scheduler_->CancelRequests(receiver); + } + + virtual void CancelAllRequests() ORTHANC_OVERRIDE + { + that_.scheduler_->CancelAllRequests(); + } + + virtual void AddLoader(boost::shared_ptr<IObserver> loader) ORTHANC_OVERRIDE + { + that_.loaders_.push_back(loader); + } + + virtual void GetStatistics(uint64_t& scheduledCommands, + uint64_t& processedCommands) ORTHANC_OVERRIDE + { + scheduledCommands = that_.scheduler_->GetTotalScheduled(); + processedCommands = that_.scheduler_->GetTotalProcessed(); + } + }; + + + WebAssemblyLoadersContext::WebAssemblyLoadersContext(unsigned int maxHighPriority, + unsigned int maxStandardPriority, + unsigned int maxLowPriority) + { + scheduler_ = OracleScheduler::Create(oracle_, oracle_.GetOracleObservable(), oracle_, + maxHighPriority, maxStandardPriority, maxLowPriority); + + if (!scheduler_) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + } + + + ILoadersContext::ILock* WebAssemblyLoadersContext::Lock() + { + return new Locker(*this); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Loaders/WebAssemblyLoadersContext.h Tue Mar 31 15:51:02 2020 +0200 @@ -0,0 +1,63 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 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 "ILoadersContext.h" +#include "../Oracle/WebAssemblyOracle.h" +#include "OracleScheduler.h" + +#include <list> + +namespace OrthancStone +{ + class WebAssemblyLoadersContext : public ILoadersContext + { + private: + class Locker; + + WebAssemblyOracle oracle_; + boost::shared_ptr<OracleScheduler> scheduler_; + std::list< boost::shared_ptr<IObserver> > loaders_; + + public: + WebAssemblyLoadersContext(unsigned int maxHighPriority, + unsigned int maxStandardPriority, + unsigned int maxLowPriority); + + void SetLocalOrthanc(const std::string& root) + { + oracle_.SetLocalOrthanc(root); + } + + void SetRemoteOrthanc(const Orthanc::WebServiceParameters& orthanc) + { + oracle_.SetRemoteOrthanc(orthanc); + } + + void SetDicomCacheSize(size_t size) + { + oracle_.SetDicomCacheSize(size); + } + + virtual ILock* Lock() ORTHANC_OVERRIDE; + }; +}
--- a/Framework/Messages/ICallable.h Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Messages/ICallable.h Tue Mar 31 15:51:02 2020 +0200 @@ -22,23 +22,21 @@ #pragma once #include "IMessage.h" +#include "IObserver.h" #include <Core/Logging.h> #include <boost/noncopyable.hpp> +#include <boost/weak_ptr.hpp> #include <string> #include <stdint.h> -#include <stdint.h> - -namespace OrthancStone { - - class IObserver; - +namespace OrthancStone +{ // This is referencing an object and member function that can be notified // by an IObservable. The object must derive from IO - // The member functions must be of type "void Function(const IMessage& message)" or reference a derived class of IMessage + // The member functions must be of type "void Method(const IMessage& message)" or reference a derived class of IMessage class ICallable : public boost::noncopyable { public: @@ -50,61 +48,38 @@ virtual const MessageIdentifier& GetMessageIdentifier() = 0; - virtual IObserver* GetObserver() const = 0; - }; - - template <typename TMessage> - class MessageHandler: public ICallable - { + // TODO - Is this needed? + virtual boost::weak_ptr<IObserver> GetObserver() const = 0; }; template <typename TObserver, typename TMessage> - class Callable : public MessageHandler<TMessage> + class Callable : public ICallable { private: - typedef void (TObserver::* MemberFunction) (const TMessage&); + typedef void (TObserver::* MemberMethod) (const TMessage&); - TObserver& observer_; - MemberFunction function_; - int64_t observerFingerprint_; + boost::weak_ptr<IObserver> observer_; + MemberMethod function_; public: - Callable(TObserver& observer, - MemberFunction function) : + Callable(boost::shared_ptr<TObserver> observer, + MemberMethod function) : observer_(observer), - function_(function), - observerFingerprint_(observer.GetFingerprint()) - { - } - - void ApplyInternal(const TMessage& message) + function_(function) { - int64_t currentFingerprint(observer_.GetFingerprint()); - if (observerFingerprint_ != currentFingerprint) - { - LOG(TRACE) << "The observer at address " << - std::hex << &observer_ << std::dec << - ") has a different fingerprint than the one recorded at callback " << - "registration time. This means that it is not the same object as " << - "the one recorded, even though their addresses are the same. " << - "Callback will NOT be sent!"; - LOG(TRACE) << " recorded fingerprint = " << observerFingerprint_ << - " current fingerprint = " << currentFingerprint; - } - else - { - LOG(TRACE) << "The recorded fingerprint is " << observerFingerprint_ - << " and the current fingerprint is " << currentFingerprint - << " -- callable will be called."; - (observer_.*function_) (message); - } } virtual void Apply(const IMessage& message) { - ApplyInternal(dynamic_cast<const TMessage&>(message)); + boost::shared_ptr<IObserver> lock(observer_); + if (lock) + { + TObserver& observer = dynamic_cast<TObserver&>(*lock); + const TMessage& typedMessage = dynamic_cast<const TMessage&>(message); + (observer.*function_) (typedMessage); + } } virtual const MessageIdentifier& GetMessageIdentifier() @@ -112,41 +87,9 @@ return TMessage::GetStaticIdentifier(); } - virtual IObserver* GetObserver() const + virtual boost::weak_ptr<IObserver> GetObserver() const { - return &observer_; + return observer_; } }; - -#if 0 /* __cplusplus >= 201103L*/ - -#include <functional> - - 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 IObserver* GetObserver() const - { - return &observer_; - } - }; -#endif //__cplusplus >= 201103L }
--- a/Framework/Messages/IMessage.h Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Messages/IMessage.h Tue Mar 31 15:51:02 2020 +0200 @@ -21,6 +21,7 @@ #pragma once +#include <boost/lexical_cast.hpp> #include <boost/noncopyable.hpp> #include <string.h> @@ -33,6 +34,12 @@ const char* file_; int line_; + bool IsEqual(const MessageIdentifier& other) const + { + return (line_ == other.line_ && + strcmp(file_, other.file_) == 0); + } + public: MessageIdentifier(const char* file, int line) : @@ -47,6 +54,11 @@ { } + std::string AsString() const + { + return std::string(file_) + ":" + boost::lexical_cast<std::string>(line_); + } + bool operator< (const MessageIdentifier& other) const { if (file_ == NULL) @@ -62,6 +74,16 @@ return strcmp(file_, other.file_) < 0; } } + + bool operator== (const MessageIdentifier& other) const + { + return IsEqual(other); + } + + bool operator!= (const MessageIdentifier& other) const + { + return !IsEqual(other); + } };
--- a/Framework/Messages/IMessageEmitter.h Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Messages/IMessageEmitter.h Tue Mar 31 15:51:02 2020 +0200 @@ -24,6 +24,8 @@ #include "IObserver.h" #include "IMessage.h" +#include <boost/weak_ptr.hpp> + namespace OrthancStone { /** @@ -39,7 +41,7 @@ { } - virtual void EmitMessage(const IObserver& observer, + virtual void EmitMessage(boost::weak_ptr<IObserver> observer, const IMessage& message) = 0; }; }
--- a/Framework/Messages/IObservable.cpp Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Messages/IObservable.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -21,8 +21,9 @@ #include "IObservable.h" +#include "../StoneException.h" + #include <Core/Logging.h> -#include <Core/OrthancException.h> #include <cassert> @@ -40,20 +41,10 @@ delete *it2; } } - - // unregister the forwarders but don't delete them (they'll be - // deleted by the observable they are observing as any other - // callable) - for (Forwarders::iterator it = forwarders_.begin(); - it != forwarders_.end(); ++it) - { - IMessageForwarder* fw = *it; - broker_.Unregister(dynamic_cast<IObserver&>(*fw)); - } } - void IObservable::RegisterObserverCallback(ICallable* callable) + void IObservable::RegisterCallable(ICallable* callable) { if (callable == NULL) { @@ -64,35 +55,10 @@ callables_[id].insert(callable); } - void IObservable::Unregister(IObserver *observer) - { - LOG(TRACE) << "IObservable::Unregister for IObserver at addr: " - << std::hex << observer << std::dec; - // 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) - { - LOG(TRACE) << " ** IObservable::Unregister : deleting callable: " - << std::hex << (*itCallable) << std::dec; - delete *itCallable; - itCallableSet->second.erase(itCallable++); - } - else - ++itCallable; - } - } - } - void IObservable::EmitMessageInternal(const IObserver* receiver, const IMessage& message) { - LOG(TRACE) << "IObservable::EmitMessageInternal receiver = " - << std::hex << receiver << std::dec; + //LOG(TRACE) << "IObservable::EmitMessageInternal receiver = " << std::hex << receiver << std::dec; Callables::const_iterator found = callables_.find(message.GetIdentifier()); if (found != callables_.end()) @@ -102,15 +68,36 @@ { assert(*it != NULL); - const IObserver* observer = (*it)->GetObserver(); - if (broker_.IsActive(*observer)) + boost::shared_ptr<IObserver> observer((*it)->GetObserver().lock()); + + if (observer) { if (receiver == NULL || // Are we broadcasting? - observer == receiver) // Not broadcasting, but this is the receiver + observer.get() == receiver) // Not broadcasting, but this is the receiver { - (*it)->Apply(message); + try + { + (*it)->Apply(message); + } + catch (Orthanc::OrthancException& e) + { + LOG(ERROR) << "Exception on callable: " << e.What(); + } + catch (StoneException& e) + { + LOG(ERROR) << "Exception on callable: " << e.What(); + } + catch (...) + { + LOG(ERROR) << "Native exception on callable"; + } } } + else + { + // TODO => Remove "it" from the list of callables => This + // allows to suppress the need for "Unregister()" + } } } } @@ -122,21 +109,15 @@ } - void IObservable::EmitMessage(const IObserver& observer, + void IObservable::EmitMessage(boost::weak_ptr<IObserver> observer, const IMessage& message) { - LOG(TRACE) << "IObservable::EmitMessage observer = " - << std::hex << &observer << std::dec; - EmitMessageInternal(&observer, message); - } - - void IObservable::RegisterForwarder(IMessageForwarder* forwarder) - { - if (forwarder == NULL) + //LOG(TRACE) << "IObservable::EmitMessage observer = " << std::hex << &observer << std::dec; + + boost::shared_ptr<IObserver> lock(observer.lock()); + if (lock) { - throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); + EmitMessageInternal(lock.get(), message); } - - forwarders_.insert(forwarder); } }
--- a/Framework/Messages/IObservable.h Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Messages/IObservable.h Tue Mar 31 15:51:02 2020 +0200 @@ -24,8 +24,6 @@ #include "../StoneEnumerations.h" #include "ICallable.h" #include "IObserver.h" -#include "MessageBroker.h" -#include "MessageForwarder.h" #include <set> #include <map> @@ -37,39 +35,20 @@ private: typedef std::map<MessageIdentifier, std::set<ICallable*> > Callables; - typedef std::set<IMessageForwarder*> Forwarders; - - MessageBroker& broker_; Callables callables_; - Forwarders forwarders_; void EmitMessageInternal(const IObserver* receiver, const IMessage& message); public: - IObservable(MessageBroker& broker) : - broker_(broker) - { - } - virtual ~IObservable(); - MessageBroker& GetBroker() const - { - return broker_; - } - - // Takes ownsership - void RegisterObserverCallback(ICallable* callable); - - void Unregister(IObserver* observer); + // Takes ownership of the callable + void RegisterCallable(ICallable* callable); void BroadcastMessage(const IMessage& message); - void EmitMessage(const IObserver& observer, + void EmitMessage(boost::weak_ptr<IObserver> observer, const IMessage& message); - - // Takes ownsership - void RegisterForwarder(IMessageForwarder* forwarder); }; }
--- a/Framework/Messages/IObserver.cpp Tue Mar 31 15:47:29 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,94 +0,0 @@ -/** - * Stone of Orthanc - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2020 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 "IObserver.h" - -#include "IMessage.h" -#include "../StoneException.h" - -#include <Core/Logging.h> -#include <Core/Toolbox.h> - -namespace OrthancStone -{ - static const uint64_t IObserver_FIRST_UNIQUE_ID = 10973; - static uint64_t IObserver_nextUniqueId = IObserver_FIRST_UNIQUE_ID; - - IObserver::IObserver(MessageBroker& broker) - : broker_(broker) - { - AssignFingerprint(); - broker_.Register(*this); - } - - IObserver::~IObserver() - { - try - { - LOG(TRACE) << "IObserver(" << std::hex << this << std::dec << ")::~IObserver : fingerprint_[0] == " << fingerprint_[0]; - fingerprint_[0] = 0xdeadbeef; - fingerprint_[1] = 0xdeadbeef; - fingerprint_[2] = 0xdeadbeef; - broker_.Unregister(*this); - } - catch (const Orthanc::OrthancException& e) - { - if (e.HasDetails()) - { - LOG(ERROR) << "OrthancException in ~IObserver: " << e.What() << " Details: " << e.GetDetails(); - } - else - { - LOG(ERROR) << "OrthancException in ~IObserver: " << e.What(); - } - } - catch (const std::exception& e) - { - LOG(ERROR) << "std::exception in ~IObserver: " << e.what(); - } - catch (...) - { - LOG(ERROR) << "Unknown exception in ~IObserver"; - } - } - - static const int64_t IObserver_UNIQUE_ID_MAGIC_NUMBER = 2742024; - - void IObserver::AssignFingerprint() - { - fingerprint_[0] = IObserver_nextUniqueId; - fingerprint_[1] = fingerprint_[0] / 2; - fingerprint_[2] = fingerprint_[1] + IObserver_UNIQUE_ID_MAGIC_NUMBER; - IObserver_nextUniqueId++; - } - - bool IObserver::DoesFingerprintLookGood() const - { - bool ok = (fingerprint_[0] >= IObserver_FIRST_UNIQUE_ID) && - (fingerprint_[1] == fingerprint_[0] / 2) && - (fingerprint_[2] == fingerprint_[1] + IObserver_UNIQUE_ID_MAGIC_NUMBER); - if(!ok) - { - LOG(INFO) << "Fingerprint not valid: " << " fingerprint_[0] = " << fingerprint_[0] << " fingerprint_[1] = " << fingerprint_[1]<< " fingerprint_[2] = " << fingerprint_[2]; - } - return ok; - } -}
--- a/Framework/Messages/IObserver.h Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Messages/IObserver.h Tue Mar 31 15:51:02 2020 +0200 @@ -21,7 +21,7 @@ #pragma once -#include "MessageBroker.h" +#include <boost/noncopyable.hpp> #include <stdint.h> @@ -29,29 +29,13 @@ { class IObserver : public boost::noncopyable { - private: - MessageBroker& broker_; - // the following is a int64_t with some checks that is used to - // disambiguate different observers that may have the same address - int64_t fingerprint_[3]; - - void AssignFingerprint(); - public: - IObserver(MessageBroker& broker); - - virtual ~IObserver(); - - int64_t GetFingerprint() const + IObserver() { - return fingerprint_[0]; } - bool DoesFingerprintLookGood() const; - - MessageBroker& GetBroker() const + virtual ~IObserver() { - return broker_; } }; }
--- a/Framework/Messages/LockingEmitter.h Tue Mar 31 15:47:29 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,114 +0,0 @@ -/** - * Stone of Orthanc - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2020 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 <Core/Enumerations.h> -#include <Core/OrthancException.h> - -#include "IMessageEmitter.h" -#include "IObservable.h" - -#include <boost/thread.hpp> - -namespace OrthancStone -{ - /** - * This class is used when using the ThreadedOracle : since messages - * can be sent from multiple Oracle threads, this IMessageEmitter - * implementation serializes the callbacks. - * - * The internal mutex used in Oracle messaging can also be used to - * protect the application data. Thus, this class can be used as a single - * application-wide mutex. - */ - class LockingEmitter : public IMessageEmitter - { - private: - boost::shared_mutex mutex_; - MessageBroker broker_; - IObservable oracleObservable_; - - public: - LockingEmitter() : - oracleObservable_(broker_) - { - } - - MessageBroker& GetBroker() - { - return broker_; - } - - virtual void EmitMessage(const IObserver& observer, - const IMessage& message) ORTHANC_OVERRIDE - { - try - { - boost::unique_lock<boost::shared_mutex> 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: - LockingEmitter& that_; - boost::shared_lock<boost::shared_mutex> lock_; - - public: - ReaderLock(LockingEmitter& that) : - that_(that), - lock_(that.mutex_) - { - } - }; - - - class WriterLock : public boost::noncopyable - { - private: - LockingEmitter& that_; - boost::unique_lock<boost::shared_mutex> lock_; - - public: - WriterLock(LockingEmitter& that) : - that_(that), - lock_(that.mutex_) - { - } - - MessageBroker& GetBroker() - { - return that_.broker_; - } - - IObservable& GetOracleObservable() - { - return that_.oracleObservable_; - } - }; - }; -}
--- a/Framework/Messages/MessageBroker.h Tue Mar 31 15:47:29 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,61 +0,0 @@ -/** - * Stone of Orthanc - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2020 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 "boost/noncopyable.hpp" - -#include <set> - -namespace OrthancStone -{ - class IObserver; - - /* - * 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 deleted observer. - */ - class MessageBroker : public boost::noncopyable - { - private: - std::set<const IObserver*> activeObservers_; // the list of observers that are currently alive (that have not been deleted) - - public: - MessageBroker() - { - } - - void Register(const IObserver& observer) - { - activeObservers_.insert(&observer); - } - - void Unregister(const IObserver& observer) - { - activeObservers_.erase(&observer); - } - - bool IsActive(const IObserver& observer) - { - return activeObservers_.find(&observer) != activeObservers_.end(); - } - }; -}
--- a/Framework/Messages/MessageForwarder.cpp Tue Mar 31 15:47:29 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,38 +0,0 @@ -/** - * Stone of Orthanc - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2020 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 "MessageForwarder.h" - -#include "IObservable.h" - -namespace OrthancStone -{ - - void IMessageForwarder::ForwardMessageInternal(const IMessage& message) - { - emitter_.BroadcastMessage(message); - } - - void IMessageForwarder::RegisterForwarderInEmitter() - { - emitter_.RegisterForwarder(this); - } -}
--- a/Framework/Messages/MessageForwarder.h Tue Mar 31 15:47:29 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,87 +0,0 @@ -/** - * Stone of Orthanc - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2020 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 "ICallable.h" -#include "IObserver.h" - -#include <boost/noncopyable.hpp> - -namespace OrthancStone -{ - - class IObservable; - - class IMessageForwarder : public IObserver - { - IObservable& emitter_; - public: - IMessageForwarder(MessageBroker& broker, IObservable& emitter) - : IObserver(broker), - emitter_(emitter) - {} - virtual ~IMessageForwarder() {} - - protected: - void ForwardMessageInternal(const IMessage& message); - void RegisterForwarderInEmitter(); - - }; - - /* When an Observer (B) simply needs to re-emit a message it has received, instead of implementing - * a specific member function to forward the message, it can create a MessageForwarder. - * The MessageForwarder will re-emit the message "in the name of (B)" - * - * Consider the chain where - * A is an observable - * | - * B is an observer of A and observable - * | - * 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 - * - * in 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> - { - public: - MessageForwarder(MessageBroker& broker, - IObservable& emitter // the object that will emit the messages to forward - ) - : IMessageForwarder(broker, emitter), - Callable<MessageForwarder<TMessage>, TMessage>(*this, &MessageForwarder::ForwardMessage) - { - RegisterForwarderInEmitter(); - } - -protected: - void ForwardMessage(const TMessage& message) - { - ForwardMessageInternal(message); - } - - }; -}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Messages/ObserverBase.h Tue Mar 31 15:51:02 2020 +0200 @@ -0,0 +1,68 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 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 "ICallable.h" +#include "IObserver.h" +#include "IObservable.h" + +#include <Core/OrthancException.h> + +#include <boost/enable_shared_from_this.hpp> + +namespace OrthancStone +{ + template <typename TObserver> + class ObserverBase : + public IObserver, + public boost::enable_shared_from_this<TObserver> + { + public: + boost::shared_ptr<TObserver> GetSharedObserver() + { + try + { + return this->shared_from_this(); + } + catch (boost::bad_weak_ptr&) + { + throw Orthanc::OrthancException( + Orthanc::ErrorCode_InternalError, + "Cannot get a shared pointer to an observer from its constructor, " + "or the observer is not created as a shared pointer"); + } + } + + template <typename TMessage> + ICallable* CreateCallable(void (TObserver::* MemberMethod) (const TMessage&)) + { + return new Callable<TObserver, TMessage>(GetSharedObserver(), MemberMethod); + } + + template <typename TMessage> + void Register(IObservable& observable, + void (TObserver::* MemberMethod) (const TMessage&)) + { + observable.RegisterCallable(CreateCallable(MemberMethod)); + } + }; +}
--- a/Framework/OpenGL/OpenGLIncludes.h Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/OpenGL/OpenGLIncludes.h Tue Mar 31 15:51:02 2020 +0200 @@ -32,6 +32,8 @@ #if defined(__APPLE__) # include <OpenGL/gl.h> # include <OpenGL/glext.h> +#elif defined(QT_VERSION_MAJOR) && (QT_VERSION >= 5) +// Qt5 takes care of the inclusions #elif defined(_WIN32) // On Windows, use the compatibility headers provided by glew # include <GL/glew.h>
--- a/Framework/OpenGL/SdlOpenGLContext.cpp Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/OpenGL/SdlOpenGLContext.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -68,6 +68,7 @@ GLenum err = glewInit(); if (GLEW_OK != err) { + LOG(ERROR) << glewGetErrorString(err); throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError, "Cannot initialize glew"); }
--- a/Framework/OpenGL/SdlOpenGLContext.h Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/OpenGL/SdlOpenGLContext.h Tue Mar 31 15:51:02 2020 +0200 @@ -26,6 +26,8 @@ #include "IOpenGLContext.h" #include "../Viewport/SdlWindow.h" +#include <SDL_render.h> + #include <Core/Enumerations.h> namespace OrthancStone @@ -62,6 +64,11 @@ virtual unsigned int GetCanvasWidth() const ORTHANC_OVERRIDE; virtual unsigned int GetCanvasHeight() const ORTHANC_OVERRIDE; + + void ToggleMaximize() + { + window_.ToggleMaximize(); + } }; }
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Oracle/GenericOracleRunner.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -0,0 +1,521 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 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 "GenericOracleRunner.h" + +#if !defined(ORTHANC_ENABLE_DCMTK) +# error The macro ORTHANC_ENABLE_DCMTK must be defined +#endif + +#include "GetOrthancImageCommand.h" +#include "GetOrthancWebViewerJpegCommand.h" +#include "HttpCommand.h" +#include "OracleCommandExceptionMessage.h" +#include "OrthancRestApiCommand.h" +#include "ParseDicomFromFileCommand.h" +#include "ParseDicomFromWadoCommand.h" +#include "ReadFileCommand.h" + +#if ORTHANC_ENABLE_DCMTK == 1 +# include "ParseDicomSuccessMessage.h" +# include <dcmtk/dcmdata/dcdeftag.h> +# include <dcmtk/dcmdata/dcfilefo.h> +static unsigned int BUCKET_DICOMDIR = 0; +static unsigned int BUCKET_SOP = 1; +#endif + +#include <Core/Compression/GzipCompressor.h> +#include <Core/HttpClient.h> +#include <Core/OrthancException.h> +#include <Core/Toolbox.h> +#include <Core/SystemToolbox.h> + +#include <boost/filesystem.hpp> + + + +namespace OrthancStone +{ + static void CopyHttpHeaders(Orthanc::HttpClient& client, + const Orthanc::HttpClient::HttpHeaders& headers) + { + for (Orthanc::HttpClient::HttpHeaders::const_iterator + it = headers.begin(); it != headers.end(); it++ ) + { + client.AddHeader(it->first, it->second); + } + } + + + static void DecodeAnswer(std::string& answer, + const Orthanc::HttpClient::HttpHeaders& headers) + { + Orthanc::HttpCompression contentEncoding = Orthanc::HttpCompression_None; + + for (Orthanc::HttpClient::HttpHeaders::const_iterator it = headers.begin(); + it != headers.end(); ++it) + { + std::string s; + Orthanc::Toolbox::ToLowerCase(s, it->first); + + if (s == "content-encoding") + { + if (it->second == "gzip") + { + contentEncoding = Orthanc::HttpCompression_Gzip; + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol, + "Unsupported HTTP Content-Encoding: " + it->second); + } + + break; + } + } + + if (contentEncoding == Orthanc::HttpCompression_Gzip) + { + std::string compressed; + answer.swap(compressed); + + Orthanc::GzipCompressor compressor; + compressor.Uncompress(answer, compressed.c_str(), compressed.size()); + + LOG(INFO) << "Uncompressing gzip Encoding: from " << compressed.size() + << " to " << answer.size() << " bytes"; + } + } + + + static void RunHttpCommand(std::string& answer, + Orthanc::HttpClient::HttpHeaders& answerHeaders, + const HttpCommand& command) + { + Orthanc::HttpClient client; + client.SetUrl(command.GetUrl()); + client.SetMethod(command.GetMethod()); + client.SetTimeout(command.GetTimeout()); + + CopyHttpHeaders(client, command.GetHttpHeaders()); + + if (command.HasCredentials()) + { + client.SetCredentials(command.GetUsername().c_str(), command.GetPassword().c_str()); + } + + if (command.GetMethod() == Orthanc::HttpMethod_Post || + command.GetMethod() == Orthanc::HttpMethod_Put) + { + client.SetBody(command.GetBody()); + } + + client.ApplyAndThrowException(answer, answerHeaders); + DecodeAnswer(answer, answerHeaders); + } + + + static void RunInternal(boost::weak_ptr<IObserver> receiver, + IMessageEmitter& emitter, + const HttpCommand& command) + { + std::string answer; + Orthanc::HttpClient::HttpHeaders answerHeaders; + RunHttpCommand(answer, answerHeaders, command); + + HttpCommand::SuccessMessage message(command, answerHeaders, answer); + emitter.EmitMessage(receiver, message); + } + + + static void RunOrthancRestApiCommand(std::string& answer, + Orthanc::HttpClient::HttpHeaders& answerHeaders, + const Orthanc::WebServiceParameters& orthanc, + const OrthancRestApiCommand& command) + { + Orthanc::HttpClient client(orthanc, command.GetUri()); + client.SetRedirectionFollowed(false); + client.SetMethod(command.GetMethod()); + client.SetTimeout(command.GetTimeout()); + + CopyHttpHeaders(client, command.GetHttpHeaders()); + + if (command.GetMethod() == Orthanc::HttpMethod_Post || + command.GetMethod() == Orthanc::HttpMethod_Put) + { + client.SetBody(command.GetBody()); + } + + client.ApplyAndThrowException(answer, answerHeaders); + DecodeAnswer(answer, answerHeaders); + } + + + static void RunInternal(boost::weak_ptr<IObserver> receiver, + IMessageEmitter& emitter, + const Orthanc::WebServiceParameters& orthanc, + const OrthancRestApiCommand& command) + { + std::string answer; + Orthanc::HttpClient::HttpHeaders answerHeaders; + RunOrthancRestApiCommand(answer, answerHeaders, orthanc, command); + + OrthancRestApiCommand::SuccessMessage message(command, answerHeaders, answer); + emitter.EmitMessage(receiver, message); + } + + + static void RunInternal(boost::weak_ptr<IObserver> receiver, + IMessageEmitter& emitter, + const Orthanc::WebServiceParameters& orthanc, + const GetOrthancImageCommand& command) + { + Orthanc::HttpClient client(orthanc, command.GetUri()); + client.SetRedirectionFollowed(false); + client.SetTimeout(command.GetTimeout()); + + CopyHttpHeaders(client, command.GetHttpHeaders()); + + std::string answer; + Orthanc::HttpClient::HttpHeaders answerHeaders; + client.ApplyAndThrowException(answer, answerHeaders); + + DecodeAnswer(answer, answerHeaders); + + command.ProcessHttpAnswer(receiver, emitter, answer, answerHeaders); + } + + + static void RunInternal(boost::weak_ptr<IObserver> receiver, + IMessageEmitter& emitter, + const Orthanc::WebServiceParameters& orthanc, + const GetOrthancWebViewerJpegCommand& command) + { + Orthanc::HttpClient client(orthanc, command.GetUri()); + client.SetRedirectionFollowed(false); + client.SetTimeout(command.GetTimeout()); + + CopyHttpHeaders(client, command.GetHttpHeaders()); + + std::string answer; + Orthanc::HttpClient::HttpHeaders answerHeaders; + client.ApplyAndThrowException(answer, answerHeaders); + + DecodeAnswer(answer, answerHeaders); + + command.ProcessHttpAnswer(receiver, emitter, answer); + } + + + static std::string GetPath(const std::string& root, + const std::string& file) + { + boost::filesystem::path a(root); + boost::filesystem::path b(file); + + boost::filesystem::path c; + if (b.is_absolute()) + { + c = b; + } + else + { + c = a / b; + } + + return c.string(); + } + + + static void RunInternal(boost::weak_ptr<IObserver> receiver, + IMessageEmitter& emitter, + const std::string& root, + const ReadFileCommand& command) + { + std::string path = GetPath(root, command.GetPath()); + LOG(TRACE) << "Oracle reading file: " << path; + + std::string content; + Orthanc::SystemToolbox::ReadFile(content, path, true /* log */); + + ReadFileCommand::SuccessMessage message(command, content); + emitter.EmitMessage(receiver, message); + } + + +#if ORTHANC_ENABLE_DCMTK == 1 + static Orthanc::ParsedDicomFile* ParseDicom(uint64_t& fileSize, /* OUT */ + const std::string& path, + bool isPixelData) + { + if (!Orthanc::SystemToolbox::IsRegularFile(path)) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_InexistentFile); + } + + LOG(TRACE) << "Parsing DICOM file, " << (isPixelData ? "with" : "without") + << " pixel data: " << path; + + boost::posix_time::ptime start = boost::posix_time::microsec_clock::local_time(); + + fileSize = Orthanc::SystemToolbox::GetFileSize(path); + + // Check for 32bit systems + if (fileSize != static_cast<uint64_t>(static_cast<size_t>(fileSize))) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotEnoughMemory); + } + + DcmFileFormat dicom; + bool ok; + + if (isPixelData) + { + ok = dicom.loadFile(path.c_str()).good(); + } + else + { +#if DCMTK_VERSION_NUMBER >= 362 + /** + * NB : We could stop at (0x3007, 0x0000) instead of + * DCM_PixelData as the Stone framework does not use further + * tags (cf. the Orthanc::DICOM_TAG_* constants), but we still + * use "PixelData" as this does not change the runtime much, and + * as it is more explicit. + **/ + static const DcmTagKey STOP = DCM_PixelData; + //static const DcmTagKey STOP(0x3007, 0x0000); + + ok = dicom.loadFileUntilTag(path.c_str(), EXS_Unknown, EGL_noChange, + DCM_MaxReadLength, ERM_autoDetect, STOP).good(); +#else + // The primitive "loadFileUntilTag" was introduced in DCMTK 3.6.2 + ok = dicom.loadFile(path.c_str()).good(); +#endif + } + + if (ok) + { + std::unique_ptr<Orthanc::ParsedDicomFile> result(new Orthanc::ParsedDicomFile(dicom)); + + boost::posix_time::ptime end = boost::posix_time::microsec_clock::local_time(); + LOG(TRACE) << path << ": parsed in " << (end-start).total_milliseconds() << " ms"; + + return result.release(); + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, + "Cannot parse file: " + path); + } + } + + + static void RunInternal(boost::weak_ptr<IObserver> receiver, + IMessageEmitter& emitter, + boost::shared_ptr<ParsedDicomCache> cache, + const std::string& root, + const ParseDicomFromFileCommand& command) + { + const std::string path = GetPath(root, command.GetPath()); + + if (cache) + { + ParsedDicomCache::Reader reader(*cache, BUCKET_DICOMDIR, path); + if (reader.IsValid() && + (!command.IsPixelDataIncluded() || + reader.HasPixelData())) + { + // Reuse the DICOM file from the cache + ParseDicomSuccessMessage message(command, reader.GetDicom(), reader.GetFileSize(), reader.HasPixelData()); + emitter.EmitMessage(receiver, message); + return; + } + } + + uint64_t fileSize; + std::unique_ptr<Orthanc::ParsedDicomFile> parsed(ParseDicom(fileSize, path, command.IsPixelDataIncluded())); + + if (fileSize != static_cast<size_t>(fileSize)) + { + // Cannot load such a large file on 32-bit architecture + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotEnoughMemory); + } + + { + ParseDicomSuccessMessage message + (command, *parsed, static_cast<size_t>(fileSize), command.IsPixelDataIncluded()); + emitter.EmitMessage(receiver, message); + } + + if (cache) + { + // Store it into the cache for future use + + // Invalidate to overwrite DICOM instance that would already + // be stored without pixel data + cache->Invalidate(BUCKET_DICOMDIR, path); + + cache->Acquire(BUCKET_DICOMDIR, path, parsed.release(), + static_cast<size_t>(fileSize), command.IsPixelDataIncluded()); + } + } + + + static void RunInternal(boost::weak_ptr<IObserver> receiver, + IMessageEmitter& emitter, + boost::shared_ptr<ParsedDicomCache> cache, + const Orthanc::WebServiceParameters& orthanc, + const ParseDicomFromWadoCommand& command) + { + if (cache) + { + ParsedDicomCache::Reader reader(*cache, BUCKET_SOP, command.GetSopInstanceUid()); + if (reader.IsValid() && + reader.HasPixelData()) + { + // Reuse the DICOM file from the cache + ParseDicomSuccessMessage message(command, reader.GetDicom(), reader.GetFileSize(), reader.HasPixelData()); + emitter.EmitMessage(receiver, message); + return; + } + } + + std::string answer; + Orthanc::HttpClient::HttpHeaders answerHeaders; + + switch (command.GetRestCommand().GetType()) + { + case IOracleCommand::Type_Http: + RunHttpCommand(answer, answerHeaders, dynamic_cast<const HttpCommand&>(command.GetRestCommand())); + break; + + case IOracleCommand::Type_OrthancRestApi: + RunOrthancRestApiCommand(answer, answerHeaders, orthanc, + dynamic_cast<const OrthancRestApiCommand&>(command.GetRestCommand())); + break; + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); + } + + size_t fileSize; + std::unique_ptr<Orthanc::ParsedDicomFile> parsed(ParseDicomSuccessMessage::ParseWadoAnswer(fileSize, answer, answerHeaders)); + + { + ParseDicomSuccessMessage message(command, *parsed, fileSize, + true /* pixel data always is included in WADO-RS */); + emitter.EmitMessage(receiver, message); + } + + if (cache) + { + // Store it into the cache for future use + cache->Acquire(BUCKET_SOP, command.GetSopInstanceUid(), parsed.release(), fileSize, true); + } + } +#endif + + + void GenericOracleRunner::Run(boost::weak_ptr<IObserver> receiver, + IMessageEmitter& emitter, + const IOracleCommand& command) + { + Orthanc::ErrorCode error = Orthanc::ErrorCode_Success; + + try + { + switch (command.GetType()) + { + case IOracleCommand::Type_Sleep: + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadParameterType, + "Sleep command cannot be executed by the runner"); + + case IOracleCommand::Type_Http: + RunInternal(receiver, emitter, dynamic_cast<const HttpCommand&>(command)); + break; + + case IOracleCommand::Type_OrthancRestApi: + RunInternal(receiver, emitter, orthanc_, + dynamic_cast<const OrthancRestApiCommand&>(command)); + break; + + case IOracleCommand::Type_GetOrthancImage: + RunInternal(receiver, emitter, orthanc_, + dynamic_cast<const GetOrthancImageCommand&>(command)); + break; + + case IOracleCommand::Type_GetOrthancWebViewerJpeg: + RunInternal(receiver, emitter, orthanc_, + dynamic_cast<const GetOrthancWebViewerJpegCommand&>(command)); + break; + + case IOracleCommand::Type_ReadFile: + RunInternal(receiver, emitter, rootDirectory_, + dynamic_cast<const ReadFileCommand&>(command)); + break; + + case IOracleCommand::Type_ParseDicomFromFile: + case IOracleCommand::Type_ParseDicomFromWado: +#if ORTHANC_ENABLE_DCMTK == 1 + switch (command.GetType()) + { + case IOracleCommand::Type_ParseDicomFromFile: + RunInternal(receiver, emitter, dicomCache_, rootDirectory_, + dynamic_cast<const ParseDicomFromFileCommand&>(command)); + break; + + case IOracleCommand::Type_ParseDicomFromWado: + RunInternal(receiver, emitter, dicomCache_, orthanc_, + dynamic_cast<const ParseDicomFromWadoCommand&>(command)); + break; + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + break; +#else + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented, + "DCMTK must be enabled to parse DICOM files"); +#endif + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); + } + } + catch (Orthanc::OrthancException& e) + { + LOG(ERROR) << "Exception within the oracle: " << e.What(); + error = e.GetErrorCode(); + } + catch (...) + { + LOG(ERROR) << "Threaded exception within the oracle"; + error = Orthanc::ErrorCode_InternalError; + } + + if (error != Orthanc::ErrorCode_Success) + { + OracleCommandExceptionMessage message(command, error); + emitter.EmitMessage(receiver, message); + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Oracle/GenericOracleRunner.h Tue Mar 31 15:51:02 2020 +0200 @@ -0,0 +1,87 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 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 + +#if !defined(ORTHANC_ENABLE_DCMTK) +# error The macro ORTHANC_ENABLE_DCMTK must be defined +#endif + +#if ORTHANC_ENABLE_DCMTK == 1 +# include "../Toolbox/ParsedDicomCache.h" +#endif + +#include "IOracleCommand.h" +#include "../Messages/IMessageEmitter.h" + +#include <Core/Enumerations.h> // For ORTHANC_OVERRIDE +#include <Core/WebServiceParameters.h> + +namespace OrthancStone +{ + class GenericOracleRunner : public boost::noncopyable + { + private: + Orthanc::WebServiceParameters orthanc_; + std::string rootDirectory_; + +#if ORTHANC_ENABLE_DCMTK == 1 + boost::shared_ptr<ParsedDicomCache> dicomCache_; +#endif + + public: + GenericOracleRunner() : + rootDirectory_(".") + { + } + + void SetOrthanc(const Orthanc::WebServiceParameters& orthanc) + { + orthanc_ = orthanc; + } + + const Orthanc::WebServiceParameters& GetOrthanc() const + { + return orthanc_; + } + + void SetRootDirectory(const std::string& rootDirectory) + { + rootDirectory_ = rootDirectory; + } + + const std::string GetRootDirectory() const + { + return rootDirectory_; + } + +#if ORTHANC_ENABLE_DCMTK == 1 + void SetDicomCache(boost::shared_ptr<ParsedDicomCache> cache) + { + dicomCache_ = cache; + } +#endif + + void Run(boost::weak_ptr<IObserver> receiver, + IMessageEmitter& emitter, + const IOracleCommand& command); + }; +}
--- a/Framework/Oracle/GetOrthancImageCommand.cpp Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Oracle/GetOrthancImageCommand.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -29,20 +29,6 @@ namespace OrthancStone { - GetOrthancImageCommand::SuccessMessage::SuccessMessage(const GetOrthancImageCommand& command, - Orthanc::ImageAccessor* image, // Takes ownership - Orthanc::MimeType mime) : - OriginMessage(command), - image_(image), - mime_(mime) - { - if (image == NULL) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); - } - } - - GetOrthancImageCommand::GetOrthancImageCommand() : uri_("/"), timeout_(600), @@ -58,35 +44,59 @@ } - void GetOrthancImageCommand::SetInstanceUri(const std::string& instance, - Orthanc::PixelFormat pixelFormat) + static std::string GetFormatSuffix(Orthanc::PixelFormat pixelFormat) { - uri_ = "/instances/" + instance; - switch (pixelFormat) { case Orthanc::PixelFormat_RGB24: - uri_ += "/preview"; - break; + return "preview"; case Orthanc::PixelFormat_Grayscale16: - uri_ += "/image-uint16"; - break; + return "image-uint16"; case Orthanc::PixelFormat_SignedGrayscale16: - uri_ += "/image-int16"; - break; + return "image-int16"; default: throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); } } - void GetOrthancImageCommand::ProcessHttpAnswer(IMessageEmitter& emitter, - const IObserver& receiver, + + void GetOrthancImageCommand::SetInstanceUri(const std::string& instance, + Orthanc::PixelFormat pixelFormat) + { + uri_ = "/instances/" + instance + "/" + GetFormatSuffix(pixelFormat); + } + + + void GetOrthancImageCommand::SetFrameUri(const std::string& instance, + unsigned int frame, + Orthanc::PixelFormat pixelFormat) + { + uri_ = ("/instances/" + instance + "/frames/" + + boost::lexical_cast<std::string>(frame) + "/" + GetFormatSuffix(pixelFormat)); + } + + + void GetOrthancImageCommand::ProcessHttpAnswer(boost::weak_ptr<IObserver> receiver, + IMessageEmitter& emitter, const std::string& answer, const HttpHeaders& answerHeaders) const { + for (HttpHeaders::const_iterator it = answerHeaders.begin(); it != answerHeaders.end(); ++it) + { + std::string key = Orthanc::Toolbox::StripSpaces(it->first); + Orthanc::Toolbox::ToLowerCase(key); + + if (key == "content-disposition" && + it->second == "filename=\"unsupported.png\"") + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageFormat, + "Orthanc cannot decode this image"); + } + } + Orthanc::MimeType contentType = Orthanc::MimeType_Binary; for (HttpHeaders::const_iterator it = answerHeaders.begin(); @@ -147,7 +157,7 @@ } } - SuccessMessage message(*this, image.release(), contentType); + SuccessMessage message(*this, *image, contentType); emitter.EmitMessage(receiver, message); } }
--- a/Framework/Oracle/GetOrthancImageCommand.h Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Oracle/GetOrthancImageCommand.h Tue Mar 31 15:51:02 2020 +0200 @@ -22,7 +22,7 @@ #pragma once #include "../Messages/IMessageEmitter.h" -#include "OracleCommandWithPayload.h" +#include "OracleCommandBase.h" #include <Core/Images/ImageAccessor.h> @@ -30,7 +30,7 @@ namespace OrthancStone { - class GetOrthancImageCommand : public OracleCommandWithPayload + class GetOrthancImageCommand : public OracleCommandBase { public: typedef std::map<std::string, std::string> HttpHeaders; @@ -40,17 +40,22 @@ ORTHANC_STONE_MESSAGE(__FILE__, __LINE__); private: - std::unique_ptr<Orthanc::ImageAccessor> image_; - Orthanc::MimeType mime_; + const Orthanc::ImageAccessor& image_; + Orthanc::MimeType mime_; public: SuccessMessage(const GetOrthancImageCommand& command, - Orthanc::ImageAccessor* image, // Takes ownership - Orthanc::MimeType mime); + const Orthanc::ImageAccessor& image, + Orthanc::MimeType mime) : + OriginMessage(command), + image_(image), + mime_(mime) + { + } const Orthanc::ImageAccessor& GetImage() const { - return *image_; + return image_; } Orthanc::MimeType GetMimeType() const @@ -67,6 +72,15 @@ bool hasExpectedFormat_; Orthanc::PixelFormat expectedFormat_; + GetOrthancImageCommand(const GetOrthancImageCommand& other) : + uri_(other.uri_), + headers_(other.headers_), + timeout_(other.timeout_), + hasExpectedFormat_(other.hasExpectedFormat_), + expectedFormat_(other.expectedFormat_) + { + } + public: GetOrthancImageCommand(); @@ -75,6 +89,11 @@ return Type_GetOrthancImage; } + virtual IOracleCommand* Clone() const + { + return new GetOrthancImageCommand(*this); + } + void SetExpectedPixelFormat(Orthanc::PixelFormat format); void SetUri(const std::string& uri) @@ -85,6 +104,10 @@ void SetInstanceUri(const std::string& instance, Orthanc::PixelFormat pixelFormat); + void SetFrameUri(const std::string& instance, + unsigned int frame, + Orthanc::PixelFormat pixelFormat); + void SetHttpHeader(const std::string& key, const std::string& value) { @@ -111,8 +134,8 @@ return timeout_; } - void ProcessHttpAnswer(IMessageEmitter& emitter, - const IObserver& receiver, + void ProcessHttpAnswer(boost::weak_ptr<IObserver> receiver, + IMessageEmitter& emitter, const std::string& answer, const HttpHeaders& answerHeaders) const; };
--- a/Framework/Oracle/GetOrthancWebViewerJpegCommand.cpp Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Oracle/GetOrthancWebViewerJpegCommand.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -29,23 +29,16 @@ #include <Core/OrthancException.h> #include <Core/Toolbox.h> +#ifdef _MSC_VER +// 'Json::Reader': Use CharReader and CharReaderBuilder instead +#pragma warning(disable:4996) +#endif + #include <json/reader.h> #include <json/value.h> namespace OrthancStone { - GetOrthancWebViewerJpegCommand::SuccessMessage::SuccessMessage(const GetOrthancWebViewerJpegCommand& command, - Orthanc::ImageAccessor* image) : // Takes ownership - OriginMessage(command), - image_(image) - { - if (image == NULL) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); - } - } - - GetOrthancWebViewerJpegCommand::GetOrthancWebViewerJpegCommand() : frame_(0), quality_(95), @@ -76,8 +69,8 @@ } - void GetOrthancWebViewerJpegCommand::ProcessHttpAnswer(IMessageEmitter& emitter, - const IObserver& receiver, + void GetOrthancWebViewerJpegCommand::ProcessHttpAnswer(boost::weak_ptr<IObserver> receiver, + IMessageEmitter& emitter, const std::string& answer) const { // This code comes from older "OrthancSlicesLoader::ParseSliceImageJpeg()" @@ -149,7 +142,7 @@ } else { - SuccessMessage message(*this, reader.release()); + SuccessMessage message(*this, *reader); emitter.EmitMessage(receiver, message); return; } @@ -168,7 +161,7 @@ } else { - SuccessMessage message(*this, reader.release()); + SuccessMessage message(*this, *reader); emitter.EmitMessage(receiver, message); return; } @@ -210,8 +203,8 @@ float offset = static_cast<float>(stretchLow) / scaling; Orthanc::ImageProcessing::ShiftScale(*image, offset, scaling, true); } - - SuccessMessage message(*this, image.release()); + + SuccessMessage message(*this, *image); emitter.EmitMessage(receiver, message); } }
--- a/Framework/Oracle/GetOrthancWebViewerJpegCommand.h Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Oracle/GetOrthancWebViewerJpegCommand.h Tue Mar 31 15:51:02 2020 +0200 @@ -22,7 +22,7 @@ #pragma once #include "../Messages/IMessageEmitter.h" -#include "OracleCommandWithPayload.h" +#include "OracleCommandBase.h" #include <Core/Images/ImageAccessor.h> @@ -30,7 +30,7 @@ namespace OrthancStone { - class GetOrthancWebViewerJpegCommand : public OracleCommandWithPayload + class GetOrthancWebViewerJpegCommand : public OracleCommandBase { public: typedef std::map<std::string, std::string> HttpHeaders; @@ -40,15 +40,19 @@ ORTHANC_STONE_MESSAGE(__FILE__, __LINE__); private: - std::unique_ptr<Orthanc::ImageAccessor> image_; + const Orthanc::ImageAccessor& image_; public: SuccessMessage(const GetOrthancWebViewerJpegCommand& command, - Orthanc::ImageAccessor* image); // Takes ownership + const Orthanc::ImageAccessor& image) : + OriginMessage(command), + image_(image) + { + } const Orthanc::ImageAccessor& GetImage() const { - return *image_; + return image_; } }; @@ -60,6 +64,16 @@ unsigned int timeout_; Orthanc::PixelFormat expectedFormat_; + GetOrthancWebViewerJpegCommand(const GetOrthancWebViewerJpegCommand& other) : + instanceId_(other.instanceId_), + frame_(other.frame_), + quality_(other.quality_), + headers_(other.headers_), + timeout_(other.timeout_), + expectedFormat_(other.expectedFormat_) + { + } + public: GetOrthancWebViewerJpegCommand(); @@ -68,6 +82,11 @@ return Type_GetOrthancWebViewerJpeg; } + virtual IOracleCommand* Clone() const + { + return new GetOrthancWebViewerJpegCommand(*this); + } + void SetExpectedPixelFormat(Orthanc::PixelFormat format) { expectedFormat_ = format; @@ -128,8 +147,8 @@ std::string GetUri() const; - void ProcessHttpAnswer(IMessageEmitter& emitter, - const IObserver& receiver, + void ProcessHttpAnswer(boost::weak_ptr<IObserver> receiver, + IMessageEmitter& emitter, const std::string& answer) const; }; }
--- a/Framework/Oracle/HttpCommand.cpp Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Oracle/HttpCommand.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -23,21 +23,16 @@ #include <Core/OrthancException.h> +#ifdef _MSC_VER +// 'Json::Reader': Use CharReader and CharReaderBuilder instead +#pragma warning(disable:4996) +#endif + #include <json/reader.h> #include <json/writer.h> namespace OrthancStone { - HttpCommand::SuccessMessage::SuccessMessage(const HttpCommand& command, - const HttpHeaders& answerHeaders, - std::string& answer) : - OriginMessage(command), - headers_(answerHeaders), - answer_(answer) - { - } - - void HttpCommand::SuccessMessage::ParseJsonBody(Json::Value& target) const { Json::Reader reader; @@ -76,4 +71,30 @@ throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); } } + + + const std::string& HttpCommand::GetUsername() const + { + if (HasCredentials()) + { + return username_; + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + } + + + const std::string& HttpCommand::GetPassword() const + { + if (HasCredentials()) + { + return password_; + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + } }
--- a/Framework/Oracle/HttpCommand.h Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Oracle/HttpCommand.h Tue Mar 31 15:51:02 2020 +0200 @@ -22,7 +22,7 @@ #pragma once #include "../Messages/IMessage.h" -#include "OracleCommandWithPayload.h" +#include "OracleCommandBase.h" #include <Core/Enumerations.h> @@ -31,7 +31,7 @@ namespace OrthancStone { - class HttpCommand : public OracleCommandWithPayload + class HttpCommand : public OracleCommandBase { public: typedef std::map<std::string, std::string> HttpHeaders; @@ -41,13 +41,18 @@ ORTHANC_STONE_MESSAGE(__FILE__, __LINE__); private: - HttpHeaders headers_; - std::string answer_; + const HttpHeaders& headers_; + const std::string& answer_; public: SuccessMessage(const HttpCommand& command, const HttpHeaders& answerHeaders, - std::string& answer /* will be swapped to avoid a memcpy() */); + const std::string& answer) : + OriginMessage(command), + headers_(answerHeaders), + answer_(answer) + { + } const std::string& GetAnswer() const { @@ -56,7 +61,7 @@ void ParseJsonBody(Json::Value& target) const; - const HttpHeaders& GetAnswerHeaders() const + const HttpHeaders& GetAnswerHeaders() const { return headers_; } @@ -69,6 +74,19 @@ std::string body_; HttpHeaders headers_; unsigned int timeout_; + std::string username_; + std::string password_; + + HttpCommand(const HttpCommand& other) : + method_(other.method_), + url_(other.url_), + body_(other.body_), + headers_(other.headers_), + timeout_(other.timeout_), + username_(other.username_), + password_(other.password_) + { + } public: HttpCommand(); @@ -78,6 +96,11 @@ return Type_Http; } + virtual IOracleCommand* Clone() const + { + return new HttpCommand(*this); + } + void SetMethod(Orthanc::HttpMethod method) { method_ = method; @@ -137,5 +160,27 @@ { return timeout_; } + + void SetCredentials(const std::string& username, + const std::string& password) + { + username_ = username; + password_ = password; + } + + void ClearCredentials() + { + username_.clear(); + password_.clear(); + } + + bool HasCredentials() const + { + return !username_.empty(); + } + + const std::string& GetUsername() const; + + const std::string& GetPassword() const; }; }
--- a/Framework/Oracle/IOracle.h Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Oracle/IOracle.h Tue Mar 31 15:51:02 2020 +0200 @@ -24,6 +24,8 @@ #include "../Messages/IObserver.h" #include "IOracleCommand.h" +#include <boost/shared_ptr.hpp> + namespace OrthancStone { class IOracle : public boost::noncopyable @@ -33,7 +35,12 @@ { } - virtual void Schedule(const IObserver& receiver, + /** + * Returns "true" iff the command has actually been queued. If + * "false" is returned, the command has been freed, and it won't + * be processed (this is the case if the oracle is stopped). + **/ + virtual bool Schedule(boost::shared_ptr<IObserver> receiver, IOracleCommand* command) = 0; // Takes ownership }; }
--- a/Framework/Oracle/IOracleCommand.h Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Oracle/IOracleCommand.h Tue Mar 31 15:51:02 2020 +0200 @@ -21,7 +21,7 @@ #pragma once -#include <boost/noncopyable.hpp> +#include <Core/IDynamicObject.h> namespace OrthancStone { @@ -30,11 +30,14 @@ public: enum Type { + Type_GetOrthancImage, + Type_GetOrthancWebViewerJpeg, Type_Http, - Type_Sleep, Type_OrthancRestApi, - Type_GetOrthancImage, - Type_GetOrthancWebViewerJpeg + Type_ParseDicomFromFile, + Type_ParseDicomFromWado, + Type_ReadFile, + Type_Sleep }; virtual ~IOracleCommand() @@ -42,5 +45,8 @@ } virtual Type GetType() const = 0; + + // This only clones the command, *not* its possibly associated payload + virtual IOracleCommand* Clone() const = 0; }; }
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Oracle/OracleCommandBase.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -0,0 +1,67 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 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 "OracleCommandBase.h" + +#include <Core/OrthancException.h> + +namespace OrthancStone +{ + void OracleCommandBase::AcquirePayload(Orthanc::IDynamicObject* payload) + { + if (payload == NULL) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); + } + else + { + payload_.reset(payload); + } + } + + + Orthanc::IDynamicObject& OracleCommandBase::GetPayload() const + { + if (HasPayload()) + { + return *payload_; + } + else + { + LOG(ERROR) << "OracleCommandBase::GetPayload(): (!HasPayload())"; + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + } + + + Orthanc::IDynamicObject* OracleCommandBase::ReleasePayload() + { + if (HasPayload()) + { + return payload_.release(); + } + else + { + LOG(ERROR) << "OracleCommandBase::ReleasePayload(): (!HasPayload())"; + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Oracle/OracleCommandBase.h Tue Mar 31 15:51:02 2020 +0200 @@ -0,0 +1,50 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 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 "IOracleCommand.h" + +#include <Core/Compatibility.h> +#include <Core/IDynamicObject.h> + +#include <memory> + +namespace OrthancStone +{ + class OracleCommandBase : public IOracleCommand + { + private: + std::unique_ptr<Orthanc::IDynamicObject> payload_; + + public: + void AcquirePayload(Orthanc::IDynamicObject* payload); + + virtual bool HasPayload() const + { + return (payload_.get() != NULL); + } + + virtual Orthanc::IDynamicObject& GetPayload() const; + + Orthanc::IDynamicObject* ReleasePayload(); + }; +}
--- a/Framework/Oracle/OracleCommandExceptionMessage.h Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Oracle/OracleCommandExceptionMessage.h Tue Mar 31 15:51:02 2020 +0200 @@ -28,34 +28,28 @@ namespace OrthancStone { - class OracleCommandExceptionMessage : public IMessage + class OracleCommandExceptionMessage : public OriginMessage<IOracleCommand> { ORTHANC_STONE_MESSAGE(__FILE__, __LINE__); private: - const IOracleCommand& command_; - Orthanc::OrthancException exception_; + Orthanc::OrthancException exception_; public: OracleCommandExceptionMessage(const IOracleCommand& command, - const Orthanc::OrthancException& exception) : - command_(command), - exception_(exception) + const Orthanc::ErrorCode& error) : + OriginMessage(command), + exception_(error) { } OracleCommandExceptionMessage(const IOracleCommand& command, - const Orthanc::ErrorCode& error) : - command_(command), - exception_(error) + const Orthanc::OrthancException& exception) : + OriginMessage(command), + exception_(exception) { } - const IOracleCommand& GetCommand() const - { - return command_; - } - const Orthanc::OrthancException& GetException() const { return exception_;
--- a/Framework/Oracle/OracleCommandWithPayload.cpp Tue Mar 31 15:47:29 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,67 +0,0 @@ -/** - * Stone of Orthanc - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2020 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 "OracleCommandWithPayload.h" - -#include <Core/OrthancException.h> - -namespace OrthancStone -{ - void OracleCommandWithPayload::SetPayload(Orthanc::IDynamicObject* payload) - { - if (payload == NULL) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); - } - else - { - payload_.reset(payload); - } - } - - - Orthanc::IDynamicObject& OracleCommandWithPayload::GetPayload() const - { - if (HasPayload()) - { - return *payload_; - } - else - { - LOG(ERROR) << "OracleCommandWithPayload::GetPayload(): (!HasPayload())"; - throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); - } - } - - - Orthanc::IDynamicObject* OracleCommandWithPayload::ReleasePayload() - { - if (HasPayload()) - { - return payload_.release(); - } - else - { - LOG(ERROR) << "OracleCommandWithPayload::ReleasePayload(): (!HasPayload())"; - throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); - } - } -}
--- a/Framework/Oracle/OracleCommandWithPayload.h Tue Mar 31 15:47:29 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,50 +0,0 @@ -/** - * Stone of Orthanc - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2020 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 "IOracleCommand.h" - -#include <Core/Compatibility.h> -#include <Core/IDynamicObject.h> - -#include <memory> - -namespace OrthancStone -{ - class OracleCommandWithPayload : public IOracleCommand - { - private: - std::unique_ptr<Orthanc::IDynamicObject> payload_; - - public: - void SetPayload(Orthanc::IDynamicObject* payload); - - bool HasPayload() const - { - return (payload_.get() != NULL); - } - - Orthanc::IDynamicObject& GetPayload() const; - - Orthanc::IDynamicObject* ReleasePayload(); - }; -}
--- a/Framework/Oracle/OrthancRestApiCommand.cpp Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Oracle/OrthancRestApiCommand.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -23,21 +23,16 @@ #include <Core/OrthancException.h> +#ifdef _MSC_VER +// 'Json::Reader': Use CharReader and CharReaderBuilder instead +#pragma warning(disable:4996) +#endif + #include <json/reader.h> #include <json/writer.h> namespace OrthancStone { - OrthancRestApiCommand::SuccessMessage::SuccessMessage(const OrthancRestApiCommand& command, - const HttpHeaders& answerHeaders, - std::string& answer) : - OriginMessage(command), - headers_(answerHeaders), - answer_(answer) - { - } - - void OrthancRestApiCommand::SuccessMessage::ParseJsonBody(Json::Value& target) const { Json::Reader reader; @@ -51,7 +46,8 @@ OrthancRestApiCommand::OrthancRestApiCommand() : method_(Orthanc::HttpMethod_Get), uri_("/"), - timeout_(600) + timeout_(600), + applyPlugins_(false) { }
--- a/Framework/Oracle/OrthancRestApiCommand.h Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Oracle/OrthancRestApiCommand.h Tue Mar 31 15:51:02 2020 +0200 @@ -22,7 +22,7 @@ #pragma once #include "../Messages/IMessage.h" -#include "OracleCommandWithPayload.h" +#include "OracleCommandBase.h" #include <Core/Enumerations.h> @@ -31,7 +31,7 @@ namespace OrthancStone { - class OrthancRestApiCommand : public OracleCommandWithPayload + class OrthancRestApiCommand : public OracleCommandBase { public: typedef std::map<std::string, std::string> HttpHeaders; @@ -41,14 +41,19 @@ ORTHANC_STONE_MESSAGE(__FILE__, __LINE__); private: - HttpHeaders headers_; - std::string answer_; + const HttpHeaders& headers_; + const std::string& answer_; public: SuccessMessage(const OrthancRestApiCommand& command, const HttpHeaders& answerHeaders, - std::string& answer /* will be swapped to avoid a memcpy() */); - + const std::string& answer) : + OriginMessage(command), + headers_(answerHeaders), + answer_(answer) + { + } + const std::string& GetAnswer() const { return answer_; @@ -69,7 +74,18 @@ std::string body_; HttpHeaders headers_; unsigned int timeout_; + bool applyPlugins_; // Only makes sense for Stone as an Orthanc plugin + OrthancRestApiCommand(const OrthancRestApiCommand& other) : + method_(other.method_), + uri_(other.uri_), + body_(other.body_), + headers_(other.headers_), + timeout_(other.timeout_), + applyPlugins_(other.applyPlugins_) + { + } + public: OrthancRestApiCommand(); @@ -78,6 +94,11 @@ return Type_OrthancRestApi; } + virtual IOracleCommand* Clone() const + { + return new OrthancRestApiCommand(*this); + } + void SetMethod(Orthanc::HttpMethod method) { method_ = method; @@ -137,5 +158,15 @@ { return timeout_; } + + void SetApplyPlugins(bool applyPlugins) + { + applyPlugins_ = applyPlugins; + } + + bool IsApplyPlugins() const + { + return applyPlugins_; + } }; }
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Oracle/ParseDicomFromFileCommand.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -0,0 +1,43 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 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 "ParseDicomFromFileCommand.h" + +#include <Core/OrthancException.h> + +#include <boost/filesystem/path.hpp> + +namespace OrthancStone +{ + std::string ParseDicomFromFileCommand::GetDicomDirPath(const std::string& dicomDirPath, + const std::string& file) + { + std::string tmp = file; + +#if !defined(_WIN32) + std::replace(tmp.begin(), tmp.end(), '\\', '/'); +#endif + + boost::filesystem::path base = boost::filesystem::path(dicomDirPath).parent_path(); + + return (base / tmp).string(); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Oracle/ParseDicomFromFileCommand.h Tue Mar 31 15:51:02 2020 +0200 @@ -0,0 +1,84 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 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 "OracleCommandBase.h" + +#include <string> + +namespace OrthancStone +{ + class ParseDicomFromFileCommand : public OracleCommandBase + { + private: + std::string path_; + bool pixelDataIncluded_; + + ParseDicomFromFileCommand(const ParseDicomFromFileCommand& other) : + path_(other.path_), + pixelDataIncluded_(other.pixelDataIncluded_) + { + } + + public: + ParseDicomFromFileCommand(const std::string& path) : + path_(path), + pixelDataIncluded_(true) + { + } + + ParseDicomFromFileCommand(const std::string& dicomDirPath, + const std::string& file) : + path_(GetDicomDirPath(dicomDirPath, file)), + pixelDataIncluded_(true) + { + } + + static std::string GetDicomDirPath(const std::string& dicomDirPath, + const std::string& file); + + virtual Type GetType() const + { + return Type_ParseDicomFromFile; + } + + virtual IOracleCommand* Clone() const + { + return new ParseDicomFromFileCommand(*this); + } + + const std::string& GetPath() const + { + return path_; + } + + bool IsPixelDataIncluded() const + { + return pixelDataIncluded_; + } + + void SetPixelDataIncluded(bool included) + { + pixelDataIncluded_ = included; + } + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Oracle/ParseDicomFromWadoCommand.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -0,0 +1,58 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 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 "ParseDicomFromWadoCommand.h" + +#include <Core/OrthancException.h> + +namespace OrthancStone +{ + ParseDicomFromWadoCommand::ParseDicomFromWadoCommand(const std::string& sopInstanceUid, + IOracleCommand* restCommand) : + sopInstanceUid_(sopInstanceUid), + restCommand_(restCommand) + { + if (restCommand == NULL) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); + } + + if (restCommand_->GetType() != Type_Http && + restCommand_->GetType() != Type_OrthancRestApi) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadParameterType); + } + } + + + IOracleCommand* ParseDicomFromWadoCommand::Clone() const + { + assert(restCommand_.get() != NULL); + return new ParseDicomFromWadoCommand(sopInstanceUid_, restCommand_->Clone()); + } + + + const IOracleCommand& ParseDicomFromWadoCommand::GetRestCommand() const + { + assert(restCommand_.get() != NULL); + return *restCommand_; + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Oracle/ParseDicomFromWadoCommand.h Tue Mar 31 15:51:02 2020 +0200 @@ -0,0 +1,54 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 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 "OracleCommandBase.h" + +#include <string> + +namespace OrthancStone +{ + class ParseDicomFromWadoCommand : public OracleCommandBase + { + private: + std::string sopInstanceUid_; + std::unique_ptr<IOracleCommand> restCommand_; + + public: + ParseDicomFromWadoCommand(const std::string& sopInstanceUid, + IOracleCommand* restCommand); + + virtual Type GetType() const + { + return Type_ParseDicomFromWado; + } + + virtual IOracleCommand* Clone() const; + + const std::string& GetSopInstanceUid() const + { + return sopInstanceUid_; + } + + const IOracleCommand& GetRestCommand() const; + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Oracle/ParseDicomSuccessMessage.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -0,0 +1,107 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 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 "ParseDicomSuccessMessage.h" + +#include <Core/DicomParsing/ParsedDicomFile.h> +#include <Core/HttpServer/MultipartStreamReader.h> +#include <Core/OrthancException.h> + +namespace OrthancStone +{ + class MultipartHandler : public Orthanc::MultipartStreamReader::IHandler + { + private: + std::unique_ptr<Orthanc::ParsedDicomFile> dicom_; + size_t size_; + + public: + MultipartHandler() : + size_(0) + { + } + + virtual void HandlePart(const std::map<std::string, std::string>& headers, + const void* part, + size_t size) + { + if (dicom_.get()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol, + "Multiple DICOM instances were contained in a WADO-RS request"); + } + else + { + dicom_.reset(new Orthanc::ParsedDicomFile(part, size)); + size_ = size; + } + } + + Orthanc::ParsedDicomFile* ReleaseDicom() + { + if (dicom_.get()) + { + return dicom_.release(); + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol, + "WADO-RS request didn't contain any DICOM instance"); + } + } + + size_t GetSize() const + { + return size_; + } + }; + + + Orthanc::ParsedDicomFile* ParseDicomSuccessMessage::ParseWadoAnswer( + size_t& fileSize /* OUT */, + const std::string& answer, + const std::map<std::string, std::string>& headers) + { + std::string contentType, subType, boundary, header; + if (Orthanc::MultipartStreamReader::GetMainContentType(header, headers) && + Orthanc::MultipartStreamReader::ParseMultipartContentType(contentType, subType, boundary, header) && + contentType == "multipart/related" && + subType == "application/dicom") + { + MultipartHandler handler; + + { + Orthanc::MultipartStreamReader reader(boundary); + reader.SetHandler(handler); + reader.AddChunk(answer); + reader.CloseStream(); + } + + fileSize = handler.GetSize(); + return handler.ReleaseDicom(); + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol, + "Multipart/related answer of application/dicom was expected from DICOMweb server"); + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Oracle/ParseDicomSuccessMessage.h Tue Mar 31 15:51:02 2020 +0200 @@ -0,0 +1,85 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 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 + +#if !defined(ORTHANC_ENABLE_DCMTK) +# error The macro ORTHANC_ENABLE_DCMTK must be defined +#endif + +#if ORTHANC_ENABLE_DCMTK != 1 +# error Support for DCMTK must be enabled to use ParseDicomFromFileCommand +#endif + +#include "OracleCommandBase.h" +#include "../Messages/IMessageEmitter.h" +#include "../Messages/IObserver.h" + +#include <map> + +namespace Orthanc +{ + class ParsedDicomFile; +} + +namespace OrthancStone +{ + class ParseDicomSuccessMessage : public OriginMessage<OracleCommandBase> + { + ORTHANC_STONE_MESSAGE(__FILE__, __LINE__); + + private: + Orthanc::ParsedDicomFile& dicom_; + size_t fileSize_; + bool hasPixelData_; + + public: + ParseDicomSuccessMessage(const OracleCommandBase& command, + Orthanc::ParsedDicomFile& dicom, + size_t fileSize, + bool hasPixelData) : + OriginMessage(command), + dicom_(dicom), + fileSize_(fileSize), + hasPixelData_(hasPixelData) + { + } + + Orthanc::ParsedDicomFile& GetDicom() const + { + return dicom_; + } + + size_t GetFileSize() const + { + return fileSize_; + } + + bool HasPixelData() const + { + return hasPixelData_; + } + + static Orthanc::ParsedDicomFile* ParseWadoAnswer(size_t& fileSize /* OUT */, + const std::string& answer, + const std::map<std::string, std::string>& headers); + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Oracle/ReadFileCommand.h Tue Mar 31 15:51:02 2020 +0200 @@ -0,0 +1,78 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 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 "../Messages/IMessage.h" +#include "OracleCommandBase.h" + +namespace OrthancStone +{ + class ReadFileCommand : public OracleCommandBase + { + public: + class SuccessMessage : public OriginMessage<ReadFileCommand> + { + ORTHANC_STONE_MESSAGE(__FILE__, __LINE__); + + private: + const std::string& content_; + + public: + SuccessMessage(const ReadFileCommand& command, + const std::string& content) : + OriginMessage(command), + content_(content) + { + } + + const std::string& GetContent() const + { + return content_; + } + }; + + + private: + std::string path_; + + public: + ReadFileCommand(const std::string& path) : + path_(path) + { + } + + virtual Type GetType() const + { + return Type_ReadFile; + } + + virtual IOracleCommand* Clone() const + { + return new ReadFileCommand(path_); + } + + const std::string& GetPath() const + { + return path_; + } + }; +}
--- a/Framework/Oracle/SleepOracleCommand.h Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Oracle/SleepOracleCommand.h Tue Mar 31 15:51:02 2020 +0200 @@ -22,11 +22,11 @@ #pragma once #include "../Messages/IMessage.h" -#include "OracleCommandWithPayload.h" +#include "OracleCommandBase.h" namespace OrthancStone { - class SleepOracleCommand : public OracleCommandWithPayload + class SleepOracleCommand : public OracleCommandBase { private: unsigned int milliseconds_; @@ -44,6 +44,11 @@ return Type_Sleep; } + virtual IOracleCommand* Clone() const + { + return new SleepOracleCommand(milliseconds_); + } + unsigned int GetDelay() const { return milliseconds_;
--- a/Framework/Oracle/ThreadedOracle.cpp Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Oracle/ThreadedOracle.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -21,29 +21,21 @@ #include "ThreadedOracle.h" -#include "GetOrthancImageCommand.h" -#include "GetOrthancWebViewerJpegCommand.h" -#include "HttpCommand.h" -#include "OrthancRestApiCommand.h" #include "SleepOracleCommand.h" -#include "OracleCommandExceptionMessage.h" -#include <Core/Compression/GzipCompressor.h> -#include <Core/HttpClient.h> +#include <Core/Logging.h> #include <Core/OrthancException.h> -#include <Core/Toolbox.h> - namespace OrthancStone { class ThreadedOracle::Item : public Orthanc::IDynamicObject { private: - const IObserver& receiver_; + boost::weak_ptr<IObserver> receiver_; std::unique_ptr<IOracleCommand> command_; public: - Item(const IObserver& receiver, + Item(boost::weak_ptr<IObserver> receiver, IOracleCommand* command) : receiver_(receiver), command_(command) @@ -54,7 +46,7 @@ } } - const IObserver& GetReceiver() const + boost::weak_ptr<IObserver> GetReceiver() { return receiver_; } @@ -73,12 +65,12 @@ class Item { private: - const IObserver& receiver_; + boost::weak_ptr<IObserver> receiver_; std::unique_ptr<SleepOracleCommand> command_; boost::posix_time::ptime expiration_; public: - Item(const IObserver& receiver, + Item(boost::weak_ptr<IObserver> receiver, SleepOracleCommand* command) : receiver_(receiver), command_(command) @@ -123,7 +115,7 @@ } } - void Add(const IObserver& receiver, + void Add(boost::weak_ptr<IObserver> receiver, SleepOracleCommand* command) // Takes ownership { boost::mutex::scoped_lock lock(mutex_); @@ -160,154 +152,6 @@ }; - static void CopyHttpHeaders(Orthanc::HttpClient& client, - const Orthanc::HttpClient::HttpHeaders& headers) - { - for (Orthanc::HttpClient::HttpHeaders::const_iterator - it = headers.begin(); it != headers.end(); it++ ) - { - client.AddHeader(it->first, it->second); - } - } - - - static void DecodeAnswer(std::string& answer, - const Orthanc::HttpClient::HttpHeaders& headers) - { - Orthanc::HttpCompression contentEncoding = Orthanc::HttpCompression_None; - - for (Orthanc::HttpClient::HttpHeaders::const_iterator it = headers.begin(); - it != headers.end(); ++it) - { - std::string s; - Orthanc::Toolbox::ToLowerCase(s, it->first); - - if (s == "content-encoding") - { - if (it->second == "gzip") - { - contentEncoding = Orthanc::HttpCompression_Gzip; - } - else - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol, - "Unsupported HTTP Content-Encoding: " + it->second); - } - - break; - } - } - - if (contentEncoding == Orthanc::HttpCompression_Gzip) - { - std::string compressed; - answer.swap(compressed); - - Orthanc::GzipCompressor compressor; - compressor.Uncompress(answer, compressed.c_str(), compressed.size()); - - LOG(INFO) << "Uncompressing gzip Encoding: from " << compressed.size() - << " to " << answer.size() << " bytes"; - } - } - - - static void Execute(IMessageEmitter& emitter, - const IObserver& receiver, - const HttpCommand& command) - { - Orthanc::HttpClient client; - client.SetUrl(command.GetUrl()); - client.SetMethod(command.GetMethod()); - client.SetTimeout(command.GetTimeout()); - - CopyHttpHeaders(client, command.GetHttpHeaders()); - - if (command.GetMethod() == Orthanc::HttpMethod_Post || - command.GetMethod() == Orthanc::HttpMethod_Put) - { - client.SetBody(command.GetBody()); - } - - std::string answer; - Orthanc::HttpClient::HttpHeaders answerHeaders; - client.ApplyAndThrowException(answer, answerHeaders); - - DecodeAnswer(answer, answerHeaders); - - HttpCommand::SuccessMessage message(command, answerHeaders, answer); - emitter.EmitMessage(receiver, message); - } - - - static void Execute(IMessageEmitter& emitter, - const Orthanc::WebServiceParameters& orthanc, - const IObserver& receiver, - const OrthancRestApiCommand& command) - { - Orthanc::HttpClient client(orthanc, command.GetUri()); - client.SetMethod(command.GetMethod()); - client.SetTimeout(command.GetTimeout()); - - CopyHttpHeaders(client, command.GetHttpHeaders()); - - if (command.GetMethod() == Orthanc::HttpMethod_Post || - command.GetMethod() == Orthanc::HttpMethod_Put) - { - client.SetBody(command.GetBody()); - } - - std::string answer; - Orthanc::HttpClient::HttpHeaders answerHeaders; - client.ApplyAndThrowException(answer, answerHeaders); - - DecodeAnswer(answer, answerHeaders); - - OrthancRestApiCommand::SuccessMessage message(command, answerHeaders, answer); - emitter.EmitMessage(receiver, message); - } - - - static void Execute(IMessageEmitter& emitter, - const Orthanc::WebServiceParameters& orthanc, - const IObserver& receiver, - const GetOrthancImageCommand& command) - { - Orthanc::HttpClient client(orthanc, command.GetUri()); - client.SetTimeout(command.GetTimeout()); - - CopyHttpHeaders(client, command.GetHttpHeaders()); - - std::string answer; - Orthanc::HttpClient::HttpHeaders answerHeaders; - client.ApplyAndThrowException(answer, answerHeaders); - - DecodeAnswer(answer, answerHeaders); - - command.ProcessHttpAnswer(emitter, receiver, answer, answerHeaders); - } - - - static void Execute(IMessageEmitter& emitter, - const Orthanc::WebServiceParameters& orthanc, - const IObserver& receiver, - const GetOrthancWebViewerJpegCommand& command) - { - Orthanc::HttpClient client(orthanc, command.GetUri()); - client.SetTimeout(command.GetTimeout()); - - CopyHttpHeaders(client, command.GetHttpHeaders()); - - std::string answer; - Orthanc::HttpClient::HttpHeaders answerHeaders; - client.ApplyAndThrowException(answer, answerHeaders); - - DecodeAnswer(answer, answerHeaders); - - command.ProcessHttpAnswer(emitter, receiver, answer); - } - - void ThreadedOracle::Step() { std::unique_ptr<Orthanc::IDynamicObject> object(queue_.Dequeue(100)); @@ -316,60 +160,37 @@ { Item& item = dynamic_cast<Item&>(*object); - try + if (item.GetCommand().GetType() == IOracleCommand::Type_Sleep) { - switch (item.GetCommand().GetType()) + SleepOracleCommand& command = dynamic_cast<SleepOracleCommand&>(item.GetCommand()); + + std::unique_ptr<SleepOracleCommand> copy(new SleepOracleCommand(command.GetDelay())); + + if (command.HasPayload()) { - case IOracleCommand::Type_Sleep: - { - SleepOracleCommand& command = dynamic_cast<SleepOracleCommand&>(item.GetCommand()); - - std::unique_ptr<SleepOracleCommand> copy(new SleepOracleCommand(command.GetDelay())); - - if (command.HasPayload()) - { - copy->SetPayload(command.ReleasePayload()); - } - - sleepingCommands_->Add(item.GetReceiver(), copy.release()); - - break; - } - - case IOracleCommand::Type_Http: - Execute(emitter_, item.GetReceiver(), - dynamic_cast<const HttpCommand&>(item.GetCommand())); - break; + copy->AcquirePayload(command.ReleasePayload()); + } + + sleepingCommands_->Add(item.GetReceiver(), copy.release()); + } + else + { + GenericOracleRunner runner; - case IOracleCommand::Type_OrthancRestApi: - Execute(emitter_, orthanc_, item.GetReceiver(), - dynamic_cast<const OrthancRestApiCommand&>(item.GetCommand())); - break; - - case IOracleCommand::Type_GetOrthancImage: - Execute(emitter_, orthanc_, item.GetReceiver(), - dynamic_cast<const GetOrthancImageCommand&>(item.GetCommand())); - break; - - case IOracleCommand::Type_GetOrthancWebViewerJpeg: - Execute(emitter_, orthanc_, item.GetReceiver(), - dynamic_cast<const GetOrthancWebViewerJpegCommand&>(item.GetCommand())); - break; + { + boost::mutex::scoped_lock lock(mutex_); + runner.SetOrthanc(orthanc_); + runner.SetRootDirectory(rootDirectory_); - default: - throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); +#if ORTHANC_ENABLE_DCMTK == 1 + if (dicomCache_) + { + runner.SetDicomCache(dicomCache_); + } +#endif } - } - catch (Orthanc::OrthancException& e) - { - LOG(ERROR) << "Exception within the oracle: " << e.What(); - emitter_.EmitMessage(item.GetReceiver(), OracleCommandExceptionMessage(item.GetCommand(), e)); - } - catch (...) - { - LOG(ERROR) << "Threaded exception within the oracle"; - emitter_.EmitMessage(item.GetReceiver(), OracleCommandExceptionMessage - (item.GetCommand(), Orthanc::ErrorCode_InternalError)); + + runner.Run(item.GetReceiver(), emitter_, item.GetCommand()); } } } @@ -453,6 +274,7 @@ ThreadedOracle::ThreadedOracle(IMessageEmitter& emitter) : emitter_(emitter), + rootDirectory_("."), state_(State_Setup), workers_(4), sleepingCommands_(new SleepingCommands), @@ -480,23 +302,21 @@ catch (...) { LOG(ERROR) << "Native exception while stopping the threaded oracle"; - } + } } void ThreadedOracle::SetOrthancParameters(const Orthanc::WebServiceParameters& orthanc) { boost::mutex::scoped_lock lock(mutex_); + orthanc_ = orthanc; + } - if (state_ != State_Setup) - { - LOG(ERROR) << "ThreadedOracle::SetOrthancParameters(): (state_ != State_Setup)"; - throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); - } - else - { - orthanc_ = orthanc; - } + + void ThreadedOracle::SetRootDirectory(const std::string& rootDirectory) + { + boost::mutex::scoped_lock lock(mutex_); + rootDirectory_ = rootDirectory; } @@ -540,6 +360,31 @@ } + void ThreadedOracle::SetDicomCacheSize(size_t size) + { +#if ORTHANC_ENABLE_DCMTK == 1 + boost::mutex::scoped_lock lock(mutex_); + + if (state_ != State_Setup) + { + LOG(ERROR) << "ThreadedOracle::SetDicomCacheSize(): (state_ != State_Setup)"; + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + else + { + if (size == 0) + { + dicomCache_.reset(); + } + else + { + dicomCache_.reset(new ParsedDicomCache(size)); + } + } +#endif + } + + void ThreadedOracle::Start() { boost::mutex::scoped_lock lock(mutex_); @@ -551,6 +396,7 @@ } else { + LOG(WARNING) << "Starting oracle with " << workers_.size() << " worker threads"; state_ = State_Running; for (unsigned int i = 0; i < workers_.size(); i++) @@ -563,9 +409,25 @@ } - void ThreadedOracle::Schedule(const IObserver& receiver, + bool ThreadedOracle::Schedule(boost::shared_ptr<IObserver> receiver, IOracleCommand* command) { - queue_.Enqueue(new Item(receiver, command)); + std::unique_ptr<Item> item(new Item(receiver, command)); + + { + boost::mutex::scoped_lock lock(mutex_); + + if (state_ == State_Running) + { + //LOG(INFO) << "New oracle command queued"; + queue_.Enqueue(item.release()); + return true; + } + else + { + LOG(TRACE) << "Command not enqueued, as the oracle has stopped"; + return false; + } + } } }
--- a/Framework/Oracle/ThreadedOracle.h Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Oracle/ThreadedOracle.h Tue Mar 31 15:51:02 2020 +0200 @@ -25,14 +25,22 @@ # error The macro ORTHANC_ENABLE_THREADS must be defined #endif +#if !defined(ORTHANC_ENABLE_DCMTK) +# error The macro ORTHANC_ENABLE_DCMTK must be defined +#endif + #if ORTHANC_ENABLE_THREADS != 1 # error This file can only compiled for native targets #endif -#include "../Messages/IMessageEmitter.h" +#if ORTHANC_ENABLE_DCMTK == 1 +# include "../Toolbox/ParsedDicomCache.h" +#endif + #include "IOracle.h" +#include "GenericOracleRunner.h" +#include "../Messages/IMessageEmitter.h" -#include <Core/WebServiceParameters.h> #include <Core/MultiThreading/SharedMessageQueue.h> @@ -53,6 +61,7 @@ IMessageEmitter& emitter_; Orthanc::WebServiceParameters orthanc_; + std::string rootDirectory_; Orthanc::SharedMessageQueue queue_; State state_; boost::mutex mutex_; @@ -61,6 +70,10 @@ boost::thread sleepingWorker_; unsigned int sleepingTimeResolution_; +#if ORTHANC_ENABLE_DCMTK == 1 + boost::shared_ptr<ParsedDicomCache> dicomCache_; +#endif + void Step(); static void Worker(ThreadedOracle* that); @@ -74,13 +87,16 @@ virtual ~ThreadedOracle(); - // The reference is not stored. void SetOrthancParameters(const Orthanc::WebServiceParameters& orthanc); + void SetRootDirectory(const std::string& rootDirectory); + void SetThreadsCount(unsigned int count); void SetSleepingTimeResolution(unsigned int milliseconds); + void SetDicomCacheSize(size_t size); + void Start(); void Stop() @@ -88,7 +104,7 @@ StopInternal(); } - virtual void Schedule(const IObserver& receiver, - IOracleCommand* command); + virtual bool Schedule(boost::shared_ptr<IObserver> receiver, + IOracleCommand* command) ORTHANC_OVERRIDE; }; }
--- a/Framework/Oracle/WebAssemblyOracle.cpp Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Oracle/WebAssemblyOracle.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -21,8 +21,13 @@ #include "WebAssemblyOracle.h" +#include "OracleCommandExceptionMessage.h" #include "SleepOracleCommand.h" -#include "OracleCommandExceptionMessage.h" + +#if ORTHANC_ENABLE_DCMTK == 1 +# include "ParseDicomSuccessMessage.h" +static unsigned int BUCKET_SOP = 1; +#endif #include <Core/OrthancException.h> #include <Core/Toolbox.h> @@ -31,23 +36,18 @@ #include <emscripten/html5.h> #include <emscripten/fetch.h> -#if 0 -extern bool logbgo233; -extern bool logbgo115; -#endif - namespace OrthancStone { class WebAssemblyOracle::TimeoutContext { private: WebAssemblyOracle& oracle_; - const IObserver& receiver_; + boost::weak_ptr<IObserver> receiver_; std::unique_ptr<SleepOracleCommand> command_; public: TimeoutContext(WebAssemblyOracle& oracle, - const IObserver& receiver, + boost::weak_ptr<IObserver> receiver, IOracleCommand* command) : oracle_(oracle), receiver_(receiver) @@ -63,7 +63,9 @@ } void EmitMessage() - { + { + assert(command_.get() != NULL); + SleepOracleCommand::TimeoutMessage message(*command_); oracle_.EmitMessage(receiver_, message); } @@ -76,26 +78,6 @@ }; - class WebAssemblyOracle::Emitter : public IMessageEmitter - { - private: - WebAssemblyOracle& oracle_; - - public: - Emitter(WebAssemblyOracle& oracle) : - oracle_(oracle) - { - } - - virtual void EmitMessage(const IObserver& receiver, - const IMessage& message) - { - LOG(TRACE) << "WebAssemblyOracle::Emitter::EmitMessage receiver = " - << std::hex << &receiver << std::dec; - oracle_.EmitMessage(receiver, message); - } - }; - /** This object is created on the heap for every http request. It is deleted in the success (or error) callbacks. @@ -108,26 +90,23 @@ class WebAssemblyOracle::FetchContext : public boost::noncopyable { private: - Emitter emitter_; - const IObserver& receiver_; - std::unique_ptr<IOracleCommand> command_; - std::string expectedContentType_; - int64_t receiverFingerprint_; + WebAssemblyOracle& oracle_; + boost::weak_ptr<IObserver> receiver_; + std::unique_ptr<IOracleCommand> command_; + std::string expectedContentType_; public: FetchContext(WebAssemblyOracle& oracle, - const IObserver& receiver, + boost::weak_ptr<IObserver> receiver, IOracleCommand* command, const std::string& expectedContentType) : - emitter_(oracle), + oracle_(oracle), receiver_(receiver), command_(command), - expectedContentType_(expectedContentType), - receiverFingerprint_(receiver.GetFingerprint()) + expectedContentType_(expectedContentType) { LOG(TRACE) << "WebAssemblyOracle::FetchContext::FetchContext() | " - << "receiver address = " << std::hex << &receiver << std::dec - << " with fingerprint = " << receiverFingerprint_; + << "receiver address = " << std::hex << &receiver; if (command == NULL) { @@ -140,21 +119,21 @@ return expectedContentType_; } + IMessageEmitter& GetEmitter() const + { + return oracle_; + } + + boost::weak_ptr<IObserver> GetReceiver() const + { + return receiver_; + } + void EmitMessage(const IMessage& message) { LOG(TRACE) << "WebAssemblyOracle::FetchContext::EmitMessage receiver_ = " << std::hex << &receiver_ << std::dec; - emitter_.EmitMessage(receiver_, message); - } - - IMessageEmitter& GetEmitter() - { - return emitter_; - } - - const IObserver& GetReceiver() const - { - return receiver_; + oracle_.EmitMessage(receiver_, message); } IOracleCommand& GetCommand() const @@ -168,41 +147,17 @@ return dynamic_cast<T&>(*command_); } -#if 0 - static std::string ToString(Orthanc::HttpMethod method) - { - switch (method) { - case Orthanc::HttpMethod_Get: - return "GET"; - break; - case Orthanc::HttpMethod_Post: - return "POST"; - break; - default: - throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); - break; - } - } - static void DumpCommand(emscripten_fetch_t* fetch, std::string answer) +#if ORTHANC_ENABLE_DCMTK == 1 + void StoreInCache(const std::string& sopInstanceUid, + std::unique_ptr<Orthanc::ParsedDicomFile>& dicom, + size_t fileSize) { - FetchContext* context = reinterpret_cast<FetchContext*>(fetch->userData); - - const auto& command = context->GetTypedCommand<OrthancRestApiCommand>(); - auto commandStr = ToString(command.GetMethod()); - LOG(TRACE) << "SuccessCallback for REST command. Method is : " << commandStr; - switch (command.GetMethod()) { - case Orthanc::HttpMethod_Get: - LOG(TRACE) << " * SuccessCallback GET URI = " << command.GetUri() << " timeout = " << command.GetTimeout(); - LOG(TRACE) << " * SuccessCallback GET RESPONSE = " << answer; - break; - case Orthanc::HttpMethod_Post: - LOG(TRACE) << " * SuccessCallback POST URI = " << command.GetUri() << " body = " << command.GetBody() << " timeout = " << command.GetTimeout(); - LOG(TRACE) << " * SuccessCallback POST RESPONSE = " << answer; - break; - default: - throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); - break; - } + if (oracle_.dicomCache_.get()) + { + // Store it into the cache for future use + oracle_.dicomCache_->Acquire(BUCKET_SOP, sopInstanceUid, + dicom.release(), fileSize, true); + } } #endif @@ -213,64 +168,43 @@ * free data associated with the fetch. **/ - std::unique_ptr<FetchContext> context(reinterpret_cast<FetchContext*>(fetch->userData)); - - // an UUID is 36 chars : 32 hex chars + 4 hyphens: char #0 --> char #35 - // char #36 is \0. - bool callHandler = true; - - // TODO: remove this line because we are NOT allowed to call methods on GetReceiver that is maybe a dangling ref - if (context->GetReceiver().DoesFingerprintLookGood()) - { - callHandler = true; - int64_t currentFingerprint(context->GetReceiver().GetFingerprint()); - - LOG(TRACE) << "SuccessCallback for object at address (" << std::hex - << &(context->GetReceiver()) << std::dec - << " with current fingerprint = " << currentFingerprint - << ". Fingerprint looks OK"; - - if (currentFingerprint != context->receiverFingerprint_) - { - LOG(TRACE) << " ** SuccessCallback: BUT currentFingerprint != " - << "receiverFingerprint_(" << context->receiverFingerprint_ << ")"; - callHandler = false; - } - else - { - LOG(TRACE) << " ** SuccessCallback: FetchContext-level " - << "fingerprints are the same: " - << context->receiverFingerprint_ - << " ---> oracle will dispatch the response to observer: " - << std::hex << &(context->GetReceiver()) << std::dec; - } - } - else { - LOG(TRACE) << "SuccessCallback for object at address (" << std::hex << &(context->GetReceiver()) << std::dec << " with current fingerprint is XXXXX -- NOT A VALID FINGERPRINT! OBJECT IS READ ! CALLBACK WILL NOT BE CALLED!"; - callHandler = false; - } - if (fetch->userData == NULL) { LOG(ERROR) << "WebAssemblyOracle::FetchContext::SuccessCallback fetch->userData is NULL!!!!!!!"; + return; } + std::unique_ptr<FetchContext> context(reinterpret_cast<FetchContext*>(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. + * Retrieving the headers of the HTTP answer. + **/ + HttpHeaders headers; + +#if (__EMSCRIPTEN_major__ < 1 || \ + (__EMSCRIPTEN_major__ == 1 && __EMSCRIPTEN_minor__ < 38) || \ + (__EMSCRIPTEN_major__ == 1 && __EMSCRIPTEN_minor__ == 38 && __EMSCRIPTEN_tiny__ < 37)) +# warning Consider upgrading Emscripten to a version above 1.38.37, incomplete support of Fetch API + + /** + * HACK - If emscripten < 1.38.37, 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 is fixed thanks to the + * "emscripten_fetch_get_response_headers()" function that was + * added to "fetch.h" at emscripten-1.38.37 on 2019-06-26. + * + * https://github.com/emscripten-core/emscripten/blob/1.38.37/system/include/emscripten/fetch.h * https://github.com/emscripten-core/emscripten/pull/8486 **/ - - HttpHeaders headers; if (fetch->userData != NULL) { if (!context->GetExpectedContentType().empty()) @@ -278,13 +212,29 @@ headers["Content-Type"] = context->GetExpectedContentType(); } } - -#if 0 - if (context->GetCommand().GetType() == IOracleCommand::Type_OrthancRestApi) { - //if (logbgo115) - DumpCommand(fetch, answer); +#else + { + size_t size = emscripten_fetch_get_response_headers_length(fetch); + + std::string plainHeaders(size + 1, '\0'); + emscripten_fetch_get_response_headers(fetch, &plainHeaders[0], size + 1); + + std::vector<std::string> tokens; + Orthanc::Toolbox::TokenizeString(tokens, plainHeaders, '\n'); + + for (size_t i = 0; i < tokens.size(); i++) + { + size_t p = tokens[i].find(':'); + if (p != std::string::npos) + { + std::string key = Orthanc::Toolbox::StripSpaces(tokens[i].substr(0, p)); + std::string value = Orthanc::Toolbox::StripSpaces(tokens[i].substr(p + 1)); + headers[key] = value; + } + } } #endif + LOG(TRACE) << "About to call emscripten_fetch_close"; emscripten_fetch_close(fetch); LOG(TRACE) << "Successfully called emscripten_fetch_close"; @@ -303,50 +253,75 @@ } else { - if (callHandler) + switch (context->GetCommand().GetType()) { - switch (context->GetCommand().GetType()) + case IOracleCommand::Type_Http: + { + HttpCommand::SuccessMessage message(context->GetTypedCommand<HttpCommand>(), headers, answer); + context->EmitMessage(message); + break; + } + + case IOracleCommand::Type_OrthancRestApi: + { + LOG(TRACE) << "WebAssemblyOracle::FetchContext::SuccessCallback. About to call context->EmitMessage(message);"; + OrthancRestApiCommand::SuccessMessage message + (context->GetTypedCommand<OrthancRestApiCommand>(), headers, answer); + context->EmitMessage(message); + break; + } + + case IOracleCommand::Type_GetOrthancImage: { - case IOracleCommand::Type_Http: + context->GetTypedCommand<GetOrthancImageCommand>().ProcessHttpAnswer + (context->GetReceiver(), context->GetEmitter(), answer, headers); + break; + } + + case IOracleCommand::Type_GetOrthancWebViewerJpeg: + { + context->GetTypedCommand<GetOrthancWebViewerJpegCommand>().ProcessHttpAnswer + (context->GetReceiver(), context->GetEmitter(), answer); + break; + } + + case IOracleCommand::Type_ParseDicomFromWado: + { +#if ORTHANC_ENABLE_DCMTK == 1 + const ParseDicomFromWadoCommand& command = + context->GetTypedCommand<ParseDicomFromWadoCommand>(); + + size_t fileSize; + std::unique_ptr<Orthanc::ParsedDicomFile> dicom + (ParseDicomSuccessMessage::ParseWadoAnswer(fileSize, answer, headers)); + { - HttpCommand::SuccessMessage message(context->GetTypedCommand<HttpCommand>(), headers, answer); + ParseDicomSuccessMessage message(command, *dicom, fileSize, true); context->EmitMessage(message); - break; } - case IOracleCommand::Type_OrthancRestApi: - { - LOG(TRACE) << "WebAssemblyOracle::FetchContext::SuccessCallback. About to call context->EmitMessage(message);"; - OrthancRestApiCommand::SuccessMessage message - (context->GetTypedCommand<OrthancRestApiCommand>(), headers, answer); - context->EmitMessage(message); - break; - } + context->StoreInCache(command.GetSopInstanceUid(), dicom, fileSize); +#else + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); +#endif + break; + } - case IOracleCommand::Type_GetOrthancImage: - { - context->GetTypedCommand<GetOrthancImageCommand>().ProcessHttpAnswer - (context->GetEmitter(), context->GetReceiver(), answer, headers); - break; - } - - case IOracleCommand::Type_GetOrthancWebViewerJpeg: - { - context->GetTypedCommand<GetOrthancWebViewerJpegCommand>().ProcessHttpAnswer - (context->GetEmitter(), context->GetReceiver(), answer); - break; - } - - default: - LOG(ERROR) << "Command type not implemented by the WebAssembly Oracle: " - << context->GetCommand().GetType(); - } + default: + LOG(ERROR) << "Command type not implemented by the WebAssembly Oracle (in SuccessCallback): " + << context->GetCommand().GetType(); + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); } } } catch (Orthanc::OrthancException& e) { - LOG(ERROR) << "Error while processing a fetch answer in the oracle: " << e.What(); + LOG(INFO) << "Error while processing a fetch answer in the oracle: " << e.What(); + + { + OracleCommandExceptionMessage message(context->GetCommand(), e); + context->EmitMessage(message); + } } } @@ -388,7 +363,7 @@ { private: WebAssemblyOracle& oracle_; - const IObserver& receiver_; + boost::weak_ptr<IObserver> receiver_; std::unique_ptr<IOracleCommand> command_; Orthanc::HttpMethod method_; std::string url_; @@ -396,16 +371,20 @@ HttpHeaders headers_; unsigned int timeout_; std::string expectedContentType_; + bool hasCredentials_; + std::string username_; + std::string password_; public: FetchCommand(WebAssemblyOracle& oracle, - const IObserver& receiver, + boost::weak_ptr<IObserver> receiver, IOracleCommand* command) : oracle_(oracle), receiver_(receiver), command_(command), method_(Orthanc::HttpMethod_Get), - timeout_(0) + timeout_(0), + hasCredentials_(false) { if (command == NULL) { @@ -418,11 +397,6 @@ method_ = method; } - void SetOrthancUri(const std::string& uri) - { - url_ = oracle_.orthancRoot_ + uri; - } - void SetUrl(const std::string& url) { url_ = url; @@ -433,9 +407,12 @@ body_.swap(body); } - void SetHttpHeaders(const HttpHeaders& headers) + void AddHttpHeaders(const HttpHeaders& headers) { - headers_ = headers; + for (HttpHeaders::const_iterator it = headers.begin(); it != headers.end(); ++it) + { + headers_[it->first] = it->second; + } } void SetTimeout(unsigned int timeout) @@ -443,15 +420,16 @@ timeout_ = timeout; } + void SetCredentials(const std::string& username, + const std::string& password) + { + hasCredentials_ = true; + username_ = username; + password_ = password; + } + void Execute() { -#if 0 - if (logbgo233) { - if (logbgo115) - LOG(TRACE) << " WebAssemblyOracle::Execute () command addr " << - std::hex << command_.get() << std::dec; - } -#endif if (command_.get() == NULL) { // Cannot call Execute() twice @@ -493,6 +471,13 @@ attr.onerror = FetchContext::FailureCallback; attr.timeoutMSecs = timeout_ * 1000; + if (hasCredentials_) + { + attr.withCredentials = EM_TRUE; + attr.userName = username_.c_str(); + attr.password = password_.c_str(); + } + std::vector<const char*> headers; headers.reserve(2 * headers_.size() + 1); @@ -534,9 +519,6 @@ attr.userData = new FetchContext(oracle_, receiver_, command_.release(), expectedContentType); // Must be the last call to prevent memory leak on error -#if 0 - LOG(TRACE) << "Performing " << method << " request on URI: \"" << url_ << "\""; -#endif emscripten_fetch(&attr, url_.c_str()); } catch(...) @@ -548,34 +530,35 @@ } }; -#if 0 - static void DumpCommand(OrthancRestApiCommand* pCommand) + + void WebAssemblyOracle::SetOrthancUrl(FetchCommand& command, + const std::string& uri) const { - OrthancRestApiCommand& command = *pCommand; - LOG(TRACE) << "WebAssemblyOracle::Execute for REST command."; - switch (command.GetMethod()) { - case Orthanc::HttpMethod_Get: - LOG(TRACE) << " * WebAssemblyOracle::Execute GET URI = " << command.GetUri() << " timeout = " << command.GetTimeout(); - break; - case Orthanc::HttpMethod_Post: - LOG(TRACE) << " * WebAssemblyOracle::Execute POST URI = " << command.GetUri() << " body = " << command.GetBody() << " timeout = " << command.GetTimeout(); - break; - default: - throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); - break; + if (isLocalOrthanc_) + { + command.SetUrl(localOrthancRoot_ + uri); + } + else + { + command.SetUrl(remoteOrthanc_.GetUrl() + uri); + command.AddHttpHeaders(remoteOrthanc_.GetHttpHeaders()); + + if (!remoteOrthanc_.GetUsername().empty()) + { + command.SetCredentials(remoteOrthanc_.GetUsername(), remoteOrthanc_.GetPassword()); + } } } -#endif + - - void WebAssemblyOracle::Execute(const IObserver& receiver, + void WebAssemblyOracle::Execute(boost::weak_ptr<IObserver> receiver, HttpCommand* command) { FetchCommand fetch(*this, receiver, command); fetch.SetMethod(command->GetMethod()); fetch.SetUrl(command->GetUrl()); - fetch.SetHttpHeaders(command->GetHttpHeaders()); + fetch.AddHttpHeaders(command->GetHttpHeaders()); fetch.SetTimeout(command->GetTimeout()); if (command->GetMethod() == Orthanc::HttpMethod_Post || @@ -590,19 +573,9 @@ } - void WebAssemblyOracle::Execute(const IObserver& receiver, + void WebAssemblyOracle::Execute(boost::weak_ptr<IObserver> receiver, OrthancRestApiCommand* command) { -#if 0 - DumpCommand(command); - - if (logbgo233) { - if (logbgo115) - LOG(TRACE) << " WebAssemblyOracle::Execute (OrthancRestApiCommand) command addr " << - std::hex << command << std::dec; - } -#endif - try { //LOG(TRACE) << "*********** WebAssemblyOracle::Execute."; @@ -610,8 +583,8 @@ FetchCommand fetch(*this, receiver, command); fetch.SetMethod(command->GetMethod()); - fetch.SetOrthancUri(command->GetUri()); - fetch.SetHttpHeaders(command->GetHttpHeaders()); + SetOrthancUrl(fetch, command->GetUri()); + fetch.AddHttpHeaders(command->GetHttpHeaders()); fetch.SetTimeout(command->GetTimeout()); if (command->GetMethod() == Orthanc::HttpMethod_Post || @@ -653,54 +626,112 @@ } - void WebAssemblyOracle::Execute(const IObserver& receiver, + void WebAssemblyOracle::Execute(boost::weak_ptr<IObserver> receiver, GetOrthancImageCommand* command) { -#if 0 - if (logbgo233) { - if (logbgo115) - LOG(TRACE) << " WebAssemblyOracle::Execute (GetOrthancImageCommand) command addr " << - std::hex << command << std::dec; - } -#endif - FetchCommand fetch(*this, receiver, command); - fetch.SetOrthancUri(command->GetUri()); - fetch.SetHttpHeaders(command->GetHttpHeaders()); + SetOrthancUrl(fetch, command->GetUri()); + fetch.AddHttpHeaders(command->GetHttpHeaders()); fetch.SetTimeout(command->GetTimeout()); fetch.Execute(); } - void WebAssemblyOracle::Execute(const IObserver& receiver, + void WebAssemblyOracle::Execute(boost::weak_ptr<IObserver> receiver, GetOrthancWebViewerJpegCommand* command) { -#if 0 - if (logbgo233) { - if (logbgo115) - LOG(TRACE) << " WebAssemblyOracle::Execute (GetOrthancWebViewerJpegCommand) command addr " << std::hex << command << std::dec; - } -#endif - FetchCommand fetch(*this, receiver, command); - fetch.SetOrthancUri(command->GetUri()); - fetch.SetHttpHeaders(command->GetHttpHeaders()); + SetOrthancUrl(fetch, command->GetUri()); + fetch.AddHttpHeaders(command->GetHttpHeaders()); fetch.SetTimeout(command->GetTimeout()); fetch.Execute(); } + void WebAssemblyOracle::Execute(boost::weak_ptr<IObserver> receiver, + ParseDicomFromWadoCommand* command) + { + std::unique_ptr<ParseDicomFromWadoCommand> protection(command); + +#if ORTHANC_ENABLE_DCMTK == 1 + if (dicomCache_.get()) + { + ParsedDicomCache::Reader reader(*dicomCache_, BUCKET_SOP, protection->GetSopInstanceUid()); + if (reader.IsValid() && + reader.HasPixelData()) + { + // Reuse the DICOM file from the cache + ParseDicomSuccessMessage message(*protection, reader.GetDicom(), + reader.GetFileSize(), reader.HasPixelData()); + EmitMessage(receiver, message); + return; + } + } +#endif - void WebAssemblyOracle::Schedule(const IObserver& receiver, + switch (command->GetRestCommand().GetType()) + { + case IOracleCommand::Type_Http: + { + const HttpCommand& rest = + dynamic_cast<const HttpCommand&>(protection->GetRestCommand()); + + FetchCommand fetch(*this, receiver, protection.release()); + + fetch.SetMethod(rest.GetMethod()); + fetch.SetUrl(rest.GetUrl()); + fetch.AddHttpHeaders(rest.GetHttpHeaders()); + fetch.SetTimeout(rest.GetTimeout()); + + if (rest.GetMethod() == Orthanc::HttpMethod_Post || + rest.GetMethod() == Orthanc::HttpMethod_Put) + { + std::string body = rest.GetBody(); + fetch.SetBody(body); + } + + fetch.Execute(); + break; + } + + case IOracleCommand::Type_OrthancRestApi: + { + const OrthancRestApiCommand& rest = + dynamic_cast<const OrthancRestApiCommand&>(protection->GetRestCommand()); + + FetchCommand fetch(*this, receiver, protection.release()); + + fetch.SetMethod(rest.GetMethod()); + SetOrthancUrl(fetch, rest.GetUri()); + fetch.AddHttpHeaders(rest.GetHttpHeaders()); + fetch.SetTimeout(rest.GetTimeout()); + + if (rest.GetMethod() == Orthanc::HttpMethod_Post || + rest.GetMethod() == Orthanc::HttpMethod_Put) + { + std::string body = rest.GetBody(); + fetch.SetBody(body); + } + + fetch.Execute(); + break; + } + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); + } + } + + + bool WebAssemblyOracle::Schedule(boost::shared_ptr<IObserver> receiver, IOracleCommand* command) { LOG(TRACE) << "WebAssemblyOracle::Schedule : receiver = " - << std::hex << &receiver << std::dec - << " | Current fingerprint is " << receiver.GetFingerprint(); + << std::hex << &receiver; std::unique_ptr<IOracleCommand> protection(command); @@ -716,37 +747,14 @@ break; case IOracleCommand::Type_OrthancRestApi: - //// DIAGNOSTIC. PLEASE REMOVE IF IT HAS BEEN COMMITTED BY MISTAKE - //{ - // const IObserver* pReceiver = &receiver; - // LOG(TRACE) << "WebAssemblyOracle::Schedule | pReceiver is " << pReceiver; - // LOG(TRACE) << "WebAssemblyOracle::Schedule | command = " << command; - // OrthancRestApiCommand* rac = dynamic_cast<OrthancRestApiCommand*>(protection.get()); - // LOG(TRACE) << "WebAssemblyOracle::Schedule | typed command = " << rac; - // LOG(TRACE) << "WebAssemblyOracle::Schedule" << rac->GetUri(); - //} - //// END OF BLOCK TO REMOVE Execute(receiver, dynamic_cast<OrthancRestApiCommand*>(protection.release())); break; case IOracleCommand::Type_GetOrthancImage: - //// DIAGNOSTIC. PLEASE REMOVE IF IT HAS BEEN COMMITTED BY MISTAKE - //{ - // GetOrthancImageCommand* rac = dynamic_cast<GetOrthancImageCommand*>(protection.get()); - // LOG(TRACE) << "WebAssemblyOracle::Schedule" << rac->GetUri(); - //} - //// END OF BLOCK TO REMOVE Execute(receiver, dynamic_cast<GetOrthancImageCommand*>(protection.release())); break; case IOracleCommand::Type_GetOrthancWebViewerJpeg: - //// DIAGNOSTIC. PLEASE REMOVE IF IT HAS BEEN COMMITTED BY MISTAKE - //{ - // GetOrthancWebViewerJpegCommand* rac = dynamic_cast<GetOrthancWebViewerJpegCommand*>(protection.get()); - // LOG(TRACE) << "WebAssemblyOracle::Schedule" << rac->GetUri(); - //} - //// END OF BLOCK TO REMOVE - Execute(receiver, dynamic_cast<GetOrthancWebViewerJpegCommand*>(protection.release())); break; case IOracleCommand::Type_Sleep: @@ -757,8 +765,38 @@ break; } + case IOracleCommand::Type_ParseDicomFromWado: +#if ORTHANC_ENABLE_DCMTK == 1 + Execute(receiver, dynamic_cast<ParseDicomFromWadoCommand*>(protection.release())); +#else + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented, + "DCMTK must be enabled to parse DICOM files"); +#endif + break; + default: - LOG(ERROR) << "Command type not implemented by the WebAssembly Oracle: " << command->GetType(); + LOG(ERROR) << "Command type not implemented by the WebAssembly Oracle (in Schedule): " + << command->GetType(); + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); } + + return true; + } + + + void WebAssemblyOracle::SetDicomCacheSize(size_t size) + { +#if ORTHANC_ENABLE_DCMTK == 1 + if (size == 0) + { + dicomCache_.reset(); + } + else + { + dicomCache_.reset(new ParsedDicomCache(size)); + } +#else + LOG(INFO) << "DCMTK support is disabled, the DICOM cache is disabled"; +#endif } }
--- a/Framework/Oracle/WebAssemblyOracle.h Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Oracle/WebAssemblyOracle.h Tue Mar 31 15:51:02 2020 +0200 @@ -35,48 +35,87 @@ #include "HttpCommand.h" #include "IOracle.h" #include "OrthancRestApiCommand.h" +#include "ParseDicomFromWadoCommand.h" + +#if ORTHANC_ENABLE_DCMTK == 1 +# include "../Toolbox/ParsedDicomCache.h" +#endif + +#include <Core/WebServiceParameters.h> namespace OrthancStone { class WebAssemblyOracle : public IOracle, - public IObservable + public IMessageEmitter { private: typedef std::map<std::string, std::string> HttpHeaders; class TimeoutContext; - class Emitter; class FetchContext; - class FetchCommand; + class FetchCommand; + + void SetOrthancUrl(FetchCommand& command, + const std::string& uri) const; - void Execute(const IObserver& receiver, + void Execute(boost::weak_ptr<IObserver> receiver, HttpCommand* command); - void Execute(const IObserver& receiver, + void Execute(boost::weak_ptr<IObserver> receiver, OrthancRestApiCommand* command); - void Execute(const IObserver& receiver, + void Execute(boost::weak_ptr<IObserver> receiver, GetOrthancImageCommand* command); - void Execute(const IObserver& receiver, + void Execute(boost::weak_ptr<IObserver> receiver, GetOrthancWebViewerJpegCommand* command); + + void Execute(boost::weak_ptr<IObserver> receiver, + ParseDicomFromWadoCommand* command); - std::string orthancRoot_; + IObservable oracleObservable_; + bool isLocalOrthanc_; + std::string localOrthancRoot_; + Orthanc::WebServiceParameters remoteOrthanc_; + +#if ORTHANC_ENABLE_DCMTK == 1 + std::unique_ptr<ParsedDicomCache> dicomCache_; +#endif public: - WebAssemblyOracle(MessageBroker& broker) : - IObservable(broker) + WebAssemblyOracle() : + isLocalOrthanc_(false) { } - - void SetOrthancRoot(const std::string& root) + + virtual void EmitMessage(boost::weak_ptr<IObserver> observer, + const IMessage& message) ORTHANC_OVERRIDE { - orthancRoot_ = root; + oracleObservable_.EmitMessage(observer, message); } - virtual void Schedule(const IObserver& receiver, - IOracleCommand* command); + virtual bool Schedule(boost::shared_ptr<IObserver> receiver, + IOracleCommand* command) ORTHANC_OVERRIDE; + + IObservable& GetOracleObservable() + { + return oracleObservable_; + } + + void SetLocalOrthanc(const std::string& root) + { + isLocalOrthanc_ = true; + localOrthancRoot_ = root; + } + + void SetRemoteOrthanc(const Orthanc::WebServiceParameters& orthanc) + { + isLocalOrthanc_ = false; + remoteOrthanc_ = orthanc; + } + + void SetDicomCacheSize(size_t size); }; }
--- a/Framework/Radiography/RadiographyAlphaLayer.h Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Radiography/RadiographyAlphaLayer.h Tue Mar 31 15:51:02 2020 +0200 @@ -39,8 +39,8 @@ float foreground_; // in the range [0.0, 65535.0] public: - RadiographyAlphaLayer(MessageBroker& broker, const RadiographyScene& scene) : - RadiographyLayer(broker, scene), + RadiographyAlphaLayer(const RadiographyScene& scene) : + RadiographyLayer(scene), foreground_(0) { }
--- a/Framework/Radiography/RadiographyDicomLayer.cpp Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Radiography/RadiographyDicomLayer.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -48,7 +48,8 @@ } - RadiographyDicomLayer::RadiographyDicomLayer(MessageBroker& broker, const RadiographyScene& scene) : RadiographyLayer(broker, scene) + RadiographyDicomLayer::RadiographyDicomLayer(const RadiographyScene& scene) : + RadiographyLayer(scene) { }
--- a/Framework/Radiography/RadiographyDicomLayer.h Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Radiography/RadiographyDicomLayer.h Tue Mar 31 15:51:02 2020 +0200 @@ -42,7 +42,7 @@ void ApplyConverter(); public: - RadiographyDicomLayer(MessageBroker& broker, const RadiographyScene& scene); + RadiographyDicomLayer(const RadiographyScene& scene); void SetInstance(const std::string& instanceId, unsigned int frame) {
--- a/Framework/Radiography/RadiographyLayer.cpp Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Radiography/RadiographyLayer.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -119,8 +119,7 @@ } - RadiographyLayer::RadiographyLayer(MessageBroker& broker, const RadiographyScene& scene) : - IObservable(broker), + RadiographyLayer::RadiographyLayer(const RadiographyScene& scene) : index_(0), hasSize_(false), width_(0),
--- a/Framework/Radiography/RadiographyLayer.h Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Radiography/RadiographyLayer.h Tue Mar 31 15:51:02 2020 +0200 @@ -240,7 +240,7 @@ double zoom); public: - RadiographyLayer(MessageBroker& broker, const RadiographyScene& scene); + RadiographyLayer(const RadiographyScene& scene); virtual ~RadiographyLayer() {
--- a/Framework/Radiography/RadiographyMaskLayer.h Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Radiography/RadiographyMaskLayer.h Tue Mar 31 15:51:02 2020 +0200 @@ -42,9 +42,9 @@ mutable std::unique_ptr<Orthanc::ImageAccessor> mask_; public: - RadiographyMaskLayer(MessageBroker& broker, const RadiographyScene& scene, const RadiographyDicomLayer& dicomLayer, + RadiographyMaskLayer(const RadiographyScene& scene, const RadiographyDicomLayer& dicomLayer, float foreground) : - RadiographyLayer(broker, scene), + RadiographyLayer(scene), dicomLayer_(dicomLayer), invalidated_(true), foreground_(foreground)
--- a/Framework/Radiography/RadiographyScene.cpp Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Radiography/RadiographyScene.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -147,7 +147,7 @@ BroadcastMessage(GeometryChangedMessage(*this, *layer)); BroadcastMessage(ContentChangedMessage(*this, *layer)); - layer->RegisterObserverCallback(new Callable<RadiographyScene, RadiographyLayer::LayerEditedMessage>(*this, &RadiographyScene::OnLayerEdited)); + Register<RadiographyLayer::LayerEditedMessage>(*layer, &RadiographyScene::OnLayerEdited); return *layer; } @@ -167,9 +167,8 @@ BroadcastMessage(RadiographyScene::LayerEditedMessage(*this, message.GetOrigin())); } - RadiographyScene::RadiographyScene(MessageBroker& broker) : - IObserver(broker), - IObservable(broker), + + RadiographyScene::RadiographyScene() : nextLayerIndex_(0), hasWindowing_(false), windowingCenter_(0), // Dummy initialization @@ -325,7 +324,7 @@ RadiographyLayer::Geometry* centerGeometry, bool isCenterGeometry) { - std::unique_ptr<RadiographyTextLayer> alpha(new RadiographyTextLayer(IObservable::GetBroker(), *this)); + std::unique_ptr<RadiographyTextLayer> alpha(new RadiographyTextLayer(*this)); alpha->SetText(utf8, font, fontSize, foreground); if (centerGeometry != NULL) { @@ -382,7 +381,7 @@ float foreground, RadiographyLayer::Geometry* geometry) { - std::unique_ptr<RadiographyMaskLayer> mask(new RadiographyMaskLayer(IObservable::GetBroker(), *this, dicomLayer, foreground)); + std::unique_ptr<RadiographyMaskLayer> mask(new RadiographyMaskLayer(*this, dicomLayer, foreground)); mask->SetCorners(corners); if (geometry != NULL) { @@ -395,7 +394,7 @@ RadiographyLayer& RadiographyScene::LoadAlphaBitmap(Orthanc::ImageAccessor* bitmap, RadiographyLayer::Geometry *geometry) { - std::unique_ptr<RadiographyAlphaLayer> alpha(new RadiographyAlphaLayer(IObservable::GetBroker(), *this)); + std::unique_ptr<RadiographyAlphaLayer> alpha(new RadiographyAlphaLayer(*this)); alpha->SetAlpha(bitmap); if (geometry != NULL) { @@ -412,7 +411,7 @@ RadiographyPhotometricDisplayMode preferredPhotometricDisplayMode, RadiographyLayer::Geometry* geometry) { - RadiographyDicomLayer& layer = dynamic_cast<RadiographyDicomLayer&>(RegisterLayer(new RadiographyDicomLayer(IObservable::GetBroker(), *this))); + RadiographyDicomLayer& layer = dynamic_cast<RadiographyDicomLayer&>(RegisterLayer(new RadiographyDicomLayer(*this))); layer.SetInstance(instance, frame); @@ -434,7 +433,7 @@ bool httpCompression, RadiographyLayer::Geometry* geometry) { - RadiographyDicomLayer& layer = dynamic_cast<RadiographyDicomLayer&>(RegisterLayer(new RadiographyDicomLayer(IObservable::GetBroker(), *this))); + RadiographyDicomLayer& layer = dynamic_cast<RadiographyDicomLayer&>(RegisterLayer(new RadiographyDicomLayer( *this))); layer.SetInstance(instance, frame); if (geometry != NULL) @@ -448,8 +447,8 @@ orthanc.GetBinaryAsync( uri, headers, - new Callable<RadiographyScene, Deprecated::OrthancApiClient::BinaryResponseReadyMessage> - (*this, &RadiographyScene::OnTagsReceived), NULL, + new Deprecated::DeprecatedCallable<RadiographyScene, Deprecated::OrthancApiClient::BinaryResponseReadyMessage> + (GetSharedObserver(), &RadiographyScene::OnTagsReceived), NULL, new Orthanc::SingleValueObject<size_t>(layer.GetIndex())); } @@ -467,8 +466,8 @@ orthanc.GetBinaryAsync( uri, headers, - new Callable<RadiographyScene, Deprecated::OrthancApiClient::BinaryResponseReadyMessage> - (*this, &RadiographyScene::OnFrameReceived), NULL, + new Deprecated::DeprecatedCallable<RadiographyScene, Deprecated::OrthancApiClient::BinaryResponseReadyMessage> + (GetSharedObserver(), &RadiographyScene::OnFrameReceived), NULL, new Orthanc::SingleValueObject<size_t>(layer.GetIndex())); } @@ -478,7 +477,7 @@ RadiographyLayer& RadiographyScene::LoadDicomWebFrame(Deprecated::IWebService& web) { - RadiographyLayer& layer = RegisterLayer(new RadiographyDicomLayer(IObservable::GetBroker(), *this)); + RadiographyLayer& layer = RegisterLayer(new RadiographyDicomLayer(*this)); return layer; @@ -854,8 +853,8 @@ orthanc.PostJsonAsyncExpectJson( "/tools/create-dicom", createDicomRequestContent, - new Callable<RadiographyScene, Deprecated::OrthancApiClient::JsonResponseReadyMessage> - (*this, &RadiographyScene::OnDicomExported), + new Deprecated::DeprecatedCallable<RadiographyScene, Deprecated::OrthancApiClient::JsonResponseReadyMessage> + (GetSharedObserver(), &RadiographyScene::OnDicomExported), NULL, NULL); }
--- a/Framework/Radiography/RadiographyScene.h Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Radiography/RadiographyScene.h Tue Mar 31 15:51:02 2020 +0200 @@ -22,6 +22,7 @@ #pragma once #include "RadiographyLayer.h" +#include "../Messages/ObserverBase.h" #include "../Deprecated/Toolbox/DicomFrameConverter.h" #include "../Deprecated/Toolbox/OrthancApiClient.h" #include "../StoneEnumerations.h" @@ -33,8 +34,8 @@ class RadiographyDicomLayer; class RadiographyScene : - public IObserver, - public IObservable + public ObserverBase<RadiographyScene>, + public IObservable { friend class RadiographySceneGeometryReader; public: @@ -191,7 +192,7 @@ virtual void OnLayerEdited(const RadiographyLayer::LayerEditedMessage& message); public: - RadiographyScene(MessageBroker& broker); + RadiographyScene(); virtual ~RadiographyScene();
--- a/Framework/Radiography/RadiographySceneReader.cpp Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Radiography/RadiographySceneReader.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -55,7 +55,7 @@ RadiographyDicomLayer* RadiographySceneGeometryReader::LoadDicom(const std::string& instanceId, unsigned int frame, RadiographyLayer::Geometry* geometry) { - std::unique_ptr<RadiographyPlaceholderLayer> layer(new RadiographyPlaceholderLayer(dynamic_cast<IObservable&>(scene_).GetBroker(), scene_)); + std::unique_ptr<RadiographyPlaceholderLayer> layer(new RadiographyPlaceholderLayer(scene_)); layer->SetGeometry(*geometry); layer->SetSize(dicomImageWidth_, dicomImageHeight_); scene_.RegisterLayer(layer.get());
--- a/Framework/Radiography/RadiographySceneReader.h Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Radiography/RadiographySceneReader.h Tue Mar 31 15:51:02 2020 +0200 @@ -37,8 +37,8 @@ class RadiographyPlaceholderLayer : public RadiographyDicomLayer { public: - RadiographyPlaceholderLayer(MessageBroker& broker, const RadiographyScene& scene) : - RadiographyDicomLayer(broker, scene) + RadiographyPlaceholderLayer(const RadiographyScene& scene) : + RadiographyDicomLayer(scene) { }
--- a/Framework/Radiography/RadiographyTextLayer.cpp Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Radiography/RadiographyTextLayer.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -22,7 +22,7 @@ #include "Core/OrthancException.h" #include "RadiographyScene.h" -#include "Framework/Toolbox/TextRenderer.h" +#include "../Toolbox/TextRenderer.h" namespace OrthancStone {
--- a/Framework/Radiography/RadiographyTextLayer.h Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Radiography/RadiographyTextLayer.h Tue Mar 31 15:51:02 2020 +0200 @@ -37,8 +37,8 @@ static std::map<std::string, Orthanc::EmbeddedResources::FileResourceId> fonts_; public: - RadiographyTextLayer(MessageBroker& broker, const RadiographyScene& scene) : - RadiographyAlphaLayer(broker, scene) + RadiographyTextLayer(const RadiographyScene& scene) : + RadiographyAlphaLayer(scene) { }
--- a/Framework/Radiography/RadiographyWidget.cpp Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Radiography/RadiographyWidget.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -174,12 +174,9 @@ } - RadiographyWidget::RadiographyWidget(MessageBroker& broker, - boost::shared_ptr<RadiographyScene> scene, + RadiographyWidget::RadiographyWidget(boost::shared_ptr<RadiographyScene> scene, const std::string& name) : WorldSceneWidget(name), - IObserver(broker), - IObservable(broker), invert_(false), interpolation_(ImageInterpolation_Nearest), hasSelection_(false), @@ -271,24 +268,11 @@ 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)); - - scene_->RegisterObserverCallback( - new Callable<RadiographyWidget, RadiographyScene::LayerRemovedMessage> - (*this, &RadiographyWidget::OnLayerRemoved)); + Register<RadiographyScene::GeometryChangedMessage>(*scene_, &RadiographyWidget::OnGeometryChanged); + Register<RadiographyScene::ContentChangedMessage>(*scene_, &RadiographyWidget::OnContentChanged); + Register<RadiographyScene::LayerRemovedMessage>(*scene_, &RadiographyWidget::OnLayerRemoved); Unselect();
--- a/Framework/Radiography/RadiographyWidget.h Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Radiography/RadiographyWidget.h Tue Mar 31 15:51:02 2020 +0200 @@ -22,6 +22,7 @@ #pragma once #include "../Deprecated/Widgets/WorldSceneWidget.h" +#include "../Messages/ObserverBase.h" #include "RadiographyScene.h" @@ -31,7 +32,7 @@ class RadiographyWidget : public Deprecated::WorldSceneWidget, - public IObserver, + public ObserverBase<RadiographyWidget>, public IObservable { public: @@ -64,8 +65,7 @@ bool IsInvertedInternal() const; public: - RadiographyWidget(MessageBroker& broker, - 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) + RadiographyWidget(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
--- a/Framework/Scene2D/CairoCompositor.cpp Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Scene2D/CairoCompositor.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -85,11 +85,10 @@ } - CairoCompositor::CairoCompositor(const Scene2D& scene, - unsigned int canvasWidth, - unsigned int canvasHeight) : - helper_(scene, *this) + CairoCompositor::CairoCompositor(unsigned int canvasWidth, + unsigned int canvasHeight) { + ResetScene(); UpdateSize(canvasWidth, canvasHeight); } @@ -154,7 +153,7 @@ #endif - void CairoCompositor::Refresh() + void CairoCompositor::Refresh(const Scene2D& scene) { context_.reset(new CairoContext(canvas_)); @@ -162,7 +161,7 @@ cairo_set_source_rgba(context_->GetObject(), 0, 0, 0, 255); cairo_paint(context_->GetObject()); - helper_.Refresh(canvas_.GetWidth(), canvas_.GetHeight()); + helper_->Refresh(scene, canvas_.GetWidth(), canvas_.GetHeight()); context_.reset(); }
--- a/Framework/Scene2D/CairoCompositor.h Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Scene2D/CairoCompositor.h Tue Mar 31 15:51:02 2020 +0200 @@ -37,7 +37,7 @@ private: typedef std::map<size_t, GlyphBitmapAlphabet*> Fonts; - Internals::CompositorHelper helper_; + std::unique_ptr<Internals::CompositorHelper> helper_; CairoSurface canvas_; Fonts fonts_; @@ -49,8 +49,7 @@ virtual Internals::CompositorHelper::ILayerRenderer* Create(const ISceneLayer& layer) ORTHANC_OVERRIDE; public: - CairoCompositor(const Scene2D& scene, - unsigned int canvasWidth, + CairoCompositor(unsigned int canvasWidth, unsigned int canvasHeight); virtual ~CairoCompositor(); @@ -80,7 +79,12 @@ Orthanc::Encoding codepage) ORTHANC_OVERRIDE; #endif - virtual void Refresh() ORTHANC_OVERRIDE; + virtual void Refresh(const Scene2D& scene) ORTHANC_OVERRIDE; + + virtual void ResetScene() ORTHANC_OVERRIDE + { + helper_.reset(new Internals::CompositorHelper(*this)); + } void UpdateSize(unsigned int canvasWidth, unsigned int canvasHeight);
--- a/Framework/Scene2D/FloatTextureSceneLayer.cpp Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Scene2D/FloatTextureSceneLayer.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -30,7 +30,8 @@ namespace OrthancStone { FloatTextureSceneLayer::FloatTextureSceneLayer(const Orthanc::ImageAccessor& texture) : - inverted_(false) + inverted_(false), + applyLog_(false) { { std::unique_ptr<Orthanc::ImageAccessor> t( @@ -95,6 +96,14 @@ IncrementRevision(); } + + void FloatTextureSceneLayer::SetApplyLog(bool apply) + { + applyLog_ = apply; + IncrementRevision(); + } + + void FloatTextureSceneLayer::FitRange() { float minValue, maxValue; @@ -126,6 +135,7 @@ cloned->customCenter_ = customCenter_; cloned->customWidth_ = customWidth_; cloned->inverted_ = inverted_; + cloned->applyLog_ = applyLog_; return cloned.release(); }
--- a/Framework/Scene2D/FloatTextureSceneLayer.h Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Scene2D/FloatTextureSceneLayer.h Tue Mar 31 15:51:02 2020 +0200 @@ -32,6 +32,7 @@ float customCenter_; float customWidth_; bool inverted_; + bool applyLog_; public: // The pixel format must be convertible to "Float32" @@ -60,6 +61,13 @@ void FitRange(); + void SetApplyLog(bool apply); + + bool IsApplyLog() const + { + return applyLog_; + } + virtual ISceneLayer* Clone() const; virtual Type GetType() const
--- a/Framework/Scene2D/GrayscaleStyleConfigurator.cpp Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Scene2D/GrayscaleStyleConfigurator.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -27,6 +27,18 @@ namespace OrthancStone { + GrayscaleStyleConfigurator::GrayscaleStyleConfigurator() : + revision_(0), + linearInterpolation_(false), + hasWindowingOverride_(false), + customWindowWidth_(0), + customWindowCenter_(0), + hasInversionOverride_(false), + inverted_(false), + applyLog_(false) + { + } + void GrayscaleStyleConfigurator::SetWindowing(ImageWindowing windowing) { hasWindowingOverride_ = true; @@ -60,6 +72,12 @@ revision_++; } + void GrayscaleStyleConfigurator::SetApplyLog(bool apply) + { + applyLog_ = apply; + revision_++; + } + TextureBaseSceneLayer* GrayscaleStyleConfigurator::CreateTextureFromImage( const Orthanc::ImageAccessor& image) const { @@ -100,9 +118,12 @@ l.SetCustomWindowing(customWindowCenter_, customWindowWidth_); } } + if (hasInversionOverride_) { l.SetInverted(inverted_); } + + l.SetApplyLog(applyLog_); } }
--- a/Framework/Scene2D/GrayscaleStyleConfigurator.h Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Scene2D/GrayscaleStyleConfigurator.h Tue Mar 31 15:51:02 2020 +0200 @@ -40,18 +40,10 @@ float customWindowCenter_; bool hasInversionOverride_; bool inverted_; + bool applyLog_; public: - GrayscaleStyleConfigurator() : - revision_(0), - linearInterpolation_(false), - hasWindowingOverride_(false), - customWindowWidth_(0), - customWindowCenter_(0), - hasInversionOverride_(false), - inverted_(false) - { - } + GrayscaleStyleConfigurator(); void SetWindowing(ImageWindowing windowing); @@ -68,6 +60,13 @@ return linearInterpolation_; } + void SetApplyLog(bool apply); + + bool IsApplyLog() const + { + return applyLog_; + } + virtual uint64_t GetRevision() const { return revision_;
--- a/Framework/Scene2D/ICompositor.h Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Scene2D/ICompositor.h Tue Mar 31 15:51:02 2020 +0200 @@ -1,8 +1,9 @@ #pragma once -#include <boost/noncopyable.hpp> +#include "Scene2D.h" +#include "ScenePoint2D.h" + #include <EmbeddedResources.h> -#include <Core/Enumerations.h> namespace OrthancStone { @@ -17,7 +18,14 @@ virtual unsigned int GetCanvasHeight() const = 0; - virtual void Refresh() = 0; + /** + * WARNING: "Refresh()" must always be called with the same + * scene. If the scene changes, a call to "ResetScene()" must be + * done to reset the tracking of the revisions of the layers. + **/ + virtual void Refresh(const Scene2D& scene) = 0; + + virtual void ResetScene() = 0; #if ORTHANC_ENABLE_LOCALE == 1 virtual void SetFont(size_t index, @@ -25,5 +33,18 @@ unsigned int fontSize, Orthanc::Encoding codepage) = 0; #endif + + // Get the center of the given pixel, in canvas coordinates + ScenePoint2D GetPixelCenterCoordinates(int x, int y) const + { + return ScenePoint2D( + static_cast<double>(x) + 0.5 - static_cast<double>(GetCanvasWidth()) / 2.0, + static_cast<double>(y) + 0.5 - static_cast<double>(GetCanvasHeight()) / 2.0); + } + + void FitContent(Scene2D& scene) const + { + scene.FitContent(GetCanvasWidth(), GetCanvasHeight()); + } }; }
--- a/Framework/Scene2D/ISceneLayer.h Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Scene2D/ISceneLayer.h Tue Mar 31 15:51:02 2020 +0200 @@ -33,6 +33,7 @@ public: enum Type { + Type_NullLayer, Type_InfoPanel, Type_ColorTexture, Type_Polyline,
--- a/Framework/Scene2D/InfoPanelSceneLayer.cpp Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Scene2D/InfoPanelSceneLayer.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -28,10 +28,12 @@ { InfoPanelSceneLayer::InfoPanelSceneLayer(const Orthanc::ImageAccessor& texture, BitmapAnchor anchor, - bool isLinearInterpolation) : + bool isLinearInterpolation, + bool applySceneRotation) : texture_(Orthanc::Image::Clone(texture)), anchor_(anchor), - isLinearInterpolation_(isLinearInterpolation) + isLinearInterpolation_(isLinearInterpolation), + applySceneRotation_(applySceneRotation) { if (texture_->GetFormat() != Orthanc::PixelFormat_RGBA32 && texture_->GetFormat() != Orthanc::PixelFormat_RGB24)
--- a/Framework/Scene2D/InfoPanelSceneLayer.h Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Scene2D/InfoPanelSceneLayer.h Tue Mar 31 15:51:02 2020 +0200 @@ -37,15 +37,26 @@ std::unique_ptr<Orthanc::ImageAccessor> texture_; BitmapAnchor anchor_; bool isLinearInterpolation_; + bool applySceneRotation_; public: + /** + * If you supply `true` for `applySceneRotation`, then, in addition to + * a translation to bring it at the desired anchoring location, the image + * will be rotated around its center by the same rotation as the scene + * transformation. + */ InfoPanelSceneLayer(const Orthanc::ImageAccessor& texture, BitmapAnchor anchor, - bool isLinearInterpolation); + bool isLinearInterpolation, + bool applySceneRotation = false); virtual ISceneLayer* Clone() const { - return new InfoPanelSceneLayer(*texture_, anchor_, isLinearInterpolation_); + return new InfoPanelSceneLayer(*texture_, + anchor_, + isLinearInterpolation_, + applySceneRotation_); } const Orthanc::ImageAccessor& GetTexture() const @@ -58,6 +69,11 @@ return anchor_; } + bool ShouldApplySceneRotation() const + { + return applySceneRotation_; + } + bool IsLinearInterpolation() const { return isLinearInterpolation_;
--- a/Framework/Scene2D/Internals/CairoFloatTextureRenderer.cpp Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Scene2D/Internals/CairoFloatTextureRenderer.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -53,6 +53,8 @@ target.GetFormat() == Orthanc::PixelFormat_BGRA32 && sizeof(float) == 4); + static const float LOG_NORMALIZATION = 255.0f / log(1.0f + 255.0f); + for (unsigned int y = 0; y < height; y++) { const float* p = reinterpret_cast<const float*>(source.GetConstRow(y)); @@ -70,6 +72,14 @@ v = 255; } + if (l.IsApplyLog()) + { + // https://theailearner.com/2019/01/01/log-transformation/ + v = LOG_NORMALIZATION * log(1.0f + static_cast<float>(v)); + } + + assert(v >= 0.0f && v <= 255.0f); + uint8_t vv = static_cast<uint8_t>(v); if (l.IsInverted())
--- a/Framework/Scene2D/Internals/CairoInfoPanelRenderer.cpp Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Scene2D/Internals/CairoInfoPanelRenderer.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -34,9 +34,9 @@ texture_.Copy(l.GetTexture(), true); anchor_ = l.GetAnchor(); isLinearInterpolation_ = l.IsLinearInterpolation(); + applySceneRotation_ = l.ShouldApplySceneRotation(); } - void CairoInfoPanelRenderer::Render(const AffineTransform2D& transform, unsigned int canvasWidth, unsigned int canvasHeight) @@ -47,14 +47,71 @@ canvasWidth, canvasHeight); cairo_t* cr = target_.GetCairoContext(); - cairo_save(cr); - cairo_matrix_t t; - cairo_matrix_init_identity(&t); - cairo_matrix_translate(&t, dx, dy); - cairo_transform(cr, &t); + if (applySceneRotation_) + { + // the transformation is as follows: + // - originally, the image is aligned so that its top left corner + // is at 0,0 + // - first, we translate the image by -w/2,-h/2 + // - then we rotate it, so that the next rotation will make the + // image rotate around its center. + // - then, we translate the image by +w/2,+h/2 to put it + // back in place + // - the fourth and last transform is the one that brings the + // image to its desired anchored location. + + int32_t halfWidth = + static_cast<int32_t>(0.5 * texture_.GetWidth()); + + int32_t halfHeight = + static_cast<int32_t>(0.5 * texture_.GetHeight()); + + AffineTransform2D translation1 = + AffineTransform2D::CreateOffset(-halfWidth, -halfHeight); + + const Matrix& sceneTransformM = transform.GetHomogeneousMatrix(); + Matrix r; + Matrix q; + LinearAlgebra::RQDecomposition3x3(r, q, sceneTransformM); + + // first, put the scene rotation in a cairo matrix + cairo_matrix_t m; + cairo_matrix_init( + &m, q(0, 0), q(1, 0), q(0, 1), q(1, 1), q(0, 2), q(1, 2)); + // now let's build the transform piece by piece + // first translation (directly written in `transform`) + cairo_matrix_t transform; + cairo_matrix_init_identity(&transform); + cairo_matrix_translate(&transform, -halfWidth, -halfHeight); + + // then the rotation + cairo_matrix_multiply(&transform, &transform, &m); + + // then the second translation + { + cairo_matrix_t translation2; + cairo_matrix_init_translate(&translation2, halfWidth, halfHeight); + cairo_matrix_multiply(&transform, &transform, &m); + } + + // then the last translation + { + cairo_matrix_t translation3; + cairo_matrix_init_translate(&translation3, dx, dy); + cairo_matrix_multiply(&transform, &transform, &translation3); + } + cairo_transform(cr, &transform); + } + else + { + cairo_matrix_t t; + cairo_matrix_init_identity(&t); + cairo_matrix_translate(&t, dx, dy); + cairo_transform(cr, &t); + } cairo_set_operator(cr, CAIRO_OPERATOR_OVER); cairo_set_source_surface(cr, texture_.GetObject(), 0, 0);
--- a/Framework/Scene2D/Internals/CairoInfoPanelRenderer.h Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Scene2D/Internals/CairoInfoPanelRenderer.h Tue Mar 31 15:51:02 2020 +0200 @@ -36,11 +36,15 @@ CairoSurface texture_; BitmapAnchor anchor_; bool isLinearInterpolation_; + bool applySceneRotation_; public: CairoInfoPanelRenderer(ICairoContextProvider& target, const ISceneLayer& layer) : - target_(target) + target_(target), + anchor_(BitmapAnchor_TopLeft), + applySceneRotation_(false) + { Update(layer); }
--- a/Framework/Scene2D/Internals/CairoLookupTableTextureRenderer.cpp Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Scene2D/Internals/CairoLookupTableTextureRenderer.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -37,18 +37,6 @@ textureTransform_ = l.GetTransform(); isLinearInterpolation_ = l.IsLinearInterpolation(); - const float a = l.GetMinValue(); - float slope; - - if (l.GetMinValue() >= l.GetMaxValue()) - { - slope = 0; - } - else - { - slope = 256.0f / (l.GetMaxValue() - l.GetMinValue()); - } - const Orthanc::ImageAccessor& source = l.GetTexture(); const unsigned int width = source.GetWidth(); const unsigned int height = source.GetHeight(); @@ -56,46 +44,8 @@ Orthanc::ImageAccessor target; texture_.GetWriteableAccessor(target); - - const std::vector<uint8_t>& lut = l.GetLookupTable(); - if (lut.size() != 4 * 256) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); - } - - assert(source.GetFormat() == Orthanc::PixelFormat_Float32 && - target.GetFormat() == Orthanc::PixelFormat_BGRA32 && - sizeof(float) == 4); - - for (unsigned int y = 0; y < height; y++) - { - const float* p = reinterpret_cast<const float*>(source.GetConstRow(y)); - uint8_t* q = reinterpret_cast<uint8_t*>(target.GetRow(y)); - - for (unsigned int x = 0; x < width; x++) - { - float v = (*p - a) * slope; - if (v <= 0) - { - v = 0; - } - else if (v >= 255) - { - v = 255; - } - - uint8_t vv = static_cast<uint8_t>(v); - - q[0] = lut[4 * vv + 2]; // B - q[1] = lut[4 * vv + 1]; // G - q[2] = lut[4 * vv + 0]; // R - q[3] = lut[4 * vv + 3]; // A - - p++; - q += 4; - } - } - + l.Render(target); + cairo_surface_mark_dirty(texture_.GetObject()); }
--- a/Framework/Scene2D/Internals/CompositorHelper.cpp Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Scene2D/Internals/CompositorHelper.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -80,12 +80,13 @@ }; - void CompositorHelper::Visit(const ISceneLayer& layer, + void CompositorHelper::Visit(const Scene2D& scene, + const ISceneLayer& layer, uint64_t layerIdentifier, int depth) { // "Visit()" is only applied to layers existing in the scene - assert(scene_.HasLayer(depth)); + assert(scene.HasLayer(depth)); Content::iterator found = content_.find(depth); @@ -103,6 +104,8 @@ content_.erase(found); } + // the returned renderer can be NULL in case of an unknown layer + // or a NullLayer std::unique_ptr<ILayerRenderer> renderer(factory_.Create(layer)); if (renderer.get() != NULL) @@ -115,7 +118,7 @@ { // This layer has already been rendered assert(found->second->GetLastRevision() <= layer.GetRevision()); - + if (found->second->GetLastRevision() < layer.GetRevision()) { found->second->UpdateRenderer(); @@ -141,18 +144,34 @@ } - void CompositorHelper::Refresh(unsigned int canvasWidth, + void CompositorHelper::Refresh(const Scene2D& scene, + unsigned int canvasWidth, unsigned int canvasHeight) { + /** + * Safeguard mechanism to enforce the fact that the same scene + * is always used with the compositor. Note that the safeguard + * is not 100% bullet-proof, as a new scene might reuse the same + * address as a previous scene. + **/ + if (lastScene_ != NULL && + lastScene_ != &scene) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls, + "ICompositor::ResetScene() should have been called"); + } + + lastScene_ = &scene; + // Bring coordinate (0,0) to the center of the canvas AffineTransform2D offset = AffineTransform2D::CreateOffset( static_cast<double>(canvasWidth) / 2.0, static_cast<double>(canvasHeight) / 2.0); - sceneTransform_ = AffineTransform2D::Combine(offset, scene_.GetSceneToCanvasTransform()); + sceneTransform_ = AffineTransform2D::Combine(offset, scene.GetSceneToCanvasTransform()); canvasWidth_ = canvasWidth; canvasHeight_ = canvasHeight; - scene_.Apply(*this); + scene.Apply(*this); } } }
--- a/Framework/Scene2D/Internals/CompositorHelper.h Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Scene2D/Internals/CompositorHelper.h Tue Mar 31 15:51:02 2020 +0200 @@ -64,9 +64,9 @@ typedef std::map<int, Item*> Content; - const Scene2D& scene_; IRendererFactory& factory_; Content content_; + const Scene2D* lastScene_; // This is only a safeguard, don't use it! // Only valid during a call to Refresh() AffineTransform2D sceneTransform_; @@ -74,21 +74,22 @@ unsigned int canvasHeight_; protected: - virtual void Visit(const ISceneLayer& layer, + virtual void Visit(const Scene2D& scene, + const ISceneLayer& layer, uint64_t layerIdentifier, int depth); public: - CompositorHelper(const Scene2D& scene, - IRendererFactory& factory) : - scene_(scene), - factory_(factory) + CompositorHelper(IRendererFactory& factory) : + factory_(factory), + lastScene_(NULL) { } ~CompositorHelper(); - void Refresh(unsigned int canvasWidth, + void Refresh(const Scene2D& scene, + unsigned int canvasWidth, unsigned int canvasHeight); }; }
--- a/Framework/Scene2D/Internals/FixedPointAligner.cpp Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Scene2D/Internals/FixedPointAligner.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -25,26 +25,28 @@ { namespace Internals { - FixedPointAligner::FixedPointAligner(boost::weak_ptr<ViewportController> controllerW, + FixedPointAligner::FixedPointAligner(boost::shared_ptr<IViewport> viewport, const ScenePoint2D& p) - : controllerW_(controllerW) + : viewport_(viewport) , canvas_(p) { - boost::shared_ptr<ViewportController> controller = controllerW_.lock(); - pivot_ = canvas_.Apply(controller->GetCanvasToSceneTransform()); + std::unique_ptr<IViewport::ILock> lock(viewport_->Lock()); + pivot_ = canvas_.Apply(lock->GetController().GetCanvasToSceneTransform()); } void FixedPointAligner::Apply() { - boost::shared_ptr<ViewportController> controller = controllerW_.lock(); - ScenePoint2D p = canvas_.Apply(controller->GetCanvasToSceneTransform()); + std::unique_ptr<IViewport::ILock> lock(viewport_->Lock()); + ScenePoint2D p = canvas_.Apply( + lock->GetController().GetCanvasToSceneTransform()); - controller->SetSceneToCanvasTransform( + lock->GetController().SetSceneToCanvasTransform( AffineTransform2D::Combine( - controller->GetSceneToCanvasTransform(), + lock->GetController().GetSceneToCanvasTransform(), AffineTransform2D::CreateOffset(p.GetX() - pivot_.GetX(), p.GetY() - pivot_.GetY()))); + lock->Invalidate(); } } }
--- a/Framework/Scene2D/Internals/FixedPointAligner.h Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Scene2D/Internals/FixedPointAligner.h Tue Mar 31 15:51:02 2020 +0200 @@ -22,6 +22,9 @@ #include "../../Scene2DViewport/PredeclaredTypes.h" #include "../../Scene2D/ScenePoint2D.h" +#include "../../Viewport/IViewport.h" + +#include <boost/weak_ptr.hpp> namespace OrthancStone { @@ -32,12 +35,12 @@ class FixedPointAligner : public boost::noncopyable { private: - boost::weak_ptr<ViewportController> controllerW_; + boost::shared_ptr<IViewport> viewport_; ScenePoint2D pivot_; ScenePoint2D canvas_; public: - FixedPointAligner(boost::weak_ptr<ViewportController> controllerW, + FixedPointAligner(boost::shared_ptr<IViewport> viewport, const ScenePoint2D& p); void Apply();
--- a/Framework/Scene2D/Internals/OpenGLFloatTextureRenderer.cpp Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Scene2D/Internals/OpenGLFloatTextureRenderer.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -21,6 +21,8 @@ #include "OpenGLFloatTextureRenderer.h" +#include <Core/OrthancException.h> + namespace OrthancStone { namespace Internals @@ -32,6 +34,11 @@ { if (loadTexture) { + if (layer.IsApplyLog()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); + } + context_.MakeCurrent(); texture_.reset(new OpenGLFloatTextureProgram::Data( context_, layer.GetTexture(), layer.IsLinearInterpolation()));
--- a/Framework/Scene2D/Internals/OpenGLInfoPanelRenderer.cpp Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Scene2D/Internals/OpenGLInfoPanelRenderer.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -32,6 +32,7 @@ context_.MakeCurrent(); texture_.reset(new OpenGL::OpenGLTexture(context_)); texture_->Load(layer.GetTexture(), layer.IsLinearInterpolation()); + applySceneRotation_ = layer.ShouldApplySceneRotation(); anchor_ = layer.GetAnchor(); } } @@ -41,11 +42,11 @@ const InfoPanelSceneLayer& layer) : context_(context), program_(program), - anchor_(BitmapAnchor_TopLeft) + anchor_(BitmapAnchor_TopLeft), + applySceneRotation_(false) { LoadTexture(layer); } - void OpenGLInfoPanelRenderer::Render(const AffineTransform2D& transform, unsigned int canvasWidth, @@ -60,7 +61,56 @@ // The position of this type of layer is layer: Ignore the // "transform" coming from the scene - program_.Apply(*texture_, AffineTransform2D::CreateOffset(dx, dy), true); + AffineTransform2D actualTransform = + AffineTransform2D::CreateOffset(dx, dy); + + if (applySceneRotation_) + { + // the transformation is as follows: + // - originally, the image is aligned so that its top left corner + // is at 0,0 + // - first, we translate the image by -w/2,-h/2 + // - then we rotate it, so that the next rotation will make the + // image rotate around its center. + // - then, we translate the image by +w/2,+h/2 to put it + // back in place + // - the fourth and last transform is the one that brings the + // image to its desired anchored location. + + int32_t halfWidth = + static_cast<int32_t>(0.5 * texture_->GetWidth()); + + int32_t halfHeight= + static_cast<int32_t>(0.5 * texture_->GetHeight()); + + AffineTransform2D translation1 = + AffineTransform2D::CreateOffset(-halfWidth, -halfHeight); + + const Matrix& sceneTransformM = transform.GetHomogeneousMatrix(); + Matrix r; + Matrix q; + LinearAlgebra::RQDecomposition3x3(r, q, sceneTransformM); + + // counterintuitively, q is the rotation and r is the upper + // triangular + AffineTransform2D rotation(q); + + AffineTransform2D translation2 = + AffineTransform2D::CreateOffset(halfWidth, halfHeight); + + // please note that the last argument is the 1st applied + // transformation (rationale: if arguments are a, b and c, then + // the resulting matrix is a*b*c: + // x2 = (a*b*c)*x1 = (a*(b*(c*x1))) (you can see that the result + // of c*x1 is transformed by b, and the result of b*c*x1 is trans- + // formed by a) + actualTransform = AffineTransform2D::Combine(actualTransform, + translation2, + rotation, + translation1); + } + + program_.Apply(*texture_, actualTransform, true); } } }
--- a/Framework/Scene2D/Internals/OpenGLInfoPanelRenderer.h Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Scene2D/Internals/OpenGLInfoPanelRenderer.h Tue Mar 31 15:51:02 2020 +0200 @@ -36,6 +36,7 @@ OpenGLColorTextureProgram& program_; std::unique_ptr<OpenGL::OpenGLTexture> texture_; BitmapAnchor anchor_; + bool applySceneRotation_; void LoadTexture(const InfoPanelSceneLayer& layer);
--- a/Framework/Scene2D/Internals/OpenGLLookupTableTextureRenderer.cpp Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Scene2D/Internals/OpenGLLookupTableTextureRenderer.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -39,76 +39,14 @@ const unsigned int width = source.GetWidth(); const unsigned int height = source.GetHeight(); - if ((texture_.get() == NULL) || - (texture_->GetWidth() != width) || - (texture_->GetHeight() != height)) + if (texture_.get() == NULL || + texture_->GetWidth() != width || + texture_->GetHeight() != height) { - - texture_.reset(new Orthanc::Image( - Orthanc::PixelFormat_RGBA32, - width, - height, - false)); + texture_.reset(new Orthanc::Image(Orthanc::PixelFormat_RGBA32, width, height, false)); } - { - - const float a = layer.GetMinValue(); - float slope = 0; - - if (layer.GetMinValue() >= layer.GetMaxValue()) - { - slope = 0; - } - else - { - slope = 256.0f / (layer.GetMaxValue() - layer.GetMinValue()); - } - - Orthanc::ImageAccessor target; - texture_->GetWriteableAccessor(target); - - const std::vector<uint8_t>& lut = layer.GetLookupTable(); - if (lut.size() != 4 * 256) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); - } - - assert(source.GetFormat() == Orthanc::PixelFormat_Float32 && - target.GetFormat() == Orthanc::PixelFormat_RGBA32 && - sizeof(float) == 4); - - - for (unsigned int y = 0; y < height; y++) - { - const float* p = reinterpret_cast<const float*>(source.GetConstRow(y)); - uint8_t* q = reinterpret_cast<uint8_t*>(target.GetRow(y)); - - for (unsigned int x = 0; x < width; x++) - { - float v = (*p - a) * slope; - if (v <= 0) - { - v = 0; - } - else if (v >= 255) - { - v = 255; - } - - uint8_t vv = static_cast<uint8_t>(v); - - q[0] = lut[4 * vv + 0]; // R - q[1] = lut[4 * vv + 1]; // G - q[2] = lut[4 * vv + 2]; // B - q[3] = lut[4 * vv + 3]; // A - - p++; - q += 4; - } - } - - } + layer.Render(*texture_); context_.MakeCurrent(); glTexture_.reset(new OpenGL::OpenGLTexture(context_));
--- a/Framework/Scene2D/LookupTableStyleConfigurator.cpp Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Scene2D/LookupTableStyleConfigurator.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -39,7 +39,8 @@ LookupTableStyleConfigurator::LookupTableStyleConfigurator() : revision_(0), hasLut_(false), - hasRange_(false) + hasRange_(false), + applyLog_(false) { } @@ -82,6 +83,12 @@ } } + void LookupTableStyleConfigurator::SetApplyLog(bool apply) + { + applyLog_ = apply; + revision_++; + } + TextureBaseSceneLayer* LookupTableStyleConfigurator::CreateTextureFromImage(const Orthanc::ImageAccessor& image) const { throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); @@ -104,5 +111,7 @@ { l.FitRange(); } + + l.SetApplyLog(applyLog_); } }
--- a/Framework/Scene2D/LookupTableStyleConfigurator.h Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Scene2D/LookupTableStyleConfigurator.h Tue Mar 31 15:51:02 2020 +0200 @@ -39,6 +39,7 @@ bool hasRange_; float minValue_; float maxValue_; + bool applyLog_; public: LookupTableStyleConfigurator(); @@ -55,6 +56,13 @@ void SetRange(float minValue, float maxValue); + void SetApplyLog(bool apply); + + bool IsApplyLog() const + { + return applyLog_; + } + virtual uint64_t GetRevision() const { return revision_; @@ -63,7 +71,7 @@ virtual TextureBaseSceneLayer* CreateTextureFromImage(const Orthanc::ImageAccessor& image) const; virtual TextureBaseSceneLayer* CreateTextureFromDicom(const Orthanc::ImageAccessor& frame, - const DicomInstanceParameters& parameters) const + const DicomInstanceParameters& parameters) const { return parameters.CreateLookupTableTexture(frame); }
--- a/Framework/Scene2D/LookupTableTextureSceneLayer.cpp Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Scene2D/LookupTableTextureSceneLayer.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -27,7 +27,8 @@ namespace OrthancStone { - LookupTableTextureSceneLayer::LookupTableTextureSceneLayer(const Orthanc::ImageAccessor& texture) + LookupTableTextureSceneLayer::LookupTableTextureSceneLayer(const Orthanc::ImageAccessor& texture) : + applyLog_(false) { { std::unique_ptr<Orthanc::ImageAccessor> t( @@ -132,6 +133,12 @@ } } + void LookupTableTextureSceneLayer::SetApplyLog(bool apply) + { + applyLog_ = apply; + IncrementRevision(); + } + void LookupTableTextureSceneLayer::FitRange() { Orthanc::ImageProcessing::GetMinMaxFloatValue(minValue_, maxValue_, GetTexture()); @@ -158,4 +165,147 @@ return cloned.release(); } + + + // Templatized function to speed up computations, by avoiding + // testing conditions on each pixel + template <bool IsApplyLog, + Orthanc::PixelFormat TargetFormat> + static void RenderInternal(Orthanc::ImageAccessor& target, + const Orthanc::ImageAccessor& source, + float minValue, + float slope, + const std::vector<uint8_t>& lut) + { + static const float LOG_NORMALIZATION = 255.0f / log(1.0f + 255.0f); + + const unsigned int width = source.GetWidth(); + const unsigned int height = source.GetHeight(); + + for (unsigned int y = 0; y < height; y++) + { + const float* p = reinterpret_cast<const float*>(source.GetConstRow(y)); + uint8_t* q = reinterpret_cast<uint8_t*>(target.GetRow(y)); + + for (unsigned int x = 0; x < width; x++) + { + float v = (*p - minValue) * slope; + if (v <= 0) + { + v = 0; + } + else if (v >= 255) + { + v = 255; + } + + if (IsApplyLog) + { + // https://theailearner.com/2019/01/01/log-transformation/ + v = LOG_NORMALIZATION * log(1.0f + static_cast<float>(v)); + } + + assert(v >= 0.0f && v <= 255.0f); + + uint8_t vv = static_cast<uint8_t>(v); + + switch (TargetFormat) + { + case Orthanc::PixelFormat_BGRA32: + // For Cairo surfaces + q[0] = lut[4 * vv + 2]; // B + q[1] = lut[4 * vv + 1]; // G + q[2] = lut[4 * vv + 0]; // R + q[3] = lut[4 * vv + 3]; // A + break; + + case Orthanc::PixelFormat_RGBA32: + // For OpenGL + q[0] = lut[4 * vv + 0]; // R + q[1] = lut[4 * vv + 1]; // G + q[2] = lut[4 * vv + 2]; // B + q[3] = lut[4 * vv + 3]; // A + break; + + default: + assert(0); + } + + p++; + q += 4; + } + } + } + + + void LookupTableTextureSceneLayer::Render(Orthanc::ImageAccessor& target) const + { + assert(sizeof(float) == 4); + + if (!HasTexture()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + + const Orthanc::ImageAccessor& source = GetTexture(); + + if (source.GetFormat() != Orthanc::PixelFormat_Float32 || + (target.GetFormat() != Orthanc::PixelFormat_RGBA32 && + target.GetFormat() != Orthanc::PixelFormat_BGRA32)) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageFormat); + } + + if (source.GetWidth() != target.GetWidth() || + source.GetHeight() != target.GetHeight()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageSize); + } + + const float minValue = GetMinValue(); + float slope; + + if (GetMinValue() >= GetMaxValue()) + { + slope = 0; + } + else + { + slope = 256.0f / (GetMaxValue() - GetMinValue()); + } + + const std::vector<uint8_t>& lut = GetLookupTable(); + if (lut.size() != 4 * 256) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + + switch (target.GetFormat()) + { + case Orthanc::PixelFormat_RGBA32: + if (applyLog_) + { + RenderInternal<true, Orthanc::PixelFormat_RGBA32>(target, source, minValue, slope, lut); + } + else + { + RenderInternal<false, Orthanc::PixelFormat_RGBA32>(target, source, minValue, slope, lut); + } + break; + + case Orthanc::PixelFormat_BGRA32: + if (applyLog_) + { + RenderInternal<true, Orthanc::PixelFormat_BGRA32>(target, source, minValue, slope, lut); + } + else + { + RenderInternal<false, Orthanc::PixelFormat_BGRA32>(target, source, minValue, slope, lut); + } + break; + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + } }
--- a/Framework/Scene2D/LookupTableTextureSceneLayer.h Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Scene2D/LookupTableTextureSceneLayer.h Tue Mar 31 15:51:02 2020 +0200 @@ -32,6 +32,7 @@ float minValue_; float maxValue_; std::vector<uint8_t> lut_; + bool applyLog_; void SetLookupTableRgb(const std::vector<uint8_t>& lut); @@ -66,11 +67,22 @@ return lut_; } + void SetApplyLog(bool apply); + + bool IsApplyLog() const + { + return applyLog_; + } + virtual ISceneLayer* Clone() const; virtual Type GetType() const { return Type_LookupTableTexture; } + + // Render the texture to a color image of format BGRA32 (Cairo + // surfaces) or RGBA32 (OpenGL) + void Render(Orthanc::ImageAccessor& target) const; }; }
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Scene2D/NullLayer.h Tue Mar 31 15:51:02 2020 +0200 @@ -0,0 +1,62 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 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 "ISceneLayer.h" + +#include <Core/Enumerations.h> + +#include <stdint.h> + +/** + This layer can be used when a z-index needs to be booked inside a Scene2D. + + It can later be replaced by the actual layer. +*/ +namespace OrthancStone +{ + class NullLayer : public ISceneLayer + { + public: + NullLayer() {} + + virtual ISceneLayer* Clone() const ORTHANC_OVERRIDE + { + return new NullLayer(); + } + + virtual Type GetType() const ORTHANC_OVERRIDE + { + return Type_NullLayer; + } + + virtual bool GetBoundingBox(Extent2D& target) const ORTHANC_OVERRIDE + { + target = Extent2D(); + return false; + } + + virtual uint64_t GetRevision() const ORTHANC_OVERRIDE + { + return 0; + } + }; +}
--- a/Framework/Scene2D/OpenGLCompositor.cpp Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Scene2D/OpenGLCompositor.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -13,7 +13,7 @@ * 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/>. **/ @@ -128,10 +128,8 @@ } } - OpenGLCompositor::OpenGLCompositor(OpenGL::IOpenGLContext& context, - const Scene2D& scene) : + OpenGLCompositor::OpenGLCompositor(OpenGL::IOpenGLContext& context) : context_(context), - helper_(scene, *this), colorTextureProgram_(context), floatTextureProgram_(context), linesProgram_(context), @@ -139,22 +137,52 @@ canvasWidth_(0), canvasHeight_(0) { + if (!context_.IsContextLost()) + { + canvasWidth_ = context_.GetCanvasWidth(); + canvasHeight_ = context_.GetCanvasHeight(); + } + + ResetScene(); } OpenGLCompositor::~OpenGLCompositor() { if (!context_.IsContextLost()) { - context_.MakeCurrent(); // this can throw if context lost! - for (Fonts::iterator it = fonts_.begin(); it != fonts_.end(); ++it) + try { - assert(it->second != NULL); - delete it->second; + try + { + context_.MakeCurrent(); // this can throw if context lost! + } + catch (...) + { + LOG(ERROR) << "context_.MakeCurrent() failed in OpenGLCompositor::~OpenGLCompositor()!"; + } + + for (Fonts::iterator it = fonts_.begin(); it != fonts_.end(); ++it) + { + try + { + + assert(it->second != NULL); + delete it->second; + } + catch (...) + { + LOG(ERROR) << "Exception thrown while deleting OpenGL-based font!"; + } + } + } + catch (...) + { + // logging threw an exception! } } } - void OpenGLCompositor::Refresh() + void OpenGLCompositor::Refresh(const Scene2D& scene) { if (!context_.IsContextLost()) { @@ -167,11 +195,10 @@ glClearColor(0, 0, 0, 1); glClear(GL_COLOR_BUFFER_BIT); - helper_.Refresh(canvasWidth_, canvasHeight_); + helper_->Refresh(scene, canvasWidth_, canvasHeight_); context_.SwapBuffer(); } - } void OpenGLCompositor::SetFont(size_t index,
--- a/Framework/Scene2D/OpenGLCompositor.h Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Scene2D/OpenGLCompositor.h Tue Mar 31 15:51:02 2020 +0200 @@ -37,27 +37,31 @@ typedef std::map<size_t, Font*> Fonts; - OpenGL::IOpenGLContext& context_; - Fonts fonts_; - Internals::CompositorHelper helper_; - Internals::OpenGLColorTextureProgram colorTextureProgram_; - Internals::OpenGLFloatTextureProgram floatTextureProgram_; - Internals::OpenGLLinesProgram linesProgram_; - Internals::OpenGLTextProgram textProgram_; - unsigned int canvasWidth_; - unsigned int canvasHeight_; + OpenGL::IOpenGLContext& context_; + Fonts fonts_; + std::unique_ptr<Internals::CompositorHelper> helper_; + Internals::OpenGLColorTextureProgram colorTextureProgram_; + Internals::OpenGLFloatTextureProgram floatTextureProgram_; + Internals::OpenGLLinesProgram linesProgram_; + Internals::OpenGLTextProgram textProgram_; + unsigned int canvasWidth_; + unsigned int canvasHeight_; const Font* GetFont(size_t fontIndex) const; virtual Internals::CompositorHelper::ILayerRenderer* Create(const ISceneLayer& layer) ORTHANC_OVERRIDE; public: - OpenGLCompositor(OpenGL::IOpenGLContext& context, - const Scene2D& scene); + OpenGLCompositor(OpenGL::IOpenGLContext& context); virtual ~OpenGLCompositor(); - virtual void Refresh() ORTHANC_OVERRIDE; + virtual void Refresh(const Scene2D& scene) ORTHANC_OVERRIDE; + + virtual void ResetScene() ORTHANC_OVERRIDE + { + helper_.reset(new Internals::CompositorHelper(*this)); + } void SetFont(size_t index, const GlyphBitmapAlphabet& dict);
--- a/Framework/Scene2D/PanSceneTracker.cpp Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Scene2D/PanSceneTracker.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -20,16 +20,23 @@ #include "PanSceneTracker.h" +#include "../Viewport/IViewport.h" #include "../Scene2DViewport/ViewportController.h" +#include <memory> + namespace OrthancStone { - PanSceneTracker::PanSceneTracker(boost::weak_ptr<ViewportController> controllerW, + PanSceneTracker::PanSceneTracker(boost::shared_ptr<IViewport> viewport, const PointerEvent& event) - : OneGesturePointerTracker(controllerW) - , originalSceneToCanvas_(GetController()->GetSceneToCanvasTransform()) - , originalCanvasToScene_(GetController()->GetCanvasToSceneTransform()) + : OneGesturePointerTracker(viewport) { + + std::unique_ptr<IViewport::ILock> lock(viewport_->Lock()); + + originalSceneToCanvas_ = lock->GetController().GetSceneToCanvasTransform(); + originalCanvasToScene_ = lock->GetController().GetCanvasToSceneTransform(); + pivot_ = event.GetMainPosition().Apply(originalCanvasToScene_); } @@ -38,25 +45,19 @@ { ScenePoint2D p = event.GetMainPosition().Apply(originalCanvasToScene_); - // The controller is a weak pointer. It could be deleted when the - // tracker is still alive (for instance, because of a lost WebGL - // context) - if(GetController().get() != NULL) - { - GetController()->SetSceneToCanvasTransform( - AffineTransform2D::Combine( - originalSceneToCanvas_, - AffineTransform2D::CreateOffset(p.GetX() - pivot_.GetX(), - p.GetY() - pivot_.GetY()))); - } + std::unique_ptr<IViewport::ILock> lock(viewport_->Lock()); + + lock->GetController().SetSceneToCanvasTransform( + AffineTransform2D::Combine( + originalSceneToCanvas_, + AffineTransform2D::CreateOffset(p.GetX() - pivot_.GetX(), + p.GetY() - pivot_.GetY()))); + lock->Invalidate(); } void PanSceneTracker::Cancel() { - if(GetController().get() != NULL) - { - GetController()->SetSceneToCanvasTransform(originalSceneToCanvas_); - } + std::unique_ptr<IViewport::ILock> lock(viewport_->Lock()); + lock->GetController().SetSceneToCanvasTransform(originalSceneToCanvas_); } - }
--- a/Framework/Scene2D/PanSceneTracker.h Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Scene2D/PanSceneTracker.h Tue Mar 31 15:51:02 2020 +0200 @@ -28,14 +28,13 @@ class PanSceneTracker : public OneGesturePointerTracker { public: - PanSceneTracker(boost::weak_ptr<ViewportController> controllerW, + PanSceneTracker(boost::shared_ptr<IViewport> viewport, const PointerEvent& event); virtual void PointerMove(const PointerEvent& event) ORTHANC_OVERRIDE; virtual void Cancel() ORTHANC_OVERRIDE; private: - boost::weak_ptr<ViewportController> controllerW_; ScenePoint2D pivot_; AffineTransform2D originalSceneToCanvas_; AffineTransform2D originalCanvasToScene_;
--- a/Framework/Scene2D/PointerEvent.cpp Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Scene2D/PointerEvent.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -26,6 +26,7 @@ namespace OrthancStone { PointerEvent::PointerEvent() : + button_(MouseButton_None), hasAltModifier_(false), hasControlModifier_(false), hasShiftModifier_(false)
--- a/Framework/Scene2D/PointerEvent.h Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Scene2D/PointerEvent.h Tue Mar 31 15:51:02 2020 +0200 @@ -31,6 +31,7 @@ class PointerEvent : public boost::noncopyable { private: + MouseButton button_; std::vector<ScenePoint2D> positions_; bool hasAltModifier_; bool hasControlModifier_; @@ -88,5 +89,15 @@ { return hasShiftModifier_; } + + void SetMouseButton(MouseButton button) + { + button_ = button; + } + + MouseButton GetMouseButton() const + { + return button_; + } }; }
--- a/Framework/Scene2D/RotateSceneTracker.cpp Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Scene2D/RotateSceneTracker.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -13,7 +13,7 @@ * 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/>. **/ @@ -23,23 +23,25 @@ namespace OrthancStone { - RotateSceneTracker::RotateSceneTracker(boost::weak_ptr<ViewportController> controllerW, + RotateSceneTracker::RotateSceneTracker(boost::shared_ptr<IViewport> viewport, const PointerEvent& event) - : OneGesturePointerTracker(controllerW) + : OneGesturePointerTracker(viewport) , click_(event.GetMainPosition()) - , aligner_(controllerW, click_) + , aligner_(viewport, click_) , isFirst_(true) - , originalSceneToCanvas_(GetController()->GetSceneToCanvasTransform()) { + std::unique_ptr<IViewport::ILock> lock(viewport_->Lock()); + originalSceneToCanvas_ = lock->GetController().GetSceneToCanvasTransform(); + } - + void RotateSceneTracker::PointerMove(const PointerEvent& event) { ScenePoint2D p = event.GetMainPosition(); double dx = p.GetX() - click_.GetX(); double dy = p.GetY() - click_.GetY(); - if (std::abs(dx) > 5.0 || + if (std::abs(dx) > 5.0 || std::abs(dy) > 5.0) { double a = atan2(dy, dx); @@ -50,28 +52,22 @@ isFirst_ = false; } - // The controller is a weak pointer. It could be deleted when the - // tracker is still alive (for instance, because of a lost WebGL - // context that triggers a recreation of the viewport) - if(GetController().get() != NULL) - { - GetController()->SetSceneToCanvasTransform( - AffineTransform2D::Combine( - AffineTransform2D::CreateRotation(a - referenceAngle_), - originalSceneToCanvas_)); - - aligner_.Apply(); - } + std::unique_ptr<IViewport::ILock> lock(viewport_->Lock()); + + lock->GetController().SetSceneToCanvasTransform( + AffineTransform2D::Combine( + AffineTransform2D::CreateRotation(a - referenceAngle_), + originalSceneToCanvas_)); + aligner_.Apply(); + lock->Invalidate(); } } void RotateSceneTracker::Cancel() { - // See remark above - if(GetController().get() != NULL) - { - GetController()->SetSceneToCanvasTransform(originalSceneToCanvas_); - } + // See remark above + std::unique_ptr<IViewport::ILock> lock(viewport_->Lock()); + lock->GetController().SetSceneToCanvasTransform(originalSceneToCanvas_); + lock->Invalidate(); } - }
--- a/Framework/Scene2D/RotateSceneTracker.h Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Scene2D/RotateSceneTracker.h Tue Mar 31 15:51:02 2020 +0200 @@ -30,7 +30,7 @@ class RotateSceneTracker : public OneGesturePointerTracker { public: - RotateSceneTracker(boost::weak_ptr<ViewportController> controllerW, + RotateSceneTracker(boost::shared_ptr<IViewport> viewport, const PointerEvent& event); virtual void PointerMove(const PointerEvent& event) ORTHANC_OVERRIDE;
--- a/Framework/Scene2D/Scene2D.cpp Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Scene2D/Scene2D.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -208,7 +208,7 @@ it != content_.end(); ++it) { assert(it->second != NULL); - visitor.Visit(it->second->GetLayer(), it->second->GetIdentifier(), it->first); + visitor.Visit(*this, it->second->GetLayer(), it->second->GetIdentifier(), it->first); } }
--- a/Framework/Scene2D/Scene2D.h Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Scene2D/Scene2D.h Tue Mar 31 15:51:02 2020 +0200 @@ -40,7 +40,8 @@ { } - virtual void Visit(const ISceneLayer& layer, + virtual void Visit(const Scene2D& scene, + const ISceneLayer& layer, uint64_t layerIdentifier, int depth) = 0; };
--- a/Framework/Scene2D/ScenePoint2D.h Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Scene2D/ScenePoint2D.h Tue Mar 31 15:51:02 2020 +0200 @@ -64,115 +64,115 @@ return ScenePoint2D(x, y); } - const ScenePoint2D operator-(const ScenePoint2D& a) const - { - ScenePoint2D v; - v.x_ = x_ - a.x_; - v.y_ = y_ - a.y_; - - return v; - } - - const ScenePoint2D operator+(const ScenePoint2D& a) const - { - ScenePoint2D v; - v.x_ = x_ + a.x_; - v.y_ = y_ + a.y_; - - return v; - } - - const ScenePoint2D operator*(double a) const - { - ScenePoint2D v; - v.x_ = x_ * a; - v.y_ = y_ * a; - - return v; - } - - const ScenePoint2D operator/(double a) const - { - ScenePoint2D v; - v.x_ = x_ / a; - v.y_ = y_ / a; - - return v; - } - - static void MidPoint(ScenePoint2D& result, const ScenePoint2D& a, const ScenePoint2D& b) - { - result.x_ = 0.5 * (a.x_ + b.x_); - result.y_ = 0.5 * (a.y_ + b.y_); - } - - static double Dot(const ScenePoint2D& a, const ScenePoint2D& b) - { - return a.x_ * b.x_ + a.y_ * b.y_; - } + const ScenePoint2D operator-(const ScenePoint2D& a) const + { + ScenePoint2D v; + v.x_ = x_ - a.x_; + v.y_ = y_ - a.y_; + + return v; + } + + const ScenePoint2D operator+(const ScenePoint2D& a) const + { + ScenePoint2D v; + v.x_ = x_ + a.x_; + v.y_ = y_ + a.y_; + + return v; + } + + const ScenePoint2D operator*(double a) const + { + ScenePoint2D v; + v.x_ = x_ * a; + v.y_ = y_ * a; - static double SquaredMagnitude(const ScenePoint2D& v) - { - return v.x_ * v.x_ + v.y_ * v.y_; - } + return v; + } + + const ScenePoint2D operator/(double a) const + { + ScenePoint2D v; + v.x_ = x_ / a; + v.y_ = y_ / a; + + return v; + } + + static void MidPoint(ScenePoint2D& result, const ScenePoint2D& a, const ScenePoint2D& b) + { + result.x_ = 0.5 * (a.x_ + b.x_); + result.y_ = 0.5 * (a.y_ + b.y_); + } + + static double Dot(const ScenePoint2D& a, const ScenePoint2D& b) + { + return a.x_ * b.x_ + a.y_ * b.y_; + } + + static double SquaredMagnitude(const ScenePoint2D& v) + { + return v.x_ * v.x_ + v.y_ * v.y_; + } - static double Magnitude(const ScenePoint2D& v) - { - double squaredMagnitude = SquaredMagnitude(v); - if (LinearAlgebra::IsCloseToZero(squaredMagnitude)) - return 0.0; - return sqrt(squaredMagnitude); - } + static double Magnitude(const ScenePoint2D& v) + { + double squaredMagnitude = SquaredMagnitude(v); + if (LinearAlgebra::IsCloseToZero(squaredMagnitude)) + return 0.0; + return sqrt(squaredMagnitude); + } - static double SquaredDistancePtPt(const ScenePoint2D& a, const ScenePoint2D& b) - { - ScenePoint2D n = b - a; - return Dot(n, n); - } + static double SquaredDistancePtPt(const ScenePoint2D& a, const ScenePoint2D& b) + { + ScenePoint2D n = b - a; + return Dot(n, n); + } - static double DistancePtPt(const ScenePoint2D& a, const ScenePoint2D& b) - { - double squaredDist = SquaredDistancePtPt(a, b); - return sqrt(squaredDist); - } + static double DistancePtPt(const ScenePoint2D& a, const ScenePoint2D& b) + { + double squaredDist = SquaredDistancePtPt(a, b); + return sqrt(squaredDist); + } + + /** + Distance from point p to [a,b] segment - /** - Distance from point p to [a,b] segment - - Rewritten from https://www.randygaul.net/2014/07/23/distance-point-to-line-segment/ - */ - static double SquaredDistancePtSegment(const ScenePoint2D& a, const ScenePoint2D& b, const ScenePoint2D& p) - { - ScenePoint2D n = b - a; - ScenePoint2D pa = a - p; - - double c = Dot(n, pa); - - // Closest point is a - if (c > 0.0) - return Dot(pa, pa); - - ScenePoint2D bp = p - b; - - // Closest point is b - if (Dot(n, bp) > 0.0) - return Dot(bp, bp); - - // if segment length is very short, we approximate distance to the - // distance with a - double nq = Dot(n, n); - if (LinearAlgebra::IsCloseToZero(nq)) - { - // segment is very small: approximate distance from point to segment - // with distance from p to a - return Dot(pa, pa); - } - else - { - // Closest point is between a and b - ScenePoint2D e = pa - n * (c / nq); - return Dot(e, e); - } + Rewritten from https://www.randygaul.net/2014/07/23/distance-point-to-line-segment/ + */ + static double SquaredDistancePtSegment(const ScenePoint2D& a, const ScenePoint2D& b, const ScenePoint2D& p) + { + ScenePoint2D n = b - a; + ScenePoint2D pa = a - p; + + double c = Dot(n, pa); + + // Closest point is a + if (c > 0.0) + return Dot(pa, pa); + + ScenePoint2D bp = p - b; + + // Closest point is b + if (Dot(n, bp) > 0.0) + return Dot(bp, bp); + + // if segment length is very short, we approximate distance to the + // distance with a + double nq = Dot(n, n); + if (LinearAlgebra::IsCloseToZero(nq)) + { + // segment is very small: approximate distance from point to segment + // with distance from p to a + return Dot(pa, pa); + } + else + { + // Closest point is between a and b + ScenePoint2D e = pa - n * (c / nq); + return Dot(e, e); + } } }; }
--- a/Framework/Scene2D/ZoomSceneTracker.cpp Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Scene2D/ZoomSceneTracker.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -22,19 +22,19 @@ #include "ZoomSceneTracker.h" #include "../Scene2DViewport/ViewportController.h" -using boost::weak_ptr; -using boost::shared_ptr; - namespace OrthancStone { - ZoomSceneTracker::ZoomSceneTracker(weak_ptr<ViewportController> controllerW, + ZoomSceneTracker::ZoomSceneTracker(boost::shared_ptr<IViewport> viewport, const PointerEvent& event, unsigned int canvasHeight) - : OneGesturePointerTracker(controllerW) + : OneGesturePointerTracker(viewport) , clickY_(event.GetMainPosition().GetY()) - , aligner_(controllerW, event.GetMainPosition()) - , originalSceneToCanvas_(GetController()->GetSceneToCanvasTransform()) + , aligner_(viewport, event.GetMainPosition()) { + + std::unique_ptr<IViewport::ILock> lock(viewport_->Lock()); + originalSceneToCanvas_ = lock->GetController().GetSceneToCanvasTransform(); + if (canvasHeight <= 3) { active_ = false; @@ -76,26 +76,20 @@ double zoom = pow(2.0, z); - // The controller is a weak pointer. It could be deleted when the - // tracker is still alive (for instance, because of a lost WebGL - // context) - if(GetController().get() != NULL) - { - GetController()->SetSceneToCanvasTransform( - AffineTransform2D::Combine( - AffineTransform2D::CreateScaling(zoom, zoom), - originalSceneToCanvas_)); - - aligner_.Apply(); - } + std::unique_ptr<IViewport::ILock> lock(viewport_->Lock()); + lock->GetController().SetSceneToCanvasTransform( + AffineTransform2D::Combine( + AffineTransform2D::CreateScaling(zoom, zoom), + originalSceneToCanvas_)); + aligner_.Apply(); + lock->Invalidate(); } } void ZoomSceneTracker::Cancel() { - if(GetController().get() != NULL) - { - GetController()->SetSceneToCanvasTransform(originalSceneToCanvas_); - } + std::unique_ptr<IViewport::ILock> lock(viewport_->Lock()); + lock->GetController().SetSceneToCanvasTransform(originalSceneToCanvas_); + lock->Invalidate(); } }
--- a/Framework/Scene2D/ZoomSceneTracker.h Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Scene2D/ZoomSceneTracker.h Tue Mar 31 15:51:02 2020 +0200 @@ -23,14 +23,17 @@ #include "../Scene2DViewport/OneGesturePointerTracker.h" +#include "../Viewport/IViewport.h" #include "Internals/FixedPointAligner.h" +#include <boost/weak_ptr.hpp> + namespace OrthancStone { class ZoomSceneTracker : public OneGesturePointerTracker { public: - ZoomSceneTracker(boost::weak_ptr<ViewportController> controllerW, + ZoomSceneTracker(boost::shared_ptr<IViewport> viewport, const PointerEvent& event, unsigned int canvasHeight);
--- a/Framework/Scene2DViewport/AngleMeasureTool.cpp Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Scene2DViewport/AngleMeasureTool.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -42,16 +42,23 @@ // the params in the LayerHolder ctor specify the number of polyline and text // layers AngleMeasureTool::AngleMeasureTool( - MessageBroker& broker, boost::weak_ptr<ViewportController> controllerW) - : MeasureTool(broker, controllerW) + boost::shared_ptr<IViewport> viewport) + : MeasureTool(viewport) #if ORTHANC_STONE_ENABLE_OUTLINED_TEXT == 1 - , layerHolder_(boost::make_shared<LayerHolder>(controllerW,1,5)) + , layerHolder_(boost::shared_ptr<LayerHolder>(new LayerHolder(viewport,1,5))) #else - , layerHolder_(boost::make_shared<LayerHolder>(controllerW, 1, 1)) + , layerHolder_(boost::shared_ptr<LayerHolder>(new LayerHolder(viewport,1,1))) #endif , angleHighlightArea_(AngleHighlightArea_None) { - RefreshScene(); + } + + boost::shared_ptr<AngleMeasureTool> AngleMeasureTool::Create(boost::shared_ptr<IViewport> viewport) + { + boost::shared_ptr<AngleMeasureTool> obj(new AngleMeasureTool(viewport)); + obj->MeasureTool::PostConstructor(); + obj->RefreshScene(); + return obj; } AngleMeasureTool::~AngleMeasureTool() @@ -108,7 +115,9 @@ void AngleMeasureTool::SetMemento(boost::shared_ptr<MeasureToolMemento> mementoBase) { - boost::shared_ptr<AngleMeasureToolMemento> memento = boost::dynamic_pointer_cast<AngleMeasureToolMemento>(mementoBase); + boost::shared_ptr<AngleMeasureToolMemento> memento = + boost::dynamic_pointer_cast<AngleMeasureToolMemento>(mementoBase); + ORTHANC_ASSERT(memento.get() != NULL, "Internal error: wrong (or bad) memento"); center_ = memento->center_; side1End_ = memento->side1End_; @@ -119,7 +128,8 @@ std::string AngleMeasureTool::GetDescription() { std::stringstream ss; - ss << "AngleMeasureTool. Center = " << center_ << " Side1End = " << side1End_ << " Side2End = " << side2End_; + ss << "AngleMeasureTool. Center = " << center_ << " Side1End = " + << side1End_ << " Side2End = " << side2End_; return ss.str(); } @@ -131,36 +141,51 @@ AngleMeasureTool::AngleHighlightArea AngleMeasureTool::AngleHitTest(ScenePoint2D p) const { - const double pixelToScene = - GetController()->GetScene().GetCanvasToSceneTransform().ComputeZoom(); - const double SQUARED_HIT_TEST_MAX_DISTANCE_SCENE_COORD = pixelToScene * HIT_TEST_MAX_DISTANCE_CANVAS_COORD * pixelToScene * HIT_TEST_MAX_DISTANCE_CANVAS_COORD; + std::unique_ptr<IViewport::ILock> lock(viewport_->Lock()); + ViewportController& controller = lock->GetController(); + Scene2D& scene = controller.GetScene(); + + const double pixelToScene = scene.GetCanvasToSceneTransform().ComputeZoom(); + + const double SQUARED_HIT_TEST_MAX_DISTANCE_SCENE_COORD = + pixelToScene * HIT_TEST_MAX_DISTANCE_CANVAS_COORD * + pixelToScene * HIT_TEST_MAX_DISTANCE_CANVAS_COORD; { - const double sqDistanceFromSide1End = ScenePoint2D::SquaredDistancePtPt(p, side1End_); + const double sqDistanceFromSide1End = + ScenePoint2D::SquaredDistancePtPt(p, side1End_); + if (sqDistanceFromSide1End <= SQUARED_HIT_TEST_MAX_DISTANCE_SCENE_COORD) return AngleHighlightArea_Side1End; } { - const double sqDistanceFromSide2End = ScenePoint2D::SquaredDistancePtPt(p, side2End_); + const double sqDistanceFromSide2End = + ScenePoint2D::SquaredDistancePtPt(p, side2End_); + if (sqDistanceFromSide2End <= SQUARED_HIT_TEST_MAX_DISTANCE_SCENE_COORD) return AngleHighlightArea_Side2End; } { - const double sqDistanceFromCenter = ScenePoint2D::SquaredDistancePtPt(p, center_); + const double sqDistanceFromCenter = + ScenePoint2D::SquaredDistancePtPt(p, center_); if (sqDistanceFromCenter <= SQUARED_HIT_TEST_MAX_DISTANCE_SCENE_COORD) return AngleHighlightArea_Center; } { - const double sqDistanceFromSide1 = ScenePoint2D::SquaredDistancePtSegment(center_, side1End_, p); + const double sqDistanceFromSide1 = + ScenePoint2D::SquaredDistancePtSegment(center_, side1End_, p); + if (sqDistanceFromSide1 <= SQUARED_HIT_TEST_MAX_DISTANCE_SCENE_COORD) return AngleHighlightArea_Side1; } { - const double sqDistanceFromSide2 = ScenePoint2D::SquaredDistancePtSegment(center_, side2End_, p); + const double sqDistanceFromSide2 = + ScenePoint2D::SquaredDistancePtSegment(center_, side2End_, p); + if (sqDistanceFromSide2 <= SQUARED_HIT_TEST_MAX_DISTANCE_SCENE_COORD) return AngleHighlightArea_Side2; } @@ -168,7 +193,7 @@ return AngleHighlightArea_None; } - bool AngleMeasureTool::HitTest(ScenePoint2D p) const + bool AngleMeasureTool::HitTest(ScenePoint2D p) { return AngleHitTest(p) != AngleHighlightArea_None; } @@ -176,8 +201,12 @@ boost::shared_ptr<IFlexiblePointerTracker> AngleMeasureTool::CreateEditionTracker(const PointerEvent& e) { + std::unique_ptr<IViewport::ILock> lock(viewport_->Lock()); + ViewportController& controller = lock->GetController(); + Scene2D& scene = controller.GetScene(); + ScenePoint2D scenePos = e.GetMainPosition().Apply( - GetController()->GetScene().GetCanvasToSceneTransform()); + scene.GetCanvasToSceneTransform()); if (!HitTest(scenePos)) return boost::shared_ptr<IFlexiblePointerTracker>(); @@ -186,11 +215,12 @@ new EditLineMeasureTracker( boost::shared_ptr<LineMeasureTool> measureTool; MessageBroker & broker, - boost::weak_ptr<ViewportController> controllerW, + boost::shared_ptr<IViewport> viewport, const PointerEvent & e); */ + boost::shared_ptr<EditAngleMeasureTracker> editAngleMeasureTracker( - new EditAngleMeasureTracker(shared_from_this(), GetBroker(), GetController(), e)); + new EditAngleMeasureTracker(shared_from_this(), viewport_, e)); return editAngleMeasureTracker; } @@ -204,7 +234,10 @@ { if (IsSceneAlive()) { - boost::shared_ptr<ViewportController> controller = GetController(); + std::unique_ptr<IViewport::ILock> lock(viewport_->Lock()); + ViewportController& controller = lock->GetController(); + Scene2D& scene = controller.GetScene(); + if (IsEnabled()) { layerHolder_->CreateLayersIfNeeded(); @@ -212,72 +245,90 @@ { // Fill the polyline layer with the measurement lines PolylineSceneLayer* polylineLayer = layerHolder_->GetPolylineLayer(0); - polylineLayer->ClearAllChains(); + if (polylineLayer) + { + polylineLayer->ClearAllChains(); + + const Color color(TOOL_ANGLE_LINES_COLOR_RED, + TOOL_ANGLE_LINES_COLOR_GREEN, + TOOL_ANGLE_LINES_COLOR_BLUE); + + const Color highlightColor(TOOL_ANGLE_LINES_HL_COLOR_RED, + TOOL_ANGLE_LINES_HL_COLOR_GREEN, + TOOL_ANGLE_LINES_HL_COLOR_BLUE); + + // sides + { + { + PolylineSceneLayer::Chain chain; + chain.push_back(side1End_); + chain.push_back(center_); - const Color color(TOOL_ANGLE_LINES_COLOR_RED, TOOL_ANGLE_LINES_COLOR_GREEN, TOOL_ANGLE_LINES_COLOR_BLUE); - const Color highlightColor(TOOL_ANGLE_LINES_HL_COLOR_RED, TOOL_ANGLE_LINES_HL_COLOR_GREEN, TOOL_ANGLE_LINES_HL_COLOR_BLUE); + if ((angleHighlightArea_ == AngleHighlightArea_Side1) || + (angleHighlightArea_ == AngleHighlightArea_Side2)) + { + polylineLayer->AddChain(chain, false, highlightColor); + } + else + { + polylineLayer->AddChain(chain, false, color); + } + } + { + PolylineSceneLayer::Chain chain; + chain.push_back(side2End_); + chain.push_back(center_); + if ((angleHighlightArea_ == AngleHighlightArea_Side1) || + (angleHighlightArea_ == AngleHighlightArea_Side2)) + { + polylineLayer->AddChain(chain, false, highlightColor); + } + else + { + polylineLayer->AddChain(chain, false, color); + } + } + } - // sides - { + // Create the handles + { + { + PolylineSceneLayer::Chain chain; + //TODO: take DPI into account + AddSquare(chain, controller.GetScene(), side1End_, + controller.GetHandleSideLengthS()); + + if (angleHighlightArea_ == AngleHighlightArea_Side1End) + polylineLayer->AddChain(chain, true, highlightColor); + else + polylineLayer->AddChain(chain, true, color); + + } + { + PolylineSceneLayer::Chain chain; + //TODO: take DPI into account + AddSquare(chain, controller.GetScene(), side2End_, + controller.GetHandleSideLengthS()); + + if (angleHighlightArea_ == AngleHighlightArea_Side2End) + polylineLayer->AddChain(chain, true, highlightColor); + else + polylineLayer->AddChain(chain, true, color); + } + } + + // Create the arc { PolylineSceneLayer::Chain chain; - chain.push_back(side1End_); - chain.push_back(center_); - if ((angleHighlightArea_ == AngleHighlightArea_Side1) || (angleHighlightArea_ == AngleHighlightArea_Side2)) - polylineLayer->AddChain(chain, false, highlightColor); - else - polylineLayer->AddChain(chain, false, color); - } - { - PolylineSceneLayer::Chain chain; - chain.push_back(side2End_); - chain.push_back(center_); - if ((angleHighlightArea_ == AngleHighlightArea_Side1) || (angleHighlightArea_ == AngleHighlightArea_Side2)) + AddShortestArc(chain, side1End_, center_, side2End_, + controller.GetAngleToolArcRadiusS()); + if (angleHighlightArea_ == AngleHighlightArea_Center) polylineLayer->AddChain(chain, false, highlightColor); else polylineLayer->AddChain(chain, false, color); } } - - // Create the handles - { - { - PolylineSceneLayer::Chain chain; - //TODO: take DPI into account - AddSquare(chain, GetController()->GetScene(), side1End_, - GetController()->GetHandleSideLengthS()); - - if (angleHighlightArea_ == AngleHighlightArea_Side1End) - polylineLayer->AddChain(chain, true, highlightColor); - else - polylineLayer->AddChain(chain, true, color); - - } - { - PolylineSceneLayer::Chain chain; - //TODO: take DPI into account - AddSquare(chain, GetController()->GetScene(), side2End_, - GetController()->GetHandleSideLengthS()); - - if (angleHighlightArea_ == AngleHighlightArea_Side2End) - polylineLayer->AddChain(chain, true, highlightColor); - else - polylineLayer->AddChain(chain, true, color); - } - } - - // Create the arc - { - PolylineSceneLayer::Chain chain; - - AddShortestArc(chain, side1End_, center_, side2End_, - controller->GetAngleToolArcRadiusS()); - if (angleHighlightArea_ == AngleHighlightArea_Center) - polylineLayer->AddChain(chain, false, highlightColor); - else - polylineLayer->AddChain(chain, false, color); - } } { // Set the text layer @@ -293,8 +344,8 @@ double delta = NormalizeAngle(p2cAngle - p1cAngle); double theta = p1cAngle + delta / 2; - double ox = controller->GetAngleTopTextLabelDistanceS() * cos(theta); - double oy = controller->GetAngleTopTextLabelDistanceS() * sin(theta); + double ox = controller.GetAngleTopTextLabelDistanceS() * cos(theta); + double oy = controller.GetAngleTopTextLabelDistanceS() * sin(theta); double pointX = center_.GetX() + ox; double pointY = center_.GetY() + oy; @@ -307,10 +358,10 @@ #if ORTHANC_STONE_ENABLE_OUTLINED_TEXT == 1 SetTextLayerOutlineProperties( - GetController()->GetScene(), layerHolder_, buf, ScenePoint2D(pointX, pointY), 0); + scene, layerHolder_, buf, ScenePoint2D(pointX, pointY), 0); #else SetTextLayerProperties( - GetController()->GetScene(), layerHolder_, buf, ScenePoint2D(pointX, pointY) , 0); + scene, layerHolder_, buf, ScenePoint2D(pointX, pointY) , 0); #endif #if 0 @@ -370,6 +421,7 @@ { RemoveFromScene(); } + lock->Invalidate(); } } }
--- a/Framework/Scene2DViewport/AngleMeasureTool.h Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Scene2DViewport/AngleMeasureTool.h Tue Mar 31 15:51:02 2020 +0200 @@ -37,10 +37,10 @@ namespace OrthancStone { - class AngleMeasureTool : public MeasureTool, public boost::enable_shared_from_this<AngleMeasureTool> + class AngleMeasureTool : public MeasureTool { public: - AngleMeasureTool(MessageBroker& broker, boost::weak_ptr<ViewportController> controllerW); + static boost::shared_ptr<AngleMeasureTool> Create(boost::shared_ptr<IViewport> viewport); ~AngleMeasureTool(); @@ -48,7 +48,7 @@ void SetCenter(ScenePoint2D start); void SetSide2End(ScenePoint2D start); - virtual bool HitTest(ScenePoint2D p) const ORTHANC_OVERRIDE; + virtual bool HitTest(ScenePoint2D p) ORTHANC_OVERRIDE; virtual void Highlight(ScenePoint2D p) ORTHANC_OVERRIDE; virtual void ResetHighlightState() ORTHANC_OVERRIDE; virtual boost::shared_ptr<IFlexiblePointerTracker> CreateEditionTracker(const PointerEvent& e) ORTHANC_OVERRIDE; @@ -70,6 +70,8 @@ AngleHighlightArea AngleHitTest(ScenePoint2D p) const; private: + AngleMeasureTool(boost::shared_ptr<IViewport> viewport); + virtual void RefreshScene() ORTHANC_OVERRIDE; void RemoveFromScene(); void SetAngleHighlightArea(AngleHighlightArea area);
--- a/Framework/Scene2DViewport/CreateAngleMeasureCommand.cpp Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Scene2DViewport/CreateAngleMeasureCommand.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -26,14 +26,16 @@ namespace OrthancStone { CreateAngleMeasureCommand::CreateAngleMeasureCommand( - MessageBroker& broker, - boost::weak_ptr<ViewportController> controllerW, + boost::shared_ptr<IViewport> viewport, ScenePoint2D point) - : CreateMeasureCommand(controllerW) - , measureTool_( - boost::make_shared<AngleMeasureTool>(boost::ref(broker), controllerW)) + : CreateMeasureCommand(viewport) + , measureTool_(AngleMeasureTool::Create(viewport)) { - GetController()->AddMeasureTool(measureTool_); + + std::unique_ptr<IViewport::ILock> lock(viewport_->Lock()); + ViewportController& controller = lock->GetController(); + + controller.AddMeasureTool(measureTool_); measureTool_->SetSide1End(point); measureTool_->SetCenter(point); measureTool_->SetSide2End(point);
--- a/Framework/Scene2DViewport/CreateAngleMeasureCommand.h Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Scene2DViewport/CreateAngleMeasureCommand.h Tue Mar 31 15:51:02 2020 +0200 @@ -28,8 +28,7 @@ public: /** Ctor sets end of side 1*/ CreateAngleMeasureCommand( - MessageBroker& broker, - boost::weak_ptr<ViewportController> controllerW, + boost::shared_ptr<IViewport> viewport, ScenePoint2D point); /** This method sets center*/
--- a/Framework/Scene2DViewport/CreateAngleMeasureTracker.cpp Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Scene2DViewport/CreateAngleMeasureTracker.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -26,17 +26,18 @@ namespace OrthancStone { CreateAngleMeasureTracker::CreateAngleMeasureTracker( - MessageBroker& broker, - boost::weak_ptr<ViewportController> controllerW, - const PointerEvent& e) - : CreateMeasureTracker(controllerW) + boost::shared_ptr<IViewport> viewport, + const PointerEvent& e) + : CreateMeasureTracker(viewport) , state_(CreatingSide1) { - command_.reset( - new CreateAngleMeasureCommand( - broker, - controllerW, - e.GetMainPosition().Apply(GetScene().GetCanvasToSceneTransform()))); + ScenePoint2D point = e.GetMainPosition(); + { + std::unique_ptr<IViewport::ILock> lock(viewport_->Lock()); + Scene2D& scene = lock->GetController().GetScene(); + point = e.GetMainPosition().Apply(scene.GetCanvasToSceneTransform()); + } + command_.reset(new CreateAngleMeasureCommand(viewport, point)); } CreateAngleMeasureTracker::~CreateAngleMeasureTracker() @@ -52,24 +53,31 @@ "PointerMove: active_ == false"); } - ScenePoint2D scenePos = event.GetMainPosition().Apply( - GetScene().GetCanvasToSceneTransform()); - - switch (state_) + { - case CreatingSide1: - GetCommand()->SetCenter(scenePos); - break; - case CreatingSide2: - GetCommand()->SetSide2End(scenePos); - break; - default: - throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError, - "Wrong state in CreateAngleMeasureTracker::" - "PointerMove: state_ invalid"); + std::unique_ptr<IViewport::ILock> lock(viewport_->Lock()); + ViewportController& controller = lock->GetController(); + + ScenePoint2D scenePos = event.GetMainPosition().Apply( + controller.GetScene().GetCanvasToSceneTransform()); + + switch (state_) + { + case CreatingSide1: + GetCommand()->SetCenter(scenePos); + break; + case CreatingSide2: + GetCommand()->SetSide2End(scenePos); + break; + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError, + "Wrong state in CreateAngleMeasureTracker::" + "PointerMove: state_ invalid"); + } + //LOG(TRACE) << "scenePos.GetX() = " << scenePos.GetX() << " " << + // "scenePos.GetY() = " << scenePos.GetY(); + lock->Invalidate(); } - //LOG(TRACE) << "scenePos.GetX() = " << scenePos.GetX() << " " << - // "scenePos.GetY() = " << scenePos.GetY(); } void CreateAngleMeasureTracker::PointerUp(const PointerEvent& e)
--- a/Framework/Scene2DViewport/CreateAngleMeasureTracker.h Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Scene2DViewport/CreateAngleMeasureTracker.h Tue Mar 31 15:51:02 2020 +0200 @@ -38,8 +38,7 @@ must be supplied, too */ CreateAngleMeasureTracker( - MessageBroker& broker, - boost::weak_ptr<ViewportController> controllerW, + boost::shared_ptr<IViewport> viewport, const PointerEvent& e); ~CreateAngleMeasureTracker();
--- a/Framework/Scene2DViewport/CreateLineMeasureCommand.cpp Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Scene2DViewport/CreateLineMeasureCommand.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -26,15 +26,17 @@ namespace OrthancStone { CreateLineMeasureCommand::CreateLineMeasureCommand( - MessageBroker& broker, - boost::weak_ptr<ViewportController> controllerW, + boost::shared_ptr<IViewport> viewport, ScenePoint2D point) - : CreateMeasureCommand(controllerW) - , measureTool_( - boost::make_shared<LineMeasureTool>(boost::ref(broker), controllerW)) + : CreateMeasureCommand(viewport) + , measureTool_(LineMeasureTool::Create(viewport)) { - GetController()->AddMeasureTool(measureTool_); + + std::unique_ptr<IViewport::ILock> lock(viewport_->Lock()); + ViewportController& controller = lock->GetController(); + controller.AddMeasureTool(measureTool_); measureTool_->Set(point, point); + lock->Invalidate(); } void CreateLineMeasureCommand::SetEnd(ScenePoint2D scenePos)
--- a/Framework/Scene2DViewport/CreateLineMeasureCommand.h Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Scene2DViewport/CreateLineMeasureCommand.h Tue Mar 31 15:51:02 2020 +0200 @@ -27,8 +27,7 @@ { public: CreateLineMeasureCommand( - MessageBroker& broker, - boost::weak_ptr<ViewportController> controllerW, + boost::shared_ptr<IViewport> viewport, ScenePoint2D point); // the starting position is set in the ctor
--- a/Framework/Scene2DViewport/CreateLineMeasureTracker.cpp Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Scene2DViewport/CreateLineMeasureTracker.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -26,16 +26,17 @@ namespace OrthancStone { CreateLineMeasureTracker::CreateLineMeasureTracker( - MessageBroker& broker, - boost::weak_ptr<ViewportController> controllerW, + boost::shared_ptr<IViewport> viewport, const PointerEvent& e) - : CreateMeasureTracker(controllerW) + : CreateMeasureTracker(viewport) { - command_.reset( - new CreateLineMeasureCommand( - broker, - controllerW, - e.GetMainPosition().Apply(GetScene().GetCanvasToSceneTransform()))); + ScenePoint2D point = e.GetMainPosition(); + { + std::unique_ptr<IViewport::ILock> lock(viewport_->Lock()); + ViewportController& controller = lock->GetController(); + point = e.GetMainPosition().Apply(controller.GetScene().GetCanvasToSceneTransform()); + } + command_.reset(new CreateLineMeasureCommand(viewport, point)); } CreateLineMeasureTracker::~CreateLineMeasureTracker() @@ -52,12 +53,15 @@ "PointerMove: active_ == false"); } + std::unique_ptr<IViewport::ILock> lock(viewport_->Lock()); + ViewportController& controller = lock->GetController(); + ScenePoint2D scenePos = event.GetMainPosition().Apply( - GetScene().GetCanvasToSceneTransform()); - + controller.GetScene().GetCanvasToSceneTransform()); + //LOG(TRACE) << "scenePos.GetX() = " << scenePos.GetX() << " " << // "scenePos.GetY() = " << scenePos.GetY(); - + CreateLineMeasureTracker* concreteThis = dynamic_cast<CreateLineMeasureTracker*>(this); assert(concreteThis != NULL); @@ -84,5 +88,4 @@ { return boost::dynamic_pointer_cast<CreateLineMeasureCommand>(command_); } - }
--- a/Framework/Scene2DViewport/CreateLineMeasureTracker.h Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Scene2DViewport/CreateLineMeasureTracker.h Tue Mar 31 15:51:02 2020 +0200 @@ -38,8 +38,7 @@ must be supplied, too */ CreateLineMeasureTracker( - MessageBroker& broker, - boost::weak_ptr<ViewportController> controllerW, + boost::shared_ptr<IViewport> viewport, const PointerEvent& e); ~CreateLineMeasureTracker();
--- a/Framework/Scene2DViewport/EditAngleMeasureCommand.cpp Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Scene2DViewport/EditAngleMeasureCommand.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -23,31 +23,30 @@ namespace OrthancStone { EditAngleMeasureCommand::EditAngleMeasureCommand( - boost::shared_ptr<AngleMeasureTool> measureTool, - MessageBroker& broker, - boost::weak_ptr<ViewportController> controllerW) - : EditMeasureCommand(measureTool, controllerW) + boost::shared_ptr<MeasureTool> measureTool, + boost::shared_ptr<IViewport> viewport) + : EditMeasureCommand(measureTool, viewport) , measureTool_(measureTool) { } void EditAngleMeasureCommand::SetCenter(ScenePoint2D scenePos) { - measureTool_->SetCenter(scenePos); + dynamic_cast<AngleMeasureTool&>(*measureTool_).SetCenter(scenePos); mementoModified_ = measureTool_->GetMemento(); } void EditAngleMeasureCommand::SetSide1End(ScenePoint2D scenePos) { - measureTool_->SetSide1End(scenePos); + dynamic_cast<AngleMeasureTool&>(*measureTool_).SetSide1End(scenePos); mementoModified_ = measureTool_->GetMemento(); } void EditAngleMeasureCommand::SetSide2End(ScenePoint2D scenePos) { - measureTool_->SetSide2End(scenePos); + dynamic_cast<AngleMeasureTool&>(*measureTool_).SetSide2End(scenePos); mementoModified_ = measureTool_->GetMemento(); } }
--- a/Framework/Scene2DViewport/EditAngleMeasureCommand.h Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Scene2DViewport/EditAngleMeasureCommand.h Tue Mar 31 15:51:02 2020 +0200 @@ -28,9 +28,8 @@ public: /** Ctor sets end of side 1*/ EditAngleMeasureCommand( - boost::shared_ptr<AngleMeasureTool> measureTool, - MessageBroker& broker, - boost::weak_ptr<ViewportController> controllerW); + boost::shared_ptr<MeasureTool> measureTool, + boost::shared_ptr<IViewport> viewport); /** This method sets center*/ void SetCenter(ScenePoint2D scenePos); @@ -46,6 +45,6 @@ { return measureTool_; } - boost::shared_ptr<AngleMeasureTool> measureTool_; + boost::shared_ptr<MeasureTool> measureTool_; }; }
--- a/Framework/Scene2DViewport/EditAngleMeasureTracker.cpp Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Scene2DViewport/EditAngleMeasureTracker.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -26,18 +26,19 @@ namespace OrthancStone { EditAngleMeasureTracker::EditAngleMeasureTracker( - boost::shared_ptr<AngleMeasureTool> measureTool, - MessageBroker& broker, - boost::weak_ptr<ViewportController> controllerW, + boost::shared_ptr<MeasureTool> measureTool, + boost::shared_ptr<IViewport> viewport, const PointerEvent& e) - : EditMeasureTracker(controllerW, e) + : EditMeasureTracker(viewport, e) { - ScenePoint2D scenePos = e.GetMainPosition().Apply( - GetScene().GetCanvasToSceneTransform()); - - modifiedZone_ = measureTool->AngleHitTest(scenePos); - - command_.reset(new EditAngleMeasureCommand(measureTool, broker, controllerW)); + ScenePoint2D scenePos = e.GetMainPosition(); + { + std::unique_ptr<IViewport::ILock> lock(viewport_->Lock()); + ViewportController& controller = lock->GetController(); + scenePos = e.GetMainPosition().Apply(controller.GetScene().GetCanvasToSceneTransform()); + } + modifiedZone_ = dynamic_cast<AngleMeasureTool&>(*measureTool).AngleHitTest(scenePos); + command_.reset(new EditAngleMeasureCommand(measureTool, viewport)); } EditAngleMeasureTracker::~EditAngleMeasureTracker() @@ -47,8 +48,12 @@ void EditAngleMeasureTracker::PointerMove(const PointerEvent& e) { + std::unique_ptr<IViewport::ILock> lock(viewport_->Lock()); + ViewportController& controller = lock->GetController(); + Scene2D& scene = controller.GetScene(); + ScenePoint2D scenePos = e.GetMainPosition().Apply( - GetScene().GetCanvasToSceneTransform()); + scene.GetCanvasToSceneTransform()); ScenePoint2D delta = scenePos - GetOriginalClickPosition(); @@ -59,38 +64,38 @@ switch (modifiedZone_) { - case AngleMeasureTool::AngleHighlightArea_Center: - { - ScenePoint2D newCenter = memento->center_ + delta; - GetCommand()->SetCenter(newCenter); - } - break; - case AngleMeasureTool::AngleHighlightArea_Side1: - case AngleMeasureTool::AngleHighlightArea_Side2: - { - ScenePoint2D newCenter = memento->center_ + delta; - ScenePoint2D newSide1End = memento->side1End_ + delta; - ScenePoint2D newSide2End = memento->side2End_ + delta; - GetCommand()->SetCenter(newCenter); - GetCommand()->SetSide1End(newSide1End); - GetCommand()->SetSide2End(newSide2End); - } - break; - case AngleMeasureTool::AngleHighlightArea_Side1End: - { - ScenePoint2D newSide1End = memento->side1End_ + delta; - GetCommand()->SetSide1End(newSide1End); - } - break; - case AngleMeasureTool::AngleHighlightArea_Side2End: - { - ScenePoint2D newSide2End = memento->side2End_ + delta; - GetCommand()->SetSide2End(newSide2End); - } - break; - default: - LOG(WARNING) << "Warning: please retry the measuring tool editing operation!"; + case AngleMeasureTool::AngleHighlightArea_Center: + { + ScenePoint2D newCenter = memento->center_ + delta; + GetCommand()->SetCenter(newCenter); + } break; + case AngleMeasureTool::AngleHighlightArea_Side1: + case AngleMeasureTool::AngleHighlightArea_Side2: + { + ScenePoint2D newCenter = memento->center_ + delta; + ScenePoint2D newSide1End = memento->side1End_ + delta; + ScenePoint2D newSide2End = memento->side2End_ + delta; + GetCommand()->SetCenter(newCenter); + GetCommand()->SetSide1End(newSide1End); + GetCommand()->SetSide2End(newSide2End); + } + break; + case AngleMeasureTool::AngleHighlightArea_Side1End: + { + ScenePoint2D newSide1End = memento->side1End_ + delta; + GetCommand()->SetSide1End(newSide1End); + } + break; + case AngleMeasureTool::AngleHighlightArea_Side2End: + { + ScenePoint2D newSide2End = memento->side2End_ + delta; + GetCommand()->SetSide2End(newSide2End); + } + break; + default: + LOG(WARNING) << "Warning: please retry the measuring tool editing operation!"; + break; } } @@ -111,5 +116,4 @@ ORTHANC_ASSERT(ret.get() != NULL, "Internal error in EditAngleMeasureTracker::GetCommand()"); return ret; } - }
--- a/Framework/Scene2DViewport/EditAngleMeasureTracker.h Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Scene2DViewport/EditAngleMeasureTracker.h Tue Mar 31 15:51:02 2020 +0200 @@ -37,9 +37,8 @@ must be supplied, too */ EditAngleMeasureTracker( - boost::shared_ptr<AngleMeasureTool> measureTool, - MessageBroker& broker, - boost::weak_ptr<ViewportController> controllerW, + boost::shared_ptr<MeasureTool> measureTool, + boost::shared_ptr<IViewport> viewport, const PointerEvent& e); ~EditAngleMeasureTracker();
--- a/Framework/Scene2DViewport/EditLineMeasureCommand.cpp Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Scene2DViewport/EditLineMeasureCommand.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -23,10 +23,9 @@ namespace OrthancStone { EditLineMeasureCommand::EditLineMeasureCommand( - boost::shared_ptr<LineMeasureTool> measureTool, - MessageBroker& broker, - boost::weak_ptr<ViewportController> controllerW) - : EditMeasureCommand(measureTool, controllerW) + boost::shared_ptr<MeasureTool> measureTool, + boost::shared_ptr<IViewport> viewport) + : EditMeasureCommand(measureTool, viewport) , measureTool_(measureTool) { } @@ -34,14 +33,14 @@ void EditLineMeasureCommand::SetStart(ScenePoint2D scenePos) { - measureTool_->SetStart(scenePos); + dynamic_cast<LineMeasureTool&>(*measureTool_).SetStart(scenePos); mementoModified_ = measureTool_->GetMemento(); } void EditLineMeasureCommand::SetEnd(ScenePoint2D scenePos) { - measureTool_->SetEnd(scenePos); + dynamic_cast<LineMeasureTool&>(*measureTool_).SetEnd(scenePos); mementoModified_ = measureTool_->GetMemento(); } }
--- a/Framework/Scene2DViewport/EditLineMeasureCommand.h Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Scene2DViewport/EditLineMeasureCommand.h Tue Mar 31 15:51:02 2020 +0200 @@ -27,9 +27,8 @@ { public: EditLineMeasureCommand( - boost::shared_ptr<LineMeasureTool> measureTool, - MessageBroker& broker, - boost::weak_ptr<ViewportController> controllerW); + boost::shared_ptr<MeasureTool> measureTool, + boost::shared_ptr<IViewport> viewport); void SetStart(ScenePoint2D scenePos); void SetEnd(ScenePoint2D scenePos); @@ -39,7 +38,6 @@ { return measureTool_; } - boost::shared_ptr<LineMeasureTool> measureTool_; + boost::shared_ptr<MeasureTool> measureTool_; }; } -
--- a/Framework/Scene2DViewport/EditLineMeasureTracker.cpp Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Scene2DViewport/EditLineMeasureTracker.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -27,22 +27,19 @@ namespace OrthancStone { EditLineMeasureTracker::EditLineMeasureTracker( - boost::shared_ptr<LineMeasureTool> measureTool, - MessageBroker& broker, - boost::weak_ptr<ViewportController> controllerW, - const PointerEvent& e) - : EditMeasureTracker(controllerW, e) + boost::shared_ptr<MeasureTool> measureTool, + boost::shared_ptr<IViewport> viewport, + const PointerEvent& e) + : EditMeasureTracker(viewport, e) { - ScenePoint2D scenePos = e.GetMainPosition().Apply( - GetScene().GetCanvasToSceneTransform()); - - modifiedZone_ = measureTool->LineHitTest(scenePos); - - command_.reset( - new EditLineMeasureCommand( - measureTool, - broker, - controllerW)); + ScenePoint2D scenePos = e.GetMainPosition(); + { + std::unique_ptr<IViewport::ILock> lock(viewport_->Lock()); + Scene2D& scene = lock->GetController().GetScene(); + scenePos = e.GetMainPosition().Apply(scene.GetCanvasToSceneTransform()); + } + modifiedZone_ = dynamic_cast<LineMeasureTool&>(*measureTool).LineHitTest(scenePos); + command_.reset(new EditLineMeasureCommand(measureTool, viewport)); } EditLineMeasureTracker::~EditLineMeasureTracker() @@ -52,8 +49,12 @@ void EditLineMeasureTracker::PointerMove(const PointerEvent& e) { + std::unique_ptr<IViewport::ILock> lock(viewport_->Lock()); + ViewportController& controller = lock->GetController(); + Scene2D& scene = controller.GetScene(); + ScenePoint2D scenePos = e.GetMainPosition().Apply( - GetScene().GetCanvasToSceneTransform()); + scene.GetCanvasToSceneTransform()); ScenePoint2D delta = scenePos - GetOriginalClickPosition(); @@ -86,7 +87,7 @@ break; default: LOG(WARNING) << "Warning: please retry the measuring tool editing operation!"; - break; + break; } }
--- a/Framework/Scene2DViewport/EditLineMeasureTracker.h Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Scene2DViewport/EditLineMeasureTracker.h Tue Mar 31 15:51:02 2020 +0200 @@ -37,9 +37,8 @@ must be supplied, too */ EditLineMeasureTracker( - boost::shared_ptr<LineMeasureTool> measureTool, - MessageBroker& broker, - boost::weak_ptr<ViewportController> controllerW, + boost::shared_ptr<MeasureTool> measureTool, + boost::shared_ptr<IViewport> viewport, const PointerEvent& e); ~EditLineMeasureTracker();
--- a/Framework/Scene2DViewport/LayerHolder.cpp Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Scene2DViewport/LayerHolder.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -22,20 +22,20 @@ #include "../Scene2D/TextSceneLayer.h" #include "../Scene2D/PolylineSceneLayer.h" #include "../Scene2D/Scene2D.h" -#include "../Scene2DViewport/ViewportController.h" +#include "../Viewport/IViewport.h" #include "../StoneException.h" namespace OrthancStone { LayerHolder::LayerHolder( - boost::weak_ptr<ViewportController> controllerW, - int polylineLayerCount, - int textLayerCount, - int infoTextCount) + boost::shared_ptr<IViewport> viewport, + int polylineLayerCount, + int textLayerCount, + int infoTextCount) : textLayerCount_(textLayerCount) , polylineLayerCount_(polylineLayerCount) , infoTextCount_(infoTextCount) - , controllerW_(controllerW) + , viewport_(viewport) , baseLayerIndex_(-1) { @@ -43,24 +43,26 @@ void LayerHolder::CreateLayers() { + std::unique_ptr<IViewport::ILock> lock(viewport_->Lock()); + ViewportController& controller = lock->GetController(); + Scene2D& scene = controller.GetScene(); + assert(baseLayerIndex_ == -1); - baseLayerIndex_ = GetScene().GetMaxDepth() + 100; + baseLayerIndex_ = scene.GetMaxDepth() + 100; for (int i = 0; i < polylineLayerCount_; ++i) { std::unique_ptr<PolylineSceneLayer> layer(new PolylineSceneLayer()); - GetScene().SetLayer(baseLayerIndex_ + i, layer.release()); + scene.SetLayer(baseLayerIndex_ + i, layer.release()); } for (int i = 0; i < textLayerCount_; ++i) { std::unique_ptr<TextSceneLayer> layer(new TextSceneLayer()); - GetScene().SetLayer( - baseLayerIndex_ + polylineLayerCount_ + i, - layer.release()); + scene.SetLayer(baseLayerIndex_ + polylineLayerCount_ + i, layer.release()); } - + lock->Invalidate(); } void LayerHolder::CreateLayersIfNeeded() @@ -74,13 +76,6 @@ return (baseLayerIndex_ != -1); } - Scene2D& LayerHolder::GetScene() - { - boost::shared_ptr<ViewportController> controller = controllerW_.lock(); - ORTHANC_ASSERT(controller.get() != 0, "Zombie attack!"); - return controller->GetScene(); - } - void LayerHolder::DeleteLayersIfNeeded() { if (baseLayerIndex_ != -1) @@ -89,40 +84,48 @@ void LayerHolder::DeleteLayers() { + std::unique_ptr<IViewport::ILock> lock(viewport_->Lock()); + Scene2D& scene = lock->GetController().GetScene(); + for (int i = 0; i < textLayerCount_ + polylineLayerCount_; ++i) { - ORTHANC_ASSERT(GetScene().HasLayer(baseLayerIndex_ + i), "No layer"); - GetScene().DeleteLayer(baseLayerIndex_ + i); + ORTHANC_ASSERT(scene.HasLayer(baseLayerIndex_ + i), "No layer"); + scene.DeleteLayer(baseLayerIndex_ + i); } baseLayerIndex_ = -1; + lock->Invalidate(); } - + PolylineSceneLayer* LayerHolder::GetPolylineLayer(int index /*= 0*/) { + std::unique_ptr<IViewport::ILock> lock(viewport_->Lock()); + Scene2D& scene = lock->GetController().GetScene(); + using namespace Orthanc; ORTHANC_ASSERT(baseLayerIndex_ != -1); - ORTHANC_ASSERT(GetScene().HasLayer(GetPolylineLayerIndex(index))); - ISceneLayer* layer = - &(GetScene().GetLayer(GetPolylineLayerIndex(index))); - + ORTHANC_ASSERT(scene.HasLayer(GetPolylineLayerIndex(index))); + ISceneLayer* layer = &(scene.GetLayer(GetPolylineLayerIndex(index))); + PolylineSceneLayer* concreteLayer = dynamic_cast<PolylineSceneLayer*>(layer); - + ORTHANC_ASSERT(concreteLayer != NULL); return concreteLayer; } TextSceneLayer* LayerHolder::GetTextLayer(int index /*= 0*/) { + std::unique_ptr<IViewport::ILock> lock(viewport_->Lock()); + Scene2D& scene = lock->GetController().GetScene(); + using namespace Orthanc; ORTHANC_ASSERT(baseLayerIndex_ != -1); - ORTHANC_ASSERT(GetScene().HasLayer(GetTextLayerIndex(index))); - ISceneLayer* layer = - &(GetScene().GetLayer(GetTextLayerIndex(index))); - + ORTHANC_ASSERT(scene.HasLayer(GetTextLayerIndex(index))); + ISceneLayer* layer = &(scene.GetLayer(GetTextLayerIndex(index))); + TextSceneLayer* concreteLayer = dynamic_cast<TextSceneLayer*>(layer); - + ORTHANC_ASSERT(concreteLayer != NULL); return concreteLayer; } @@ -133,8 +136,7 @@ ORTHANC_ASSERT(index < polylineLayerCount_); return baseLayerIndex_ + index; } - - + int LayerHolder::GetTextLayerIndex(int index /*= 0*/) { using namespace Orthanc;
--- a/Framework/Scene2DViewport/LayerHolder.h Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Scene2DViewport/LayerHolder.h Tue Mar 31 15:51:02 2020 +0200 @@ -43,7 +43,7 @@ performed at this time */ LayerHolder( - boost::weak_ptr<ViewportController> controllerW, + boost::shared_ptr<IViewport> viewport, int polylineLayerCount, int textLayerCount, int infoTextCount = 0); /** @@ -97,12 +97,11 @@ int GetPolylineLayerIndex(int index = 0); int GetTextLayerIndex(int index = 0); int GetInfoTextLayerIndex(int index = 0); - Scene2D& GetScene(); int textLayerCount_; int polylineLayerCount_; int infoTextCount_; - boost::weak_ptr<ViewportController> controllerW_; + boost::shared_ptr<IViewport> viewport_; int baseLayerIndex_; }; }
--- a/Framework/Scene2DViewport/LineMeasureTool.cpp Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Scene2DViewport/LineMeasureTool.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -32,18 +32,26 @@ { LineMeasureTool::LineMeasureTool( - MessageBroker& broker, boost::weak_ptr<ViewportController> controllerW) - : MeasureTool(broker, controllerW) + boost::shared_ptr<IViewport> viewport) + : MeasureTool(viewport) #if ORTHANC_STONE_ENABLE_OUTLINED_TEXT == 1 - , layerHolder_(boost::make_shared<LayerHolder>(controllerW, 1, 5)) + , layerHolder_(boost::shared_ptr<LayerHolder>(new LayerHolder(viewport,1,5))) #else - , layerHolder_(boost::make_shared<LayerHolder>(controllerW, 1, 1)) + , layerHolder_(boost::shared_ptr<LayerHolder>(new LayerHolder(viewport,1,1))) #endif , lineHighlightArea_(LineHighlightArea_None) { } + boost::shared_ptr<LineMeasureTool> LineMeasureTool::Create(boost::shared_ptr<IViewport> viewport) + { + boost::shared_ptr<LineMeasureTool> obj(new LineMeasureTool(viewport)); + obj->MeasureTool::PostConstructor(); + obj->RefreshScene(); + return obj; + } + LineMeasureTool::~LineMeasureTool() { // this measuring tool is a RABI for the corresponding visual layers @@ -106,53 +114,59 @@ SetLineHighlightArea(lineHighlightArea); } - LineMeasureTool::LineHighlightArea LineMeasureTool::LineHitTest(ScenePoint2D p) const + LineMeasureTool::LineHighlightArea LineMeasureTool::LineHitTest(ScenePoint2D p) { - const double pixelToScene = - GetController()->GetScene().GetCanvasToSceneTransform().ComputeZoom(); - const double SQUARED_HIT_TEST_MAX_DISTANCE_SCENE_COORD = pixelToScene * HIT_TEST_MAX_DISTANCE_CANVAS_COORD * pixelToScene * HIT_TEST_MAX_DISTANCE_CANVAS_COORD; + std::unique_ptr<IViewport::ILock> lock(viewport_->Lock()); + ViewportController& controller = lock->GetController(); + Scene2D& scene = controller.GetScene(); - const double sqDistanceFromStart = ScenePoint2D::SquaredDistancePtPt(p, start_); + const double pixelToScene = scene.GetCanvasToSceneTransform().ComputeZoom(); + const double SQUARED_HIT_TEST_MAX_DISTANCE_SCENE_COORD = + pixelToScene * HIT_TEST_MAX_DISTANCE_CANVAS_COORD * + pixelToScene * HIT_TEST_MAX_DISTANCE_CANVAS_COORD; + + const double sqDistanceFromStart = + ScenePoint2D::SquaredDistancePtPt(p, start_); + if (sqDistanceFromStart <= SQUARED_HIT_TEST_MAX_DISTANCE_SCENE_COORD) return LineHighlightArea_Start; const double sqDistanceFromEnd = ScenePoint2D::SquaredDistancePtPt(p, end_); + if (sqDistanceFromEnd <= SQUARED_HIT_TEST_MAX_DISTANCE_SCENE_COORD) return LineHighlightArea_End; - const double sqDistanceFromPtSegment = ScenePoint2D::SquaredDistancePtSegment(start_, end_, p); + const double sqDistanceFromPtSegment = + ScenePoint2D::SquaredDistancePtSegment(start_, end_, p); + if (sqDistanceFromPtSegment <= SQUARED_HIT_TEST_MAX_DISTANCE_SCENE_COORD) return LineHighlightArea_Segment; return LineHighlightArea_None; } - bool LineMeasureTool::HitTest(ScenePoint2D p) const + bool LineMeasureTool::HitTest(ScenePoint2D p) { return LineHitTest(p) != LineHighlightArea_None; } boost::shared_ptr<IFlexiblePointerTracker> LineMeasureTool::CreateEditionTracker(const PointerEvent& e) { + std::unique_ptr<IViewport::ILock> lock(viewport_->Lock()); + ViewportController& controller = lock->GetController(); + Scene2D& scene = controller.GetScene(); + ScenePoint2D scenePos = e.GetMainPosition().Apply( - GetController()->GetScene().GetCanvasToSceneTransform()); + scene.GetCanvasToSceneTransform()); if (!HitTest(scenePos)) return boost::shared_ptr<IFlexiblePointerTracker>(); - /** - new EditLineMeasureTracker( - boost::shared_ptr<LineMeasureTool> measureTool; - MessageBroker & broker, - boost::weak_ptr<ViewportController> controllerW, - const PointerEvent & e); - */ boost::shared_ptr<EditLineMeasureTracker> editLineMeasureTracker( - new EditLineMeasureTracker(shared_from_this(), GetBroker(), GetController(), e)); + new EditLineMeasureTracker(shared_from_this(), viewport_, e)); return editLineMeasureTracker; } - boost::shared_ptr<MeasureToolMemento> LineMeasureTool::GetMemento() const { boost::shared_ptr<LineMeasureToolMemento> memento(new LineMeasureToolMemento()); @@ -161,10 +175,14 @@ return memento; } - void LineMeasureTool::SetMemento(boost::shared_ptr<MeasureToolMemento> mementoBase) + void LineMeasureTool::SetMemento( + boost::shared_ptr<MeasureToolMemento> mementoBase) { - boost::shared_ptr<LineMeasureToolMemento> memento = boost::dynamic_pointer_cast<LineMeasureToolMemento>(mementoBase); + boost::shared_ptr<LineMeasureToolMemento> memento = + boost::dynamic_pointer_cast<LineMeasureToolMemento>(mementoBase); + ORTHANC_ASSERT(memento.get() != NULL, "Internal error: wrong (or bad) memento"); + start_ = memento->start_; end_ = memento->end_; RefreshScene(); @@ -176,61 +194,67 @@ { if (IsEnabled()) { + + std::unique_ptr<IViewport::ILock> lock(viewport_->Lock()); + ViewportController& controller = lock->GetController(); + Scene2D& scene = controller.GetScene(); + layerHolder_->CreateLayersIfNeeded(); - { // Fill the polyline layer with the measurement line PolylineSceneLayer* polylineLayer = layerHolder_->GetPolylineLayer(0); - polylineLayer->ClearAllChains(); - - const Color color(TOOL_LINES_COLOR_RED, - TOOL_LINES_COLOR_GREEN, - TOOL_LINES_COLOR_BLUE); - - const Color highlightColor(TOOL_LINES_HL_COLOR_RED, - TOOL_LINES_HL_COLOR_GREEN, - TOOL_LINES_HL_COLOR_BLUE); - + if (polylineLayer) { - PolylineSceneLayer::Chain chain; - chain.push_back(start_); - chain.push_back(end_); - if(lineHighlightArea_ == LineHighlightArea_Segment) - polylineLayer->AddChain(chain, false, highlightColor); - else - polylineLayer->AddChain(chain, false, color); - } + polylineLayer->ClearAllChains(); - // handles - { - { - PolylineSceneLayer::Chain chain; - - //TODO: take DPI into account - AddSquare(chain, GetController()->GetScene(), start_, - GetController()->GetHandleSideLengthS()); - - if (lineHighlightArea_ == LineHighlightArea_Start) - polylineLayer->AddChain(chain, true, highlightColor); - else - polylineLayer->AddChain(chain, true, color); - } + const Color color(TOOL_LINES_COLOR_RED, + TOOL_LINES_COLOR_GREEN, + TOOL_LINES_COLOR_BLUE); + + const Color highlightColor(TOOL_LINES_HL_COLOR_RED, + TOOL_LINES_HL_COLOR_GREEN, + TOOL_LINES_HL_COLOR_BLUE); { PolylineSceneLayer::Chain chain; + chain.push_back(start_); + chain.push_back(end_); + if(lineHighlightArea_ == LineHighlightArea_Segment) + polylineLayer->AddChain(chain, false, highlightColor); + else + polylineLayer->AddChain(chain, false, color); + } + + // handles + { + { + PolylineSceneLayer::Chain chain; - //TODO: take DPI into account - AddSquare(chain, GetController()->GetScene(), end_, - GetController()->GetHandleSideLengthS()); + //TODO: take DPI into account + AddSquare(chain, controller.GetScene(), start_, + controller.GetHandleSideLengthS()); - if (lineHighlightArea_ == LineHighlightArea_End) - polylineLayer->AddChain(chain, true, highlightColor); - else - polylineLayer->AddChain(chain, true, color); + if (lineHighlightArea_ == LineHighlightArea_Start) + polylineLayer->AddChain(chain, true, highlightColor); + else + polylineLayer->AddChain(chain, true, color); + } + + { + PolylineSceneLayer::Chain chain; + + //TODO: take DPI into account + AddSquare(chain, controller.GetScene(), end_, + controller.GetHandleSideLengthS()); + + if (lineHighlightArea_ == LineHighlightArea_End) + polylineLayer->AddChain(chain, true, highlightColor); + else + polylineLayer->AddChain(chain, true, color); + } } } - } { // Set the text layer propreties @@ -246,14 +270,19 @@ double midX = 0.5 * (end_.GetX() + start_.GetX()); double midY = 0.5 * (end_.GetY() + start_.GetY()); + { + #if ORTHANC_STONE_ENABLE_OUTLINED_TEXT == 1 - SetTextLayerOutlineProperties( - GetController()->GetScene(), layerHolder_, buf, ScenePoint2D(midX, midY), 0); + SetTextLayerOutlineProperties( + scene, layerHolder_, buf, ScenePoint2D(midX, midY), 0); #else - SetTextLayerProperties( - GetController()->GetScene(), layerHolder_, buf, ScenePoint2D(midX, midY), 0); + SetTextLayerProperties( + scene, layerHolder_, buf, ScenePoint2D(midX, midY), 0); #endif + lock->Invalidate(); + } } + lock->Invalidate(); } else {
--- a/Framework/Scene2DViewport/LineMeasureTool.h Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Scene2DViewport/LineMeasureTool.h Tue Mar 31 15:51:02 2020 +0200 @@ -35,10 +35,10 @@ namespace OrthancStone { - class LineMeasureTool : public MeasureTool, public boost::enable_shared_from_this<LineMeasureTool> + class LineMeasureTool : public MeasureTool { public: - LineMeasureTool(MessageBroker& broker, boost::weak_ptr<ViewportController> controllerW); + static boost::shared_ptr<LineMeasureTool> Create(boost::shared_ptr<IViewport> viewport); ~LineMeasureTool(); @@ -47,7 +47,7 @@ void Set(ScenePoint2D start, ScenePoint2D end); - virtual bool HitTest(ScenePoint2D p) const ORTHANC_OVERRIDE; + virtual bool HitTest(ScenePoint2D p) ORTHANC_OVERRIDE; virtual void Highlight(ScenePoint2D p) ORTHANC_OVERRIDE; virtual void ResetHighlightState() ORTHANC_OVERRIDE; virtual boost::shared_ptr<IFlexiblePointerTracker> CreateEditionTracker(const PointerEvent& e) ORTHANC_OVERRIDE; @@ -64,9 +64,11 @@ }; - LineHighlightArea LineHitTest(ScenePoint2D p) const; + LineHighlightArea LineHitTest(ScenePoint2D p); private: + LineMeasureTool(boost::shared_ptr<IViewport> viewport); + virtual void RefreshScene() ORTHANC_OVERRIDE; void RemoveFromScene(); void SetLineHighlightArea(LineHighlightArea area);
--- a/Framework/Scene2DViewport/MeasureCommands.cpp Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Scene2DViewport/MeasureCommands.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -20,6 +20,8 @@ #include "MeasureCommands.h" +#include <memory> + #include <boost/make_shared.hpp> #include <boost/ref.hpp> @@ -27,19 +29,21 @@ { void CreateMeasureCommand::Undo() { + std::unique_ptr<OrthancStone::IViewport::ILock> lock(viewport_->Lock()); // simply disable the measure tool upon undo GetMeasureTool()->Disable(); - GetController()->RemoveMeasureTool(GetMeasureTool()); + lock->GetController().RemoveMeasureTool(GetMeasureTool()); } void CreateMeasureCommand::Redo() { + std::unique_ptr<OrthancStone::IViewport::ILock> lock(viewport_->Lock()); GetMeasureTool()->Enable(); - GetController()->AddMeasureTool(GetMeasureTool()); + lock->GetController().AddMeasureTool(GetMeasureTool()); } - CreateMeasureCommand::CreateMeasureCommand(boost::weak_ptr<ViewportController> controllerW) - : MeasureCommand(controllerW) + CreateMeasureCommand::CreateMeasureCommand(boost::shared_ptr<IViewport> viewport) + : MeasureCommand(viewport) { } @@ -52,15 +56,17 @@ void DeleteMeasureCommand::Redo() { + std::unique_ptr<OrthancStone::IViewport::ILock> lock(viewport_->Lock()); // simply disable the measure tool upon undo GetMeasureTool()->Disable(); - GetController()->RemoveMeasureTool(GetMeasureTool()); + lock->GetController().RemoveMeasureTool(GetMeasureTool()); } void DeleteMeasureCommand::Undo() { + std::unique_ptr<OrthancStone::IViewport::ILock> lock(viewport_->Lock()); GetMeasureTool()->Enable(); - GetController()->AddMeasureTool(GetMeasureTool()); + lock->GetController().AddMeasureTool(GetMeasureTool()); } DeleteMeasureCommand::~DeleteMeasureCommand() @@ -69,18 +75,19 @@ // we thus leave it as is } - DeleteMeasureCommand::DeleteMeasureCommand(boost::shared_ptr<MeasureTool> measureTool, boost::weak_ptr<ViewportController> controllerW) - : MeasureCommand(controllerW) + DeleteMeasureCommand::DeleteMeasureCommand(boost::shared_ptr<MeasureTool> measureTool, boost::shared_ptr<IViewport> viewport) + : MeasureCommand(viewport) , mementoOriginal_(measureTool->GetMemento()) , measureTool_(measureTool) , mementoModified_(measureTool->GetMemento()) { + std::unique_ptr<OrthancStone::IViewport::ILock> lock(viewport_->Lock()); GetMeasureTool()->Disable(); - GetController()->RemoveMeasureTool(GetMeasureTool()); + lock->GetController().RemoveMeasureTool(GetMeasureTool()); } - EditMeasureCommand::EditMeasureCommand(boost::shared_ptr<MeasureTool> measureTool, boost::weak_ptr<ViewportController> controllerW) - : MeasureCommand(controllerW) + EditMeasureCommand::EditMeasureCommand(boost::shared_ptr<MeasureTool> measureTool, boost::shared_ptr<IViewport> viewport) + : MeasureCommand(viewport) , mementoOriginal_(measureTool->GetMemento()) , mementoModified_(measureTool->GetMemento()) { @@ -102,11 +109,4 @@ { GetMeasureTool()->SetMemento(mementoModified_); } - - boost::shared_ptr<ViewportController> MeasureCommand::GetController() - { - boost::shared_ptr<ViewportController> controller = controllerW_.lock(); - assert(controller); // accessing dead object? - return controller; - } }
--- a/Framework/Scene2DViewport/MeasureCommands.h Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Scene2DViewport/MeasureCommands.h Tue Mar 31 15:51:02 2020 +0200 @@ -19,7 +19,7 @@ **/ #pragma once -#include "../Scene2D/Scene2D.h" +#include "../Viewport/IViewport.h" // to be moved into Stone #include "PredeclaredTypes.h" @@ -35,25 +35,21 @@ class MeasureCommand : public boost::noncopyable { public: - MeasureCommand(boost::weak_ptr<ViewportController> controllerW) - : controllerW_(controllerW) - { - - } + MeasureCommand(boost::shared_ptr<IViewport> viewport) : viewport_(viewport) + {} virtual void Undo() = 0; virtual void Redo() = 0; virtual ~MeasureCommand() {}; protected: - boost::shared_ptr<ViewportController> GetController(); - boost::weak_ptr<ViewportController> controllerW_; + boost::shared_ptr<IViewport> viewport_; }; class CreateMeasureCommand : public MeasureCommand { public: - CreateMeasureCommand(boost::weak_ptr<ViewportController> controllerW); + CreateMeasureCommand(boost::shared_ptr<IViewport> viewport); virtual ~CreateMeasureCommand(); virtual void Undo() ORTHANC_OVERRIDE; virtual void Redo() ORTHANC_OVERRIDE; @@ -65,7 +61,7 @@ class EditMeasureCommand : public MeasureCommand { public: - EditMeasureCommand(boost::shared_ptr<MeasureTool> measureTool, boost::weak_ptr<ViewportController> controllerW); + EditMeasureCommand(boost::shared_ptr<MeasureTool> measureTool, boost::shared_ptr<IViewport> viewport); virtual ~EditMeasureCommand(); virtual void Undo() ORTHANC_OVERRIDE; virtual void Redo() ORTHANC_OVERRIDE; @@ -86,7 +82,7 @@ class DeleteMeasureCommand : public MeasureCommand { public: - DeleteMeasureCommand(boost::shared_ptr<MeasureTool> measureTool, boost::weak_ptr<ViewportController> controllerW); + DeleteMeasureCommand(boost::shared_ptr<MeasureTool> measureTool, boost::shared_ptr<IViewport> viewport); virtual ~DeleteMeasureCommand(); virtual void Undo() ORTHANC_OVERRIDE; virtual void Redo() ORTHANC_OVERRIDE;
--- a/Framework/Scene2DViewport/MeasureTool.cpp Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Scene2DViewport/MeasureTool.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -26,16 +26,10 @@ #include <boost/math/constants/constants.hpp> +#include "../Viewport/IViewport.h" + namespace OrthancStone { - MeasureTool::~MeasureTool() - { - // if the controller is dead, let's not bother. - boost::shared_ptr<ViewportController> controller = controllerW_.lock(); - if (controller) - controller->Unregister(this); - } - void MeasureTool::Enable() { enabled_ = true; @@ -53,48 +47,30 @@ return enabled_; } - - boost::shared_ptr<const ViewportController> MeasureTool::GetController() const + MeasureTool::MeasureTool( + boost::shared_ptr<IViewport> viewport) + : viewport_(viewport) + , enabled_(true) { - boost::shared_ptr<const ViewportController> controller = controllerW_.lock(); - if (!controller) - throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError, - "Using dead ViewportController object!"); - return controller; + } - boost::shared_ptr<ViewportController> MeasureTool::GetController() + void MeasureTool::PostConstructor() { -#if 1 - return boost::const_pointer_cast<ViewportController> - (const_cast<const MeasureTool*>(this)->GetController()); - //return boost::const_<boost::shared_ptr<ViewportController>> - // (const_cast<const MeasureTool*>(this)->GetController()); -#else - boost::shared_ptr<ViewportController> controller = controllerW_.lock(); - if (!controller) - throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError, - "Using dead ViewportController object!"); - return controller; -#endif + std::unique_ptr<IViewport::ILock> lock(viewport_->Lock()); + ViewportController& controller = lock->GetController(); + + Register<ViewportController::SceneTransformChanged>( + controller, + &MeasureTool::OnSceneTransformChanged); } - MeasureTool::MeasureTool(MessageBroker& broker, - boost::weak_ptr<ViewportController> controllerW) - : IObserver(broker) - , controllerW_(controllerW) - , enabled_(true) - { - GetController()->RegisterObserverCallback( - new Callable<MeasureTool, ViewportController::SceneTransformChanged> - (*this, &MeasureTool::OnSceneTransformChanged)); - } - - bool MeasureTool::IsSceneAlive() const { - boost::shared_ptr<ViewportController> controller = controllerW_.lock(); - return (controller.get() != NULL); + // since the lifetimes of the viewport, viewportcontroller (and the + // measuring tools inside it) are linked, the scene is always alive as + // long as "this" is alive + return true; } void MeasureTool::OnSceneTransformChanged(
--- a/Framework/Scene2DViewport/MeasureTool.h Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Scene2DViewport/MeasureTool.h Tue Mar 31 15:51:02 2020 +0200 @@ -20,6 +20,7 @@ #pragma once +#include "../Messages/ObserverBase.h" #include "../Scene2D/PolylineSceneLayer.h" #include "../Scene2D/Scene2D.h" #include "../Scene2D/ScenePoint2D.h" @@ -27,7 +28,6 @@ #include "../Scene2DViewport/PredeclaredTypes.h" #include "../Scene2DViewport/ViewportController.h" -#include <boost/shared_ptr.hpp> #include <boost/weak_ptr.hpp> #include <vector> @@ -38,10 +38,12 @@ class IFlexiblePointerTracker; class MeasureToolMemento; - class MeasureTool : public IObserver + class MeasureTool : public ObserverBase<MeasureTool> { public: - virtual ~MeasureTool(); + virtual ~MeasureTool() + { + } /** Enabled tools are rendered in the scene. @@ -73,7 +75,7 @@ true, then a click at that position will return a tracker to edit the measuring tool */ - virtual bool HitTest(ScenePoint2D p) const = 0; + virtual bool HitTest(ScenePoint2D p) = 0; /** This method must return a memento the captures the tool state (not including @@ -111,7 +113,9 @@ virtual std::string GetDescription() = 0; protected: - MeasureTool(MessageBroker& broker, boost::weak_ptr<ViewportController> controllerW); + MeasureTool(boost::shared_ptr<IViewport> viewport); + + void PostConstructor(); /** The measuring tool may exist in a standalone fashion, without any available @@ -127,17 +131,20 @@ */ virtual void RefreshScene() = 0; - boost::shared_ptr<const ViewportController> GetController() const; - boost::shared_ptr<ViewportController> GetController(); - /** enabled_ is not accessible by subclasses because there is a state machine that we do not wanna mess with */ bool IsEnabled() const; + /** + Protected to allow sub-classes to use this weak pointer in factory methods + (pass them to created objects) + */ + boost::shared_ptr<IViewport> viewport_; + + private: - boost::weak_ptr<ViewportController> controllerW_; bool enabled_; };
--- a/Framework/Scene2DViewport/MeasureToolsToolbox.cpp Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Scene2DViewport/MeasureToolsToolbox.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -311,28 +311,30 @@ for (int i = startingLayerIndex; i < startingLayerIndex + 5; ++i) { TextSceneLayer* textLayer = layerHolder->GetTextLayer(i); - ORTHANC_ASSERT(textLayer != NULL); - textLayer->SetText(text); - - if (i == startingLayerIndex + 4) - { - textLayer->SetColor(TEXT_COLOR_RED, - TEXT_COLOR_GREEN, - TEXT_COLOR_BLUE); - } - else + if (textLayer != NULL) { - textLayer->SetColor(TEXT_OUTLINE_COLOR_RED, - TEXT_OUTLINE_COLOR_GREEN, - TEXT_OUTLINE_COLOR_BLUE); - } + textLayer->SetText(text); - ScenePoint2D textAnchor; - int offIndex = i - startingLayerIndex; - ORTHANC_ASSERT(offIndex >= 0 && offIndex < 5); - textLayer->SetPosition( - p.GetX() + xoffsets[offIndex] * pixelToScene, - p.GetY() + yoffsets[offIndex] * pixelToScene); + if (i == startingLayerIndex + 4) + { + textLayer->SetColor(TEXT_COLOR_RED, + TEXT_COLOR_GREEN, + TEXT_COLOR_BLUE); + } + else + { + textLayer->SetColor(TEXT_OUTLINE_COLOR_RED, + TEXT_OUTLINE_COLOR_GREEN, + TEXT_OUTLINE_COLOR_BLUE); + } + + ScenePoint2D textAnchor; + int offIndex = i - startingLayerIndex; + ORTHANC_ASSERT(offIndex >= 0 && offIndex < 5); + textLayer->SetPosition( + p.GetX() + xoffsets[offIndex] * pixelToScene, + p.GetY() + yoffsets[offIndex] * pixelToScene); + } } } #else @@ -344,20 +346,21 @@ , int layerIndex) { TextSceneLayer* textLayer = layerHolder->GetTextLayer(layerIndex); - ORTHANC_ASSERT(textLayer != NULL); - textLayer->SetText(text); + if (textLayer != NULL) + { + textLayer->SetText(text); + textLayer->SetColor(TEXT_COLOR_RED, TEXT_COLOR_GREEN, TEXT_COLOR_BLUE); - textLayer->SetColor(TEXT_COLOR_RED, TEXT_COLOR_GREEN, TEXT_COLOR_BLUE); - - ScenePoint2D textAnchor; - textLayer->SetPosition(p.GetX(), p.GetY()); + ScenePoint2D textAnchor; + textLayer->SetPosition(p.GetX(), p.GetY()); + } } #endif - std::ostream& operator<<(std::ostream& os, const ScenePoint2D& p) - { - os << "x = " << p.GetX() << " , y = " << p.GetY(); - return os; - } + std::ostream& operator<<(std::ostream& os, const ScenePoint2D& p) + { + os << "x = " << p.GetX() << " , y = " << p.GetY(); + return os; + } }
--- a/Framework/Scene2DViewport/MeasureTrackers.cpp Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Scene2DViewport/MeasureTrackers.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -24,8 +24,8 @@ namespace OrthancStone { - CreateMeasureTracker::CreateMeasureTracker(boost::weak_ptr<ViewportController> controllerW) - : controllerW_(controllerW) + CreateMeasureTracker::CreateMeasureTracker(boost::shared_ptr<IViewport> viewport) + : viewport_(viewport) , alive_(true) , commitResult_(true) { @@ -47,29 +47,28 @@ // if the tracker completes successfully, we add the command // to the undo stack // otherwise, we simply undo it + + std::unique_ptr<IViewport::ILock> lock(viewport_->Lock()); + ViewportController& controller = lock->GetController(); + if (commitResult_) - controllerW_.lock()->PushCommand(command_); + lock->GetController().PushCommand(command_); else command_->Undo(); + + lock->Invalidate(); } - Scene2D& CreateMeasureTracker::GetScene() - { - return controllerW_.lock()->GetScene(); - } - - EditMeasureTracker::EditMeasureTracker(boost::weak_ptr<ViewportController> controllerW, const PointerEvent& e) - : controllerW_(controllerW) + EditMeasureTracker::EditMeasureTracker(boost::shared_ptr<IViewport> viewport, const PointerEvent& e) + : viewport_(viewport) , alive_(true) , commitResult_(true) { - boost::shared_ptr<ViewportController> controller = controllerW.lock(); - originalClickPosition_ = e.GetMainPosition().Apply(controller->GetScene().GetCanvasToSceneTransform()); - } + std::unique_ptr<IViewport::ILock> lock(viewport_->Lock()); + ViewportController& controller = lock->GetController(); - Scene2D& EditMeasureTracker::GetScene() - { - return controllerW_.lock()->GetScene(); + originalClickPosition_ = e.GetMainPosition().Apply( + controller.GetScene().GetCanvasToSceneTransform()); } void EditMeasureTracker::Cancel() @@ -88,10 +87,16 @@ // if the tracker completes successfully, we add the command // to the undo stack // otherwise, we simply undo it + + std::unique_ptr<IViewport::ILock> lock(viewport_->Lock()); + ViewportController& controller = lock->GetController(); + if (commitResult_) - controllerW_.lock()->PushCommand(command_); + lock->GetController().PushCommand(command_); else command_->Undo(); + + lock->Invalidate(); } }
--- a/Framework/Scene2DViewport/MeasureTrackers.h Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Scene2DViewport/MeasureTrackers.h Tue Mar 31 15:51:02 2020 +0200 @@ -40,15 +40,14 @@ virtual void Cancel() ORTHANC_OVERRIDE; virtual bool IsAlive() const ORTHANC_OVERRIDE; protected: - CreateMeasureTracker(boost::weak_ptr<ViewportController> controllerW); + CreateMeasureTracker(boost::shared_ptr<IViewport> viewport); ~CreateMeasureTracker(); protected: boost::shared_ptr<CreateMeasureCommand> command_; - boost::weak_ptr<ViewportController> controllerW_; + boost::shared_ptr<IViewport> viewport_; bool alive_; - Scene2D& GetScene(); private: bool commitResult_; @@ -60,15 +59,14 @@ virtual void Cancel() ORTHANC_OVERRIDE; virtual bool IsAlive() const ORTHANC_OVERRIDE; protected: - EditMeasureTracker(boost::weak_ptr<ViewportController> controllerW, const PointerEvent& e); + EditMeasureTracker(boost::shared_ptr<IViewport> viewport, const PointerEvent& e); ~EditMeasureTracker(); protected: boost::shared_ptr<EditMeasureCommand> command_; - boost::weak_ptr<ViewportController> controllerW_; + boost::shared_ptr<IViewport> viewport_; bool alive_; - Scene2D& GetScene(); ScenePoint2D GetOriginalClickPosition() const {
--- a/Framework/Scene2DViewport/OneGesturePointerTracker.cpp Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Scene2DViewport/OneGesturePointerTracker.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -28,8 +28,8 @@ namespace OrthancStone { OneGesturePointerTracker::OneGesturePointerTracker( - boost::weak_ptr<ViewportController> controllerW) - : controllerW_(controllerW) + boost::shared_ptr<IViewport> viewport) + : viewport_(viewport) , alive_(true) , currentTouchCount_(1) { @@ -55,15 +55,19 @@ // the number of active touches currentTouchCount_++; LOG(INFO) << "currentTouchCount_ becomes: " << currentTouchCount_; + + /** + * 2019-12-06 (SJO): Patch to have consistent behavior when mouse + * leaves the canvas while the tracker is still active, then + * button is released while out-of-canvas. Such an event is not + * caught (at least in WebAssembly), so we delete the tracker on + * the next click inside the canvas. + **/ + alive_ = false; } bool OneGesturePointerTracker::IsAlive() const { return alive_; } - - boost::shared_ptr<ViewportController> OneGesturePointerTracker::GetController() - { - return controllerW_.lock(); - } }
--- a/Framework/Scene2DViewport/OneGesturePointerTracker.h Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Scene2DViewport/OneGesturePointerTracker.h Tue Mar 31 15:51:02 2020 +0200 @@ -22,6 +22,11 @@ #include "IFlexiblePointerTracker.h" +#include "../Viewport/IViewport.h" + +#include <boost/shared_ptr.hpp> +#include <boost/weak_ptr.hpp> + namespace OrthancStone { /** @@ -39,16 +44,15 @@ class OneGesturePointerTracker : public IFlexiblePointerTracker { public: - OneGesturePointerTracker(boost::weak_ptr<ViewportController> controllerW); + OneGesturePointerTracker(boost::shared_ptr<IViewport> viewport); virtual void PointerUp(const PointerEvent& event) ORTHANC_OVERRIDE; virtual void PointerDown(const PointerEvent& event) ORTHANC_OVERRIDE; virtual bool IsAlive() const ORTHANC_OVERRIDE; protected: - boost::shared_ptr<ViewportController> GetController(); + boost::shared_ptr<IViewport> viewport_; private: - boost::weak_ptr<ViewportController> controllerW_; bool alive_; int currentTouchCount_; };
--- a/Framework/Scene2DViewport/PredeclaredTypes.h Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Scene2DViewport/PredeclaredTypes.h Tue Mar 31 15:51:02 2020 +0200 @@ -38,4 +38,5 @@ class MeasureCommand; class ViewportController; class LayerHolder; + class IViewport; }
--- a/Framework/Scene2DViewport/ViewportController.cpp Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Scene2DViewport/ViewportController.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -24,44 +24,69 @@ #include "MeasureCommands.h" #include "../StoneException.h" +#include "../Scene2D/PanSceneTracker.h" +#include "../Scene2D/RotateSceneTracker.h" +#include "../Scene2D/ZoomSceneTracker.h" #include <boost/make_shared.hpp> namespace OrthancStone { - ViewportController::ViewportController(boost::weak_ptr<UndoStack> undoStackW, - MessageBroker& broker, - IViewport& viewport) - : IObservable(broker) - , undoStackW_(undoStackW) - , canvasToSceneFactor_(0.0) - , viewport_(viewport) + IFlexiblePointerTracker* DefaultViewportInteractor::CreateTracker( + boost::shared_ptr<IViewport> viewport, + const PointerEvent& event, + unsigned int viewportWidth, + unsigned int viewportHeight) { - } - - ViewportController::~ViewportController() - { + switch (event.GetMouseButton()) + { + case MouseButton_Left: + return new RotateSceneTracker(viewport, event); + case MouseButton_Middle: + return new PanSceneTracker(viewport, event); + + case MouseButton_Right: + { + if (viewportWidth != 0) + { + return new ZoomSceneTracker(viewport, event, viewportWidth); + } + else + { + return NULL; + } + } + + default: + return NULL; + } } - boost::shared_ptr<UndoStack> ViewportController::GetUndoStack() + ViewportController::ViewportController(boost::shared_ptr<IViewport> viewport) + : viewport_(viewport) + , scene_(new Scene2D) + , canvasToSceneFactor_(1) { - return undoStackW_.lock(); + // undoStack_ is not default-initialized, which basically means empty. + // The controller must be able to cope with this. } - boost::shared_ptr<const UndoStack> ViewportController::GetUndoStack() const + ViewportController::~ViewportController() { - return undoStackW_.lock(); } - void ViewportController::PushCommand(boost::shared_ptr<MeasureCommand> command) + void ViewportController::PushCommand( + boost::shared_ptr<MeasureCommand> command) { boost::shared_ptr<UndoStack> undoStack = undoStackW_.lock(); - if(undoStack.get() != NULL) + if (undoStack.get() != NULL) + { undoStack->PushCommand(command); + } else { - LOG(ERROR) << "Internal error: no undo stack in the viewport controller!"; + LOG(ERROR) << "Internal error: no undo stack!"; } } @@ -69,10 +94,12 @@ { boost::shared_ptr<UndoStack> undoStack = undoStackW_.lock(); if (undoStack.get() != NULL) + { undoStack->Undo(); + } else { - LOG(ERROR) << "Internal error: no undo stack in the viewport controller!"; + LOG(ERROR) << "Internal error: no undo stack!"; } } @@ -80,10 +107,12 @@ { boost::shared_ptr<UndoStack> undoStack = undoStackW_.lock(); if (undoStack.get() != NULL) + { undoStack->Redo(); + } else { - LOG(ERROR) << "Internal error: no undo stack in the viewport controller!"; + LOG(ERROR) << "Internal error: no undo stack!"; } } @@ -91,10 +120,12 @@ { boost::shared_ptr<UndoStack> undoStack = undoStackW_.lock(); if (undoStack.get() != NULL) + { return undoStack->CanUndo(); + } else { - LOG(ERROR) << "Internal error: no undo stack in the viewport controller!"; + LOG(ERROR) << "Internal error: no undo stack!"; return false; } } @@ -103,21 +134,18 @@ { boost::shared_ptr<UndoStack> undoStack = undoStackW_.lock(); if (undoStack.get() != NULL) + { return undoStack->CanRedo(); + } else { - LOG(ERROR) << "Internal error: no undo stack in the viewport controller!"; + LOG(ERROR) << "Internal error: no undo stack!"; return false; } } - bool ViewportController::HandlePointerEvent(PointerEvent e) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); - } - - std::vector<boost::shared_ptr<MeasureTool> > ViewportController::HitTestMeasureTools( - ScenePoint2D p) + std::vector<boost::shared_ptr<MeasureTool> > + ViewportController::HitTestMeasureTools(ScenePoint2D p) { std::vector<boost::shared_ptr<MeasureTool> > ret; @@ -128,8 +156,7 @@ } return ret; } - - + void ViewportController::ResetMeasuringToolsHighlight() { for (size_t i = 0; i < measureTools_.size(); ++i) @@ -138,68 +165,59 @@ } } - const OrthancStone::AffineTransform2D& ViewportController::GetCanvasToSceneTransform() const + OrthancStone::AffineTransform2D + ViewportController::GetCanvasToSceneTransform() const { - return GetScene().GetCanvasToSceneTransform(); + return scene_->GetCanvasToSceneTransform(); } - const OrthancStone::AffineTransform2D& ViewportController::GetSceneToCanvasTransform() const + OrthancStone::AffineTransform2D + ViewportController::GetSceneToCanvasTransform() const { - return GetScene().GetSceneToCanvasTransform(); + return scene_->GetSceneToCanvasTransform(); } void ViewportController::SetSceneToCanvasTransform( const AffineTransform2D& transform) { - viewport_.GetScene().SetSceneToCanvasTransform(transform); + scene_->SetSceneToCanvasTransform(transform); + + canvasToSceneFactor_ = scene_->GetCanvasToSceneTransform().ComputeZoom(); BroadcastMessage(SceneTransformChanged(*this)); - - // update the canvas to scene factor - canvasToSceneFactor_ = 0.0; - canvasToSceneFactor_ = GetCanvasToSceneFactor(); } - void ViewportController::FitContent( - unsigned int canvasWidth, unsigned int canvasHeight) + void ViewportController::FitContent(unsigned int viewportWidth, + unsigned int viewportHeight) { - viewport_.GetScene().FitContent(canvasWidth, canvasHeight); + scene_->FitContent(viewportWidth, viewportHeight); + canvasToSceneFactor_ = scene_->GetCanvasToSceneTransform().ComputeZoom(); BroadcastMessage(SceneTransformChanged(*this)); } - void ViewportController::FitContent() + void ViewportController::AddMeasureTool( + boost::shared_ptr<MeasureTool> measureTool) { - if (viewport_.HasCompositor()) - { - const ICompositor& compositor = viewport_.GetCompositor(); - viewport_.GetScene().FitContent(compositor.GetCanvasWidth(), compositor.GetCanvasHeight()); - BroadcastMessage(SceneTransformChanged(*this)); - } - } - - void ViewportController::AddMeasureTool(boost::shared_ptr<MeasureTool> measureTool) - { - ORTHANC_ASSERT(std::find(measureTools_.begin(), measureTools_.end(), measureTool) - == measureTools_.end(), "Duplicate measure tool"); + ORTHANC_ASSERT(std::find(measureTools_.begin(), + measureTools_.end(), + measureTool) == measureTools_.end(), + "Duplicate measure tool"); measureTools_.push_back(measureTool); } - void ViewportController::RemoveMeasureTool(boost::shared_ptr<MeasureTool> measureTool) + void ViewportController::RemoveMeasureTool( + boost::shared_ptr<MeasureTool> measureTool) { - ORTHANC_ASSERT(std::find(measureTools_.begin(), measureTools_.end(), measureTool) - != measureTools_.end(), "Measure tool not found"); + ORTHANC_ASSERT(std::find(measureTools_.begin(), + measureTools_.end(), + measureTool) != measureTools_.end(), + "Measure tool not found"); measureTools_.erase( std::remove(measureTools_.begin(), measureTools_.end(), measureTool), measureTools_.end()); } - double ViewportController::GetCanvasToSceneFactor() const { - if (canvasToSceneFactor_ == 0) - { - canvasToSceneFactor_ = - GetScene().GetCanvasToSceneTransform().ComputeZoom(); - } return canvasToSceneFactor_; } @@ -222,4 +240,68 @@ { return TEXT_CENTER_DISTANCE_CANVAS_COORD * GetCanvasToSceneFactor(); } + + + void ViewportController::HandleMousePress( + OrthancStone::IViewportInteractor& interactor, + const PointerEvent& event, + unsigned int viewportWidth, + unsigned int viewportHeight) + { + if (activeTracker_) + { + // We are dealing with a multi-stage tracker (that is made of several + // interactions) + activeTracker_->PointerDown(event); + + if (!activeTracker_->IsAlive()) + { + activeTracker_.reset(); + } + } + else + { + // Check whether there is already a measure tool at that position + for (size_t i = 0; i < measureTools_.size(); ++i) + { + if (measureTools_[i]->HitTest(event.GetMainPosition())) + { + activeTracker_ = measureTools_[i]->CreateEditionTracker(event); + return; + } + } + + // No measure tool, create new tracker from the interactor + activeTracker_.reset(interactor.CreateTracker(viewport_, + event, + viewportWidth, + viewportHeight)); + } + } + + bool ViewportController::HandleMouseMove(const PointerEvent& event) + { + if (activeTracker_) + { + activeTracker_->PointerMove(event); + return true; + } + else + { + return false; + } + } + + void ViewportController::HandleMouseRelease(const PointerEvent& event) + { + if (activeTracker_) + { + activeTracker_->PointerUp(event); + + if (!activeTracker_->IsAlive()) + { + activeTracker_.reset(); + } + } + } }
--- a/Framework/Scene2DViewport/ViewportController.h Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Scene2DViewport/ViewportController.h Tue Mar 31 15:51:02 2020 +0200 @@ -22,14 +22,43 @@ #include "PredeclaredTypes.h" -#include "../Viewport/IViewport.h" -#include "../Scene2D/PointerEvent.h" +#include "../Messages/IObservable.h" +#include "../Scene2D/Scene2D.h" #include "../Scene2DViewport/IFlexiblePointerTracker.h" +#include <Core/Compatibility.h> + +#include <boost/enable_shared_from_this.hpp> #include <stack> namespace OrthancStone { + // TODO - Move this to another file + class IViewportInteractor : public boost::noncopyable + { + public: + virtual ~IViewportInteractor() + { + } + + virtual IFlexiblePointerTracker* CreateTracker(boost::shared_ptr<IViewport> viewport, + const PointerEvent& event, + unsigned int viewportWidth, + unsigned int viewportHeight) = 0; + }; + + + // TODO - Move this to another file + class DefaultViewportInteractor : public IViewportInteractor + { + public: + virtual IFlexiblePointerTracker* CreateTracker(boost::shared_ptr<IViewport> viewport, + const PointerEvent& event, + unsigned int viewportWidth, + unsigned int viewportHeight) ORTHANC_OVERRIDE; + }; + + class UndoStack; const double ARC_RADIUS_CANVAS_COORD = 30.0; @@ -74,31 +103,26 @@ Each canvas or other GUI area where we want to display a 2D image, either directly or through slicing must be assigned a ViewportController. */ - class ViewportController : public IObservable + class ViewportController : + public IObservable, + public boost::enable_shared_from_this<ViewportController> { public: ORTHANC_STONE_DEFINE_ORIGIN_MESSAGE(__FILE__, __LINE__, \ - SceneTransformChanged, ViewportController); + SceneTransformChanged, \ + ViewportController); - ViewportController(boost::weak_ptr<UndoStack> undoStackW, - MessageBroker& broker, - IViewport& viewport); - + ViewportController(boost::shared_ptr<IViewport> viewport); ~ViewportController(); - /** - This method is called by the GUI system and should update/delete the - current tracker - */ - bool HandlePointerEvent(PointerEvent e); - /** This method returns the list of measure tools containing the supplied point (in scene coords). A tracker can then be requested from the chosen measure tool, if needed */ - std::vector<boost::shared_ptr<MeasureTool> > HitTestMeasureTools(ScenePoint2D p); + std::vector<boost::shared_ptr<MeasureTool> > HitTestMeasureTools( + ScenePoint2D p); /** This function will traverse the measuring tools and will clear their @@ -110,20 +134,20 @@ With this method, the object takes ownership of the supplied tracker and updates it according to user interaction */ - void SetActiveTracker(boost::shared_ptr<IFlexiblePointerTracker> tracker); + void AcquireActiveTracker(IFlexiblePointerTracker* tracker); /** Forwarded to the underlying scene */ - const AffineTransform2D& GetCanvasToSceneTransform() const; + AffineTransform2D GetCanvasToSceneTransform() const; /** Forwarded to the underlying scene */ - const AffineTransform2D& GetSceneToCanvasTransform() const; + AffineTransform2D GetSceneToCanvasTransform() const; /** Forwarded to the underlying scene, and broadcasted to the observers */ void SetSceneToCanvasTransform(const AffineTransform2D& transform); /** Forwarded to the underlying scene, and broadcasted to the observers */ - void FitContent(unsigned int canvasWidth, unsigned int canvasHeight); - void FitContent(); + void FitContent(unsigned int viewportWidth, + unsigned int viewportHeight); /** Adds a new measure tool */ void AddMeasureTool(boost::shared_ptr<MeasureTool> measureTool); @@ -173,31 +197,74 @@ /** forwarded to the UndoStack */ bool CanRedo() const; - Scene2D& GetScene() - { - return viewport_.GetScene(); - } + + // Must be expressed in canvas coordinates + void HandleMousePress(IViewportInteractor& interactor, + const PointerEvent& event, + unsigned int viewportWidth, + unsigned int viewportHeight); + + // Must be expressed in canvas coordinates. Returns "true" if the + // state has changed, so that "Invalidate()" can be called. + bool HandleMouseMove(const PointerEvent& event); + + // Must be expressed in canvas coordinates + void HandleMouseRelease(const PointerEvent& event); const Scene2D& GetScene() const { - return const_cast<IViewport&>(viewport_).GetScene(); + return *scene_; + } + + Scene2D& GetScene() + { + return *scene_; + } + + /** + This method is used in a move pattern: when the ownership of the scene + managed by this viewport controller must be transferred to another + controller. + */ + Scene2D* ReleaseScene() + { + return scene_.release(); + } + + /** + This method is used when one wishes to replace the scene that is currently + managed by the controller. The previous scene is deleted and the controller + now has ownership of the new one. + */ + void AcquireScene(Scene2D* scene) + { + scene_.reset(scene); + } + + /** + Sets the undo stack that is used by PushCommand, Undo... + */ + void SetUndoStack(boost::weak_ptr<UndoStack> undoStackW) + { + undoStackW_ = undoStackW; + } + + bool HasActiveTracker() const + { + return activeTracker_.get() != NULL; } private: double GetCanvasToSceneFactor() const; - boost::weak_ptr<UndoStack> undoStackW_; - - boost::shared_ptr<UndoStack> GetUndoStack(); - boost::shared_ptr<const UndoStack> GetUndoStack() const; + boost::shared_ptr<IViewport> viewport_; + boost::weak_ptr<UndoStack> undoStackW_; // Global stack, possibly shared by all viewports + std::vector<boost::shared_ptr<MeasureTool> > measureTools_; + boost::shared_ptr<IFlexiblePointerTracker> activeTracker_; // TODO - Couldn't this be a "std::unique_ptr"? - std::vector<boost::shared_ptr<MeasureTool> > measureTools_; - boost::shared_ptr<IFlexiblePointerTracker> tracker_; - + std::unique_ptr<Scene2D> scene_; + // this is cached - mutable double canvasToSceneFactor_; - - // Refactoring on 2019-07-10: Removing shared_ptr from scene - IViewport& viewport_; + double canvasToSceneFactor_; }; }
--- a/Framework/StoneEnumerations.h Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/StoneEnumerations.h Tue Mar 31 15:51:02 2020 +0200 @@ -24,21 +24,6 @@ #include <string> -namespace Deprecated -{ - enum SliceImageQuality - { - SliceImageQuality_FullPng, // smaller to transmit but longer to generate on Orthanc side (better choice when on low bandwidth) - SliceImageQuality_FullPam, // bigger to transmit but faster to generate on Orthanc side (better choice when on localhost or LAN) - SliceImageQuality_Jpeg50, - SliceImageQuality_Jpeg90, - SliceImageQuality_Jpeg95, - - SliceImageQuality_InternalRaw // downloads the raw pixels data as they are stored in the DICOM file (internal use only) - }; -} - - namespace OrthancStone { enum SliceOffsetMode @@ -59,7 +44,8 @@ { MouseButton_Left, MouseButton_Right, - MouseButton_Middle + MouseButton_Middle, + MouseButton_None // For instance, because of touch event }; enum MouseWheelDirection @@ -135,6 +121,15 @@ BitmapAnchor_TopRight }; + enum SliceAction + { + SliceAction_FastPlus, + SliceAction_Plus, + SliceAction_None, + SliceAction_Minus, + SliceAction_FastMinus + }; + SopClassUid StringToSopClassUid(const std::string& source); void ComputeWindowing(float& targetCenter,
--- a/Framework/StoneInitialization.cpp Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/StoneInitialization.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -21,20 +21,58 @@ #include "StoneInitialization.h" -#include <Core/OrthancException.h> - #if !defined(ORTHANC_ENABLE_SDL) # error Macro ORTHANC_ENABLE_SDL must be defined #endif +#if !defined(ORTHANC_ENABLE_QT) +# error Macro ORTHANC_ENABLE_QT must be defined +#endif + +#if !defined(ORTHANC_ENABLE_SSL) +# error Macro ORTHANC_ENABLE_SSL must be defined +#endif + +#if !defined(ORTHANC_ENABLE_CURL) +# error Macro ORTHANC_ENABLE_CURL must be defined +#endif + +#if !defined(ORTHANC_ENABLE_DCMTK) +# error Macro ORTHANC_ENABLE_DCMTK must be defined +# if !defined(DCMTK_VERSION_NUMBER) +# error Macro DCMTK_VERSION_NUMBER must be defined +# endif +#endif + #if ORTHANC_ENABLE_SDL == 1 # include "Viewport/SdlWindow.h" #endif +#if ORTHANC_ENABLE_QT == 1 +# include <QCoreApplication> +#endif + #if ORTHANC_ENABLE_CURL == 1 -#include <Core/HttpClient.h> +# include <Core/HttpClient.h> +#endif + +#if ORTHANC_ENABLE_DCMTK == 1 +# include <Core/DicomParsing/FromDcmtkBridge.h> #endif +#if ORTHANC_ENABLE_WASM == 1 +static double viewportsTimeout_ = 1000; +static std::unique_ptr<OrthancStone::WebGLViewportsRegistry> viewportsRegistry_; +#endif + +#include "Toolbox/LinearAlgebra.h" + +#include <Core/OrthancException.h> +#include <Core/Toolbox.h> + +#include <locale> + + namespace OrthancStone { #if ORTHANC_ENABLE_LOGGING_PLUGIN == 1 @@ -49,25 +87,142 @@ Orthanc::Logging::Initialize(); #endif -#if ORTHANC_ENABLE_SDL == 1 - OrthancStone::SdlWindow::GlobalInitialize(); +#if ORTHANC_ENABLE_SSL == 1 + // Must be before curl + Orthanc::Toolbox::InitializeOpenSsl(); #endif #if ORTHANC_ENABLE_CURL == 1 Orthanc::HttpClient::GlobalInitialize(); +# if ORTHANC_ENABLE_SSL == 1 + Orthanc::HttpClient::ConfigureSsl(false, ""); +# endif +#endif + +#if ORTHANC_ENABLE_DCMTK == 1 + Orthanc::FromDcmtkBridge::InitializeDictionary(true); + Orthanc::FromDcmtkBridge::InitializeCodecs(); +# if DCMTK_VERSION_NUMBER <= 360 + OFLog::configure(OFLogger::FATAL_LOG_LEVEL); +# else + OFLog::configure(OFLogger::OFF_LOG_LEVEL); +# endif +#endif + + /** + * This call is necessary to make "boost::lexical_cast<>" work in + * a consistent way in the presence of "double" or "float", and of + * a numeric locale that replaces dot (".") by comma (",") as the + * decimal separator. + * https://stackoverflow.com/a/18981514/881731 + **/ + std::locale::global(std::locale::classic()); + + { + // Run-time checks of locale settings, to be run after Qt has + // been initialized, as Qt changes locale settings + +#if ORTHANC_ENABLE_QT == 1 + if (QCoreApplication::instance() == NULL) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls, + "Qt must be initialized before Stone"); + } +#endif + + { + OrthancStone::Vector v; + if (!OrthancStone::LinearAlgebra::ParseVector(v, "1.3671875\\-1.3671875") || + v.size() != 2 || + !OrthancStone::LinearAlgebra::IsNear(1.3671875f, v[0]) || + !OrthancStone::LinearAlgebra::IsNear(-1.3671875f, v[1])) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError, + "Error in the locale settings, giving up"); + } + } + + { + Json::Value dicomweb = Json::objectValue; + dicomweb["00280030"] = Json::objectValue; + dicomweb["00280030"]["vr"] = "DS"; + dicomweb["00280030"]["Value"] = Json::arrayValue; + dicomweb["00280030"]["Value"].append(1.2f); + dicomweb["00280030"]["Value"].append(-1.5f); + + Orthanc::DicomMap source; + source.FromDicomWeb(dicomweb); + + std::string s; + OrthancStone::Vector v; + if (!source.LookupStringValue(s, Orthanc::DICOM_TAG_PIXEL_SPACING, false) || + !OrthancStone::LinearAlgebra::ParseVector(v, s) || + v.size() != 2 || + !OrthancStone::LinearAlgebra::IsNear(1.2f, v[0]) || + !OrthancStone::LinearAlgebra::IsNear(-1.5f, v[1])) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError, + "Error in the locale settings, giving up"); + } + } + } + +#if ORTHANC_ENABLE_SDL == 1 + OrthancStone::SdlWindow::GlobalInitialize(); #endif } + void StoneFinalize() { +#if ORTHANC_ENABLE_WASM == 1 + viewportsRegistry_.reset(); +#endif + #if ORTHANC_ENABLE_SDL == 1 OrthancStone::SdlWindow::GlobalFinalize(); #endif +#if ORTHANC_ENABLE_DCMTK == 1 + Orthanc::FromDcmtkBridge::FinalizeCodecs(); +#endif + #if ORTHANC_ENABLE_CURL == 1 Orthanc::HttpClient::GlobalFinalize(); #endif +#if ORTHANC_ENABLE_SSL == 1 + Orthanc::Toolbox::FinalizeOpenSsl(); +#endif + Orthanc::Logging::Finalize(); } + + +#if ORTHANC_ENABLE_WASM == 1 + void SetWebGLViewportsRegistryTimeout(double timeout) + { + if (viewportsRegistry_.get()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + else + { + viewportsTimeout_ = timeout; + } + } +#endif + + +#if ORTHANC_ENABLE_WASM == 1 + WebGLViewportsRegistry& GetWebGLViewportsRegistry() + { + if (viewportsRegistry_.get() == NULL) + { + viewportsRegistry_.reset(new WebGLViewportsRegistry(viewportsTimeout_)); + } + + return *viewportsRegistry_; + } +#endif }
--- a/Framework/StoneInitialization.h Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/StoneInitialization.h Tue Mar 31 15:51:02 2020 +0200 @@ -21,6 +21,18 @@ #pragma once +#if !defined(ORTHANC_ENABLE_WASM) +# error Macro ORTHANC_ENABLE_WASM must be defined +#endif + +#if !defined(ORTHANC_ENABLE_LOGGING_PLUGIN) +# error Macro ORTHANC_ENABLE_LOGGING_PLUGIN must be defined +#endif + +#if ORTHANC_ENABLE_WASM == 1 +# include "Viewport/WebGLViewportsRegistry.h" +#endif + #include <Core/Logging.h> namespace OrthancStone @@ -32,4 +44,12 @@ #endif void StoneFinalize(); + +#if ORTHANC_ENABLE_WASM == 1 + void SetWebGLViewportsRegistryTimeout(double timeout); +#endif + +#if ORTHANC_ENABLE_WASM == 1 + WebGLViewportsRegistry& GetWebGLViewportsRegistry(); +#endif }
--- a/Framework/Toolbox/CoordinateSystem3D.cpp Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Toolbox/CoordinateSystem3D.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -241,4 +241,14 @@ return s; } + + CoordinateSystem3D CoordinateSystem3D::NormalizeCuttingPlane(const CoordinateSystem3D& plane) + { + double ox, oy; + plane.ProjectPoint(ox, oy, LinearAlgebra::CreateVector(0, 0, 0)); + + CoordinateSystem3D normalized(plane); + normalized.SetOrigin(plane.MapSliceToWorldCoordinates(ox, oy)); + return normalized; + } }
--- a/Framework/Toolbox/CoordinateSystem3D.h Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Toolbox/CoordinateSystem3D.h Tue Mar 31 15:51:02 2020 +0200 @@ -22,6 +22,7 @@ #pragma once #include "LinearAlgebra.h" +#include "../Scene2D/ScenePoint2D.h" #include <Plugins/Samples/Common/IDicomDataset.h> @@ -95,12 +96,24 @@ Vector MapSliceToWorldCoordinates(double x, double y) const; + Vector MapSliceToWorldCoordinates(const ScenePoint2D& p) const + { + return MapSliceToWorldCoordinates(p.GetX(), p.GetY()); + } + double ProjectAlongNormal(const Vector& point) const; void ProjectPoint(double& offsetX, double& offsetY, const Vector& point) const; + ScenePoint2D ProjectPoint(const Vector& point) const + { + double x, y; + ProjectPoint(x, y, point); + return ScenePoint2D(x, y); + } + /* Alternated faster implementation (untested yet) */ @@ -120,5 +133,9 @@ static bool ComputeDistance(double& distance, const CoordinateSystem3D& a, const CoordinateSystem3D& b); + + // Normalize a cutting plane so that the origin (0,0,0) of the 3D + // world is mapped to the origin of its (x,y) coordinate system + static CoordinateSystem3D NormalizeCuttingPlane(const CoordinateSystem3D& plane); }; }
--- a/Framework/Toolbox/DicomInstanceParameters.cpp Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Toolbox/DicomInstanceParameters.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -265,7 +265,7 @@ } void DicomInstanceParameters::Data::ApplyRescaleAndDoseScaling(Orthanc::ImageAccessor& image, - bool useDouble) const + bool useDouble) const { if (image.GetFormat() != Orthanc::PixelFormat_Float32) { @@ -468,4 +468,48 @@ throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); } } + + + double DicomInstanceParameters::Data::ApplyRescale(double value) const + { + double factor = doseGridScaling_; + double offset = 0.0; + + if (hasRescale_) + { + factor *= rescaleSlope_; + offset = rescaleIntercept_; + } + + return (value * factor + offset); + } + + + bool DicomInstanceParameters::Data::ComputeRegularSpacing(double& spacing) const + { + if (frameOffsets_.size() == 0) // Not a RT-DOSE + { + return false; + } + else if (frameOffsets_.size() == 1) + { + spacing = 1; // Edge case: RT-DOSE with one single frame + return true; + } + else + { + spacing = std::abs(frameOffsets_[1] - frameOffsets_[0]); + + for (size_t i = 1; i + 1 < frameOffsets_.size(); i++) + { + double s = frameOffsets_[i + 1] - frameOffsets_[i]; + if (!LinearAlgebra::IsNear(spacing, s, 0.001)) + { + return false; + } + } + + return true; + } + } }
--- a/Framework/Toolbox/DicomInstanceParameters.h Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Toolbox/DicomInstanceParameters.h Tue Mar 31 15:51:02 2020 +0200 @@ -71,8 +71,12 @@ bool IsPlaneWithinSlice(unsigned int frame, const CoordinateSystem3D& plane) const; - void ApplyRescaleAndDoseScaling( - Orthanc::ImageAccessor& image, bool useDouble) const; + void ApplyRescaleAndDoseScaling(Orthanc::ImageAccessor& image, + bool useDouble) const; + + double ApplyRescale(double value) const; + + bool ComputeRegularSpacing(double& target) const; }; @@ -207,9 +211,25 @@ return data_.doseUnits_; } + void SetDoseGridScaling(double value) + { + data_.doseGridScaling_ = value; + } + double GetDoseGridScaling() const { return data_.doseGridScaling_; } + + double ApplyRescale(double value) const + { + return data_.ApplyRescale(value); + } + + // Required for RT-DOSE + bool ComputeRegularSpacing(double& target) const + { + return data_.ComputeRegularSpacing(target); + } }; }
--- a/Framework/Toolbox/DicomStructureSet.cpp Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Toolbox/DicomStructureSet.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -27,12 +27,11 @@ #include <Core/Logging.h> #include <Core/OrthancException.h> #include <Core/Toolbox.h> -#include <Plugins/Samples/Common/FullOrthancDataset.h> #include <Plugins/Samples/Common/DicomDatasetReader.h> #if defined(_MSC_VER) -#pragma warning(push) -#pragma warning(disable:4244) +# pragma warning(push) +# pragma warning(disable:4244) #endif #include <limits> @@ -43,9 +42,14 @@ #include <boost/geometry/multi/geometries/multi_polygon.hpp> #if defined(_MSC_VER) -#pragma warning(pop) +# pragma warning(pop) #endif +#if ORTHANC_ENABLE_DCMTK == 1 +# include "ParsedDicomDataset.h" +#endif + + typedef boost::geometry::model::d2::point_xy<double> BoostPoint; typedef boost::geometry::model::polygon<BoostPoint> BoostPolygon; typedef boost::geometry::model::multi_polygon<BoostPolygon> BoostMultiPolygon; @@ -81,7 +85,7 @@ } } -#ifdef USE_BOOST_UNION_FOR_POLYGONS +#if USE_BOOST_UNION_FOR_POLYGONS == 1 static BoostPolygon CreateRectangle(float x1, float y1, float x2, float y2) @@ -99,7 +103,7 @@ namespace OrthancStone { static RtStructRectangleInSlab CreateRectangle(float x1, float y1, - float x2, float y2) + float x2, float y2) { RtStructRectangleInSlab rect; rect.xmin = std::min(x1, x2); @@ -174,15 +178,15 @@ double magnitude = GeometryToolbox::ProjectAlongNormal(v, geometry_.GetNormal()); if(!LinearAlgebra::IsNear( - magnitude, - projectionAlongNormal_, - sliceThickness_ / 2.0 /* in mm */ )) + magnitude, + projectionAlongNormal_, + sliceThickness_ / 2.0 /* in mm */ )) { LOG(ERROR) << "This RT-STRUCT contains a point that is off the " - << "slice of its instance | " - << "magnitude = " << magnitude << " | " - << "projectionAlongNormal_ = " << projectionAlongNormal_ << " | " - << "tolerance (sliceThickness_ / 2.0) = " << (sliceThickness_ / 2.0); + << "slice of its instance | " + << "magnitude = " << magnitude << " | " + << "projectionAlongNormal_ = " << projectionAlongNormal_ << " | " + << "tolerance (sliceThickness_ / 2.0) = " << (sliceThickness_ / 2.0); throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat); } @@ -202,10 +206,10 @@ if (!onSlice) { LOG(WARNING) << "This RT-STRUCT contains a point that is off the " - << "slice of its instance | " - << "magnitude = " << magnitude << " | " - << "projectionAlongNormal_ = " << projectionAlongNormal_ << " | " - << "tolerance (sliceThickness_ / 2.0) = " << (sliceThickness_ / 2.0); + << "slice of its instance | " + << "magnitude = " << magnitude << " | " + << "projectionAlongNormal_ = " << projectionAlongNormal_ << " | " + << "tolerance (sliceThickness_ / 2.0) = " << (sliceThickness_ / 2.0); } return onSlice; } @@ -373,12 +377,14 @@ else if (GeometryToolbox::IsParallelOrOpposite (isOpposite, slice.GetNormal(), geometry_.GetAxisX())) { - // plane is constant X + // plane is constant X => Sagittal view (remember that in the + // sagittal projection, the normal must be swapped) + /* - Please read the comments in the section above, by taking into account - the fact that, in this case, the plane has a constant X, not Y (in - polygon geometry_ coordinates) + Please read the comments in the section above, by taking into account + the fact that, in this case, the plane has a constant X, not Y (in + polygon geometry_ coordinates) */ if (x < extent_.GetX1() || @@ -427,10 +433,6 @@ slice.ProjectPoint2(x1, y1, p1); slice.ProjectPoint2(x2, y2, p2); - // TODO WHY THIS??? - y1 = -y1; - y2 = -y2; - return true; } } @@ -463,7 +465,7 @@ return structures_[index]; } - DicomStructureSet::DicomStructureSet(const OrthancPlugins::FullOrthancDataset& tags) + void DicomStructureSet::Setup(const OrthancPlugins::IDicomDataset& tags) { OrthancPlugins::DicomDatasetReader reader(tags); @@ -514,11 +516,11 @@ } LOG(INFO) << "New RT structure: \"" << structures_[i].name_ - << "\" with interpretation \"" << structures_[i].interpretation_ - << "\" containing " << countSlices << " slices (color: " - << static_cast<int>(structures_[i].red_) << "," - << static_cast<int>(structures_[i].green_) << "," - << static_cast<int>(structures_[i].blue_) << ")"; + << "\" with interpretation \"" << structures_[i].interpretation_ + << "\" containing " << countSlices << " slices (color: " + << static_cast<int>(structures_[i].red_) << "," + << static_cast<int>(structures_[i].green_) << "," + << static_cast<int>(structures_[i].blue_) << ")"; // These temporary variables avoid allocating many vectors in the loop below OrthancPlugins::DicomPath countPointsPath(DICOM_TAG_ROI_CONTOUR_SEQUENCE, i, @@ -609,6 +611,15 @@ } +#if ORTHANC_ENABLE_DCMTK == 1 + DicomStructureSet::DicomStructureSet(Orthanc::ParsedDicomFile& instance) + { + ParsedDicomDataset dataset(instance); + Setup(dataset); + } +#endif + + Vector DicomStructureSet::GetStructureCenter(size_t index) const { const Structure& structure = GetStructure(index); @@ -773,14 +784,14 @@ if (Orthanc::Toolbox::StripSpaces(sopInstanceUid) == "") { LOG(ERROR) << "DicomStructureSet::CheckReferencedSlices(): " - << " missing information about referenced instance " - << "(sopInstanceUid is empty!)"; + << " missing information about referenced instance " + << "(sopInstanceUid is empty!)"; } else { LOG(ERROR) << "DicomStructureSet::CheckReferencedSlices(): " - << " missing information about referenced instance " - << "(sopInstanceUid = " << sopInstanceUid << ")"; + << " missing information about referenced instance " + << "(sopInstanceUid = " << sopInstanceUid << ")"; } //throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); } @@ -803,17 +814,18 @@ } } -#ifdef USE_BOOST_UNION_FOR_POLYGONS - bool DicomStructureSet::ProjectStructure(std::vector< std::vector<Point2D> >& polygons, - const Structure& structure, - const CoordinateSystem3D& slice) const + bool DicomStructureSet::ProjectStructure( +#if USE_BOOST_UNION_FOR_POLYGONS == 1 + std::vector< std::vector<Point2D> >& polygons, #else - bool DicomStructureSet::ProjectStructure(std::vector< std::pair<Point2D, Point2D> >& segments, + std::vector< std::pair<Point2D, Point2D> >& segments, +#endif const Structure& structure, - const CoordinateSystem3D& slice) const -#endif + const CoordinateSystem3D& sourceSlice) const { -#ifdef USE_BOOST_UNION_FOR_POLYGONS + const CoordinateSystem3D slice = CoordinateSystem3D::NormalizeCuttingPlane(sourceSlice); + +#if USE_BOOST_UNION_FOR_POLYGONS == 1 polygons.clear(); #else segments.clear(); @@ -831,7 +843,7 @@ { if (polygon->IsOnSlice(slice)) { -#ifdef USE_BOOST_UNION_FOR_POLYGONS +#if USE_BOOST_UNION_FOR_POLYGONS == 1 polygons.push_back(std::vector<Point2D>()); for (Points::const_iterator p = polygon->GetPoints().begin(); @@ -882,15 +894,26 @@ #if 1 // Sagittal or coronal projection -#ifdef USE_BOOST_UNION_FOR_POLYGONS +#if USE_BOOST_UNION_FOR_POLYGONS == 1 std::vector<BoostPolygon> projected; + + for (Polygons::const_iterator polygon = structure.polygons_.begin(); + polygon != structure.polygons_.end(); ++polygon) + { + double x1, y1, x2, y2; + + if (polygon->Project(x1, y1, x2, y2, slice)) + { + projected.push_back(CreateRectangle(x1, y1, x2, y2)); + } + } #else // this will contain the intersection of the polygon slab with // the cutting plane, projected on the cutting plane coord system // (that yields a rectangle) + the Z coordinate of the polygon // (this is required to group polygons with the same Z later) std::vector<std::pair<RtStructRectangleInSlab, double> > projected; -#endif + for (Polygons::const_iterator polygon = structure.polygons_.begin(); polygon != structure.polygons_.end(); ++polygon) { @@ -903,13 +926,15 @@ // x1,y1 and x2,y2 are in "slice" coordinates (the cutting plane // geometry) projected.push_back(std::make_pair(CreateRectangle( - static_cast<float>(x1), - static_cast<float>(y1), - static_cast<float>(x2), - static_cast<float>(y2)),curZ)); + static_cast<float>(x1), + static_cast<float>(y1), + static_cast<float>(x2), + static_cast<float>(y2)),curZ)); } } -#ifndef USE_BOOST_UNION_FOR_POLYGONS +#endif + +#if USE_BOOST_UNION_FOR_POLYGONS != 1 // projected contains a set of rectangles specified by two opposite // corners (x1,y1,x2,y2) // we need to merge them @@ -999,4 +1024,44 @@ return false; } } + + + void DicomStructureSet::ProjectOntoLayer(PolylineSceneLayer& layer, + const CoordinateSystem3D& plane, + size_t structureIndex, + const Color& color) const + { +#if USE_BOOST_UNION_FOR_POLYGONS == 1 + std::vector< std::vector<Point2D> > polygons; + if (ProjectStructure(polygons, structureIndex, plane)) + { + for (size_t j = 0; j < polygons.size(); j++) + { + std::vector<ScenePoint2D> chain; + chain.reserve(polygons[j].size()); + + for (size_t k = 0; k < polygons[j].size(); k++) + { + chain.push_back(ScenePoint2D(polygons[j][k].x, polygons[j][k].y)); + } + + layer.AddChain(chain, true, color.GetRed(), color.GetGreen(), color.GetBlue()); + } + } + +#else + std::vector< std::pair<Point2D, Point2D> > segments; + + if (ProjectStructure(segments, structureIndex, plane)) + { + for (size_t j = 0; j < segments.size(); j++) + { + std::vector<ScenePoint2D> chain(2); + chain[0] = ScenePoint2D(segments[j].first.x, segments[j].first.y); + chain[1] = ScenePoint2D(segments[j].second.x, segments[j].second.y); + layer.AddChain(chain, false, color.GetRed(), color.GetGreen(), color.GetBlue()); + } + } +#endif + } }
--- a/Framework/Toolbox/DicomStructureSet.h Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Toolbox/DicomStructureSet.h Tue Mar 31 15:51:02 2020 +0200 @@ -21,10 +21,19 @@ #pragma once +#if !defined(ORTHANC_ENABLE_DCMTK) +# error The macro ORTHANC_ENABLE_DCMTK must be defined +#endif + #include "DicomStructureSetUtils.h" #include "CoordinateSystem3D.h" #include "Extent2D.h" #include "../Scene2D/Color.h" +#include "../Scene2D/PolylineSceneLayer.h" + +#if ORTHANC_ENABLE_DCMTK == 1 +# include <Core/DicomParsing/ParsedDicomFile.h> +#endif //#define USE_BOOST_UNION_FOR_POLYGONS 1 @@ -137,21 +146,30 @@ Structures structures_; ReferencedSlices referencedSlices_; + void Setup(const OrthancPlugins::IDicomDataset& dataset); + const Structure& GetStructure(size_t index) const; Structure& GetStructure(size_t index); -#ifdef USE_BOOST_UNION_FOR_POLYGONS - bool ProjectStructure(std::vector< std::vector<Point2D> >& polygons, - const Structure& structure, - const CoordinateSystem3D& slice) const; + bool ProjectStructure( +#if USE_BOOST_UNION_FOR_POLYGONS == 1 + std::vector< std::vector<Point2D> >& polygons, #else - bool ProjectStructure(std::vector< std::pair<Point2D, Point2D> >& segments, + std::vector< std::pair<Point2D, Point2D> >& segments, +#endif const Structure& structure, const CoordinateSystem3D& slice) const; -#endif + public: - DicomStructureSet(const OrthancPlugins::FullOrthancDataset& instance); + DicomStructureSet(const OrthancPlugins::FullOrthancDataset& instance) + { + Setup(instance); + } + +#if ORTHANC_ENABLE_DCMTK == 1 + DicomStructureSet(Orthanc::ParsedDicomFile& instance); +#endif size_t GetStructuresCount() const { @@ -185,20 +203,32 @@ Vector GetNormal() const; -#ifdef USE_BOOST_UNION_FOR_POLYGONS +#if USE_BOOST_UNION_FOR_POLYGONS == 1 bool ProjectStructure(std::vector< std::vector<Point2D> >& polygons, - size_t index, - const CoordinateSystem3D& slice) const + size_t index, + const CoordinateSystem3D& slice) const { return ProjectStructure(polygons, GetStructure(index), slice); } #else bool ProjectStructure(std::vector< std::pair<Point2D, Point2D> >& segments, - size_t index, - const CoordinateSystem3D& slice) const + size_t index, + const CoordinateSystem3D& slice) const { return ProjectStructure(segments, GetStructure(index), slice); } #endif + + void ProjectOntoLayer(PolylineSceneLayer& layer, + const CoordinateSystem3D& plane, + size_t structureIndex, + const Color& color) const; + + void ProjectOntoLayer(PolylineSceneLayer& layer, + const CoordinateSystem3D& plane, + size_t structureIndex) const + { + ProjectOntoLayer(layer, plane, structureIndex, GetStructureColor(structureIndex)); + } }; }
--- a/Framework/Toolbox/GenericToolbox.h Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Toolbox/GenericToolbox.h Tue Mar 31 15:51:02 2020 +0200 @@ -20,15 +20,27 @@ #pragma once +#include <Core/Compatibility.h> +#include <Core/OrthancException.h> + +#include <boost/shared_ptr.hpp> + #include <string> #include <stdint.h> #include <math.h> +#include <memory> namespace OrthancStone { namespace GenericToolbox { + /** + Fast floating point string validation. + No trimming applied, so the input must match regex + /^[-]?[0-9]*\.?[0-9]*([eE][-+]?[0-9]+)?$/ + The following are allowed as edge cases: "" and "-" + */ inline bool LegitDoubleString(const char* text) { const char* p = text; @@ -70,6 +82,11 @@ return true; } + /** + Fast integer string validation. + No trimming applied, so the input must match regex /^-?[0-9]*$/ + The following are allowed as edge cases: "" and "-" + */ inline bool LegitIntegerString(const char* text) { const char* p = text; @@ -86,6 +103,9 @@ } /* + Fast string --> double conversion. + Must pass the LegitDoubleString test + String to doubles with at most 18 digits */ inline bool StringToDouble(double& r, const char* text) @@ -210,10 +230,17 @@ return StringToDouble(r, text.c_str()); } + /** + Fast string to integer conversion. Leading zeroes and minus are accepted, + but a leading + sign is NOT. + Must pass the LegitIntegerString function test. + In addition, an empty string (or lone minus sign) yields 0. + */ + template<typename T> inline bool StringToInteger(T& r, const char* text) { - if (!LegitDoubleString(text)) + if (!LegitIntegerString(text)) return false; r = 0; @@ -244,22 +271,36 @@ } /** - "rgb(12,23,255)" --> red, green, blue and returns true - "everything else" --> returns false (other values left untouched) + if input is "rgb(12,23,255)" --> function fills `red`, `green` and `blue` and returns true + else ("everything else") --> function returns false and leaves all values untouched */ bool GetRgbValuesFromString(uint8_t& red, uint8_t& green, uint8_t& blue, const char* text); /** - See other overload + See main overload */ inline bool GetRgbValuesFromString(uint8_t& red, uint8_t& green, uint8_t& blue, const std::string& text) { return GetRgbValuesFromString(red, green, blue, text.c_str()); } - bool GetRgbaValuesFromString(uint8_t& red, uint8_t& green, uint8_t& blue, uint8_t& alpha, const char* text); + /** + Same as GetRgbValuesFromString + */ + bool GetRgbaValuesFromString(uint8_t& red, + uint8_t& green, + uint8_t& blue, + uint8_t& alpha, + const char* text); - inline bool GetRgbaValuesFromString(uint8_t& red, uint8_t& green, uint8_t& blue, uint8_t& alpha, const std::string& text) + /** + Same as GetRgbValuesFromString + */ + inline bool GetRgbaValuesFromString(uint8_t& red, + uint8_t& green, + uint8_t& blue, + uint8_t& alpha, + const std::string& text) { return GetRgbaValuesFromString(red, green, blue, alpha, text.c_str()); }
--- a/Framework/Toolbox/LinearAlgebra.cpp Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Toolbox/LinearAlgebra.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -22,6 +22,7 @@ #include "LinearAlgebra.h" #include "../StoneException.h" +#include "GenericToolbox.h" #include <Core/Logging.h> #include <Core/OrthancException.h> @@ -32,6 +33,7 @@ #include <stdio.h> #include <iostream> +#include <cstdlib> namespace OrthancStone { @@ -74,13 +76,30 @@ for (size_t i = 0; i < items.size(); i++) { /** + * SJO - 2019-11-19 - WARNING: I reverted from "std::stod()" + * to "boost::lexical_cast", as both "std::stod()" and + * "std::strtod()" are sensitive to locale settings, making + * this code non portable and very dangerous as it fails + * silently. A string such as "1.3671875\1.3671875" is + * interpreted as "1\1", because "std::stod()" expects a comma + * (",") instead of a point ("."). This problem is notably + * seen in Qt-based applications, that somehow set locales + * aggressively. + * + * "boost::lexical_cast<>" is also dependent on the locale + * settings, but apparently not in a way that makes this + * function fail with Qt. The Orthanc core defines macro + * "-DBOOST_LEXICAL_CAST_ASSUME_C_LOCALE" in static builds to + * this end. + **/ + +#if 0 // __cplusplus >= 201103L // Is C++11 enabled? + /** * We try and avoid the use of "boost::lexical_cast<>" here, - * as it is very slow. As we are parsing many doubles, we - * prefer to use the standard "std::stod" function if - * available: http://www.cplusplus.com/reference/string/stod/ + * as it is very slow, and as Stone has to parse many doubles. + * https://tinodidriksen.com/2011/05/cpp-convert-string-to-double-speed/ **/ -#if __cplusplus >= 201103L // Is C++11 enabled? try { target[i] = std::stod(items[i]); @@ -90,7 +109,37 @@ target.clear(); return false; } -#else // Fallback implementation using Boost + +#elif 0 + /** + * "std::strtod()" is the recommended alternative to + * "std::stod()". It is apparently as fast as plain-C + * "atof()", with more security. + **/ + char* end = NULL; + target[i] = std::strtod(items[i].c_str(), &end); + if (end == NULL || + end != items[i].c_str() + items[i].size()) + { + return false; + } + +#elif 1 + /** + * Use of our homemade implementation of + * "boost::lexical_cast<double>()". It is much faster than boost. + **/ + if (!GenericToolbox::StringToDouble(target[i], items[i].c_str())) + { + return false; + } + +#else + /** + * Fallback implementation using Boost (slower, but somehow + * independent to locale contrarily to "std::stod()", and + * generic as it does not use our custom implementation). + **/ try { target[i] = boost::lexical_cast<double>(items[i]);
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Toolbox/ParsedDicomCache.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -0,0 +1,143 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 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 "ParsedDicomCache.h" + +namespace OrthancStone +{ + class ParsedDicomCache::Item : public Orthanc::ICacheable + { + private: + std::unique_ptr<Orthanc::ParsedDicomFile> dicom_; + size_t fileSize_; + bool hasPixelData_; + + public: + Item(Orthanc::ParsedDicomFile* dicom, + size_t fileSize, + bool hasPixelData) : + dicom_(dicom), + fileSize_(fileSize), + hasPixelData_(hasPixelData) + { + if (dicom == NULL) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); + } + } + + virtual size_t GetMemoryUsage() const + { + return fileSize_; + } + + Orthanc::ParsedDicomFile& GetDicom() const + { + assert(dicom_.get() != NULL); + return *dicom_; + } + + bool HasPixelData() const + { + return hasPixelData_; + } + }; + + + std::string ParsedDicomCache::GetIndex(unsigned int bucket, + const std::string& bucketKey) + { + return boost::lexical_cast<std::string>(bucket) + "|" + bucketKey; + } + + + void ParsedDicomCache::Acquire(unsigned int bucket, + const std::string& bucketKey, + Orthanc::ParsedDicomFile* dicom, + size_t fileSize, + bool hasPixelData) + { + LOG(TRACE) << "new item stored in cache: bucket " << bucket << ", key " << bucketKey; + cache_.Acquire(GetIndex(bucket, bucketKey), new Item(dicom, fileSize, hasPixelData)); + } + + + ParsedDicomCache::Reader::Reader(ParsedDicomCache& cache, + unsigned int bucket, + const std::string& bucketKey) : + /** + * The "DcmFileFormat" object cannot be accessed from multiple + * threads, even if using only getters. An unique lock (mutex) is + * mandatory. + **/ + accessor_(cache.cache_, GetIndex(bucket, bucketKey), true /* unique */) + { + if (accessor_.IsValid()) + { + LOG(TRACE) << "accessing item within cache: bucket " << bucket << ", key " << bucketKey; + item_ = &dynamic_cast<Item&>(accessor_.GetValue()); + } + else + { + LOG(TRACE) << "missing item within cache: bucket " << bucket << ", key " << bucketKey; + item_ = NULL; + } + } + + + bool ParsedDicomCache::Reader::HasPixelData() const + { + if (item_ == NULL) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + else + { + return item_->HasPixelData(); + } + } + + + Orthanc::ParsedDicomFile& ParsedDicomCache::Reader::GetDicom() const + { + if (item_ == NULL) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + else + { + return item_->GetDicom(); + } + } + + + size_t ParsedDicomCache::Reader::GetFileSize() const + { + if (item_ == NULL) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + else + { + return item_->GetMemoryUsage(); + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Toolbox/ParsedDicomCache.h Tue Mar 31 15:51:02 2020 +0200 @@ -0,0 +1,80 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 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 <Core/Cache/MemoryObjectCache.h> +#include <Core/DicomParsing/ParsedDicomFile.h> + +namespace OrthancStone +{ + class ParsedDicomCache : public boost::noncopyable + { + private: + class Item; + + static std::string GetIndex(unsigned int bucket, + const std::string& bucketKey); + + Orthanc::MemoryObjectCache cache_; + + public: + ParsedDicomCache(size_t size) + { + cache_.SetMaximumSize(size); + } + + void Invalidate(unsigned int bucket, + const std::string& bucketKey) + { + cache_.Invalidate(GetIndex(bucket, bucketKey)); + } + + void Acquire(unsigned int bucket, + const std::string& bucketKey, + Orthanc::ParsedDicomFile* dicom, + size_t fileSize, + bool hasPixelData); + + class Reader : public boost::noncopyable + { + private: + Orthanc::MemoryObjectCache::Accessor accessor_; + Item* item_; + + public: + Reader(ParsedDicomCache& cache, + unsigned int bucket, + const std::string& bucketKey); + + bool IsValid() const + { + return item_ != NULL; + } + + bool HasPixelData() const; + + Orthanc::ParsedDicomFile& GetDicom() const; + + size_t GetFileSize() const; + }; + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Toolbox/ParsedDicomDataset.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -0,0 +1,104 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 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 "ParsedDicomDataset.h" + +#include <dcmtk/dcmdata/dcfilefo.h> + +namespace OrthancStone +{ + static DcmItem* LookupPath(Orthanc::ParsedDicomFile& dicom, + const OrthancPlugins::DicomPath& path) + { + DcmItem* node = dicom.GetDcmtkObject().getDataset(); + + for (size_t i = 0; i < path.GetPrefixLength(); i++) + { + const OrthancPlugins::DicomTag& tmp = path.GetPrefixTag(i); + DcmTagKey tag(tmp.GetGroup(), tmp.GetElement()); + + DcmSequenceOfItems* sequence = NULL; + if (!node->findAndGetSequence(tag, sequence).good() || + sequence == NULL) + { + return NULL; + } + + unsigned long pos = path.GetPrefixIndex(i); + if (pos >= sequence->card()) + { + return NULL; + } + + node = sequence->getItem(pos); + if (node == NULL) + { + return NULL; + } + } + + return node; + } + + + bool ParsedDicomDataset::GetStringValue(std::string& result, + const OrthancPlugins::DicomPath& path) const + { + DcmItem* node = LookupPath(dicom_, path); + + if (node != NULL) + { + DcmTagKey tag(path.GetFinalTag().GetGroup(), path.GetFinalTag().GetElement()); + + const char* s = NULL; + if (node->findAndGetString(tag, s).good() && + s != NULL) + { + result.assign(s); + return true; + } + } + + return false; + } + + + bool ParsedDicomDataset::GetSequenceSize(size_t& size, + const OrthancPlugins::DicomPath& path) const + { + DcmItem* node = LookupPath(dicom_, path); + + if (node != NULL) + { + DcmTagKey tag(path.GetFinalTag().GetGroup(), path.GetFinalTag().GetElement()); + + DcmSequenceOfItems* s = NULL; + if (node->findAndGetSequence(tag, s).good() && + s != NULL) + { + size = s->card(); + return true; + } + } + + return false; + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Toolbox/ParsedDicomDataset.h Tue Mar 31 15:51:02 2020 +0200 @@ -0,0 +1,46 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 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 <Core/DicomParsing/ParsedDicomFile.h> +#include <Plugins/Samples/Common/IDicomDataset.h> + +namespace OrthancStone +{ + class ParsedDicomDataset : public OrthancPlugins::IDicomDataset + { + private: + Orthanc::ParsedDicomFile& dicom_; + + public: + ParsedDicomDataset(Orthanc::ParsedDicomFile& dicom) : + dicom_(dicom) + { + } + + virtual bool GetStringValue(std::string& result, + const OrthancPlugins::DicomPath& path) const ORTHANC_OVERRIDE; + + virtual bool GetSequenceSize(size_t& size, + const OrthancPlugins::DicomPath& path) const ORTHANC_OVERRIDE; + }; +}
--- a/Framework/Toolbox/SlicesSorter.cpp Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Toolbox/SlicesSorter.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -289,13 +289,14 @@ } - double SlicesSorter::ComputeSpacingBetweenSlices() const + bool SlicesSorter::ComputeSpacingBetweenSlices(double& spacing /* out */) const { if (GetSlicesCount() <= 1) { // This is a volume that is empty or that contains one single // slice: Choose a dummy z-dimension for voxels - return 1.0; + spacing = 1.0; + return true; } const OrthancStone::CoordinateSystem3D& reference = GetSliceGeometry(0); @@ -303,28 +304,27 @@ double referencePosition = reference.ProjectAlongNormal(reference.GetOrigin()); double p = reference.ProjectAlongNormal(GetSliceGeometry(1).GetOrigin()); - double spacingZ = p - referencePosition; + spacing = p - referencePosition; - if (spacingZ <= 0) + if (spacing <= 0) { - LOG(ERROR) << "SlicesSorter::ComputeSpacingBetweenSlices(): (spacingZ <= 0)"; + LOG(ERROR) << "SlicesSorter::ComputeSpacingBetweenSlices(): (spacing <= 0)"; throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls, "Please call the Sort() method before"); } for (size_t i = 1; i < GetSlicesCount(); i++) { - OrthancStone::Vector p = reference.GetOrigin() + spacingZ * static_cast<double>(i) * reference.GetNormal(); + OrthancStone::Vector p = reference.GetOrigin() + spacing * static_cast<double>(i) * reference.GetNormal(); double d = boost::numeric::ublas::norm_2(p - GetSliceGeometry(i).GetOrigin()); if (!OrthancStone::LinearAlgebra::IsNear(d, 0, 0.001 /* tolerance expressed in mm */)) { - throw Orthanc::OrthancException(Orthanc::ErrorCode_BadGeometry, - "The origins of the slices of a volume image are not regularly spaced"); + return false; } } - return spacingZ; + return true; }
--- a/Framework/Toolbox/SlicesSorter.h Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Toolbox/SlicesSorter.h Tue Mar 31 15:51:02 2020 +0200 @@ -91,7 +91,7 @@ const CoordinateSystem3D& slice) const; // WARNING - The slices must have been sorted before calling this method - double ComputeSpacingBetweenSlices() const; + bool ComputeSpacingBetweenSlices(double& spacing /* out */) const; // WARNING - The slices must have been sorted before calling this method bool AreAllSlicesDistinct() const;
--- a/Framework/Viewport/IViewport.h Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Viewport/IViewport.h Tue Mar 31 15:51:02 2020 +0200 @@ -21,34 +21,52 @@ #pragma once #include "../Scene2D/ICompositor.h" -#include "../Scene2D/Scene2D.h" -#include "../Scene2D/ScenePoint2D.h" +#include "../Scene2DViewport/ViewportController.h" namespace OrthancStone { /** * Class that combines a Scene2D with a canvas where to draw the * scene. A call to "Refresh()" will update the content of the - * canvas. + * canvas. A "IViewport" can possibly be accessed from several + * threads depending on the rendering back-end (e.g. in SDL or Qt): + * The "ILock" subclass implements the locking mechanism to modify + * the content of the scene. + * + * NB: The lock must be a "recursive_mutex", as the viewport + * controller can lock it a second time (TODO - Why so?). **/ class IViewport : public boost::noncopyable { public: + class ILock : public boost::noncopyable + { + public: + virtual ~ILock() + { + } + + virtual bool HasCompositor() const = 0; + + /** + Do not store the result! Only access the compositor interface through + the lock. + */ + virtual ICompositor& GetCompositor() = 0; + + /** + Do not store the result! Only access the compositor interface through + the lock. + */ + virtual ViewportController& GetController() = 0; + + virtual void Invalidate() = 0; + }; + virtual ~IViewport() { } - virtual Scene2D& GetScene() = 0; - - virtual void Refresh() = 0; - - virtual ScenePoint2D GetPixelCenterCoordinates(int x, int y) const = 0; - - virtual bool HasCompositor() const = 0; - - virtual ICompositor& GetCompositor() = 0; - - virtual const ICompositor& GetCompositor() const = 0; + virtual ILock* Lock() = 0; }; } -
--- a/Framework/Viewport/SdlViewport.cpp Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Viewport/SdlViewport.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -26,40 +26,116 @@ namespace OrthancStone { - SdlOpenGLViewport::SdlOpenGLViewport(const char* title, + ICompositor& SdlViewport::SdlLock::GetCompositor() + { + if (that_.compositor_.get() == NULL) + { + // The derived class should have called "AcquireCompositor()" + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + else + { + return *that_.compositor_; + } + } + + + void SdlViewport::AcquireCompositor(ICompositor* compositor /* takes ownership */) + { + if (compositor == NULL) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); + } + + compositor_.reset(compositor); + } + + SdlViewport::SdlViewport() + { + refreshEvent_ = SDL_RegisterEvents(1); + + if (refreshEvent_ == static_cast<uint32_t>(-1)) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + } + + void SdlViewport::PostConstructor() + { + controller_ = boost::make_shared<ViewportController>(shared_from_this()); + } + + void SdlViewport::SendRefreshEvent() + { + SDL_Event event; + SDL_memset(&event, 0, sizeof(event)); + event.type = refreshEvent_; + SDL_PushEvent(&event); // This function is thread-safe, and can be called from other threads safely. + } + + + SdlOpenGLViewport::SdlOpenGLViewport(const std::string& title, unsigned int width, unsigned int height, bool allowDpiScaling) : - context_(title, width, height, allowDpiScaling) + context_(title.c_str(), width, height, allowDpiScaling) + { + AcquireCompositor(new OpenGLCompositor(context_)); // (*) + } + + boost::shared_ptr<SdlOpenGLViewport> SdlOpenGLViewport::Create( + const std::string& title, + unsigned int width, + unsigned int height, + bool allowDpiScaling) { - compositor_.reset(new OpenGLCompositor(context_, GetScene())); + boost::shared_ptr<SdlOpenGLViewport> that = + boost::shared_ptr<SdlOpenGLViewport>(new SdlOpenGLViewport(title, width, height, allowDpiScaling)); + that->SdlViewport::PostConstructor(); + return that; + } + + SdlOpenGLViewport::~SdlOpenGLViewport() + { + // Make sure that the "OpenGLCompositor" is destroyed BEFORE the + // "OpenGLContext" it references (*) + ClearCompositor(); } - SdlOpenGLViewport::SdlOpenGLViewport(const char* title, - unsigned int width, - unsigned int height, - boost::shared_ptr<Scene2D>& scene, - bool allowDpiScaling) : - SdlViewport(scene), - context_(title, width, height, allowDpiScaling) + + void SdlOpenGLViewport::Paint() { - compositor_.reset(new OpenGLCompositor(context_, GetScene())); + SdlLock lock(*this); + lock.GetCompositor().Refresh(lock.GetController().GetScene()); } - void SdlOpenGLViewport::Refresh() + + void SdlOpenGLViewport::UpdateSize(unsigned int width, unsigned int height) { - compositor_->Refresh(); + // nothing to do in OpenGL, the OpenGLCompositor::UpdateSize will be called automatically + SdlLock lock(*this); + lock.Invalidate(); } + void SdlOpenGLViewport::ToggleMaximize() + { + // No need to call "Invalidate()" here, as "UpdateSize()" will + // be invoked after event "SDL_WINDOWEVENT_SIZE_CHANGED" + SdlLock lock(*this); + context_.ToggleMaximize(); + } + + + SdlCairoViewport::SdlCairoViewport(const char* title, unsigned int width, unsigned int height, bool allowDpiScaling) : window_(title, width, height, false /* enable OpenGL */, allowDpiScaling), - compositor_(GetScene(), width, height) + sdlSurface_(NULL) { - UpdateSdlSurfaceSize(width, height); + AcquireCompositor(new CairoCompositor(width, height)); } SdlCairoViewport::~SdlCairoViewport() @@ -70,29 +146,66 @@ } } - void SdlCairoViewport::Refresh() + void SdlCairoViewport::Paint() { - compositor_.Refresh(); - window_.Render(sdlSurface_); + SdlLock lock(*this); + + lock.GetCompositor().Refresh(lock.GetController().GetScene()); + CreateSdlSurfaceFromCompositor(dynamic_cast<CairoCompositor&>(lock.GetCompositor())); + + if (sdlSurface_ != NULL) + { + window_.Render(sdlSurface_); + } } + void SdlCairoViewport::UpdateSize(unsigned int width, unsigned int height) { - compositor_.UpdateSize(width, height); - UpdateSdlSurfaceSize(width, height); - Refresh(); + SdlLock lock(*this); + dynamic_cast<CairoCompositor&>(lock.GetCompositor()).UpdateSize(width, height); + lock.Invalidate(); } - void SdlCairoViewport::UpdateSdlSurfaceSize(unsigned int width, - unsigned int height) + + void SdlCairoViewport::ToggleMaximize() + { + // No need to call "Invalidate()" here, as "UpdateSize()" will + // be invoked after event "SDL_WINDOWEVENT_SIZE_CHANGED" + SdlLock lock(*this); + window_.ToggleMaximize(); + } + + + // Assumes that the mutex is locked + void SdlCairoViewport::CreateSdlSurfaceFromCompositor(CairoCompositor& compositor) { static const uint32_t rmask = 0x00ff0000; static const uint32_t gmask = 0x0000ff00; static const uint32_t bmask = 0x000000ff; - sdlSurface_ = SDL_CreateRGBSurfaceFrom((void*)(compositor_.GetCanvas().GetBuffer()), width, height, 32, - compositor_.GetCanvas().GetPitch(), rmask, gmask, bmask, 0); + const unsigned int width = compositor.GetCanvas().GetWidth(); + const unsigned int height = compositor.GetCanvas().GetHeight(); + + if (sdlSurface_ != NULL) + { + if (sdlSurface_->pixels == compositor.GetCanvas().GetBuffer() && + sdlSurface_->w == static_cast<int>(width) && + sdlSurface_->h == static_cast<int>(height) && + sdlSurface_->pitch == static_cast<int>(compositor.GetCanvas().GetPitch())) + { + // The image from the compositor has not changed, no need to update the surface + return; + } + else + { + SDL_FreeSurface(sdlSurface_); + } + } + + sdlSurface_ = SDL_CreateRGBSurfaceFrom((void*)(compositor.GetCanvas().GetBuffer()), width, height, 32, + compositor.GetCanvas().GetPitch(), rmask, gmask, bmask, 0); if (!sdlSurface_) { LOG(ERROR) << "Cannot create a SDL surface from a Cairo surface";
--- a/Framework/Viewport/SdlViewport.h Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Viewport/SdlViewport.h Tue Mar 31 15:51:02 2020 +0200 @@ -39,26 +39,93 @@ #include "../OpenGL/SdlOpenGLContext.h" #include "../Scene2D/OpenGLCompositor.h" #include "../Scene2D/CairoCompositor.h" -#include "ViewportBase.h" +#include "IViewport.h" + +#include <SDL_events.h> + +// TODO: required for UndoStack injection +// I don't like it either :) +#include <boost/weak_ptr.hpp> + +#include <boost/thread/recursive_mutex.hpp> namespace OrthancStone { - class SdlViewport : public ViewportBase + class UndoStack; + + class SdlViewport : public IViewport, + public boost::enable_shared_from_this<SdlViewport> { - public: - SdlViewport() + private: + boost::recursive_mutex mutex_; + uint32_t refreshEvent_; + boost::shared_ptr<ViewportController> controller_; + std::unique_ptr<ICompositor> compositor_; + + void SendRefreshEvent(); + + protected: + class SdlLock : public ILock { + private: + SdlViewport& that_; + boost::recursive_mutex::scoped_lock lock_; + + public: + SdlLock(SdlViewport& that) : + that_(that), + lock_(that.mutex_) + { + } + + virtual bool HasCompositor() const ORTHANC_OVERRIDE + { + return true; + } + + virtual ICompositor& GetCompositor() ORTHANC_OVERRIDE; + + virtual ViewportController& GetController() ORTHANC_OVERRIDE + { + return *that_.controller_; + } + + virtual void Invalidate() ORTHANC_OVERRIDE + { + that_.SendRefreshEvent(); + } + }; + + void ClearCompositor() + { + compositor_.reset(); } - SdlViewport(boost::shared_ptr<Scene2D>& scene) : - ViewportBase(scene) + void AcquireCompositor(ICompositor* compositor /* takes ownership */); + + protected: + SdlViewport(); + void PostConstructor(); + + public: + + bool IsRefreshEvent(const SDL_Event& event) const { + return (event.type == refreshEvent_); } - virtual SdlWindow& GetWindow() = 0; - + virtual ILock* Lock() ORTHANC_OVERRIDE + { + return new SdlLock(*this); + } + virtual void UpdateSize(unsigned int width, unsigned int height) = 0; + + virtual void ToggleMaximize() = 0; + + // Must be invoked from the main SDL thread + virtual void Paint() = 0; }; @@ -66,87 +133,57 @@ { private: SdlOpenGLContext context_; - std::unique_ptr<OpenGLCompositor> compositor_; - - public: - SdlOpenGLViewport(const char* title, - unsigned int width, - unsigned int height, - bool allowDpiScaling = true); - - SdlOpenGLViewport(const char* title, - unsigned int width, - unsigned int height, - boost::shared_ptr<Scene2D>& scene, - bool allowDpiScaling = true); - virtual SdlWindow& GetWindow() ORTHANC_OVERRIDE - { - return context_.GetWindow(); - } - - virtual void Refresh() ORTHANC_OVERRIDE; + private: + SdlOpenGLViewport(const std::string& title, + unsigned int width, + unsigned int height, + bool allowDpiScaling = true); + public: + static boost::shared_ptr<SdlOpenGLViewport> Create(const std::string&, + unsigned int width, + unsigned int height, + bool allowDpiScaling = true); - virtual void UpdateSize(unsigned int width, unsigned int height) ORTHANC_OVERRIDE - { - // nothing to do in OpenGL, the OpenGLCompositor::UpdateSize will be called automatically - } + + virtual ~SdlOpenGLViewport(); + + virtual void Paint() ORTHANC_OVERRIDE; - virtual bool HasCompositor() const ORTHANC_OVERRIDE - { - return true; - } + virtual void UpdateSize(unsigned int width, + unsigned int height) ORTHANC_OVERRIDE; - virtual ICompositor& GetCompositor() ORTHANC_OVERRIDE - { - return *compositor_.get(); - } + virtual void ToggleMaximize() ORTHANC_OVERRIDE; }; class SdlCairoViewport : public SdlViewport { private: - SdlWindow window_; - CairoCompositor compositor_; - SDL_Surface* sdlSurface_; + SdlWindow window_; + SDL_Surface* sdlSurface_; + + void CreateSdlSurfaceFromCompositor(CairoCompositor& compositor); private: - void UpdateSdlSurfaceSize(unsigned int width, - unsigned int height); - + SdlCairoViewport(const char* title, + unsigned int width, + unsigned int height, + bool allowDpiScaling = true); public: - SdlCairoViewport(const char* title, + static boost::shared_ptr<SdlCairoViewport> Create(const char* title, unsigned int width, unsigned int height, bool allowDpiScaling = true); - SdlCairoViewport(const char* title, - unsigned int width, - unsigned int height, - boost::shared_ptr<Scene2D>& scene, - bool allowDpiScaling = true); - ~SdlCairoViewport(); + virtual ~SdlCairoViewport(); - virtual SdlWindow& GetWindow() ORTHANC_OVERRIDE - { - return window_; - } - - virtual void Refresh() ORTHANC_OVERRIDE; + virtual void Paint() ORTHANC_OVERRIDE; virtual void UpdateSize(unsigned int width, unsigned int height) ORTHANC_OVERRIDE; - virtual bool HasCompositor() const ORTHANC_OVERRIDE - { - return true; - } - - virtual ICompositor& GetCompositor() ORTHANC_OVERRIDE - { - return compositor_; - } + virtual void ToggleMaximize() ORTHANC_OVERRIDE; }; }
--- a/Framework/Viewport/SdlWindow.cpp Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Viewport/SdlWindow.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -31,6 +31,8 @@ #endif // WIN32 +#include <SDL_render.h> +#include <SDL_video.h> #include <SDL.h> namespace OrthancStone @@ -154,7 +156,14 @@ void SdlWindow::Render(SDL_Surface* surface) { - //SDL_RenderClear(renderer_); + /** + * "You are strongly encouraged to call SDL_RenderClear() to + * initialize the backbuffer before starting each new frame's + * drawing, even if you plan to overwrite every pixel." + * https://wiki.libsdl.org/SDL_RenderPresent + **/ + SDL_SetRenderDrawColor(renderer_, 0, 0, 0, 255); + SDL_RenderClear(renderer_); // Clear the entire screen to our selected color SDL_Texture *texture = SDL_CreateTextureFromSurface(renderer_, surface); if (texture != NULL)
--- a/Framework/Viewport/SdlWindow.h Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Viewport/SdlWindow.h Tue Mar 31 15:51:02 2020 +0200 @@ -23,18 +23,22 @@ #if ORTHANC_ENABLE_SDL == 1 -#include <SDL_render.h> -#include <SDL_video.h> #include <boost/noncopyable.hpp> +// Forward declaration of SDL type to avoid clashes with DCMTK headers +// on "typedef Sint8", in "StoneInitialization.cpp" +struct SDL_Window; +struct SDL_Renderer; +struct SDL_Surface; + namespace OrthancStone { class SdlWindow : public boost::noncopyable { private: - SDL_Window *window_; - SDL_Renderer *renderer_; - bool maximized_; + struct SDL_Window *window_; + struct SDL_Renderer *renderer_; + bool maximized_; public: SdlWindow(const char* title, @@ -54,7 +58,12 @@ unsigned int GetHeight() const; - void Render(SDL_Surface* surface); + /** + * WARNING: "Refresh()" cannot only be called from the main SDL + * thread, in which the window was created. Otherwise, the + * renderer displays nothing! + **/ + void Render(struct SDL_Surface* surface); void ToggleMaximize();
--- a/Framework/Viewport/ViewportBase.cpp Tue Mar 31 15:47:29 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,59 +0,0 @@ -/** - * Stone of Orthanc - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2020 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 "ViewportBase.h" - -#include <Core/OrthancException.h> - -#include <boost/make_shared.hpp> - -namespace OrthancStone -{ - ViewportBase::ViewportBase() : - scene_(boost::make_shared<Scene2D>()) - { - } - - - ViewportBase::ViewportBase(boost::shared_ptr<Scene2D>& scene) : - scene_(scene) - { - if (scene.get() == NULL) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); - } - } - - - ScenePoint2D ViewportBase::GetPixelCenterCoordinates(int x, int y) const - { - if (HasCompositor()) - { - const ICompositor& compositor = GetCompositor(); - return ScenePoint2D( - static_cast<double>(x) + 0.5 - static_cast<double>(compositor.GetCanvasWidth()) / 2.0, - static_cast<double>(y) + 0.5 - static_cast<double>(compositor.GetCanvasHeight()) / 2.0); - } - else - { - return ScenePoint2D(0, 0); - } - } -}
--- a/Framework/Viewport/ViewportBase.h Tue Mar 31 15:47:29 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,54 +0,0 @@ -/** - * Stone of Orthanc - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2020 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 "IViewport.h" - -#include <boost/shared_ptr.hpp> - -namespace OrthancStone -{ - class ViewportBase : public IViewport - { - private: - boost::shared_ptr<Scene2D> scene_; - - public: - ViewportBase(); - - ViewportBase(boost::shared_ptr<Scene2D>& scene); - - virtual Scene2D& GetScene() ORTHANC_OVERRIDE - { - return *scene_; - } - - virtual ScenePoint2D GetPixelCenterCoordinates(int x, int y) const ORTHANC_OVERRIDE; - - virtual const ICompositor& GetCompositor() const ORTHANC_OVERRIDE - { - IViewport* mutableThis = - const_cast<IViewport*>(static_cast<const IViewport*>(this)); - return mutableThis->GetCompositor(); - } - - }; -}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Viewport/WebAssemblyCairoViewport.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -0,0 +1,137 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 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 "WebAssemblyCairoViewport.h" + +#include "../Scene2D/CairoCompositor.h" + +#include <Core/Images/Image.h> + +namespace OrthancStone +{ + void WebAssemblyCairoViewport::GetCanvasSize(unsigned int& width, + unsigned int& height) + { + double w, h; + emscripten_get_element_css_size(GetFullCanvasId().c_str(), &w, &h); + + /** + * Emscripten has the function emscripten_get_element_css_size() + * to query the width and height of a named HTML element. I'm + * calling this first to get the initial size of the canvas DOM + * element, and then call emscripten_set_canvas_size() to + * initialize the framebuffer size of the canvas to the same + * size as its DOM element. + * https://floooh.github.io/2017/02/22/emsc-html.html + **/ + if (w > 0 && + h > 0) + { + width = static_cast<unsigned int>(boost::math::iround(w)); + height = static_cast<unsigned int>(boost::math::iround(h)); + } + else + { + width = 0; + height = 0; + } + } + + + void WebAssemblyCairoViewport::Paint(ICompositor& compositor, + ViewportController& controller) + { + compositor.Refresh(controller.GetScene()); + + // Create a temporary memory buffer for the canvas in JavaScript + Orthanc::ImageAccessor cairo; + dynamic_cast<CairoCompositor&>(compositor).GetCanvas().GetReadOnlyAccessor(cairo); + + const unsigned int width = cairo.GetWidth(); + const unsigned int height = cairo.GetHeight(); + + if (javascript_.get() == NULL || + javascript_->GetWidth() != width || + javascript_->GetHeight() != height) + { + javascript_.reset(new Orthanc::Image(Orthanc::PixelFormat_RGBA32, width, height, + true /* force minimal pitch */)); + } + + // Convert from BGRA32 memory layout (only color mode supported + // by Cairo, which corresponds to CAIRO_FORMAT_ARGB32) to RGBA32 + // (as expected by HTML5 canvas). This simply amounts to + // swapping the B and R channels. Alpha channel is also set to + // full opacity (255). + uint8_t* q = reinterpret_cast<uint8_t*>(javascript_->GetBuffer()); + for (unsigned int y = 0; y < height; y++) + { + const uint8_t* p = reinterpret_cast<const uint8_t*>(cairo.GetConstRow(y)); + for (unsigned int x = 0; x < width; x++) + { + q[0] = p[2]; // R + q[1] = p[1]; // G + q[2] = p[0]; // B + q[3] = 255; // A + + p += 4; + q += 4; + } + } + + // Execute JavaScript commands to blit the image buffer onto the + // 2D drawing context of the HTML5 canvas + EM_ASM({ + const data = new Uint8ClampedArray(Module.HEAP8.buffer, $1, 4 * $2 * $3); + const img = new ImageData(data, $2, $3); + const ctx = document.getElementById(UTF8ToString($0)).getContext('2d'); + ctx.putImageData(img, 0, 0); + }, + GetShortCanvasId().c_str(), // $0 + javascript_->GetBuffer(), // $1 + javascript_->GetWidth(), // $2 + javascript_->GetHeight()); // $3 + } + + + void WebAssemblyCairoViewport::UpdateSize(ICompositor& compositor) + { + unsigned int width, height; + GetCanvasSize(width, height); + emscripten_set_canvas_element_size(GetFullCanvasId().c_str(), width, height); + + dynamic_cast<CairoCompositor&>(compositor).UpdateSize(width, height); + } + + + WebAssemblyCairoViewport::WebAssemblyCairoViewport( + const std::string& canvasId) : + WebAssemblyViewport(canvasId) + { + unsigned int width, height; + GetCanvasSize(width, height); + emscripten_set_canvas_element_size(GetFullCanvasId().c_str(), + width, + height); + + AcquireCompositor(new CairoCompositor(width, height)); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Viewport/WebAssemblyCairoViewport.h Tue Mar 31 15:51:02 2020 +0200 @@ -0,0 +1,50 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 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 "WebAssemblyViewport.h" + +namespace OrthancStone +{ + class WebAssemblyCairoViewport : public WebAssemblyViewport + { + private: + std::unique_ptr<Orthanc::ImageAccessor> javascript_; + + void GetCanvasSize(unsigned int& width, + unsigned int& height); + + protected: + virtual void Paint(ICompositor& compositor, + ViewportController& controller) ORTHANC_OVERRIDE; + + virtual void UpdateSize(ICompositor& compositor) ORTHANC_OVERRIDE; + + public: + WebAssemblyCairoViewport(const std::string& canvasId); + + virtual ~WebAssemblyCairoViewport() + { + ClearCompositor(); + } + }; +}
--- a/Framework/Viewport/WebAssemblyViewport.cpp Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Viewport/WebAssemblyViewport.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -21,311 +21,304 @@ #include "WebAssemblyViewport.h" -#include "../StoneException.h" +#include "../Toolbox/GenericToolbox.h" -#include <emscripten/html5.h> +#include <Core/OrthancException.h> + +#include <boost/make_shared.hpp> +#include <boost/enable_shared_from_this.hpp> namespace OrthancStone { - WebAssemblyOpenGLViewport::WebAssemblyOpenGLViewport(const std::string& canvas) - : WebAssemblyViewport(canvas) - , context_(canvas) - , cssWidth_(0) // will be set in Refresh() - , cssHeight_(0) // ditto - , pixelWidth_(0) // ditto - , pixelHeight_(0) // ditto + static void ConvertMouseEvent(PointerEvent& target, + const EmscriptenMouseEvent& source, + const ICompositor& compositor) { - compositor_.reset(new OpenGLCompositor(context_, GetScene())); - RegisterContextCallbacks(); - } + int x = static_cast<int>(source.targetX); + int y = static_cast<int>(source.targetY); - WebAssemblyOpenGLViewport::WebAssemblyOpenGLViewport(const std::string& canvas, - boost::shared_ptr<Scene2D>& scene) - : WebAssemblyViewport(canvas, scene) - , context_(canvas) - , cssWidth_(0) // will be set in Refresh() - , cssHeight_(0) // ditto - , pixelWidth_(0) // ditto - , pixelHeight_(0) // ditto - { - compositor_.reset(new OpenGLCompositor(context_, GetScene())); - RegisterContextCallbacks(); - } + switch (source.button) + { + case 0: + target.SetMouseButton(MouseButton_Left); + break; - void WebAssemblyOpenGLViewport::UpdateSize() - { - context_.UpdateSize(); // First read the size of the canvas + case 1: + target.SetMouseButton(MouseButton_Middle); + break; - if (compositor_.get() != NULL) - { - compositor_->Refresh(); // Then refresh the content of the canvas + case 2: + target.SetMouseButton(MouseButton_Right); + break; + + default: + target.SetMouseButton(MouseButton_None); + break; } - } - - /* - typedef EM_BOOL (*em_webgl_context_callback)(int eventType, const void *reserved, void *userData); - - EMSCRIPTEN_EVENT_WEBGLCONTEXTLOST EMSCRIPTEN_EVENT_WEBGLCONTEXTRESTORED - - EMSCRIPTEN_RESULT emscripten_set_webglcontextlost_callback( - const char *target, void *userData, EM_BOOL useCapture, em_webgl_context_callback callback) - - EMSCRIPTEN_RESULT emscripten_set_webglcontextrestored_callback( - const char *target, void *userData, EM_BOOL useCapture, em_webgl_context_callback callback) - - */ - - EM_BOOL WebAssemblyOpenGLViewport_OpenGLContextLost_callback( - int eventType, const void* reserved, void* userData) - { - ORTHANC_ASSERT(eventType == EMSCRIPTEN_EVENT_WEBGLCONTEXTLOST); - WebAssemblyOpenGLViewport* viewport = reinterpret_cast<WebAssemblyOpenGLViewport*>(userData); - return viewport->OpenGLContextLost(); - } - - EM_BOOL WebAssemblyOpenGLViewport_OpenGLContextRestored_callback( - int eventType, const void* reserved, void* userData) - { - ORTHANC_ASSERT(eventType == EMSCRIPTEN_EVENT_WEBGLCONTEXTRESTORED); - WebAssemblyOpenGLViewport* viewport = reinterpret_cast<WebAssemblyOpenGLViewport*>(userData); - return viewport->OpenGLContextRestored(); - } - - void WebAssemblyOpenGLViewport::DisableCompositor() - { - compositor_.reset(); + + target.AddPosition(compositor.GetPixelCenterCoordinates(x, y)); + target.SetAltModifier(source.altKey); + target.SetControlModifier(source.ctrlKey); + target.SetShiftModifier(source.shiftKey); } - ICompositor& WebAssemblyOpenGLViewport::GetCompositor() - { - if (compositor_.get() == NULL) - { - // "HasCompositor()" should have been called - throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); - } - else - { - return *compositor_; - } - } - void WebAssemblyOpenGLViewport::UpdateSizeIfNeeded() + class WebAssemblyViewport::WasmLock : public ILock { - bool needsRefresh = false; - std::string canvasId = GetCanvasIdentifier(); - - { - double cssWidth = 0; - double cssHeight = 0; - EMSCRIPTEN_RESULT res = EMSCRIPTEN_RESULT_SUCCESS; - res = - emscripten_get_element_css_size(canvasId.c_str(), &cssWidth, &cssHeight); + private: + WebAssemblyViewport& that_; - if (res == EMSCRIPTEN_RESULT_SUCCESS) - { - if ((cssWidth != cssWidth_) || (cssHeight != cssHeight_)) - { - cssWidth_ = cssWidth; - cssHeight_ = cssHeight; - needsRefresh = true; - } - } + public: + WasmLock(WebAssemblyViewport& that) : + that_(that) + { } + virtual bool HasCompositor() const ORTHANC_OVERRIDE { - int pixelWidth = 0; - int pixelHeight = 0; - EMSCRIPTEN_RESULT res = EMSCRIPTEN_RESULT_SUCCESS; - res = - emscripten_get_canvas_element_size(canvasId.c_str(), &pixelWidth, &pixelHeight); - - if (res == EMSCRIPTEN_RESULT_SUCCESS) - { - if ((pixelWidth != pixelWidth_) || (pixelHeight != pixelHeight_)) - { - pixelWidth_ = pixelWidth; - pixelHeight_ = pixelHeight; - needsRefresh = true; - } - } + return that_.compositor_.get() != NULL; } - if (needsRefresh) - UpdateSize(); - } - - void WebAssemblyOpenGLViewport::Refresh() - { - try + virtual ICompositor& GetCompositor() ORTHANC_OVERRIDE { - // first, we check if the canvas size (both css size in css pixels and - // backing store) have changed. if so, we call UpdateSize to deal with - // it - - LOG(TRACE) << "WebAssemblyOpenGLViewport::Refresh"; - - // maybe the canvas size has changed and we need to update the - // canvas backing store size - UpdateSizeIfNeeded(); - - if (HasCompositor()) + if (that_.compositor_.get() == NULL) { - GetCompositor().Refresh(); + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); } else { - // this block was added because of (perceived?) bugs in the - // browser where the WebGL contexts are NOT automatically restored - // after being lost. - // the WebGL context has been lost. Sce - - //LOG(ERROR) << "About to call WebAssemblyOpenGLContext::TryRecreate()."; - //LOG(ERROR) << "Before calling it, isContextLost == " << context_.IsContextLost(); - - if (!context_.IsContextLost()) - { - LOG(TRACE) << "Context restored!"; - //LOG(ERROR) << "After calling it, isContextLost == " << context_.IsContextLost(); - RestoreCompositor(); - UpdateSize(); - } + return *that_.compositor_; } } - catch (const StoneException& e) + + virtual ViewportController& GetController() ORTHANC_OVERRIDE + { + assert(that_.controller_); + return *that_.controller_; + } + + virtual void Invalidate() ORTHANC_OVERRIDE + { + that_.Invalidate(); + } + }; + + + EM_BOOL WebAssemblyViewport::OnRequestAnimationFrame(double time, void *userData) + { + LOG(TRACE) << __func__; + WebAssemblyViewport* that = reinterpret_cast<WebAssemblyViewport*>(userData); + + if (that->compositor_.get() != NULL && + that->controller_ /* should always be true */) + { + that->Paint(*that->compositor_, *that->controller_); + } + + LOG(TRACE) << "Exiting: " << __func__; + return true; + } + + EM_BOOL WebAssemblyViewport::OnResize(int eventType, const EmscriptenUiEvent *uiEvent, void *userData) + { + LOG(TRACE) << __func__; + WebAssemblyViewport* that = reinterpret_cast<WebAssemblyViewport*>(userData); + + if (that->compositor_.get() != NULL) { - if (e.GetErrorCode() == ErrorCode_WebGLContextLost) + that->UpdateSize(*that->compositor_); + that->Invalidate(); + } + + LOG(TRACE) << "Exiting: " << __func__; + return true; + } + + + EM_BOOL WebAssemblyViewport::OnMouseDown(int eventType, const EmscriptenMouseEvent *mouseEvent, void *userData) + { + WebAssemblyViewport* that = reinterpret_cast<WebAssemblyViewport*>(userData); + + LOG(TRACE) << "mouse down: " << that->GetFullCanvasId(); + + if (that->compositor_.get() != NULL && + that->interactor_.get() != NULL) + { + PointerEvent pointer; + ConvertMouseEvent(pointer, *mouseEvent, *that->compositor_); + + that->controller_->HandleMousePress(*that->interactor_, pointer, + that->compositor_->GetCanvasWidth(), + that->compositor_->GetCanvasHeight()); + that->Invalidate(); + } + + LOG(TRACE) << "Exiting: " << __func__; + return true; + } + + + EM_BOOL WebAssemblyViewport::OnMouseMove(int eventType, const EmscriptenMouseEvent *mouseEvent, void *userData) + { + LOG(TRACE) << "WebAssemblyViewport::OnMouseMove CP1. userData = " << userData; + + WebAssemblyViewport* that = reinterpret_cast<WebAssemblyViewport*>(userData); + LOG(TRACE) << "WebAssemblyViewport::OnMouseMove CP2"; + + if (that->compositor_.get() != NULL && + that->controller_->HasActiveTracker()) + { + LOG(TRACE) << "WebAssemblyViewport::OnMouseMove CP3"; + PointerEvent pointer; + ConvertMouseEvent(pointer, *mouseEvent, *that->compositor_); + LOG(TRACE) << "WebAssemblyViewport::OnMouseMove CP4"; + if (that->controller_->HandleMouseMove(pointer)) { - LOG(WARNING) << "Context is lost! Compositor will be disabled."; - DisableCompositor(); - // we now need to wait for the "context restored" callback - } - else - { - throw; + LOG(TRACE) << "WebAssemblyViewport::OnMouseMove CP5"; + that->Invalidate(); + LOG(TRACE) << "WebAssemblyViewport::OnMouseMove CP6"; } } - catch (...) - { - // something else nasty happened - throw; - } + + LOG(TRACE) << "Exiting: " << __func__; + return true; } - - void WebAssemblyOpenGLViewport::RestoreCompositor() + + EM_BOOL WebAssemblyViewport::OnMouseUp(int eventType, const EmscriptenMouseEvent *mouseEvent, void *userData) { - // the context must have been restored! - ORTHANC_ASSERT(!context_.IsContextLost()); - if (compositor_.get() == NULL) + LOG(TRACE) << __func__; + WebAssemblyViewport* that = reinterpret_cast<WebAssemblyViewport*>(userData); + + if (that->compositor_.get() != NULL) { - compositor_.reset(new OpenGLCompositor(context_, GetScene())); - } - else - { - LOG(WARNING) << "RestoreCompositor() called for \"" << GetCanvasIdentifier() << "\" while it was NOT lost! Nothing done."; + PointerEvent pointer; + ConvertMouseEvent(pointer, *mouseEvent, *that->compositor_); + that->controller_->HandleMouseRelease(pointer); + that->Invalidate(); } - } - bool WebAssemblyOpenGLViewport::OpenGLContextLost() - { - LOG(ERROR) << "WebAssemblyOpenGLViewport::OpenGLContextLost() for canvas: " << GetCanvasIdentifier(); - DisableCompositor(); + LOG(TRACE) << "Exiting: " << __func__; return true; } - bool WebAssemblyOpenGLViewport::OpenGLContextRestored() + void WebAssemblyViewport::Invalidate() { - LOG(ERROR) << "WebAssemblyOpenGLViewport::OpenGLContextRestored() for canvas: " << GetCanvasIdentifier(); - - // maybe the context has already been restored by other means (the - // Refresh() function) - if (!HasCompositor()) - { - RestoreCompositor(); - UpdateSize(); - } - return false; + emscripten_request_animation_frame(OnRequestAnimationFrame, reinterpret_cast<void*>(this)); } - void WebAssemblyOpenGLViewport::RegisterContextCallbacks() + void WebAssemblyViewport::AcquireCompositor(ICompositor* compositor /* takes ownership */) { -#if 0 - // DISABLED ON 2019-08-20 and replaced by external JS calls because I could - // not get emscripten API to work - // TODO: what's the impact of userCapture=true ? - const char* canvasId = GetCanvasIdentifier().c_str(); - void* that = reinterpret_cast<void*>(this); - EMSCRIPTEN_RESULT status = EMSCRIPTEN_RESULT_SUCCESS; - - //status = emscripten_set_webglcontextlost_callback(canvasId, that, true, WebAssemblyOpenGLViewport_OpenGLContextLost_callback); - //if (status != EMSCRIPTEN_RESULT_SUCCESS) - //{ - // std::stringstream ss; - // ss << "Error while calling emscripten_set_webglcontextlost_callback for: \"" << GetCanvasIdentifier() << "\""; - // std::string msg = ss.str(); - // LOG(ERROR) << msg; - // ORTHANC_ASSERT(false, msg.c_str()); - //} - - status = emscripten_set_webglcontextrestored_callback(canvasId, that, true, WebAssemblyOpenGLViewport_OpenGLContextRestored_callback); - if (status != EMSCRIPTEN_RESULT_SUCCESS) + if (compositor == NULL) { - std::stringstream ss; - ss << "Error while calling emscripten_set_webglcontextrestored_callback for: \"" << GetCanvasIdentifier() << "\""; - std::string msg = ss.str(); - LOG(ERROR) << msg; - ORTHANC_ASSERT(false, msg.c_str()); + throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); } - LOG(TRACE) << "WebAssemblyOpenGLViewport::RegisterContextCallbacks() SUCCESS!!!"; -#endif + else + { + compositor_.reset(compositor); + } } - WebAssemblyCairoViewport::WebAssemblyCairoViewport(const std::string& canvas) : - WebAssemblyViewport(canvas), - canvas_(canvas), - compositor_(GetScene(), 1024, 768) + WebAssemblyViewport::WebAssemblyViewport( + const std::string& canvasId, bool enableEmscriptenEvents) : + shortCanvasId_(canvasId), + fullCanvasId_(canvasId), + interactor_(new DefaultViewportInteractor), + enableEmscriptenEvents_(enableEmscriptenEvents) { } - WebAssemblyCairoViewport::WebAssemblyCairoViewport(const std::string& canvas, - boost::shared_ptr<Scene2D>& scene) : - WebAssemblyViewport(canvas, scene), - canvas_(canvas), - compositor_(GetScene(), 1024, 768) + void WebAssemblyViewport::PostConstructor() { + boost::shared_ptr<IViewport> viewport = shared_from_this(); + controller_.reset(new ViewportController(viewport)); + + LOG(INFO) << "Initializing Stone viewport on HTML canvas: " + << shortCanvasId_; + + if (shortCanvasId_.empty() || + shortCanvasId_[0] == '#') + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange, + "The canvas identifier must not start with '#'"); + } + + // Disable right-click on the canvas (i.e. context menu) + EM_ASM({ + document.getElementById(UTF8ToString($0)).oncontextmenu = + function(event) + { + event.preventDefault(); + } + }, + shortCanvasId_.c_str() // $0 + ); + + if (enableEmscriptenEvents_) + { + // It is not possible to monitor the resizing of individual + // canvas, so we track the full window of the browser + emscripten_set_resize_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, + reinterpret_cast<void*>(this), + false, + OnResize); + + emscripten_set_mousedown_callback(fullCanvasId_.c_str(), + reinterpret_cast<void*>(this), + false, + OnMouseDown); + + emscripten_set_mousemove_callback(fullCanvasId_.c_str(), + reinterpret_cast<void*>(this), + false, + OnMouseMove); + + emscripten_set_mouseup_callback(fullCanvasId_.c_str(), + reinterpret_cast<void*>(this), + false, + OnMouseUp); + } } - void WebAssemblyCairoViewport::UpdateSize() + WebAssemblyViewport::~WebAssemblyViewport() { - LOG(INFO) << "updating cairo viewport size"; - double w, h; - emscripten_get_element_css_size(canvas_.c_str(), &w, &h); + if (enableEmscriptenEvents_) + { + emscripten_set_resize_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, + reinterpret_cast<void*>(this), + false, + NULL); + + emscripten_set_mousedown_callback(fullCanvasId_.c_str(), + reinterpret_cast<void*>(this), + false, + OnMouseDown); + + emscripten_set_mousemove_callback(fullCanvasId_.c_str(), + reinterpret_cast<void*>(this), + false, + OnMouseMove); - /** - * Emscripten has the function emscripten_get_element_css_size() - * to query the width and height of a named HTML element. I'm - * calling this first to get the initial size of the canvas DOM - * element, and then call emscripten_set_canvas_size() to - * initialize the framebuffer size of the canvas to the same - * size as its DOM element. - * https://floooh.github.io/2017/02/22/emsc-html.html - **/ - unsigned int canvasWidth = 0; - unsigned int canvasHeight = 0; - - if (w > 0 || - h > 0) + emscripten_set_mouseup_callback(fullCanvasId_.c_str(), + reinterpret_cast<void*>(this), + false, + OnMouseUp); + } + } + + IViewport::ILock* WebAssemblyViewport::Lock() + { + return new WasmLock(*this); + } + + void WebAssemblyViewport::AcquireInteractor(IViewportInteractor* interactor) + { + if (interactor == NULL) { - canvasWidth = static_cast<unsigned int>(boost::math::iround(w)); - canvasHeight = static_cast<unsigned int>(boost::math::iround(h)); + throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); } - - emscripten_set_canvas_element_size(canvas_.c_str(), canvasWidth, canvasHeight); - compositor_.UpdateSize(canvasWidth, canvasHeight); - } - - void WebAssemblyCairoViewport::Refresh() - { - LOG(INFO) << "refreshing cairo viewport, TODO: blit to the canvans.getContext('2d')"; - GetCompositor().Refresh(); + else + { + interactor_.reset(interactor); + } } }
--- a/Framework/Viewport/WebAssemblyViewport.h Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Viewport/WebAssemblyViewport.h Tue Mar 31 15:51:02 2020 +0200 @@ -13,7 +13,7 @@ * 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/>. **/ @@ -21,113 +21,98 @@ #pragma once -#include "../OpenGL/WebAssemblyOpenGLContext.h" -#include "../Scene2D/OpenGLCompositor.h" -#include "../Scene2D/CairoCompositor.h" -#include "ViewportBase.h" +#if !defined(ORTHANC_ENABLE_WASM) +# error Macro ORTHANC_ENABLE_WASM must be defined +#endif + +#if ORTHANC_ENABLE_WASM != 1 +# error This file can only be used if targeting WebAssembly +#endif + +#include "IViewport.h" + +#include <Core/Compatibility.h> + +#include <emscripten.h> +#include <emscripten/html5.h> + +#include <memory> +#include <string> namespace OrthancStone { - class WebAssemblyViewport : public ViewportBase + class WebAssemblyViewport : public IViewport, + public boost::enable_shared_from_this<WebAssemblyViewport> + { private: - std::string canvasIdentifier_; + class WasmLock; + + std::string shortCanvasId_; + std::string fullCanvasId_; + std::unique_ptr<ICompositor> compositor_; + std::unique_ptr<ViewportController> controller_; + std::unique_ptr<IViewportInteractor> interactor_; + bool enableEmscriptenEvents_; - public: - WebAssemblyViewport(const std::string& canvasIdentifier) - : canvasIdentifier_(canvasIdentifier) + static EM_BOOL OnRequestAnimationFrame(double time, void *userData); + + static EM_BOOL OnResize(int eventType, const EmscriptenUiEvent *uiEvent, void *userData); + + static EM_BOOL OnMouseDown(int eventType, const EmscriptenMouseEvent *mouseEvent, void *userData); + + static EM_BOOL OnMouseMove(int eventType, const EmscriptenMouseEvent *mouseEvent, void *userData); + + static EM_BOOL OnMouseUp(int eventType, const EmscriptenMouseEvent *mouseEvent, void *userData); + + protected: + void Invalidate(); + + void ClearCompositor() { - } - - WebAssemblyViewport(const std::string& canvasIdentifier, - boost::shared_ptr<Scene2D>& scene) - : ViewportBase(scene) - , canvasIdentifier_(canvasIdentifier) - { + compositor_.reset(); } - const std::string& GetCanvasIdentifier() const + bool HasCompositor() const { - return canvasIdentifier_; + return compositor_.get() != NULL; } - }; + + void AcquireCompositor(ICompositor* compositor /* takes ownership */); + virtual void Paint(ICompositor& compositor, + ViewportController& controller) = 0; - class WebAssemblyOpenGLViewport : public WebAssemblyViewport - { - private: - OpenGL::WebAssemblyOpenGLContext context_; - std::unique_ptr<OpenGLCompositor> compositor_; - double cssWidth_; - double cssHeight_; - int pixelWidth_; - int pixelHeight_; + virtual void UpdateSize(ICompositor& compositor) = 0; - private: - void UpdateSizeIfNeeded(); + /** + The second argument is temporary and should be deleted once the migration + to interactors is finished. + */ + WebAssemblyViewport(const std::string& canvasId, + bool enableEmscriptenEvents = true); + + void PostConstructor(); public: - WebAssemblyOpenGLViewport(const std::string& canvas); - - WebAssemblyOpenGLViewport(const std::string& canvas, - boost::shared_ptr<Scene2D>& scene); - - // This function must be called each time the browser window is resized - void UpdateSize(); + virtual ILock* Lock() ORTHANC_OVERRIDE; + + ~WebAssemblyViewport(); + - virtual bool HasCompositor() const ORTHANC_OVERRIDE + /** + This method takes ownership + */ + void AcquireInteractor(IViewportInteractor* interactor); + + const std::string& GetShortCanvasId() const { - return (compositor_.get() != NULL); - } - - bool IsContextLost() - { - return context_.IsContextLost(); + return shortCanvasId_; } - virtual ICompositor& GetCompositor() ORTHANC_OVERRIDE; - - virtual void Refresh() ORTHANC_OVERRIDE; - - // this does NOT return whether the context is lost! This is called to - // tell Stone that the context has been lost - bool OpenGLContextLost(); - - // This should be called to indicate that the context has been lost - bool OpenGLContextRestored(); - - private: - void DisableCompositor(); - void RestoreCompositor(); - - void RegisterContextCallbacks(); - }; - - - class WebAssemblyCairoViewport : public WebAssemblyViewport - { - private: - CairoCompositor compositor_; - std::string canvas_; - - public: - WebAssemblyCairoViewport(const std::string& canvas); - - WebAssemblyCairoViewport(const std::string& canvas, - boost::shared_ptr<Scene2D>& scene); - - void UpdateSize(); - - virtual void Refresh() ORTHANC_OVERRIDE; - - virtual bool HasCompositor() const ORTHANC_OVERRIDE + const std::string& GetFullCanvasId() const { - return true; - } - - virtual ICompositor& GetCompositor() ORTHANC_OVERRIDE - { - return compositor_; + return fullCanvasId_; } }; }
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Viewport/WebGLViewport.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -0,0 +1,104 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 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 "WebGLViewport.h" + +#include "../StoneException.h" +#include "../Scene2D/OpenGLCompositor.h" + +namespace OrthancStone +{ + void WebGLViewport::Paint(ICompositor& compositor, + ViewportController& controller) + { + try + { + compositor.Refresh(controller.GetScene()); + + /** + * No need to manually swap the buffer: "Rendered WebGL content + * is implicitly presented (displayed to the user) on the canvas + * when the event handler that renders with WebGL returns back + * to the browser event loop." + * https://emscripten.org/docs/api_reference/html5.h.html#webgl-context + * + * Could call "emscripten_webgl_commit_frame()" if + * "explicitSwapControl" option were set to "true". + **/ + } + catch (const StoneException& e) + { + // Ignore problems about the loss of the WebGL context (edge case) + if (e.GetErrorCode() == ErrorCode_WebGLContextLost) + { + return; + } + else + { + throw; + } + } + } + + + void WebGLViewport::UpdateSize(ICompositor& compositor) + { + try + { + context_.UpdateSize(); + } + catch (const StoneException& e) + { + // Ignore problems about the loss of the WebGL context (edge case) + if (e.GetErrorCode() == ErrorCode_WebGLContextLost) + { + return; + } + else + { + throw; + } + } + } + + WebGLViewport::WebGLViewport(const std::string& canvasId) : + WebAssemblyViewport(canvasId), + context_(GetFullCanvasId()) + { + AcquireCompositor(new OpenGLCompositor(context_)); + } + + boost::shared_ptr<WebGLViewport> WebGLViewport::Create( + const std::string& canvasId) + { + boost::shared_ptr<WebGLViewport> that = + boost::shared_ptr<WebGLViewport>(new WebGLViewport(canvasId)); + that->WebAssemblyViewport::PostConstructor(); + return that; + } + + WebGLViewport::~WebGLViewport() + { + // Make sure to delete the compositor before its parent "context_" gets + // deleted + ClearCompositor(); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Viewport/WebGLViewport.h Tue Mar 31 15:51:02 2020 +0200 @@ -0,0 +1,52 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 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 "WebAssemblyViewport.h" +#include "../OpenGL/WebAssemblyOpenGLContext.h" + +namespace OrthancStone +{ + class WebGLViewport : public WebAssemblyViewport + { + private: + OpenGL::WebAssemblyOpenGLContext context_; + + WebGLViewport(const std::string& canvasId); + + protected: + virtual void Paint(ICompositor& compositor, + ViewportController& controller) ORTHANC_OVERRIDE; + + virtual void UpdateSize(ICompositor& compositor) ORTHANC_OVERRIDE; + + public: + static boost::shared_ptr<WebGLViewport> Create(const std::string& canvasId); + + virtual ~WebGLViewport(); + + bool IsContextLost() + { + return context_.IsContextLost(); + } + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Viewport/WebGLViewportsRegistry.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -0,0 +1,185 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 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 "WebGLViewportsRegistry.h" + +#include "../Toolbox/GenericToolbox.h" + +#include <Core/OrthancException.h> + +#include <boost/make_shared.hpp> + +namespace OrthancStone +{ + void WebGLViewportsRegistry::LaunchTimer() + { + timeOutID_ = emscripten_set_timeout( + OnTimeoutCallback, + timeoutMS_, + reinterpret_cast<void*>(this)); + } + + void WebGLViewportsRegistry::OnTimeout() + { + for (Viewports::iterator it = viewports_.begin(); + it != viewports_.end(); + ++it) + { + if (it->second == NULL || + it->second->IsContextLost()) + { + LOG(INFO) << "WebGL context lost for canvas: " << it->first; + + // Try and duplicate the HTML5 canvas in the DOM + EM_ASM({ + var canvas = document.getElementById(UTF8ToString($0)); + if (canvas) { + var parent = canvas.parentElement; + if (parent) { + var cloned = canvas.cloneNode(true /* deep copy */); + parent.insertBefore(cloned, canvas); + parent.removeChild(canvas); + } + } + }, + it->first.c_str() // $0 = ID of the canvas + ); + + // At this point, the old canvas is removed from the DOM and + // replaced by a fresh one with the same ID: Recreate the + // WebGL context on the new canvas + boost::shared_ptr<WebGLViewport> viewport; + + // we need to steal the properties from the old viewport + // and set them to the new viewport + { + std::unique_ptr<IViewport::ILock> lock(it->second->Lock()); + + // TODO: remove ViewportController + Scene2D* scene = lock->GetController().ReleaseScene(); + viewport = WebGLViewport::Create(it->first); + + { + std::unique_ptr<IViewport::ILock> newLock(viewport->Lock()); + newLock->GetController().AcquireScene(scene); + } + } + + // Replace the old WebGL viewport by the new one + it->second = viewport; + + // Tag the fresh canvas as needing a repaint + { + std::unique_ptr<IViewport::ILock> lock(it->second->Lock()); + lock->Invalidate(); + } + } + } + + LaunchTimer(); + } + + void WebGLViewportsRegistry::OnTimeoutCallback(void *userData) + { + // This object dies with the process or tab. + WebGLViewportsRegistry* that = + reinterpret_cast<WebGLViewportsRegistry*>(userData); + that->OnTimeout(); + } + + WebGLViewportsRegistry::WebGLViewportsRegistry(double timeoutMS) : + timeoutMS_(timeoutMS), + timeOutID_(0) + { + if (timeoutMS <= 0) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + + LaunchTimer(); + } + + WebGLViewportsRegistry::~WebGLViewportsRegistry() + { + emscripten_clear_timeout(timeOutID_); + Clear(); + } + + boost::shared_ptr<WebGLViewport> WebGLViewportsRegistry::Add( + const std::string& canvasId) + { + if (viewports_.find(canvasId) != viewports_.end()) + { + LOG(ERROR) << "Canvas was already registered: " << canvasId; + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + else + { + boost::shared_ptr<WebGLViewport> viewport = + WebGLViewport::Create(canvasId); + viewports_[canvasId] = viewport; + return viewport; + } + } + + void WebGLViewportsRegistry::Remove(const std::string& canvasId) + { + Viewports::iterator found = viewports_.find(canvasId); + + if (found == viewports_.end()) + { + LOG(ERROR) << "Cannot remove unregistered canvas: " << canvasId; + } + else + { + viewports_.erase(found); + } + } + + void WebGLViewportsRegistry::Clear() + { + viewports_.clear(); + } + + WebGLViewportsRegistry::Accessor::Accessor(WebGLViewportsRegistry& that, + const std::string& canvasId) : + that_(that) + { + Viewports::iterator viewport = that.viewports_.find(canvasId); + if (viewport != that.viewports_.end() && + viewport->second != NULL) + { + lock_.reset(viewport->second->Lock()); + } + } + + IViewport::ILock& WebGLViewportsRegistry::Accessor::GetViewport() const + { + if (IsValid()) + { + return *lock_; + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Viewport/WebGLViewportsRegistry.h Tue Mar 31 15:51:02 2020 +0200 @@ -0,0 +1,83 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 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 "WebGLViewport.h" + +#include <boost/enable_shared_from_this.hpp> + +namespace OrthancStone +{ + /** + * This singleton class must be used if many WebGL viewports are + * created by the higher-level application, implying possible loss + * of WebGL contexts. The object will run an infinite update loop + * that checks whether all the WebGL context are still valid (not + * lost). If some WebGL context is lost, it is automatically + * reinitialized by created a fresh HTML5 canvas. + **/ + class WebGLViewportsRegistry : public boost::noncopyable, + public boost::enable_shared_from_this<WebGLViewportsRegistry> + { + private: + typedef std::map<std::string, boost::shared_ptr<WebGLViewport> > Viewports; + + double timeoutMS_; + Viewports viewports_; + long timeOutID_; + + void LaunchTimer(); + + void OnTimeout(); + + static void OnTimeoutCallback(void *userData); + + public: + WebGLViewportsRegistry(double timeoutMS /* in milliseconds */); + + ~WebGLViewportsRegistry(); + + boost::shared_ptr<WebGLViewport> Add(const std::string& canvasId); + + void Remove(const std::string& canvasId); + + void Clear(); + + class Accessor : public boost::noncopyable + { + private: + WebGLViewportsRegistry& that_; + std::unique_ptr<IViewport::ILock> lock_; + + public: + Accessor(WebGLViewportsRegistry& that, + const std::string& canvasId); + + bool IsValid() const + { + return lock_.get() != NULL; + } + + IViewport::ILock& GetViewport() const; + }; + }; +}
--- a/Framework/Volumes/DicomVolumeImage.h Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Volumes/DicomVolumeImage.h Tue Mar 31 15:51:02 2020 +0200 @@ -28,14 +28,6 @@ namespace OrthancStone { - class IGeometryProvider - { - public: - virtual ~IGeometryProvider() {} - virtual bool HasGeometry() const = 0; - virtual const VolumeImageGeometry& GetImageGeometry() const = 0; - }; - /** This class combines a 3D image buffer, a 3D volume geometry and information about the DICOM parameters of the series. @@ -44,6 +36,7 @@ class DicomVolumeImage : public boost::noncopyable { public: + // TODO - Are these messages still useful? ORTHANC_STONE_DEFINE_ORIGIN_MESSAGE(__FILE__, __LINE__, GeometryReadyMessage, DicomVolumeImage); ORTHANC_STONE_DEFINE_ORIGIN_MESSAGE(__FILE__, __LINE__, ContentUpdatedMessage, DicomVolumeImage); @@ -67,8 +60,10 @@ } void Initialize(const VolumeImageGeometry& geometry, - Orthanc::PixelFormat format, bool computeRange = false); + Orthanc::PixelFormat format, + bool computeRange = false); + // Used by volume slicers void SetDicomParameters(const DicomInstanceParameters& parameters); uint64_t GetRevision() const
--- a/Framework/Volumes/DicomVolumeImageMPRSlicer.cpp Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Volumes/DicomVolumeImageMPRSlicer.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -47,8 +47,7 @@ revision_(volume_.GetRevision()) { valid_ = (volume_.HasDicomParameters() && - volume_.GetGeometry().DetectSlice(projection_, sliceIndex_, - cuttingPlane)); + volume_.GetGeometry().DetectSlice(projection_, sliceIndex_, cuttingPlane)); } @@ -85,21 +84,6 @@ texture.reset(dynamic_cast<TextureBaseSceneLayer*> (configurator->CreateTextureFromDicom(reader.GetAccessor(), parameters))); - - // <DEBUG-BLOCK> -#if 0 - Orthanc::JpegWriter writer; - writer.SetQuality(60); - static int index = 0; - std::string filePath = "C:\\temp\\sliceReader_P"; - filePath += boost::lexical_cast<std::string>(projection_); - filePath += "_I"; - filePath += boost::lexical_cast<std::string>(index); - filePath += ".jpg"; - index++; - writer.WriteToFile(filePath, reader.GetAccessor()); -#endif - // <END-OF-DEBUG-BLOCK> } const CoordinateSystem3D& system = volume_.GetGeometry().GetProjectionGeometry(projection_); @@ -108,53 +92,11 @@ cuttingPlane.ProjectPoint(x0, y0, system.GetOrigin()); cuttingPlane.ProjectPoint(x1, y1, system.GetOrigin() + system.GetAxisX()); - // <DEBUG-BLOCK> -#if 0 { - LOG(ERROR) << "+----------------------------------------------------+"; - LOG(ERROR) << "| DicomVolumeImageMPRSlicer::Slice::CreateSceneLayer |"; - LOG(ERROR) << "+----------------------------------------------------+"; - std::string projectionString; - switch (projection_) - { - case VolumeProjection_Coronal: - projectionString = "CORONAL"; - break; - case VolumeProjection_Axial: - projectionString = "CORONAL"; - break; - case VolumeProjection_Sagittal: - projectionString = "SAGITTAL"; - break; - default: - ORTHANC_ASSERT(false); - } - if(volume_.GetGeometry().GetDepth() == 200) - LOG(ERROR) << "| CT IMAGE 512x512 with projection " << projectionString; - else - LOG(ERROR) << "| RTDOSE IMAGE NNNxNNN with projection " << projectionString; - LOG(ERROR) << "+----------------------------------------------------+"; - LOG(ERROR) << "| cuttingPlane = " << cuttingPlane; - LOG(ERROR) << "| point to project = " << system.GetOrigin(); - LOG(ERROR) << "| result = x0: " << x0 << " y0: " << y0; - LOG(ERROR) << "+----------------------- END ------------------------+"; + double xz, yz; + cuttingPlane.ProjectPoint(xz, yz, LinearAlgebra::CreateVector(0, 0, 0)); + texture->SetOrigin(x0 - xz, y0 - yz); } -#endif - // <END-OF-DEBUG-BLOCK> - -#if 1 // BGO 2019-08-13 - // The sagittal coordinate system has a Y vector going down. The displayed - // image (scene coords) has a Y vector pointing upwards (towards the patient - // coord Z index) - // we need to flip the Y local coordinates to get the scene-coord offset. - // TODO: this is quite ugly. Isn't there a better way? - if(projection_ == VolumeProjection_Sagittal) - texture->SetOrigin(x0, -y0); - else - texture->SetOrigin(x0, y0); -#else - texture->SetOrigin(x0, y0); -#endif double dx = x1 - x0; double dy = y1 - y0; @@ -167,19 +109,6 @@ Vector tmp = volume_.GetGeometry().GetVoxelDimensions(projection_); texture->SetPixelSpacing(tmp[0], tmp[1]); - // <DEBUG-BLOCK> - { - //using std::endl; - //std::stringstream ss; - //ss << "DicomVolumeImageMPRSlicer::Slice::CreateSceneLayer | cuttingPlane = " << cuttingPlane << " | projection_ = " << projection_ << endl; - //ss << "volume_.GetGeometry().GetProjectionGeometry(projection_) = " << system << endl; - //ss << "cuttingPlane.ProjectPoint(x0, y0, system.GetOrigin()); --> | x0 = " << x0 << " | y0 = " << y0 << "| x1 = " << x1 << " | y1 = " << y1 << endl; - //ss << "volume_.GetGeometry() = " << volume_.GetGeometry() << endl; - //ss << "volume_.GetGeometry() = " << volume_.GetGeometry() << endl; - //LOG(ERROR) << ss.str(); - } - // <END-OF-DEBUG-BLOCK> - return texture.release(); }
--- a/Framework/Volumes/IVolumeSlicer.cpp~ Tue Mar 31 15:47:29 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,113 +0,0 @@ -/** - * Stone of Orthanc - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2020 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 - -namespace OrthancStone -{ - /** - This interface is implemented by objects representing 3D volume data and - that are able to return an object that: - - represent a slice of their data - - are able to create the corresponding slice visual representation. - */ - class IVolumeSlicer : public boost::noncopyable - { - public: - /** - This interface is implemented by objects representing a slice of - volume data and that are able to create a 2D layer to display a this - slice. - - The CreateSceneLayer factory method is called with an optional - configurator that possibly impacts the ISceneLayer subclass that is - created (for instance, if a LUT must be applied on the texture when - displaying it) - */ - class IExtractedSlice : public boost::noncopyable - { - public: - virtual ~IExtractedSlice() - { - } - - /** - Invalid slices are created when the data is not ready yet or if the - cut is outside of the available geometry. - */ - virtual bool IsValid() = 0; - - /** - This retrieves the *revision* that gets incremented every time the - underlying object undergoes a mutable operation (that it, changes its - state). - This **must** be a cheap call. - */ - virtual uint64_t GetRevision() = 0; - - /** Creates the slice visual representation */ - virtual ISceneLayer* CreateSceneLayer( - const ILayerStyleConfigurator* configurator, // possibly absent - const CoordinateSystem3D& cuttingPlane) = 0; - }; - - /** - See IExtractedSlice.IsValid() - */ - class InvalidSlice : public IExtractedSlice - { - public: - virtual bool IsValid() - { - return false; - } - - virtual uint64_t GetRevision() - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); - } - - virtual ISceneLayer* CreateSceneLayer(const ILayerStyleConfigurator* configurator, - const CoordinateSystem3D& cuttingPlane) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); - } - }; - - - virtual ~IVolumeSlicer() - { - } - - /** - This method is implemented by the objects representing volumetric data - and must returns an IExtractedSlice subclass that contains all the data - needed to, later on, create its visual representation through - CreateSceneLayer. - Subclasses a.o.: - - InvalidSlice, - - DicomVolumeImageMPRSlicer::Slice, - - DicomVolumeImageReslicer::Slice - - DicomStructureSetLoader::Slice - */ - virtual IExtractedSlice* ExtractSlice(const CoordinateSystem3D& cuttingPlane) = 0; - }; -}
--- a/Framework/Volumes/IVolumeSlicer.h~ Tue Mar 31 15:47:29 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,113 +0,0 @@ -/** - * Stone of Orthanc - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2020 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 - -namespace OrthancStone -{ - /** - This interface is implemented by objects representing 3D volume data and - that are able to return an object that: - - represent a slice of their data - - are able to create the corresponding slice visual representation. - */ - class IVolumeSlicer : public boost::noncopyable - { - public: - /** - This interface is implemented by objects representing a slice of - volume data and that are able to create a 2D layer to display a this - slice. - - The CreateSceneLayer factory method is called with an optional - configurator that possibly impacts the ISceneLayer subclass that is - created (for instance, if a LUT must be applied on the texture when - displaying it) - */ - class IExtractedSlice : public boost::noncopyable - { - public: - virtual ~IExtractedSlice() - { - } - - /** - Invalid slices are created when the data is not ready yet or if the - cut is outside of the available geometry. - */ - virtual bool IsValid() = 0; - - /** - This retrieves the *revision* that gets incremented every time the - underlying object undergoes a mutable operation (that it, changes its - state). - This **must** be a cheap call. - */ - virtual uint64_t GetRevision() = 0; - - /** Creates the slice visual representation */ - virtual ISceneLayer* CreateSceneLayer( - const ILayerStyleConfigurator* configurator, // possibly absent - const CoordinateSystem3D& cuttingPlane) = 0; - }; - - /** - See IExtractedSlice.IsValid() - */ - class InvalidSlice : public IExtractedSlice - { - public: - virtual bool IsValid() - { - return false; - } - - virtual uint64_t GetRevision() - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); - } - - virtual ISceneLayer* CreateSceneLayer(const ILayerStyleConfigurator* configurator, - const CoordinateSystem3D& cuttingPlane) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); - } - }; - - - virtual ~IVolumeSlicer() - { - } - - /** - This method is implemented by the objects representing volumetric data - and must returns an IExtractedSlice subclass that contains all the data - needed to, later on, create its visual representation through - CreateSceneLayer. - Subclasses a.o.: - - InvalidSlice, - - DicomVolumeImageMPRSlicer::Slice, - - DicomVolumeImageReslicer::Slice - - DicomStructureSetLoader::Slice - */ - virtual IExtractedSlice* ExtractSlice(const CoordinateSystem3D& cuttingPlane) = 0; - }; -}
--- a/Framework/Volumes/VolumeImageGeometry.cpp Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Volumes/VolumeImageGeometry.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -39,7 +39,7 @@ sagittalGeometry_ = CoordinateSystem3D(p, axialGeometry_.GetAxisY(), - axialGeometry_.GetNormal()); + -axialGeometry_.GetNormal()); Vector origin = ( axialGeometry_.MapSliceToWorldCoordinates(-0.5 * voxelDimensions_[0], @@ -296,16 +296,18 @@ { return false; } - - unsigned int d = static_cast<unsigned int>(std::floor(z)); - if (d >= projectionDepth) - { - return false; - } else { - slice = d; - return true; + unsigned int d = static_cast<unsigned int>(std::floor(z)); + if (d >= projectionDepth) + { + return false; + } + else + { + slice = d; + return true; + } } } @@ -321,7 +323,18 @@ Vector dim = GetVoxelDimensions(projection); CoordinateSystem3D plane = GetProjectionGeometry(projection); - plane.SetOrigin(plane.GetOrigin() + static_cast<double>(z) * plane.GetNormal() * dim[2]); + Vector normal = plane.GetNormal(); + if (projection == VolumeProjection_Sagittal) + { + /** + * WARNING: In sagittal geometry, the normal points to REDUCING + * X-axis in the 3D world. This is necessary to keep the + * right-hand coordinate system. Hence the negation. + **/ + normal = -normal; + } + + plane.SetOrigin(plane.GetOrigin() + static_cast<double>(z) * dim[2] * normal); return plane; }
--- a/Framework/Volumes/VolumeSceneLayerSource.cpp Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Volumes/VolumeSceneLayerSource.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -21,6 +21,9 @@ #include "VolumeSceneLayerSource.h" +#include "../Scene2D/NullLayer.h" +#include "../StoneException.h" + #include <Core/OrthancException.h> namespace OrthancStone @@ -42,17 +45,23 @@ } - VolumeSceneLayerSource::VolumeSceneLayerSource(Scene2D& scene, - int layerDepth, - const boost::shared_ptr<IVolumeSlicer>& slicer) : - scene_(scene), - layerDepth_(layerDepth), - slicer_(slicer) + VolumeSceneLayerSource::VolumeSceneLayerSource( + Scene2D& scene, + int layerDepth, + const boost::shared_ptr<IVolumeSlicer>& slicer) + : scene_(scene) + , layerDepth_(layerDepth) + , slicer_(slicer) { if (slicer == NULL) { throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); } + ORTHANC_ASSERT(!scene_.HasLayer(layerDepth_)); + + // we need to book the scene layer depth by adding a dummy layer + std::unique_ptr<NullLayer> nullLayer(new NullLayer); + scene_.SetLayer(layerDepth_,nullLayer.release()); } VolumeSceneLayerSource::~VolumeSceneLayerSource()
--- a/Framework/Volumes/VolumeSceneLayerSource.h Tue Mar 31 15:47:29 2020 +0200 +++ b/Framework/Volumes/VolumeSceneLayerSource.h Tue Mar 31 15:51:02 2020 +0200 @@ -38,13 +38,14 @@ class VolumeSceneLayerSource : public boost::noncopyable { private: - Scene2D& scene_; - int layerDepth_; - boost::shared_ptr<IVolumeSlicer> slicer_; + Scene2D& scene_; + int layerDepth_; + boost::shared_ptr<IVolumeSlicer> slicer_; std::unique_ptr<ILayerStyleConfigurator> configurator_; std::unique_ptr<CoordinateSystem3D> lastPlane_; - uint64_t lastRevision_; - uint64_t lastConfiguratorRevision_; + uint64_t lastRevision_; + uint64_t lastConfiguratorRevision_; + bool layerInScene_; void ClearLayer(); @@ -71,6 +72,14 @@ ILayerStyleConfigurator& GetConfigurator() const; + /** + Make sure the Scene2D is protected from concurrent accesses before + calling this method. + + If the scene that has been supplied to the ctor is part of an IViewport, + you can lock the whole viewport data (including scene) by means of the + IViewport::Lock method. + */ void Update(const CoordinateSystem3D& plane); }; }
--- a/Platforms/Generic/DelayedCallCommand.cpp Tue Mar 31 15:47:29 2020 +0200 +++ b/Platforms/Generic/DelayedCallCommand.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -26,13 +26,11 @@ namespace Deprecated { - DelayedCallCommand::DelayedCallCommand(OrthancStone::MessageBroker& broker, - OrthancStone::MessageHandler<IDelayedCallExecutor::TimeoutMessage>* callback, // takes ownership + DelayedCallCommand::DelayedCallCommand(MessageHandler<IDelayedCallExecutor::TimeoutMessage>* callback, // takes ownership unsigned int timeoutInMs, Orthanc::IDynamicObject* payload /* takes ownership */, OrthancStone::NativeStoneApplicationContext& context ) : - IObservable(broker), callback_(callback), payload_(payload), context_(context),
--- a/Platforms/Generic/DelayedCallCommand.h Tue Mar 31 15:47:29 2020 +0200 +++ b/Platforms/Generic/DelayedCallCommand.h Tue Mar 31 15:51:02 2020 +0200 @@ -35,15 +35,14 @@ class DelayedCallCommand : public IOracleCommand, OrthancStone::IObservable { protected: - std::unique_ptr<OrthancStone::MessageHandler<IDelayedCallExecutor::TimeoutMessage> > callback_; + std::unique_ptr<MessageHandler<IDelayedCallExecutor::TimeoutMessage> > callback_; std::unique_ptr<Orthanc::IDynamicObject> payload_; OrthancStone::NativeStoneApplicationContext& context_; boost::posix_time::ptime expirationTimePoint_; unsigned int timeoutInMs_; public: - DelayedCallCommand(OrthancStone::MessageBroker& broker, - OrthancStone::MessageHandler<IDelayedCallExecutor::TimeoutMessage>* callback, // takes ownership + DelayedCallCommand(MessageHandler<IDelayedCallExecutor::TimeoutMessage>* callback, // takes ownership unsigned int timeoutInMs, Orthanc::IDynamicObject* payload /* takes ownership */, OrthancStone::NativeStoneApplicationContext& context
--- a/Platforms/Generic/OracleDelayedCallExecutor.h Tue Mar 31 15:47:29 2020 +0200 +++ b/Platforms/Generic/OracleDelayedCallExecutor.h Tue Mar 31 15:51:02 2020 +0200 @@ -36,19 +36,17 @@ OrthancStone::NativeStoneApplicationContext& context_; public: - OracleDelayedCallExecutor(OrthancStone::MessageBroker& broker, - Oracle& oracle, + OracleDelayedCallExecutor(Oracle& oracle, OrthancStone::NativeStoneApplicationContext& context) : - IDelayedCallExecutor(broker), oracle_(oracle), context_(context) { } - virtual void Schedule(OrthancStone::MessageHandler<IDelayedCallExecutor::TimeoutMessage>* callback, + virtual void Schedule(MessageHandler<IDelayedCallExecutor::TimeoutMessage>* callback, unsigned int timeoutInMs = 1000) { - oracle_.Submit(new DelayedCallCommand(broker_, callback, timeoutInMs, NULL, context_)); + oracle_.Submit(new DelayedCallCommand(callback, timeoutInMs, NULL, context_)); } }; }
--- a/Platforms/Generic/OracleWebService.cpp Tue Mar 31 15:47:29 2020 +0200 +++ b/Platforms/Generic/OracleWebService.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -29,19 +29,17 @@ class OracleWebService::WebServiceCachedGetCommand : public IOracleCommand, OrthancStone::IObservable { protected: - std::unique_ptr<OrthancStone::MessageHandler<IWebService::HttpRequestSuccessMessage> > successCallback_; + std::unique_ptr<MessageHandler<IWebService::HttpRequestSuccessMessage> > successCallback_; std::unique_ptr<Orthanc::IDynamicObject> payload_; boost::shared_ptr<BaseWebService::CachedHttpRequestSuccessMessage> cachedMessage_; OrthancStone::NativeStoneApplicationContext& context_; public: - WebServiceCachedGetCommand(OrthancStone::MessageBroker& broker, - OrthancStone::MessageHandler<IWebService::HttpRequestSuccessMessage>* successCallback, // takes ownership + WebServiceCachedGetCommand(MessageHandler<IWebService::HttpRequestSuccessMessage>* successCallback, // takes ownership boost::shared_ptr<BaseWebService::CachedHttpRequestSuccessMessage> cachedMessage, Orthanc::IDynamicObject* payload /* takes ownership */, OrthancStone::NativeStoneApplicationContext& context ) : - IObservable(broker), successCallback_(successCallback), payload_(payload), cachedMessage_(cachedMessage), @@ -73,9 +71,9 @@ void OracleWebService::NotifyHttpSuccessLater(boost::shared_ptr<BaseWebService::CachedHttpRequestSuccessMessage> cachedMessage, Orthanc::IDynamicObject* payload, // takes ownership - OrthancStone::MessageHandler<IWebService::HttpRequestSuccessMessage>* successCallback) + MessageHandler<IWebService::HttpRequestSuccessMessage>* successCallback) { - oracle_.Submit(new WebServiceCachedGetCommand(GetBroker(), successCallback, cachedMessage, payload, context_)); + oracle_.Submit(new WebServiceCachedGetCommand(successCallback, cachedMessage, payload, context_)); }
--- a/Platforms/Generic/OracleWebService.h Tue Mar 31 15:47:29 2020 +0200 +++ b/Platforms/Generic/OracleWebService.h Tue Mar 31 15:51:02 2020 +0200 @@ -43,11 +43,9 @@ class WebServiceCachedGetCommand; public: - OracleWebService(OrthancStone::MessageBroker& broker, - Oracle& oracle, + OracleWebService(Oracle& oracle, const Orthanc::WebServiceParameters& parameters, OrthancStone::NativeStoneApplicationContext& context) : - BaseWebService(broker), oracle_(oracle), context_(context), parameters_(parameters) @@ -58,37 +56,37 @@ const HttpHeaders& headers, const std::string& body, Orthanc::IDynamicObject* payload, // takes ownership - OrthancStone::MessageHandler<IWebService::HttpRequestSuccessMessage>* successCallback, // takes ownership - OrthancStone::MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback = NULL, // takes ownership + MessageHandler<IWebService::HttpRequestSuccessMessage>* successCallback, // takes ownership + MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback = NULL, // takes ownership unsigned int timeoutInSeconds = 60) { - oracle_.Submit(new WebServicePostCommand(GetBroker(), successCallback, failureCallback, parameters_, uri, headers, timeoutInSeconds, body, payload, context_)); + oracle_.Submit(new WebServicePostCommand(successCallback, failureCallback, parameters_, uri, headers, timeoutInSeconds, body, payload, context_)); } virtual void DeleteAsync(const std::string& uri, const HttpHeaders& headers, Orthanc::IDynamicObject* payload, - OrthancStone::MessageHandler<IWebService::HttpRequestSuccessMessage>* successCallback, - OrthancStone::MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback = NULL, + MessageHandler<IWebService::HttpRequestSuccessMessage>* successCallback, + MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback = NULL, unsigned int timeoutInSeconds = 60) { - oracle_.Submit(new WebServiceDeleteCommand(GetBroker(), successCallback, failureCallback, parameters_, uri, headers, timeoutInSeconds, payload, context_)); + oracle_.Submit(new WebServiceDeleteCommand(successCallback, failureCallback, parameters_, uri, headers, timeoutInSeconds, payload, context_)); } protected: virtual void GetAsyncInternal(const std::string& uri, const HttpHeaders& headers, Orthanc::IDynamicObject* payload, // takes ownership - OrthancStone::MessageHandler<IWebService::HttpRequestSuccessMessage>* successCallback, // takes ownership - OrthancStone::MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback = NULL,// takes ownership + MessageHandler<IWebService::HttpRequestSuccessMessage>* successCallback, // takes ownership + MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback = NULL,// takes ownership unsigned int timeoutInSeconds = 60) { - oracle_.Submit(new WebServiceGetCommand(GetBroker(), successCallback, failureCallback, parameters_, uri, headers, timeoutInSeconds, payload, context_)); + oracle_.Submit(new WebServiceGetCommand(successCallback, failureCallback, parameters_, uri, headers, timeoutInSeconds, payload, context_)); } virtual void NotifyHttpSuccessLater(boost::shared_ptr<BaseWebService::CachedHttpRequestSuccessMessage> cachedHttpMessage, Orthanc::IDynamicObject* payload, // takes ownership - OrthancStone::MessageHandler<IWebService::HttpRequestSuccessMessage>* successCallback); + MessageHandler<IWebService::HttpRequestSuccessMessage>* successCallback); }; }
--- a/Platforms/Generic/WebServiceCommandBase.cpp Tue Mar 31 15:47:29 2020 +0200 +++ b/Platforms/Generic/WebServiceCommandBase.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -25,16 +25,14 @@ namespace Deprecated { - WebServiceCommandBase::WebServiceCommandBase(OrthancStone::MessageBroker& broker, - OrthancStone::MessageHandler<IWebService::HttpRequestSuccessMessage>* successCallback, - OrthancStone::MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback, + WebServiceCommandBase::WebServiceCommandBase(MessageHandler<IWebService::HttpRequestSuccessMessage>* successCallback, + MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback, const Orthanc::WebServiceParameters& parameters, const std::string& url, const IWebService::HttpHeaders& headers, unsigned int timeoutInSeconds, Orthanc::IDynamicObject* payload /* takes ownership */, OrthancStone::NativeStoneApplicationContext& context) : - IObservable(broker), successCallback_(successCallback), failureCallback_(failureCallback), parameters_(parameters),
--- a/Platforms/Generic/WebServiceCommandBase.h Tue Mar 31 15:47:29 2020 +0200 +++ b/Platforms/Generic/WebServiceCommandBase.h Tue Mar 31 15:51:02 2020 +0200 @@ -37,8 +37,8 @@ class WebServiceCommandBase : public IOracleCommand, OrthancStone::IObservable { protected: - std::unique_ptr<OrthancStone::MessageHandler<IWebService::HttpRequestSuccessMessage> > successCallback_; - std::unique_ptr<OrthancStone::MessageHandler<IWebService::HttpRequestErrorMessage> > failureCallback_; + std::unique_ptr<MessageHandler<IWebService::HttpRequestSuccessMessage> > successCallback_; + std::unique_ptr<MessageHandler<IWebService::HttpRequestErrorMessage> > failureCallback_; Orthanc::WebServiceParameters parameters_; std::string url_; IWebService::HttpHeaders headers_; @@ -51,9 +51,8 @@ unsigned int timeoutInSeconds_; public: - WebServiceCommandBase(OrthancStone::MessageBroker& broker, - OrthancStone::MessageHandler<IWebService::HttpRequestSuccessMessage>* successCallback, // takes ownership - OrthancStone::MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback, // takes ownership + WebServiceCommandBase(MessageHandler<IWebService::HttpRequestSuccessMessage>* successCallback, // takes ownership + MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback, // takes ownership const Orthanc::WebServiceParameters& parameters, const std::string& url, const IWebService::HttpHeaders& headers,
--- a/Platforms/Generic/WebServiceDeleteCommand.cpp Tue Mar 31 15:47:29 2020 +0200 +++ b/Platforms/Generic/WebServiceDeleteCommand.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -25,16 +25,15 @@ namespace Deprecated { - WebServiceDeleteCommand::WebServiceDeleteCommand(OrthancStone::MessageBroker& broker, - OrthancStone::MessageHandler<Deprecated::IWebService::HttpRequestSuccessMessage>* successCallback, // takes ownership - OrthancStone::MessageHandler<Deprecated::IWebService::HttpRequestErrorMessage>* failureCallback, // takes ownership + WebServiceDeleteCommand::WebServiceDeleteCommand(MessageHandler<Deprecated::IWebService::HttpRequestSuccessMessage>* successCallback, // takes ownership + MessageHandler<Deprecated::IWebService::HttpRequestErrorMessage>* failureCallback, // takes ownership const Orthanc::WebServiceParameters& parameters, const std::string& url, const Deprecated::IWebService::HttpHeaders& headers, unsigned int timeoutInSeconds, Orthanc::IDynamicObject* payload /* takes ownership */, OrthancStone::NativeStoneApplicationContext& context) : - WebServiceCommandBase(broker, successCallback, failureCallback, parameters, url, headers, timeoutInSeconds, payload, context) + WebServiceCommandBase(successCallback, failureCallback, parameters, url, headers, timeoutInSeconds, payload, context) { }
--- a/Platforms/Generic/WebServiceDeleteCommand.h Tue Mar 31 15:47:29 2020 +0200 +++ b/Platforms/Generic/WebServiceDeleteCommand.h Tue Mar 31 15:51:02 2020 +0200 @@ -28,9 +28,8 @@ class WebServiceDeleteCommand : public WebServiceCommandBase { public: - WebServiceDeleteCommand(OrthancStone::MessageBroker& broker, - OrthancStone::MessageHandler<IWebService::HttpRequestSuccessMessage>* successCallback, // takes ownership - OrthancStone::MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback, // takes ownership + WebServiceDeleteCommand(MessageHandler<IWebService::HttpRequestSuccessMessage>* successCallback, // takes ownership + MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback, // takes ownership const Orthanc::WebServiceParameters& parameters, const std::string& url, const IWebService::HttpHeaders& headers,
--- a/Platforms/Generic/WebServiceGetCommand.cpp Tue Mar 31 15:47:29 2020 +0200 +++ b/Platforms/Generic/WebServiceGetCommand.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -25,17 +25,15 @@ namespace Deprecated { - - WebServiceGetCommand::WebServiceGetCommand(OrthancStone::MessageBroker& broker, - OrthancStone::MessageHandler<IWebService::HttpRequestSuccessMessage>* successCallback, // takes ownership - OrthancStone::MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback, // takes ownership + WebServiceGetCommand::WebServiceGetCommand(MessageHandler<IWebService::HttpRequestSuccessMessage>* successCallback, // takes ownership + MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback, // takes ownership const Orthanc::WebServiceParameters& parameters, const std::string& url, const IWebService::HttpHeaders& headers, unsigned int timeoutInSeconds, Orthanc::IDynamicObject* payload /* takes ownership */, OrthancStone::NativeStoneApplicationContext& context) : - WebServiceCommandBase(broker, successCallback, failureCallback, parameters, url, headers, timeoutInSeconds, payload, context) + WebServiceCommandBase(successCallback, failureCallback, parameters, url, headers, timeoutInSeconds, payload, context) { }
--- a/Platforms/Generic/WebServiceGetCommand.h Tue Mar 31 15:47:29 2020 +0200 +++ b/Platforms/Generic/WebServiceGetCommand.h Tue Mar 31 15:51:02 2020 +0200 @@ -28,9 +28,8 @@ class WebServiceGetCommand : public WebServiceCommandBase { public: - WebServiceGetCommand(OrthancStone::MessageBroker& broker, - OrthancStone::MessageHandler<IWebService::HttpRequestSuccessMessage>* successCallback, // takes ownership - OrthancStone::MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback, // takes ownership + WebServiceGetCommand(MessageHandler<IWebService::HttpRequestSuccessMessage>* successCallback, // takes ownership + MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback, // takes ownership const Orthanc::WebServiceParameters& parameters, const std::string& url, const IWebService::HttpHeaders& headers,
--- a/Platforms/Generic/WebServicePostCommand.cpp Tue Mar 31 15:47:29 2020 +0200 +++ b/Platforms/Generic/WebServicePostCommand.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -25,9 +25,8 @@ namespace Deprecated { - WebServicePostCommand::WebServicePostCommand(OrthancStone::MessageBroker& broker, - OrthancStone::MessageHandler<Deprecated::IWebService::HttpRequestSuccessMessage>* successCallback, // takes ownership - OrthancStone::MessageHandler<Deprecated::IWebService::HttpRequestErrorMessage>* failureCallback, // takes ownership + WebServicePostCommand::WebServicePostCommand(MessageHandler<Deprecated::IWebService::HttpRequestSuccessMessage>* successCallback, // takes ownership + MessageHandler<Deprecated::IWebService::HttpRequestErrorMessage>* failureCallback, // takes ownership const Orthanc::WebServiceParameters& parameters, const std::string& url, const Deprecated::IWebService::HttpHeaders& headers, @@ -35,7 +34,7 @@ const std::string& body, Orthanc::IDynamicObject* payload /* takes ownership */, OrthancStone::NativeStoneApplicationContext& context) : - WebServiceCommandBase(broker, successCallback, failureCallback, parameters, url, headers, timeoutInSeconds, payload, context), + WebServiceCommandBase(successCallback, failureCallback, parameters, url, headers, timeoutInSeconds, payload, context), body_(body) { }
--- a/Platforms/Generic/WebServicePostCommand.h Tue Mar 31 15:47:29 2020 +0200 +++ b/Platforms/Generic/WebServicePostCommand.h Tue Mar 31 15:51:02 2020 +0200 @@ -31,9 +31,8 @@ std::string body_; public: - WebServicePostCommand(OrthancStone::MessageBroker& broker, - OrthancStone::MessageHandler<IWebService::HttpRequestSuccessMessage>* successCallback, // takes ownership - OrthancStone::MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback, // takes ownership + WebServicePostCommand(MessageHandler<IWebService::HttpRequestSuccessMessage>* successCallback, // takes ownership + MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback, // takes ownership const Orthanc::WebServiceParameters& parameters, const std::string& url, const IWebService::HttpHeaders& headers,
--- a/Resources/CMake/OrthancStoneConfiguration.cmake Tue Mar 31 15:47:29 2020 +0200 +++ b/Resources/CMake/OrthancStoneConfiguration.cmake Tue Mar 31 15:51:02 2020 +0200 @@ -121,12 +121,16 @@ -DORTHANC_ENABLE_SDL=1 ) elseif(ENABLE_QT) - message("QT is enabled") - include(${CMAKE_CURRENT_LIST_DIR}/QtConfiguration.cmake) add_definitions( -DORTHANC_ENABLE_QT=1 -DORTHANC_ENABLE_SDL=0 ) + if(DISABLE_STONE_QT_CMAKE_FILE) + message("QT is enabled, but QtConfiguration.cmake will not be included") + else() + message("QT is enabled") + include(${CMAKE_CURRENT_LIST_DIR}/QtConfiguration.cmake) + endif() else() message("SDL and QT are both disabled") unset(USE_SYSTEM_SDL CACHE) @@ -250,61 +254,43 @@ ## All the source files required to build Stone of Orthanc ##################################################################### -set(APPLICATIONS_SOURCES - ${ORTHANC_STONE_ROOT}/Applications/IStoneApplication.h - ) - if (NOT ORTHANC_SANDBOXED) set(PLATFORM_SOURCES + ${ORTHANC_STONE_ROOT}/Framework/Loaders/GenericLoadersContext.cpp + ${ORTHANC_STONE_ROOT}/Framework/Loaders/GenericLoadersContext.h + ${ORTHANC_STONE_ROOT}/Platforms/Generic/DelayedCallCommand.cpp + ${ORTHANC_STONE_ROOT}/Platforms/Generic/DelayedCallCommand.h + ${ORTHANC_STONE_ROOT}/Platforms/Generic/Oracle.cpp + ${ORTHANC_STONE_ROOT}/Platforms/Generic/Oracle.h + ${ORTHANC_STONE_ROOT}/Platforms/Generic/OracleDelayedCallExecutor.h ${ORTHANC_STONE_ROOT}/Platforms/Generic/WebServiceCommandBase.cpp - ${ORTHANC_STONE_ROOT}/Platforms/Generic/WebServiceGetCommand.cpp - ${ORTHANC_STONE_ROOT}/Platforms/Generic/WebServicePostCommand.cpp + ${ORTHANC_STONE_ROOT}/Platforms/Generic/WebServiceCommandBase.h ${ORTHANC_STONE_ROOT}/Platforms/Generic/WebServiceDeleteCommand.cpp - ${ORTHANC_STONE_ROOT}/Platforms/Generic/DelayedCallCommand.cpp - ${ORTHANC_STONE_ROOT}/Platforms/Generic/Oracle.cpp - ${ORTHANC_STONE_ROOT}/Platforms/Generic/OracleDelayedCallExecutor.h + ${ORTHANC_STONE_ROOT}/Platforms/Generic/WebServiceDeleteCommand.h + ${ORTHANC_STONE_ROOT}/Platforms/Generic/WebServiceGetCommand.cpp + ${ORTHANC_STONE_ROOT}/Platforms/Generic/WebServiceGetCommand.h + ${ORTHANC_STONE_ROOT}/Platforms/Generic/WebServicePostCommand.cpp + ${ORTHANC_STONE_ROOT}/Platforms/Generic/WebServicePostCommand.h ) - if (ENABLE_STONE_DEPRECATED) - list(APPEND PLATFORM_SOURCES - ${ORTHANC_STONE_ROOT}/Platforms/Generic/OracleWebService.cpp - ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Viewport/CairoFont.cpp + if (ENABLE_SDL) + list(APPEND ORTHANC_STONE_SOURCES + ${ORTHANC_STONE_ROOT}/Framework/Viewport/SdlWindow.cpp + ${ORTHANC_STONE_ROOT}/Framework/Viewport/SdlWindow.h ) endif() if (ENABLE_SDL OR ENABLE_QT) - if (ENABLE_STONE_DEPRECATED) - list(APPEND APPLICATIONS_SOURCES - ${ORTHANC_STONE_ROOT}/Applications/Generic/NativeStoneApplicationRunner.cpp - ${ORTHANC_STONE_ROOT}/Applications/Generic/NativeStoneApplicationContext.cpp - ) - endif() - - if (ENABLE_SDL) + if (ENABLE_OPENGL) list(APPEND ORTHANC_STONE_SOURCES - ${ORTHANC_STONE_ROOT}/Framework/Viewport/SdlWindow.cpp + ${ORTHANC_STONE_ROOT}/Framework/OpenGL/SdlOpenGLContext.cpp + ${ORTHANC_STONE_ROOT}/Framework/OpenGL/SdlOpenGLContext.h + ${ORTHANC_STONE_ROOT}/Framework/Viewport/SdlViewport.cpp + ${ORTHANC_STONE_ROOT}/Framework/Viewport/SdlViewport.h ) - - list(APPEND APPLICATIONS_SOURCES - ${ORTHANC_STONE_ROOT}/Applications/Sdl/SdlCairoSurface.cpp - ${ORTHANC_STONE_ROOT}/Applications/Sdl/SdlEngine.cpp - ${ORTHANC_STONE_ROOT}/Applications/Sdl/SdlOrthancSurface.cpp - ${ORTHANC_STONE_ROOT}/Applications/Sdl/SdlStoneApplicationRunner.cpp - ) - - if (ENABLE_OPENGL) - list(APPEND ORTHANC_STONE_SOURCES - ${ORTHANC_STONE_ROOT}/Framework/OpenGL/SdlOpenGLContext.cpp - ${ORTHANC_STONE_ROOT}/Framework/Viewport/SdlViewport.cpp - ) - endif() endif() endif() elseif (ENABLE_WASM) - list(APPEND APPLICATIONS_SOURCES - ${ORTHANC_STONE_ROOT}/Applications/Wasm/StartupParametersBuilder.cpp - ) - set(STONE_WASM_SOURCES ${ORTHANC_STONE_ROOT}/Platforms/Wasm/Defaults.cpp ${ORTHANC_STONE_ROOT}/Platforms/Wasm/WasmDelayedCallExecutor.cpp @@ -331,60 +317,156 @@ DEPENDS "${ORTHANC_STONE_ROOT}/Platforms/Wasm/default-library.js") endif() -if (ENABLE_SDL OR ENABLE_WASM) - list(APPEND APPLICATIONS_SOURCES - ${ORTHANC_STONE_ROOT}/Applications/Generic/GuiAdapter.cpp - ${ORTHANC_STONE_ROOT}/Applications/Generic/GuiAdapter.h - ) -endif() +if (ENABLE_STONE_DEPRECATED) + if (NOT ORTHANC_SANDBOXED) + list(APPEND PLATFORM_SOURCES + ${ORTHANC_STONE_ROOT}/Platforms/Generic/OracleWebService.cpp + ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Viewport/CairoFont.cpp + ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Viewport/CairoFont.h + ) + endif() + + if (ENABLE_SDL OR ENABLE_WASM) + list(APPEND APPLICATIONS_SOURCES + ${ORTHANC_STONE_ROOT}/Applications/Generic/GuiAdapter.cpp + ${ORTHANC_STONE_ROOT}/Applications/Generic/GuiAdapter.h + ) + endif() + + if (ENABLE_SDL OR ENABLE_QT) + list(APPEND APPLICATIONS_SOURCES + ${ORTHANC_STONE_ROOT}/Applications/Generic/NativeStoneApplicationRunner.cpp + ${ORTHANC_STONE_ROOT}/Applications/Generic/NativeStoneApplicationContext.cpp + ) + endif() + + if (ENABLE_SDL) + list(APPEND APPLICATIONS_SOURCES + ${ORTHANC_STONE_ROOT}/Applications/Sdl/SdlCairoSurface.cpp + ${ORTHANC_STONE_ROOT}/Applications/Sdl/SdlEngine.cpp + ${ORTHANC_STONE_ROOT}/Applications/Sdl/SdlOrthancSurface.cpp + ${ORTHANC_STONE_ROOT}/Applications/Sdl/SdlStoneApplicationRunner.cpp + ) + endif() -if (ENABLE_STONE_DEPRECATED) + if (ENABLE_WASM) + list(APPEND APPLICATIONS_SOURCES + ${ORTHANC_STONE_ROOT}/Applications/Wasm/StartupParametersBuilder.cpp + ) + endif() + + if (ENABLE_THREADS) + list(APPEND ORTHANC_STONE_SOURCES + ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Messages/LockingEmitter.cpp + ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Messages/LockingEmitter.h + ) + endif() + list(APPEND ORTHANC_STONE_SOURCES + ${ORTHANC_STONE_ROOT}/Applications/IStoneApplication.h ${ORTHANC_STONE_ROOT}/Applications/StoneApplicationContext.cpp + + ${ORTHANC_STONE_ROOT}/Framework/Deprecated/dev.h + ${ORTHANC_STONE_ROOT}/Framework/Deprecated/SmartLoader.cpp + ${ORTHANC_STONE_ROOT}/Framework/Deprecated/SmartLoader.h ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Layers/CircleMeasureTracker.cpp + ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Layers/CircleMeasureTracker.h ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Layers/ColorFrameRenderer.cpp + ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Layers/ColorFrameRenderer.h ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Layers/DicomSeriesVolumeSlicer.cpp + ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Layers/DicomSeriesVolumeSlicer.h ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Layers/DicomStructureSetSlicer.cpp + ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Layers/DicomStructureSetSlicer.h ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Layers/FrameRenderer.cpp + ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Layers/FrameRenderer.h ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Layers/GrayscaleFrameRenderer.cpp + ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Layers/GrayscaleFrameRenderer.h + ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Layers/ILayerRenderer.h ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Layers/IVolumeSlicer.h ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Layers/LineLayerRenderer.cpp + ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Layers/LineLayerRenderer.h ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Layers/LineMeasureTracker.cpp + ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Layers/LineMeasureTracker.h ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Layers/RenderStyle.cpp + ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Layers/RenderStyle.h + # ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Layers/SeriesFrameRendererFactory.cpp + # ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Layers/SeriesFrameRendererFactory.h + # ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Layers/SingleFrameRendererFactory.cpp + # ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Layers/SingleFrameRendererFactory.h ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Layers/SliceOutlineRenderer.cpp - ${ORTHANC_STONE_ROOT}/Framework/Deprecated/SmartLoader.cpp + ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Layers/SliceOutlineRenderer.h + ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Loaders/DicomStructureSetLoader.cpp + ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Loaders/DicomStructureSetLoader.h + ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Loaders/DicomStructureSetLoader2.cpp + ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Loaders/DicomStructureSetLoader2.h + ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Loaders/LoaderCache.cpp + ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Loaders/LoaderCache.h + ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Loaders/LoaderStateMachine.cpp + ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Loaders/LoaderStateMachine.h + ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Loaders/OrthancMultiframeVolumeLoader.cpp + ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Loaders/OrthancMultiframeVolumeLoader.h + ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Loaders/OrthancSeriesVolumeProgressiveLoader.cpp + ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Loaders/OrthancSeriesVolumeProgressiveLoader.h ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Toolbox/BaseWebService.cpp + ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Toolbox/BaseWebService.h ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Toolbox/DicomFrameConverter.cpp + ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Toolbox/DicomFrameConverter.h ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Toolbox/DownloadStack.cpp + ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Toolbox/DownloadStack.h ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Toolbox/IDelayedCallExecutor.h + ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Toolbox/ISeriesLoader.h ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Toolbox/IWebService.cpp + ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Toolbox/IWebService.h ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Toolbox/MessagingToolbox.cpp + ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Toolbox/MessagingToolbox.h ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Toolbox/OrthancApiClient.cpp + ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Toolbox/OrthancApiClient.h ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Toolbox/OrthancSlicesLoader.cpp + ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Toolbox/OrthancSlicesLoader.h ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Toolbox/ParallelSlices.cpp + ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Toolbox/ParallelSlices.h ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Toolbox/ParallelSlicesCursor.cpp + ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Toolbox/ParallelSlicesCursor.h ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Toolbox/Slice.cpp + ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Toolbox/Slice.h ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Toolbox/ViewportGeometry.cpp + ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Toolbox/ViewportGeometry.h ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Viewport/IMouseTracker.h ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Viewport/IStatusBar.h ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Viewport/IViewport.h ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Viewport/WidgetViewport.cpp + ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Viewport/WidgetViewport.h + ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Volumes/IGeometryProvider.h + ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Volumes/ISlicedVolume.h + ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Volumes/IVolumeLoader.h ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Volumes/StructureSetLoader.cpp + ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Volumes/StructureSetLoader.h ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Widgets/CairoWidget.cpp + ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Widgets/CairoWidget.h ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Widgets/EmptyWidget.cpp + ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Widgets/EmptyWidget.h ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Widgets/IWidget.h ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Widgets/IWorldSceneInteractor.h ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Widgets/IWorldSceneMouseTracker.h ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Widgets/LayoutWidget.cpp + ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Widgets/LayoutWidget.h ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Widgets/PanMouseTracker.cpp + ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Widgets/PanMouseTracker.h ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Widgets/PanZoomMouseTracker.cpp + ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Widgets/PanZoomMouseTracker.h ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Widgets/SliceViewerWidget.cpp + ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Widgets/SliceViewerWidget.h ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Widgets/TestCairoWidget.cpp + ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Widgets/TestCairoWidget.h ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Widgets/TestWorldSceneWidget.cpp + ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Widgets/TestWorldSceneWidget.h ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Widgets/WidgetBase.cpp + ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Widgets/WidgetBase.h ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Widgets/WorldSceneWidget.cpp + ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Widgets/WorldSceneWidget.h ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Widgets/ZoomMouseTracker.cpp - ${ORTHANC_STONE_ROOT}/Framework/Deprecated/dev.h + ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Widgets/ZoomMouseTracker.h + ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographyAlphaLayer.cpp ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographyDicomLayer.cpp ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographyLayer.cpp @@ -405,17 +487,29 @@ endif() +if (ENABLE_DCMTK) + list(APPEND ORTHANC_STONE_SOURCES + ${ORTHANC_STONE_ROOT}/Framework/Oracle/ParseDicomSuccessMessage.cpp + ${ORTHANC_STONE_ROOT}/Framework/Toolbox/ParsedDicomCache.cpp + ${ORTHANC_STONE_ROOT}/Framework/Toolbox/ParsedDicomDataset.cpp + ) +endif() + if (ENABLE_THREADS) list(APPEND ORTHANC_STONE_SOURCES - ${ORTHANC_STONE_ROOT}/Framework/Messages/LockingEmitter.h ${ORTHANC_STONE_ROOT}/Framework/Oracle/ThreadedOracle.cpp + ${ORTHANC_STONE_ROOT}/Framework/Oracle/GenericOracleRunner.cpp ) endif() if (ENABLE_WASM) list(APPEND ORTHANC_STONE_SOURCES + ${ORTHANC_STONE_ROOT}/Framework/Loaders/WebAssemblyLoadersContext.cpp ${ORTHANC_STONE_ROOT}/Framework/Oracle/WebAssemblyOracle.cpp + ${ORTHANC_STONE_ROOT}/Framework/Viewport/WebAssemblyCairoViewport.cpp + ${ORTHANC_STONE_ROOT}/Framework/Viewport/WebAssemblyViewport.cpp + ${ORTHANC_STONE_ROOT}/Framework/Viewport/WebAssemblyViewport.h ) endif() @@ -441,57 +535,92 @@ ${ORTHANC_STONE_ROOT}/Framework/Loaders/BasicFetchingItemsSorter.h ${ORTHANC_STONE_ROOT}/Framework/Loaders/BasicFetchingStrategy.cpp ${ORTHANC_STONE_ROOT}/Framework/Loaders/BasicFetchingStrategy.h - ${ORTHANC_STONE_ROOT}/Framework/Loaders/DicomStructureSetLoader.cpp - ${ORTHANC_STONE_ROOT}/Framework/Loaders/DicomStructureSetLoader.h - ${ORTHANC_STONE_ROOT}/Framework/Loaders/DicomStructureSetLoader2.cpp - ${ORTHANC_STONE_ROOT}/Framework/Loaders/DicomStructureSetLoader2.h + ${ORTHANC_STONE_ROOT}/Framework/Loaders/DicomResourcesLoader.cpp + ${ORTHANC_STONE_ROOT}/Framework/Loaders/DicomSource.cpp + ${ORTHANC_STONE_ROOT}/Framework/Loaders/DicomVolumeLoader.cpp ${ORTHANC_STONE_ROOT}/Framework/Loaders/IFetchingItemsSorter.h ${ORTHANC_STONE_ROOT}/Framework/Loaders/IFetchingStrategy.h - ${ORTHANC_STONE_ROOT}/Framework/Loaders/LoaderCache.cpp - ${ORTHANC_STONE_ROOT}/Framework/Loaders/LoaderCache.h - ${ORTHANC_STONE_ROOT}/Framework/Loaders/LoaderStateMachine.cpp - ${ORTHANC_STONE_ROOT}/Framework/Loaders/LoaderStateMachine.h - ${ORTHANC_STONE_ROOT}/Framework/Loaders/OrthancMultiframeVolumeLoader.cpp - ${ORTHANC_STONE_ROOT}/Framework/Loaders/OrthancMultiframeVolumeLoader.h - ${ORTHANC_STONE_ROOT}/Framework/Loaders/OrthancSeriesVolumeProgressiveLoader.cpp - ${ORTHANC_STONE_ROOT}/Framework/Loaders/OrthancSeriesVolumeProgressiveLoader.h - + ${ORTHANC_STONE_ROOT}/Framework/Loaders/LoadedDicomResources.cpp + ${ORTHANC_STONE_ROOT}/Framework/Loaders/OracleScheduler.cpp + ${ORTHANC_STONE_ROOT}/Framework/Loaders/SeriesFramesLoader.cpp + ${ORTHANC_STONE_ROOT}/Framework/Loaders/SeriesMetadataLoader.cpp + ${ORTHANC_STONE_ROOT}/Framework/Loaders/SeriesOrderedFrames.cpp + ${ORTHANC_STONE_ROOT}/Framework/Loaders/SeriesThumbnailsLoader.cpp + ${ORTHANC_STONE_ROOT}/Framework/Messages/ICallable.h ${ORTHANC_STONE_ROOT}/Framework/Messages/IMessage.h + ${ORTHANC_STONE_ROOT}/Framework/Messages/IMessageEmitter.h ${ORTHANC_STONE_ROOT}/Framework/Messages/IObservable.cpp - ${ORTHANC_STONE_ROOT}/Framework/Messages/IObserver.cpp + ${ORTHANC_STONE_ROOT}/Framework/Messages/IObservable.h ${ORTHANC_STONE_ROOT}/Framework/Messages/IObserver.h - ${ORTHANC_STONE_ROOT}/Framework/Messages/MessageBroker.h - ${ORTHANC_STONE_ROOT}/Framework/Messages/MessageForwarder.cpp + ${ORTHANC_STONE_ROOT}/Framework/Messages/ObserverBase.h + ${ORTHANC_STONE_ROOT}/Framework/Oracle/GetOrthancImageCommand.cpp ${ORTHANC_STONE_ROOT}/Framework/Oracle/GetOrthancWebViewerJpegCommand.cpp - ${ORTHANC_STONE_ROOT}/Framework/Oracle/OracleCommandWithPayload.cpp + ${ORTHANC_STONE_ROOT}/Framework/Oracle/HttpCommand.cpp + ${ORTHANC_STONE_ROOT}/Framework/Oracle/OracleCommandBase.cpp ${ORTHANC_STONE_ROOT}/Framework/Oracle/OrthancRestApiCommand.cpp - ${ORTHANC_STONE_ROOT}/Framework/Oracle/HttpCommand.cpp + ${ORTHANC_STONE_ROOT}/Framework/Oracle/ParseDicomFromFileCommand.cpp + ${ORTHANC_STONE_ROOT}/Framework/Oracle/ParseDicomFromWadoCommand.cpp + ${ORTHANC_STONE_ROOT}/Framework/Scene2D/CairoCompositor.cpp + ${ORTHANC_STONE_ROOT}/Framework/Scene2D/CairoCompositor.h + ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Color.h + ${ORTHANC_STONE_ROOT}/Framework/Scene2D/ColorSceneLayer.h ${ORTHANC_STONE_ROOT}/Framework/Scene2D/ColorTextureSceneLayer.cpp + ${ORTHANC_STONE_ROOT}/Framework/Scene2D/ColorTextureSceneLayer.h ${ORTHANC_STONE_ROOT}/Framework/Scene2D/FloatTextureSceneLayer.cpp + ${ORTHANC_STONE_ROOT}/Framework/Scene2D/FloatTextureSceneLayer.h ${ORTHANC_STONE_ROOT}/Framework/Scene2D/GrayscaleStyleConfigurator.cpp + ${ORTHANC_STONE_ROOT}/Framework/Scene2D/GrayscaleStyleConfigurator.h ${ORTHANC_STONE_ROOT}/Framework/Scene2D/ICompositor.h + ${ORTHANC_STONE_ROOT}/Framework/Scene2D/ILayerStyleConfigurator.h ${ORTHANC_STONE_ROOT}/Framework/Scene2D/InfoPanelSceneLayer.cpp + ${ORTHANC_STONE_ROOT}/Framework/Scene2D/InfoPanelSceneLayer.h + ${ORTHANC_STONE_ROOT}/Framework/Scene2D/IPointerTracker.h + ${ORTHANC_STONE_ROOT}/Framework/Scene2D/ISceneLayer.h + ${ORTHANC_STONE_ROOT}/Framework/Scene2D/LookupTableStyleConfigurator.cpp + ${ORTHANC_STONE_ROOT}/Framework/Scene2D/LookupTableStyleConfigurator.h + ${ORTHANC_STONE_ROOT}/Framework/Scene2D/LookupTableTextureSceneLayer.cpp + ${ORTHANC_STONE_ROOT}/Framework/Scene2D/LookupTableTextureSceneLayer.h + ${ORTHANC_STONE_ROOT}/Framework/Scene2D/NullLayer.h + ${ORTHANC_STONE_ROOT}/Framework/Scene2D/PanSceneTracker.cpp + ${ORTHANC_STONE_ROOT}/Framework/Scene2D/PanSceneTracker.h + ${ORTHANC_STONE_ROOT}/Framework/Scene2D/PointerEvent.cpp + ${ORTHANC_STONE_ROOT}/Framework/Scene2D/PointerEvent.h + ${ORTHANC_STONE_ROOT}/Framework/Scene2D/PolylineSceneLayer.cpp + ${ORTHANC_STONE_ROOT}/Framework/Scene2D/PolylineSceneLayer.h + ${ORTHANC_STONE_ROOT}/Framework/Scene2D/RotateSceneTracker.cpp + ${ORTHANC_STONE_ROOT}/Framework/Scene2D/RotateSceneTracker.h + ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Scene2D.cpp + ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Scene2D.h + ${ORTHANC_STONE_ROOT}/Framework/Scene2D/ScenePoint2D.h + ${ORTHANC_STONE_ROOT}/Framework/Scene2D/TextSceneLayer.cpp + ${ORTHANC_STONE_ROOT}/Framework/Scene2D/TextSceneLayer.h + ${ORTHANC_STONE_ROOT}/Framework/Scene2D/TextureBaseSceneLayer.cpp + ${ORTHANC_STONE_ROOT}/Framework/Scene2D/TextureBaseSceneLayer.h + ${ORTHANC_STONE_ROOT}/Framework/Scene2D/ZoomSceneTracker.cpp + ${ORTHANC_STONE_ROOT}/Framework/Scene2D/ZoomSceneTracker.h + + ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/CairoBaseRenderer.h ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/CairoColorTextureRenderer.cpp + ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/CairoColorTextureRenderer.h ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/CairoFloatTextureRenderer.cpp + ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/CairoFloatTextureRenderer.h ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/CairoInfoPanelRenderer.cpp + ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/CairoInfoPanelRenderer.h ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/CairoLookupTableTextureRenderer.cpp + ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/CairoLookupTableTextureRenderer.h ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/CairoPolylineRenderer.cpp + ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/CairoPolylineRenderer.h ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/CairoTextRenderer.cpp + ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/CairoTextRenderer.h ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/CompositorHelper.cpp + ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/CompositorHelper.h ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/FixedPointAligner.cpp - ${ORTHANC_STONE_ROOT}/Framework/Scene2D/LookupTableStyleConfigurator.cpp - ${ORTHANC_STONE_ROOT}/Framework/Scene2D/LookupTableTextureSceneLayer.cpp - ${ORTHANC_STONE_ROOT}/Framework/Scene2D/PanSceneTracker.cpp - ${ORTHANC_STONE_ROOT}/Framework/Scene2D/PointerEvent.cpp - ${ORTHANC_STONE_ROOT}/Framework/Scene2D/PolylineSceneLayer.cpp - ${ORTHANC_STONE_ROOT}/Framework/Scene2D/RotateSceneTracker.cpp - ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Scene2D.cpp - ${ORTHANC_STONE_ROOT}/Framework/Scene2D/TextSceneLayer.cpp - ${ORTHANC_STONE_ROOT}/Framework/Scene2D/TextureBaseSceneLayer.cpp - ${ORTHANC_STONE_ROOT}/Framework/Scene2D/ZoomSceneTracker.cpp + ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/FixedPointAligner.h + ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/ICairoContextProvider.h + ${ORTHANC_STONE_ROOT}/Framework/Scene2DViewport/AngleMeasureTool.cpp ${ORTHANC_STONE_ROOT}/Framework/Scene2DViewport/AngleMeasureTool.h ${ORTHANC_STONE_ROOT}/Framework/Scene2DViewport/CreateAngleMeasureCommand.cpp @@ -588,8 +717,6 @@ ${ORTHANC_STONE_ROOT}/Framework/Toolbox/UndoRedoStack.h ${ORTHANC_STONE_ROOT}/Framework/Viewport/IViewport.h - ${ORTHANC_STONE_ROOT}/Framework/Viewport/ViewportBase.h - ${ORTHANC_STONE_ROOT}/Framework/Viewport/ViewportBase.cpp ${ORTHANC_STONE_ROOT}/Framework/Volumes/IVolumeSlicer.cpp ${ORTHANC_STONE_ROOT}/Framework/Volumes/IVolumeSlicer.h @@ -648,38 +775,40 @@ ${ORTHANC_STONE_ROOT}/Framework/OpenGL/OpenGLTexture.cpp ${ORTHANC_STONE_ROOT}/Framework/Scene2D/OpenGLCompositor.h ${ORTHANC_STONE_ROOT}/Framework/Scene2D/OpenGLCompositor.cpp + + ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/OpenGLAdvancedPolylineRenderer.cpp ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/OpenGLAdvancedPolylineRenderer.h - ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/OpenGLAdvancedPolylineRenderer.cpp + ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/OpenGLBasicPolylineRenderer.cpp ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/OpenGLBasicPolylineRenderer.h - ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/OpenGLBasicPolylineRenderer.cpp - ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/OpenGLColorTextureProgram.h ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/OpenGLColorTextureProgram.cpp + ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/OpenGLColorTextureProgram.h + ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/OpenGLColorTextureRenderer.cpp ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/OpenGLColorTextureRenderer.h - ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/OpenGLColorTextureRenderer.cpp + ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/OpenGLFloatTextureProgram.cpp ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/OpenGLFloatTextureProgram.h - ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/OpenGLFloatTextureProgram.cpp - ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/OpenGLFloatTextureRenderer.h ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/OpenGLFloatTextureRenderer.cpp + ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/OpenGLFloatTextureRenderer.h + ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/OpenGLInfoPanelRenderer.cpp ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/OpenGLInfoPanelRenderer.h - ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/OpenGLInfoPanelRenderer.cpp + ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/OpenGLLinesProgram.cpp ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/OpenGLLinesProgram.h - ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/OpenGLLinesProgram.cpp - ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/OpenGLLookupTableTextureRenderer.h ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/OpenGLLookupTableTextureRenderer.cpp + ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/OpenGLLookupTableTextureRenderer.h + ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/OpenGLShaderVersionDirective.h + ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/OpenGLTextProgram.cpp ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/OpenGLTextProgram.h - ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/OpenGLTextProgram.cpp + ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/OpenGLTextRenderer.cpp ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/OpenGLTextRenderer.h - ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/OpenGLTextRenderer.cpp + ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/OpenGLTextureProgram.cpp ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/OpenGLTextureProgram.h - ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/OpenGLTextureProgram.cpp ) if (ENABLE_WASM) list(APPEND ORTHANC_STONE_SOURCES + ${ORTHANC_STONE_ROOT}/Framework/OpenGL/WebAssemblyOpenGLContext.cpp ${ORTHANC_STONE_ROOT}/Framework/OpenGL/WebAssemblyOpenGLContext.h - ${ORTHANC_STONE_ROOT}/Framework/OpenGL/WebAssemblyOpenGLContext.cpp - ${ORTHANC_STONE_ROOT}/Framework/Viewport/WebAssemblyViewport.h - ${ORTHANC_STONE_ROOT}/Framework/Viewport/WebAssemblyViewport.cpp + ${ORTHANC_STONE_ROOT}/Framework/Viewport/WebGLViewport.cpp + ${ORTHANC_STONE_ROOT}/Framework/Viewport/WebGLViewportsRegistry.cpp ) endif() endif()
--- a/Resources/CMake/QtConfiguration.cmake Tue Mar 31 15:47:29 2020 +0200 +++ b/Resources/CMake/QtConfiguration.cmake Tue Mar 31 15:51:02 2020 +0200 @@ -20,26 +20,12 @@ set(CMAKE_AUTOMOC OFF) set(CMAKE_AUTOUIC OFF) -if ("${CMAKE_SYSTEM_VERSION}" STREQUAL "LinuxStandardBase") - # Linux Standard Base version 5 ships Qt 4.2.3 + +## Note that these set of macros MUST be defined as a "function()", +## otherwise it fails +function(DEFINE_QT_MACROS) include(Qt4Macros) - # The script "LinuxStandardBaseUic.py" is just a wrapper around the - # "uic" compiler from LSB that does not support the "<?xml ...?>" - # header that is automatically added by Qt Creator - set(QT_UIC_EXECUTABLE ${CMAKE_CURRENT_LIST_DIR}/LinuxStandardBaseUic.py) - - set(QT_MOC_EXECUTABLE ${LSB_PATH}/bin/moc) - - include_directories( - ${LSB_PATH}/include/QtCore - ${LSB_PATH}/include/QtGui - ${LSB_PATH}/include/QtOpenGL - ) - - link_libraries(QtCore QtGui QtOpenGL) - - ## ## This part is adapted from file "Qt4Macros.cmake" shipped with ## CMake 3.5.1, released under the following license: @@ -86,9 +72,91 @@ ## ## End of "Qt4Macros.cmake" adaptation. ## +endfunction() + +if ("${CMAKE_SYSTEM_VERSION}" STREQUAL "LinuxStandardBase") + # Linux Standard Base version 5 ships Qt 4.2.3 + DEFINE_QT_MACROS() + + # The script "LinuxStandardBaseUic.py" is just a wrapper around the + # "uic" compiler from LSB that does not support the "<?xml ...?>" + # header that is automatically added by Qt Creator + set(QT_UIC_EXECUTABLE ${CMAKE_CURRENT_LIST_DIR}/LinuxStandardBaseUic.py) + + set(QT_MOC_EXECUTABLE ${LSB_PATH}/bin/moc) + + include_directories( + ${LSB_PATH}/include/QtCore + ${LSB_PATH}/include/QtGui + ${LSB_PATH}/include/QtOpenGL + ) + + link_libraries(QtCore QtGui QtOpenGL) + +elseif (${CMAKE_SYSTEM_NAME} STREQUAL "Windows") + DEFINE_QT_MACROS() + + include_directories(${QT5_INSTALL_ROOT}/include) + link_directories(${QT5_INSTALL_ROOT}/lib) + + if (OFF) #CMAKE_CROSSCOMPILING) + set(QT_UIC_EXECUTABLE wine ${QT5_INSTALL_ROOT}/bin/uic.exe) + set(QT_MOC_EXECUTABLE wine ${QT5_INSTALL_ROOT}/bin/moc.exe) + else() + set(QT_UIC_EXECUTABLE ${QT5_INSTALL_ROOT}/bin/uic) + set(QT_MOC_EXECUTABLE ${QT5_INSTALL_ROOT}/bin/moc) + endif() + + include_directories( + ${QT5_INSTALL_ROOT}/include/QtCore + ${QT5_INSTALL_ROOT}/include/QtGui + ${QT5_INSTALL_ROOT}/include/QtOpenGL + ${QT5_INSTALL_ROOT}/include/QtWidgets + ) + + if (OFF) + # Dynamic Qt + link_libraries(Qt5Core Qt5Gui Qt5OpenGL Qt5Widgets) + + file(COPY + ${QT5_INSTALL_ROOT}/bin/Qt5Core.dll + ${QT5_INSTALL_ROOT}/bin/Qt5Gui.dll + ${QT5_INSTALL_ROOT}/bin/Qt5OpenGL.dll + ${QT5_INSTALL_ROOT}/bin/Qt5Widgets.dll + ${QT5_INSTALL_ROOT}/bin/libstdc++-6.dll + ${QT5_INSTALL_ROOT}/bin/libgcc_s_dw2-1.dll + ${QT5_INSTALL_ROOT}/bin/libwinpthread-1.dll + DESTINATION ${CMAKE_CURRENT_BINARY_DIR}) + + file(COPY + ${QT5_INSTALL_ROOT}/plugins/platforms/qwindows.dll + DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/platforms) + + else() + # Static Qt + link_libraries( + ${QT5_INSTALL_ROOT}/lib/libQt5Widgets.a + ${QT5_INSTALL_ROOT}/lib/libQt5Gui.a + ${QT5_INSTALL_ROOT}/lib/libQt5OpenGL.a + ${QT5_INSTALL_ROOT}/lib/libQt5Core.a + ${QT5_INSTALL_ROOT}/lib/libqtharfbuzz.a + ${QT5_INSTALL_ROOT}/lib/libqtpcre2.a + ${QT5_INSTALL_ROOT}/lib/libQt5FontDatabaseSupport.a + ${QT5_INSTALL_ROOT}/lib/libQt5EventDispatcherSupport.a + ${QT5_INSTALL_ROOT}/lib/libQt5ThemeSupport.a + ${QT5_INSTALL_ROOT}/plugins/platforms/libqwindows.a + winmm + version + ws2_32 + uxtheme + imm32 + dwmapi + ) + endif() + else() - # Not using Linux Standard Base + # Not using Windows, not using Linux Standard Base, # Find the QtWidgets library find_package(Qt5Widgets QUIET)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/Conventions.txt Tue Mar 31 15:51:02 2020 +0200 @@ -0,0 +1,88 @@ + +Some notes about the lifetime of objects +======================================== + +Stone applications +------------------ + +A typical Stone application can be split in 3 parts: + +1- The "loaders part" and the associated "IOracle", that communicate + through "IMessage" objects. The lifetime of these objects is + governed by the "IStoneContext". + +2- The "data part" holds the data loaded by the "loaders part". The + related objects must not be aware of the oracle, neither of the + messages. It is up to the user application to store these objects. + +3- The "viewport part" is based upon the "Scene2D" class. + + +Multithreading +-------------- + +* Stone makes the hypothesis that its objects live in a single thread. + All the content of the "Framework" folder (with the exception of + the "Oracle" stuff) must not use "boost::thread". + +* The "IOracleCommand" classes represent commands that must be + executed asynchronously from the Stone thread. Their actual + execution is done by the "IOracle". + +* In WebAssembly, the "IOracle" corresponds to the "html5.h" + facilities (notably for the Fetch API). There is no mutex here, as + JavaScript is inherently single-threaded. + +* In plain C++ applications, the "IOracle" corresponds to a FIFO queue + of commands that are executed by a pool of threads. The Stone + context holds a global mutex, that must be properly locked by the + user application, and by the "IOracle" when it sends back messages + to the Stone loaders (cf. class "IMessageEmitter"). + +* Multithreading is thus achieved by defining new oracle commands by + subclassing "IOracleCommand", then by defining a way to execute them + (cf. class "GenericCommandRunner"). + + +References between objects +-------------------------- + +* An object allocated on the heap must never store a reference/pointer + to another object. + +* A class designed to be allocated only on the stack can store a + reference/pointer to another object. Here is the list of + such classes: + + - IMessage and its derived classes: All the messages are allocated + on the stack. + + +Pointers +-------- + +* As we are targeting C++03 (for VS2008 and LSB compatibility), use + "std::unique_ptr<>" and "boost::shared_ptr<>" (*not* + "std::shared_ptr<>"). We provide an implementation of std::unique_ptr for + pre-C++11 compilers. + +* The fact of transfering the ownership of one object to another must + be tagged by naming the method "Acquire...()", and by providing a + raw pointer. + +* Use "std::unique_ptr<>" if the goal is to internally store a pointer + whose lifetime corresponds to the host object. + +* The use of "boost::weak_ptr<>" should be restricted to + oracle/message handling. + +* The use of "boost::shared_ptr<>" should be minimized to avoid + clutter. The "loaders" and "data parts" objects must however + be created as "boost::shared_ptr<>". + + +Global context +-------------- + +* As the global Stone context can be created/destroyed by other + languages than C++, we don't use a "boost:shared_ptr<>".
--- a/UnitTestsSources/GenericToolboxTests.cpp Tue Mar 31 15:47:29 2020 +0200 +++ b/UnitTestsSources/GenericToolboxTests.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -20,8 +20,9 @@ #include <Framework/Toolbox/GenericToolbox.h> -#include <boost/chrono.hpp> +#include <boost/date_time/posix_time/posix_time.hpp> #include <boost/lexical_cast.hpp> +#include <boost/enable_shared_from_this.hpp> #include "gtest/gtest.h" @@ -3878,19 +3879,17 @@ bool ok = true; { - boost::chrono::system_clock::time_point start = boost::chrono::system_clock::now(); + boost::posix_time::ptime start = boost::posix_time::microsec_clock::local_time(); for (size_t i = 0; i < NUM_TIMINGS_CONVS; ++i) { ok = StringToDouble(r, txt); } - boost::chrono::system_clock::time_point end = boost::chrono::system_clock::now(); - boost::chrono::microseconds elapsed = - boost::chrono::duration_cast<boost::chrono::microseconds>(end - start); - total_us_StringToDouble += elapsed.count(); + boost::posix_time::ptime end = boost::posix_time::microsec_clock::local_time(); + total_us_StringToDouble += (end - start).total_microseconds(); } { - boost::chrono::system_clock::time_point start = boost::chrono::system_clock::now(); + boost::posix_time::ptime start = boost::posix_time::microsec_clock::local_time(); for (size_t i = 0; i < NUM_TIMINGS_CONVS; ++i) { try @@ -3903,10 +3902,8 @@ ok = false; } } - boost::chrono::system_clock::time_point end = boost::chrono::system_clock::now(); - boost::chrono::microseconds elapsed = - boost::chrono::duration_cast<boost::chrono::microseconds>(end - start); - total_us_lexical_cast += elapsed.count(); + boost::posix_time::ptime end = boost::posix_time::microsec_clock::local_time(); + total_us_lexical_cast += (end - start).total_microseconds(); } numConversions += NUM_TIMINGS_CONVS; @@ -4095,19 +4092,17 @@ bool ok = true; { - boost::chrono::system_clock::time_point start = boost::chrono::system_clock::now(); + boost::posix_time::ptime start = boost::posix_time::microsec_clock::local_time(); for (size_t i = 0; i < NUM_TIMINGS_CONVS; ++i) { ok = StringToDouble(r, txt); } - boost::chrono::system_clock::time_point end = boost::chrono::system_clock::now(); - boost::chrono::microseconds elapsed = - boost::chrono::duration_cast<boost::chrono::microseconds>(end - start); - total_us_StringToDouble += elapsed.count(); + boost::posix_time::ptime end = boost::posix_time::microsec_clock::local_time(); + total_us_StringToDouble += (end - start).total_microseconds(); } { - boost::chrono::system_clock::time_point start = boost::chrono::system_clock::now(); + boost::posix_time::ptime start = boost::posix_time::microsec_clock::local_time(); for (size_t i = 0; i < NUM_TIMINGS_CONVS; ++i) { try @@ -4120,10 +4115,8 @@ ok = false; } } - boost::chrono::system_clock::time_point end = boost::chrono::system_clock::now(); - boost::chrono::microseconds elapsed = - boost::chrono::duration_cast<boost::chrono::microseconds>(end - start); - total_us_lexical_cast += elapsed.count(); + boost::posix_time::ptime end = boost::posix_time::microsec_clock::local_time(); + total_us_lexical_cast += (end - start).total_microseconds(); } numConversions += NUM_TIMINGS_CONVS; @@ -4251,3 +4244,10 @@ EXPECT_EQ(0, green); EXPECT_EQ(0, blue); } + + + + + + +
--- a/UnitTestsSources/TestMessageBroker.cpp Tue Mar 31 15:47:29 2020 +0200 +++ b/UnitTestsSources/TestMessageBroker.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -21,10 +21,8 @@ #include "gtest/gtest.h" -#include "../Framework/Messages/MessageBroker.h" -#include "../Framework/Messages/IObservable.h" -#include "../Framework/Messages/IObserver.h" -#include "../Framework/Messages/MessageForwarder.h" +#include "Framework/Messages/IObservable.h" +#include "Framework/Messages/ObserverBase.h" int testCounter = 0; @@ -47,51 +45,26 @@ { } }; - - MyObservable(MessageBroker& broker) : - IObservable(broker) - { - } }; - class MyObserver : public IObserver + class MyObserver : public ObserverBase<MyObserver> { 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)); - } }; } TEST(MessageBroker, TestPermanentConnectionSimpleUseCase) { - MessageBroker broker; - MyObservable observable(broker); - MyObserver observer(broker); + MyObservable observable; + boost::shared_ptr<MyObserver> observer(new MyObserver); // create a permanent connection between an observable and an observer - observable.RegisterObserverCallback(new Callable<MyObserver, MyObservable::MyCustomMessage>(observer, &MyObserver::HandleCompletedMessage)); + observer->Register<MyObservable::MyCustomMessage>(observable, &MyObserver::HandleCompletedMessage); testCounter = 0; observable.BroadcastMessage(MyObservable::MyCustomMessage(12)); @@ -103,155 +76,29 @@ ASSERT_EQ(20, testCounter); // Unregister the observer; make sure it's not called anymore - observable.Unregister(&observer); + observer.reset(); testCounter = 0; observable.BroadcastMessage(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.BroadcastMessage(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.BroadcastMessage(MyObservable::MyCustomMessage(20)); - ASSERT_EQ(20, testCounter); -} - TEST(MessageBroker, TestPermanentConnectionDeleteObserver) { - MessageBroker broker; - MyObservable observable(broker); - MyObserver* observer = new MyObserver(broker); + MyObservable observable; + boost::shared_ptr<MyObserver> observer(new MyObserver); // create a permanent connection between an observable and an observer - observable.RegisterObserverCallback(new Callable<MyObserver, MyObservable::MyCustomMessage>(*observer, &MyObserver::HandleCompletedMessage)); + observer->Register<MyObservable::MyCustomMessage>(observable, &MyObserver::HandleCompletedMessage); testCounter = 0; observable.BroadcastMessage(MyObservable::MyCustomMessage(12)); ASSERT_EQ(12, testCounter); // delete the observer and check that the callback is not called anymore - delete observer; + observer.reset(); // the connection is permanent; if we emit the same message again, the observer will be notified again testCounter = 0; observable.BroadcastMessage(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)); - - testCounter = 0; - observable.BroadcastMessage(MyObservable::MyCustomMessage(12)); - ASSERT_EQ(12, testCounter); - - delete intermediate; - - observable.BroadcastMessage(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.BroadcastMessage(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.BroadcastMessage(MyObservable::MyCustomMessage(20)); - ASSERT_EQ(20, testCounter); -} - - -#if 0 /* __cplusplus >= 201103L*/ - -TEST(MessageBroker, 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.BroadcastMessage(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.BroadcastMessage(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(MessageBroker, TestLambdaCaptureThisAndAccessPrivateMembers) -{ - MessageBroker broker; - MyObservable observable(broker); - MyObserverWithLambda* observer = new MyObserverWithLambda(broker, 3, observable); - - testCounter = 0; - observable.BroadcastMessage(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.BroadcastMessage(MyObservable::MyCustomMessage(20)); - ASSERT_EQ(0, testCounter); -} - -#endif // C++ 11
--- a/UnitTestsSources/UnitTestsMain.cpp Tue Mar 31 15:47:29 2020 +0200 +++ b/UnitTestsSources/UnitTestsMain.cpp Tue Mar 31 15:51:02 2020 +0200 @@ -25,6 +25,7 @@ #include "../Framework/Deprecated/Toolbox/DownloadStack.h" #include "../Framework/Deprecated/Toolbox/MessagingToolbox.h" #include "../Framework/Deprecated/Toolbox/OrthancSlicesLoader.h" +#include "../Framework/StoneInitialization.h" #include "../Framework/Toolbox/FiniteProjectiveCamera.h" #include "../Framework/Toolbox/GeometryToolbox.h" #include "../Framework/Volumes/ImageBuffer3D.h" @@ -566,20 +567,20 @@ } -static bool IsEqualVector(OrthancStone::Vector a, - OrthancStone::Vector b) +static bool IsEqualRotationVector(OrthancStone::Vector a, + OrthancStone::Vector b) { - if (a.size() == 3 && - b.size() == 3) + if (a.size() != b.size() || + a.size() != 3) + { + return false; + } + else { OrthancStone::LinearAlgebra::NormalizeVector(a); OrthancStone::LinearAlgebra::NormalizeVector(b); return OrthancStone::LinearAlgebra::IsCloseToZero(boost::numeric::ublas::norm_2(a - b)); } - else - { - return false; - } } @@ -593,29 +594,29 @@ OrthancStone::GeometryToolbox::AlignVectorsWithRotation(r, a, b); ASSERT_TRUE(OrthancStone::LinearAlgebra::IsRotationMatrix(r)); - ASSERT_TRUE(IsEqualVector(OrthancStone::LinearAlgebra::Product(r, a), b)); + ASSERT_TRUE(IsEqualRotationVector(OrthancStone::LinearAlgebra::Product(r, a), b)); OrthancStone::GeometryToolbox::AlignVectorsWithRotation(r, b, a); ASSERT_TRUE(OrthancStone::LinearAlgebra::IsRotationMatrix(r)); - ASSERT_TRUE(IsEqualVector(OrthancStone::LinearAlgebra::Product(r, b), a)); + ASSERT_TRUE(IsEqualRotationVector(OrthancStone::LinearAlgebra::Product(r, b), a)); OrthancStone::LinearAlgebra::AssignVector(a, 1, 0, 0); OrthancStone::LinearAlgebra::AssignVector(b, 0, 0, 1); OrthancStone::GeometryToolbox::AlignVectorsWithRotation(r, a, b); ASSERT_TRUE(OrthancStone::LinearAlgebra::IsRotationMatrix(r)); - ASSERT_TRUE(IsEqualVector(OrthancStone::LinearAlgebra::Product(r, a), b)); + ASSERT_TRUE(IsEqualRotationVector(OrthancStone::LinearAlgebra::Product(r, a), b)); OrthancStone::LinearAlgebra::AssignVector(a, 0, 1, 0); OrthancStone::LinearAlgebra::AssignVector(b, 0, 0, 1); OrthancStone::GeometryToolbox::AlignVectorsWithRotation(r, a, b); ASSERT_TRUE(OrthancStone::LinearAlgebra::IsRotationMatrix(r)); - ASSERT_TRUE(IsEqualVector(OrthancStone::LinearAlgebra::Product(r, a), b)); + ASSERT_TRUE(IsEqualRotationVector(OrthancStone::LinearAlgebra::Product(r, a), b)); OrthancStone::LinearAlgebra::AssignVector(a, 0, 0, 1); OrthancStone::LinearAlgebra::AssignVector(b, 0, 0, 1); OrthancStone::GeometryToolbox::AlignVectorsWithRotation(r, a, b); ASSERT_TRUE(OrthancStone::LinearAlgebra::IsRotationMatrix(r)); - ASSERT_TRUE(IsEqualVector(OrthancStone::LinearAlgebra::Product(r, a), b)); + ASSERT_TRUE(IsEqualRotationVector(OrthancStone::LinearAlgebra::Product(r, a), b)); OrthancStone::LinearAlgebra::AssignVector(a, 0, 0, 0); OrthancStone::LinearAlgebra::AssignVector(b, 0, 0, 1); @@ -624,11 +625,11 @@ // TODO: Deal with opposite vectors /* - OrthancStone::LinearAlgebra::AssignVector(a, 0, 0, -1); - OrthancStone::LinearAlgebra::AssignVector(b, 0, 0, 1); - OrthancStone::GeometryToolbox::AlignVectorsWithRotation(r, a, b); - OrthancStone::LinearAlgebra::Print(r); - OrthancStone::LinearAlgebra::Print(boost::numeric::ublas::prod(r, a)); + OrthancStone::LinearAlgebra::AssignVector(a, 0, 0, -1); + OrthancStone::LinearAlgebra::AssignVector(b, 0, 0, 1); + OrthancStone::GeometryToolbox::AlignVectorsWithRotation(r, a, b); + OrthancStone::LinearAlgebra::Print(r); + OrthancStone::LinearAlgebra::Print(boost::numeric::ublas::prod(r, a)); */ } @@ -639,13 +640,39 @@ ASSERT_TRUE(Deprecated::MessagingToolbox::ParseJson(response, source.c_str(), source.size())); } + + +static bool IsEqualVectorL1(OrthancStone::Vector a, + OrthancStone::Vector b) +{ + if (a.size() != b.size()) + { + return false; + } + else + { + for (size_t i = 0; i < a.size(); i++) + { + if (!OrthancStone::LinearAlgebra::IsNear(a[i], b[i], 0.0001)) + { + return false; + } + } + + return true; + } +} + + TEST(VolumeImageGeometry, Basic) { - OrthancStone::VolumeImageGeometry g; + using namespace OrthancStone; + + VolumeImageGeometry g; g.SetSizeInVoxels(10, 20, 30); g.SetVoxelDimensions(1, 2, 3); - OrthancStone::Vector p = g.GetCoordinates(0, 0, 0); + Vector p = g.GetCoordinates(0, 0, 0); ASSERT_EQ(3u, p.size()); ASSERT_DOUBLE_EQ(-1.0 / 2.0, p[0]); ASSERT_DOUBLE_EQ(-2.0 / 2.0, p[1]); @@ -656,69 +683,148 @@ ASSERT_DOUBLE_EQ(-2.0 / 2.0 + 20.0 * 2.0, p[1]); ASSERT_DOUBLE_EQ(-3.0 / 2.0 + 30.0 * 3.0, p[2]); - OrthancStone::VolumeProjection proj; + VolumeProjection proj; ASSERT_TRUE(g.DetectProjection(proj, g.GetAxialGeometry().GetNormal())); - ASSERT_EQ(OrthancStone::VolumeProjection_Axial, proj); + ASSERT_EQ(VolumeProjection_Axial, proj); ASSERT_TRUE(g.DetectProjection(proj, g.GetCoronalGeometry().GetNormal())); - ASSERT_EQ(OrthancStone::VolumeProjection_Coronal, proj); + ASSERT_EQ(VolumeProjection_Coronal, proj); ASSERT_TRUE(g.DetectProjection(proj, g.GetSagittalGeometry().GetNormal())); - ASSERT_EQ(OrthancStone::VolumeProjection_Sagittal, proj); + ASSERT_EQ(VolumeProjection_Sagittal, proj); - ASSERT_EQ(10u, g.GetProjectionWidth(OrthancStone::VolumeProjection_Axial)); - ASSERT_EQ(20u, g.GetProjectionHeight(OrthancStone::VolumeProjection_Axial)); - ASSERT_EQ(30u, g.GetProjectionDepth(OrthancStone::VolumeProjection_Axial)); - ASSERT_EQ(10u, g.GetProjectionWidth(OrthancStone::VolumeProjection_Coronal)); - ASSERT_EQ(30u, g.GetProjectionHeight(OrthancStone::VolumeProjection_Coronal)); - ASSERT_EQ(20u, g.GetProjectionDepth(OrthancStone::VolumeProjection_Coronal)); - ASSERT_EQ(20u, g.GetProjectionWidth(OrthancStone::VolumeProjection_Sagittal)); - ASSERT_EQ(30u, g.GetProjectionHeight(OrthancStone::VolumeProjection_Sagittal)); - ASSERT_EQ(10u, g.GetProjectionDepth(OrthancStone::VolumeProjection_Sagittal)); + ASSERT_EQ(10u, g.GetProjectionWidth(VolumeProjection_Axial)); + ASSERT_EQ(20u, g.GetProjectionHeight(VolumeProjection_Axial)); + ASSERT_EQ(30u, g.GetProjectionDepth(VolumeProjection_Axial)); + ASSERT_EQ(10u, g.GetProjectionWidth(VolumeProjection_Coronal)); + ASSERT_EQ(30u, g.GetProjectionHeight(VolumeProjection_Coronal)); + ASSERT_EQ(20u, g.GetProjectionDepth(VolumeProjection_Coronal)); + ASSERT_EQ(20u, g.GetProjectionWidth(VolumeProjection_Sagittal)); + ASSERT_EQ(30u, g.GetProjectionHeight(VolumeProjection_Sagittal)); + ASSERT_EQ(10u, g.GetProjectionDepth(VolumeProjection_Sagittal)); - p = g.GetVoxelDimensions(OrthancStone::VolumeProjection_Axial); + p = g.GetVoxelDimensions(VolumeProjection_Axial); ASSERT_EQ(3u, p.size()); ASSERT_DOUBLE_EQ(1, p[0]); ASSERT_DOUBLE_EQ(2, p[1]); ASSERT_DOUBLE_EQ(3, p[2]); - p = g.GetVoxelDimensions(OrthancStone::VolumeProjection_Coronal); + p = g.GetVoxelDimensions(VolumeProjection_Coronal); ASSERT_EQ(3u, p.size()); ASSERT_DOUBLE_EQ(1, p[0]); ASSERT_DOUBLE_EQ(3, p[1]); ASSERT_DOUBLE_EQ(2, p[2]); - p = g.GetVoxelDimensions(OrthancStone::VolumeProjection_Sagittal); + p = g.GetVoxelDimensions(VolumeProjection_Sagittal); ASSERT_EQ(3u, p.size()); ASSERT_DOUBLE_EQ(2, p[0]); ASSERT_DOUBLE_EQ(3, p[1]); ASSERT_DOUBLE_EQ(1, p[2]); - ASSERT_EQ(0, (int) OrthancStone::VolumeProjection_Axial); - ASSERT_EQ(1, (int) OrthancStone::VolumeProjection_Coronal); - ASSERT_EQ(2, (int) OrthancStone::VolumeProjection_Sagittal); + // Loop over all the voxels of the volume + for (unsigned int z = 0; z < g.GetDepth(); z++) + { + const float zz = (0.5f + static_cast<float>(z)) / static_cast<float>(g.GetDepth()); // Z-center of the voxel + + for (unsigned int y = 0; y < g.GetHeight(); y++) + { + const float yy = (0.5f + static_cast<float>(y)) / static_cast<float>(g.GetHeight()); // Y-center of the voxel + + for (unsigned int x = 0; x < g.GetWidth(); x++) + { + const float xx = (0.5f + static_cast<float>(x)) / static_cast<float>(g.GetWidth()); // X-center of the voxel + + const float sx = 1.0f; + const float sy = 2.0f; + const float sz = 3.0f; + + Vector p = g.GetCoordinates(xx, yy, zz); + + Vector q = (g.GetAxialGeometry().MapSliceToWorldCoordinates( + static_cast<double>(x) * sx, + static_cast<double>(y) * sy) + + z * sz * g.GetAxialGeometry().GetNormal()); + ASSERT_TRUE(IsEqualVectorL1(p, q)); + + q = (g.GetCoronalGeometry().MapSliceToWorldCoordinates( + static_cast<double>(x) * sx, + static_cast<double>(g.GetDepth() - 1 - z) * sz) + + y * sy * g.GetCoronalGeometry().GetNormal()); + ASSERT_TRUE(IsEqualVectorL1(p, q)); + + /** + * WARNING: In sagittal geometry, the normal points to + * REDUCING X-axis in the 3D world. This is necessary to keep + * the right-hand coordinate system. Hence the "-". + **/ + q = (g.GetSagittalGeometry().MapSliceToWorldCoordinates( + static_cast<double>(y) * sy, + static_cast<double>(g.GetDepth() - 1 - z) * sz) + + x * sx * (-g.GetSagittalGeometry().GetNormal())); + ASSERT_TRUE(IsEqualVectorL1(p, q)); + } + } + } + + ASSERT_EQ(0, (int) VolumeProjection_Axial); + ASSERT_EQ(1, (int) VolumeProjection_Coronal); + ASSERT_EQ(2, (int) VolumeProjection_Sagittal); for (int p = 0; p < 3; p++) { - OrthancStone::VolumeProjection projection = (OrthancStone::VolumeProjection) p; - const OrthancStone::CoordinateSystem3D& s = g.GetProjectionGeometry(projection); + VolumeProjection projection = (VolumeProjection) p; + const CoordinateSystem3D& s = g.GetProjectionGeometry(projection); ASSERT_THROW(g.GetProjectionSlice(projection, g.GetProjectionDepth(projection)), Orthanc::OrthancException); for (unsigned int i = 0; i < g.GetProjectionDepth(projection); i++) { - OrthancStone::CoordinateSystem3D plane = g.GetProjectionSlice(projection, i); + CoordinateSystem3D plane = g.GetProjectionSlice(projection, i); - ASSERT_TRUE(IsEqualVector(plane.GetOrigin(), s.GetOrigin() + static_cast<double>(i) * - s.GetNormal() * g.GetVoxelDimensions(projection)[2])); - ASSERT_TRUE(IsEqualVector(plane.GetAxisX(), s.GetAxisX())); - ASSERT_TRUE(IsEqualVector(plane.GetAxisY(), s.GetAxisY())); + if (projection == VolumeProjection_Sagittal) + { + ASSERT_TRUE(IsEqualVectorL1(plane.GetOrigin(), s.GetOrigin() + static_cast<double>(i) * + (-s.GetNormal()) * g.GetVoxelDimensions(projection)[2])); + } + else + { + ASSERT_TRUE(IsEqualVectorL1(plane.GetOrigin(), s.GetOrigin() + static_cast<double>(i) * + s.GetNormal() * g.GetVoxelDimensions(projection)[2])); + } + + ASSERT_TRUE(IsEqualVectorL1(plane.GetAxisX(), s.GetAxisX())); + ASSERT_TRUE(IsEqualVectorL1(plane.GetAxisY(), s.GetAxisY())); unsigned int slice; - OrthancStone::VolumeProjection q; + VolumeProjection q; ASSERT_TRUE(g.DetectSlice(q, slice, plane)); ASSERT_EQ(projection, q); - ASSERT_EQ(i, slice); + ASSERT_EQ(i, slice); } } } + +TEST(LinearAlgebra, ParseVectorLocale) +{ + OrthancStone::Vector v; + + ASSERT_TRUE(OrthancStone::LinearAlgebra::ParseVector(v, "1.2")); + ASSERT_EQ(1u, v.size()); + ASSERT_DOUBLE_EQ(1.2, v[0]); + + ASSERT_TRUE(OrthancStone::LinearAlgebra::ParseVector(v, "-1.2e+2")); + ASSERT_EQ(1u, v.size()); + ASSERT_DOUBLE_EQ(-120.0, v[0]); + + ASSERT_TRUE(OrthancStone::LinearAlgebra::ParseVector(v, "-1e-2\\2")); + ASSERT_EQ(2u, v.size()); + ASSERT_DOUBLE_EQ(-0.01, v[0]); + ASSERT_DOUBLE_EQ(2.0, v[1]); + + ASSERT_TRUE(OrthancStone::LinearAlgebra::ParseVector(v, "1.3671875\\1.3671875")); + ASSERT_EQ(2u, v.size()); + ASSERT_DOUBLE_EQ(1.3671875, v[0]); + ASSERT_DOUBLE_EQ(1.3671875, v[1]); +} + + int main(int argc, char **argv) { Orthanc::Logging::Initialize();