# HG changeset patch # User Sebastien Jodogne # Date 1541408778 -3600 # Node ID d6136a7e914d04ab2765c764f6a8d93ac97f2529 # Parent fe4befe03935c38c8c4407d28dde7e1a362bff6e# Parent 17d1814c2fd49bfe143fc19c955a85a40b5199cc making branch am-2 the new mainline diff -r fe4befe03935 -r d6136a7e914d .hgignore --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.hgignore Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,7 @@ +CMakeLists.txt.user +Platforms/Generic/ThirdPartyDownloads/ +Applications/Qt/archive/ +Applications/Samples/ThirdPartyDownloads/ +Applications/Samples/build-wasm/ +Applications/Samples/build-web/ +.vscode/ diff -r fe4befe03935 -r d6136a7e914d Applications/BasicApplicationContext.cpp --- a/Applications/BasicApplicationContext.cpp Mon Nov 05 10:04:56 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,144 +0,0 @@ -/** - * Stone of Orthanc - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2018 Osimis S.A., Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU Affero General Public License - * as published by the Free Software Foundation, either version 3 of - * the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - **/ - - -#include "BasicApplicationContext.h" - -namespace OrthancStone -{ - void BasicApplicationContext::UpdateThread(BasicApplicationContext* that) - { - while (!that->stopped_) - { - { - ViewportLocker locker(*that); - locker.GetViewport().UpdateContent(); - } - - boost::this_thread::sleep(boost::posix_time::milliseconds(that->updateDelay_)); - } - } - - - BasicApplicationContext::BasicApplicationContext(Orthanc::WebServiceParameters& orthanc) : - oracle_(viewportMutex_, 4), // Use 4 threads to download - //oracle_(viewportMutex_, 1), // Disable threading to be reproducible - webService_(oracle_, orthanc), - stopped_(true), - updateDelay_(100) // By default, 100ms between each refresh of the content - { - srand(time(NULL)); - } - - - BasicApplicationContext::~BasicApplicationContext() - { - for (Interactors::iterator it = interactors_.begin(); it != interactors_.end(); ++it) - { - assert(*it != NULL); - delete *it; - } - - for (SlicedVolumes::iterator it = slicedVolumes_.begin(); it != slicedVolumes_.end(); ++it) - { - assert(*it != NULL); - delete *it; - } - - for (VolumeLoaders::iterator it = volumeLoaders_.begin(); it != volumeLoaders_.end(); ++it) - { - assert(*it != NULL); - delete *it; - } - } - - - IWidget& BasicApplicationContext::SetCentralWidget(IWidget* widget) // Takes ownership - { - viewport_.SetCentralWidget(widget); - return *widget; - } - - - ISlicedVolume& BasicApplicationContext::AddSlicedVolume(ISlicedVolume* volume) - { - if (volume == NULL) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); - } - else - { - slicedVolumes_.push_back(volume); - return *volume; - } - } - - - IVolumeLoader& BasicApplicationContext::AddVolumeLoader(IVolumeLoader* loader) - { - if (loader == NULL) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); - } - else - { - volumeLoaders_.push_back(loader); - return *loader; - } - } - - - IWorldSceneInteractor& BasicApplicationContext::AddInteractor(IWorldSceneInteractor* interactor) - { - if (interactor == NULL) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); - } - - interactors_.push_back(interactor); - - return *interactor; - } - - - void BasicApplicationContext::Start() - { - oracle_.Start(); - - if (viewport_.HasUpdateContent()) - { - stopped_ = false; - updateThread_ = boost::thread(UpdateThread, this); - } - } - - - void BasicApplicationContext::Stop() - { - stopped_ = true; - - if (updateThread_.joinable()) - { - updateThread_.join(); - } - - oracle_.Stop(); - } -} diff -r fe4befe03935 -r d6136a7e914d Applications/BasicApplicationContext.h --- a/Applications/BasicApplicationContext.h Mon Nov 05 10:04:56 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,102 +0,0 @@ -/** - * Stone of Orthanc - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2018 Osimis S.A., Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU Affero General Public License - * as published by the Free Software Foundation, either version 3 of - * the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - **/ - - -#pragma once - -#include "../Framework/Viewport/WidgetViewport.h" -#include "../Framework/Volumes/ISlicedVolume.h" -#include "../Framework/Volumes/IVolumeLoader.h" -#include "../Framework/Widgets/IWorldSceneInteractor.h" -#include "../Platforms/Generic/OracleWebService.h" - -#include -#include - -namespace OrthancStone -{ - class BasicApplicationContext : public boost::noncopyable - { - private: - typedef std::list SlicedVolumes; - typedef std::list VolumeLoaders; - typedef std::list Interactors; - - static void UpdateThread(BasicApplicationContext* that); - - Oracle oracle_; - OracleWebService webService_; - boost::mutex viewportMutex_; - WidgetViewport viewport_; - SlicedVolumes slicedVolumes_; - VolumeLoaders volumeLoaders_; - Interactors interactors_; - boost::thread updateThread_; - bool stopped_; - unsigned int updateDelay_; - - public: - class ViewportLocker : public boost::noncopyable - { - private: - boost::mutex::scoped_lock lock_; - IViewport& viewport_; - - public: - ViewportLocker(BasicApplicationContext& that) : - lock_(that.viewportMutex_), - viewport_(that.viewport_) - { - } - - IViewport& GetViewport() const - { - return viewport_; - } - }; - - - BasicApplicationContext(Orthanc::WebServiceParameters& orthanc); - - ~BasicApplicationContext(); - - IWidget& SetCentralWidget(IWidget* widget); // Takes ownership - - IWebService& GetWebService() - { - return webService_; - } - - ISlicedVolume& AddSlicedVolume(ISlicedVolume* volume); - - IVolumeLoader& AddVolumeLoader(IVolumeLoader* loader); - - IWorldSceneInteractor& AddInteractor(IWorldSceneInteractor* interactor); - - void Start(); - - void Stop(); - - void SetUpdateDelay(unsigned int delay) // In milliseconds - { - updateDelay_ = delay; - } - }; -} diff -r fe4befe03935 -r d6136a7e914d Applications/Commands/BaseCommandBuilder.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Applications/Commands/BaseCommandBuilder.cpp Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,49 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + +#include "BaseCommandBuilder.h" +#include "Core/OrthancException.h" +#include +#include "Framework/StoneException.h" + +namespace OrthancStone +{ + ICommand* BaseCommandBuilder::CreateFromJson(const Json::Value& commandJson) + { + if (!commandJson.isObject() || !commandJson["command"].isString()) + { + throw StoneException(ErrorCode_CommandJsonInvalidFormat); + } + + if (commandJson["commandType"].isString() && commandJson["commandType"].asString() == "generic-no-arg-command") + { + printf("creating a simple command\n"); + return new GenericNoArgCommand(commandJson["command"].asString().c_str()); + } + else if (commandJson["commandType"].isString() && commandJson["commandType"].asString() == "generic-one-string-arg-command") + { + printf("creating a simple command\n"); + return new GenericNoArgCommand(commandJson["command"].asString().c_str()); + } + + return NULL; + } + +} diff -r fe4befe03935 -r d6136a7e914d Applications/Commands/BaseCommandBuilder.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Applications/Commands/BaseCommandBuilder.h Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,38 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + +#pragma once + +#include +#include + +#include "ICommand.h" +#include "../../Applications/Commands/ICommandBuilder.h" + +// TODO: must be reworked completely (check trello) + +namespace OrthancStone +{ + class BaseCommandBuilder : public ICommandBuilder + { + public: + virtual ICommand* CreateFromJson(const Json::Value& commandJson); + }; +} diff -r fe4befe03935 -r d6136a7e914d Applications/Commands/BaseCommands.yml --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Applications/Commands/BaseCommands.yml Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,10 @@ +SelectTool: + target: Application + toolName: string + comment: Selects the current application tool +DownloadDicom: + target: LayerWidget + comment: Downloads the slice currently displayed in the LayerWidget +Export: + target: IWidget + comment: Export the content of the widget \ No newline at end of file diff -r fe4befe03935 -r d6136a7e914d Applications/Commands/ICommand.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Applications/Commands/ICommand.h Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,94 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include + +// TODO: must be reworked completely (check trello) + +namespace OrthancStone +{ + class ICommand // TODO noncopyable + { + protected: + std::string name_; + ICommand(const std::string& name) + : name_(name) + {} + public: + virtual void Execute() = 0; +// virtual void Configure(const Json::Value& arguments) = 0; + const std::string& GetName() const + { + return name_; + } + }; + + + template + class BaseCommand : public ICommand + { + protected: + BaseCommand(const std::string& name) + : ICommand(name) + {} + + public: + static ICommand* Create() { + return new TCommand(); + } + + virtual void Configure(const Json::Value& arguments) { + } + }; + + class NoopCommand : public BaseCommand + { + public: + NoopCommand() + : BaseCommand("noop") + {} + virtual void Execute() {} + }; + + class GenericNoArgCommand : public BaseCommand + { + public: + GenericNoArgCommand(const std::string& name) + : BaseCommand(name) + {} + virtual void Execute() {} // TODO currently not used but this is not nice at all ! + }; + + class GenericOneStringArgCommand : public BaseCommand + { + std::string argument_; + public: + GenericOneStringArgCommand(const std::string& name, const std::string& argument) + : BaseCommand(name) + {} + + const std::string& GetArgument() const {return argument_;} + virtual void Execute() {} // TODO currently not used but this is not nice at all ! + }; + +} diff -r fe4befe03935 -r d6136a7e914d Applications/Commands/ICommandBuilder.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Applications/Commands/ICommandBuilder.h Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,37 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include +#include + +#include "ICommand.h" + +namespace OrthancStone +{ + + class ICommandBuilder : public boost::noncopyable + { + public: + virtual ICommand* CreateFromJson(const Json::Value& commandJson) = 0; + }; +} diff -r fe4befe03935 -r d6136a7e914d Applications/Commands/ICommandExecutor.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Applications/Commands/ICommandExecutor.h Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,32 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include + +namespace OrthancStone +{ + class ICommandExecutor : public boost::noncopyable + { + + }; +} diff -r fe4befe03935 -r d6136a7e914d Applications/Generic/NativeStoneApplicationContext.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Applications/Generic/NativeStoneApplicationContext.cpp Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,80 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#include "NativeStoneApplicationContext.h" +#include "../../Platforms/Generic/OracleWebService.h" + +namespace OrthancStone +{ + IWidget& NativeStoneApplicationContext::SetCentralWidget(IWidget* widget) // Takes ownership + { + centralViewport_->SetCentralWidget(widget); + return *widget; + } + + + void NativeStoneApplicationContext::UpdateThread(NativeStoneApplicationContext* that) + { + while (!that->stopped_) + { + { + GlobalMutexLocker locker(*that); + that->GetCentralViewport().UpdateContent(); + } + + boost::this_thread::sleep(boost::posix_time::milliseconds(that->updateDelayInMs_)); + } + } + + + NativeStoneApplicationContext::NativeStoneApplicationContext() : + centralViewport_(new OrthancStone::WidgetViewport()), + stopped_(true), + updateDelayInMs_(100) // By default, 100ms between each refresh of the content + { + srand(time(NULL)); + } + + + void NativeStoneApplicationContext::Start() + { + dynamic_cast(webService_)->Start(); + + if (centralViewport_->HasUpdateContent()) + { + stopped_ = false; + updateThread_ = boost::thread(UpdateThread, this); + } + } + + + void NativeStoneApplicationContext::Stop() + { + stopped_ = true; + + if (updateThread_.joinable()) + { + updateThread_.join(); + } + + dynamic_cast(webService_)->Stop(); + } +} diff -r fe4befe03935 -r d6136a7e914d Applications/Generic/NativeStoneApplicationContext.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Applications/Generic/NativeStoneApplicationContext.h Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,79 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "../../Framework/Viewport/WidgetViewport.h" +#include "../../Framework/Volumes/ISlicedVolume.h" +#include "../../Framework/Volumes/IVolumeLoader.h" + +#include +#include +#include "../StoneApplicationContext.h" + +namespace OrthancStone +{ + class NativeStoneApplicationContext : public StoneApplicationContext + { + private: + + static void UpdateThread(NativeStoneApplicationContext* that); + + boost::mutex globalMutex_; + std::auto_ptr centralViewport_; + boost::thread updateThread_; + bool stopped_; + unsigned int updateDelayInMs_; + + public: + class GlobalMutexLocker: public boost::noncopyable + { + boost::mutex::scoped_lock lock_; + public: + GlobalMutexLocker(NativeStoneApplicationContext& that): + lock_(that.globalMutex_) + { + } + }; + + NativeStoneApplicationContext(); + + virtual ~NativeStoneApplicationContext() + { + } + + virtual IWidget& SetCentralWidget(IWidget* widget); // Takes ownership + + IViewport& GetCentralViewport() + { + return *(centralViewport_.get()); + } + + void Start(); + + void Stop(); + + void SetUpdateDelay(unsigned int delayInMs) + { + updateDelayInMs_ = delayInMs; + } + }; +} diff -r fe4befe03935 -r d6136a7e914d Applications/Generic/NativeStoneApplicationRunner.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Applications/Generic/NativeStoneApplicationRunner.cpp Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,236 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#if ORTHANC_ENABLE_NATIVE != 1 +#error this file shall be included only with the ORTHANC_ENABLE_NATIVE set to 1 +#endif + +#include "NativeStoneApplicationRunner.h" +#include "NativeStoneApplicationContext.h" +#include + +#include "../../Framework/Toolbox/MessagingToolbox.h" + +#include +#include +#include +#include +#include "../../Platforms/Generic/OracleWebService.h" + +namespace OrthancStone +{ + // Anonymous namespace to avoid clashes against other compilation modules + namespace + { + class LogStatusBar : public IStatusBar + { + public: + virtual void ClearMessage() + { + } + + virtual void SetMessage(const std::string& message) + { + LOG(WARNING) << message; + } + }; + } + + int NativeStoneApplicationRunner::Execute(int argc, + char* argv[]) + { + /****************************************************************** + * Initialize all the subcomponents of Orthanc Stone + ******************************************************************/ + + Orthanc::Logging::Initialize(); + Orthanc::Toolbox::InitializeOpenSsl(); + Orthanc::HttpClient::GlobalInitialize(); + + Initialize(); + + /****************************************************************** + * Declare and parse the command-line options of the application + ******************************************************************/ + + boost::program_options::options_description options; + + { // generic options + boost::program_options::options_description generic("Generic options"); + generic.add_options() + ("help", "Display this help and exit") + ("verbose", "Be verbose in logs") + ("orthanc", boost::program_options::value()->default_value("http://localhost:8042/"), + "URL to the Orthanc server") + ("username", "Username for the Orthanc server") + ("password", "Password for the Orthanc server") + ("https-verify", boost::program_options::value()->default_value(true), "Check HTTPS certificates") + ; + + options.add(generic); + } + + // platform specific options + DeclareCommandLineOptions(options); + + // application specific options + application_.DeclareStartupOptions(options); + + boost::program_options::variables_map parameters; + bool error = false; + + try + { + boost::program_options::store(boost::program_options::command_line_parser(argc, argv). + options(options).run(), parameters); + boost::program_options::notify(parameters); + } + catch (boost::program_options::error& e) + { + LOG(ERROR) << "Error while parsing the command-line arguments: " << e.what(); + error = true; + } + + + /****************************************************************** + * Configure the application with the command-line parameters + ******************************************************************/ + + if (error || parameters.count("help")) + { + std::cout << std::endl + << "Usage: " << argv[0] << " [OPTION]..." + << std::endl + << "Orthanc, lightweight, RESTful DICOM server for healthcare and medical research." + << std::endl << std::endl + << "Demonstration application of Orthanc Stone in native environment." + << std::endl; + + std::cout << options << "\n"; + return error ? -1 : 0; + } + + if (parameters.count("https-verify") && + !parameters["https-verify"].as()) + { + LOG(WARNING) << "Turning off verification of HTTPS certificates (unsafe)"; + Orthanc::HttpClient::ConfigureSsl(false, ""); + } + + if (parameters.count("verbose")) + { + Orthanc::Logging::EnableInfoLevel(true); + } + + ParseCommandLineOptions(parameters); + + + bool success = true; + try + { + /**************************************************************** + * Initialize the connection to the Orthanc server + ****************************************************************/ + + Orthanc::WebServiceParameters webServiceParameters; + + if (parameters.count("orthanc")) + { + webServiceParameters.SetUrl(parameters["orthanc"].as()); + } + + if (parameters.count("username") && parameters.count("password")) + { + webServiceParameters.SetCredentials(parameters["username"].as(), + parameters["password"].as()); + } + + LOG(WARNING) << "URL to the Orthanc REST API: " << webServiceParameters.GetUrl(); + + { + OrthancPlugins::OrthancHttpConnection orthanc(webServiceParameters); + if (!MessagingToolbox::CheckOrthancVersion(orthanc)) + { + LOG(ERROR) << "Your version of Orthanc is incompatible with Stone of Orthanc, please upgrade"; + throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol); + } + } + + + /**************************************************************** + * Initialize the application + ****************************************************************/ + + LOG(WARNING) << "Creating the widgets of the application"; + + LogStatusBar statusBar; + + NativeStoneApplicationContext context; + Oracle oracle(4); // use 4 threads to download content + OracleWebService webService(broker_, oracle, webServiceParameters, context); + context.SetWebService(webService); + + application_.Initialize(&context, statusBar, parameters); + + { + NativeStoneApplicationContext::GlobalMutexLocker locker(context); + context.SetCentralWidget(application_.GetCentralWidget()); + context.GetCentralViewport().SetStatusBar(statusBar); + } + + std::string title = application_.GetTitle(); + if (title.empty()) + { + title = "Stone of Orthanc"; + } + + /**************************************************************** + * Run the application + ****************************************************************/ + + Run(context, title, argc, argv); + + /**************************************************************** + * Finalize the application + ****************************************************************/ + + LOG(WARNING) << "The application is stopping"; + application_.Finalize(); + } + catch (Orthanc::OrthancException& e) + { + LOG(ERROR) << "EXCEPTION: " << e.What(); + success = false; + } + + + /****************************************************************** + * Finalize all the subcomponents of Orthanc Stone + ******************************************************************/ + + Finalize(); + Orthanc::HttpClient::GlobalFinalize(); + Orthanc::Toolbox::FinalizeOpenSsl(); + + return (success ? 0 : -1); + } + +} diff -r fe4befe03935 -r d6136a7e914d Applications/Generic/NativeStoneApplicationRunner.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Applications/Generic/NativeStoneApplicationRunner.h Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,58 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "../IStoneApplication.h" + +#if ORTHANC_ENABLE_NATIVE != 1 +#error this file shall be included only with the ORTHANC_ENABLE_NATIVE set to 1 +#endif + +namespace OrthancStone +{ + class NativeStoneApplicationContext; + + class NativeStoneApplicationRunner + { + protected: + MessageBroker& broker_; + IStoneApplication& application_; + public: + + NativeStoneApplicationRunner(MessageBroker& broker, + IStoneApplication& application) + : broker_(broker), + application_(application) + { + } + int Execute(int argc, + char* argv[]); + + virtual void Initialize() = 0; + virtual void DeclareCommandLineOptions(boost::program_options::options_description& options) = 0; + virtual void ParseCommandLineOptions(const boost::program_options::variables_map& parameters) = 0; + + virtual void Run(NativeStoneApplicationContext& context, const std::string& title, int argc, char* argv[]) = 0; + virtual void Finalize() = 0; + }; + +} diff -r fe4befe03935 -r d6136a7e914d Applications/IBasicApplication.cpp --- a/Applications/IBasicApplication.cpp Mon Nov 05 10:04:56 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,300 +0,0 @@ -/** - * Stone of Orthanc - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2018 Osimis S.A., Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU Affero General Public License - * as published by the Free Software Foundation, either version 3 of - * the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - **/ - - -#include "IBasicApplication.h" - -#include "../Framework/Toolbox/MessagingToolbox.h" -#include "Sdl/SdlEngine.h" - -#include -#include -#include -#include - -namespace OrthancStone -{ - // Anonymous namespace to avoid clashes against other compilation modules - namespace - { - class LogStatusBar : public IStatusBar - { - public: - virtual void ClearMessage() - { - } - - virtual void SetMessage(const std::string& message) - { - LOG(WARNING) << message; - } - }; - } - - -#if ORTHANC_ENABLE_SDL == 1 - static void DeclareSdlCommandLineOptions(boost::program_options::options_description& options) - { - // Declare the supported parameters - boost::program_options::options_description generic("Generic options"); - generic.add_options() - ("help", "Display this help and exit") - ("verbose", "Be verbose in logs") - ("orthanc", boost::program_options::value()->default_value("http://localhost:8042/"), - "URL to the Orthanc server") - ("username", "Username for the Orthanc server") - ("password", "Password for the Orthanc server") - ("https-verify", boost::program_options::value()->default_value(true), "Check HTTPS certificates") - ; - - options.add(generic); - - boost::program_options::options_description sdl("SDL options"); - sdl.add_options() - ("width", boost::program_options::value()->default_value(1024), "Initial width of the SDL window") - ("height", boost::program_options::value()->default_value(768), "Initial height of the SDL window") - ("opengl", boost::program_options::value()->default_value(true), "Enable OpenGL in SDL") - ; - - options.add(sdl); - } - - - int IBasicApplication::ExecuteWithSdl(IBasicApplication& application, - int argc, - char* argv[]) - { - /****************************************************************** - * Initialize all the subcomponents of Orthanc Stone - ******************************************************************/ - - Orthanc::Logging::Initialize(); - Orthanc::Toolbox::InitializeOpenSsl(); - Orthanc::HttpClient::GlobalInitialize(); - SdlWindow::GlobalInitialize(); - - - /****************************************************************** - * Declare and parse the command-line options of the application - ******************************************************************/ - - boost::program_options::options_description options; - DeclareSdlCommandLineOptions(options); - application.DeclareCommandLineOptions(options); - - boost::program_options::variables_map parameters; - bool error = false; - - try - { - boost::program_options::store(boost::program_options::command_line_parser(argc, argv). - options(options).run(), parameters); - boost::program_options::notify(parameters); - } - catch (boost::program_options::error& e) - { - LOG(ERROR) << "Error while parsing the command-line arguments: " << e.what(); - error = true; - } - - - /****************************************************************** - * Configure the application with the command-line parameters - ******************************************************************/ - - if (error || parameters.count("help")) - { - std::cout << std::endl - << "Usage: " << argv[0] << " [OPTION]..." - << std::endl - << "Orthanc, lightweight, RESTful DICOM server for healthcare and medical research." - << std::endl << std::endl - << "Demonstration application of Orthanc Stone using SDL." - << std::endl; - - std::cout << options << "\n"; - return error ? -1 : 0; - } - - if (parameters.count("https-verify") && - !parameters["https-verify"].as()) - { - LOG(WARNING) << "Turning off verification of HTTPS certificates (unsafe)"; - Orthanc::HttpClient::ConfigureSsl(false, ""); - } - - if (parameters.count("verbose")) - { - Orthanc::Logging::EnableInfoLevel(true); - } - - if (!parameters.count("width") || - !parameters.count("height") || - !parameters.count("opengl")) - { - LOG(ERROR) << "Parameter \"width\", \"height\" or \"opengl\" is missing"; - return -1; - } - - int w = parameters["width"].as(); - int h = parameters["height"].as(); - if (w <= 0 || h <= 0) - { - LOG(ERROR) << "Parameters \"width\" and \"height\" must be positive"; - return -1; - } - - unsigned int width = static_cast(w); - unsigned int height = static_cast(h); - LOG(WARNING) << "Initial display size: " << width << "x" << height; - - bool opengl = parameters["opengl"].as(); - if (opengl) - { - LOG(WARNING) << "OpenGL is enabled, disable it with option \"--opengl=off\" if the application crashes"; - } - else - { - LOG(WARNING) << "OpenGL is disabled, enable it with option \"--opengl=on\" for best performance"; - } - - bool success = true; - try - { - /**************************************************************** - * Initialize the connection to the Orthanc server - ****************************************************************/ - - Orthanc::WebServiceParameters webService; - - if (parameters.count("orthanc")) - { - webService.SetUrl(parameters["orthanc"].as()); - } - - std::string username, password; - - if (parameters.count("username")) - { - username = parameters["username"].as(); - } - - if (parameters.count("password")) - { - password = parameters["password"].as(); - } - - if (!username.empty() || - !password.empty()) - { - webService.SetCredentials(username, password); - } - - LOG(WARNING) << "URL to the Orthanc REST API: " << webService.GetUrl(); - - { - OrthancPlugins::OrthancHttpConnection orthanc(webService); - if (!MessagingToolbox::CheckOrthancVersion(orthanc)) - { - LOG(ERROR) << "Your version of Orthanc is incompatible with Stone of Orthanc, please upgrade"; - throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol); - } - } - - - /**************************************************************** - * Initialize the application - ****************************************************************/ - - LOG(WARNING) << "Creating the widgets of the application"; - - LogStatusBar statusBar; - BasicApplicationContext context(webService); - - application.Initialize(context, statusBar, parameters); - - { - BasicApplicationContext::ViewportLocker locker(context); - locker.GetViewport().SetStatusBar(statusBar); - } - - std::string title = application.GetTitle(); - if (title.empty()) - { - title = "Stone of Orthanc"; - } - - { - /************************************************************** - * Run the application inside a SDL window - **************************************************************/ - - LOG(WARNING) << "Starting the application"; - - SdlWindow window(title.c_str(), width, height, opengl); - SdlEngine sdl(window, context); - - { - BasicApplicationContext::ViewportLocker locker(context); - locker.GetViewport().Register(sdl); // (*) - } - - context.Start(); - sdl.Run(); - - LOG(WARNING) << "Stopping the application"; - - // Don't move the "Stop()" command below out of the block, - // otherwise the application might crash, because the - // "SdlEngine" is an observer of the viewport (*) and the - // update thread started by "context.Start()" would call a - // destructed object (the "SdlEngine" is deleted with the - // lexical scope). - context.Stop(); - } - - - /**************************************************************** - * Finalize the application - ****************************************************************/ - - LOG(WARNING) << "The application has stopped"; - application.Finalize(); - } - catch (Orthanc::OrthancException& e) - { - LOG(ERROR) << "EXCEPTION: " << e.What(); - success = false; - } - - - /****************************************************************** - * Finalize all the subcomponents of Orthanc Stone - ******************************************************************/ - - SdlWindow::GlobalFinalize(); - Orthanc::HttpClient::GlobalFinalize(); - Orthanc::Toolbox::FinalizeOpenSsl(); - - return (success ? 0 : -1); - } -#endif - -} diff -r fe4befe03935 -r d6136a7e914d Applications/IBasicApplication.h --- a/Applications/IBasicApplication.h Mon Nov 05 10:04:56 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,57 +0,0 @@ -/** - * Stone of Orthanc - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2018 Osimis S.A., Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU Affero General Public License - * as published by the Free Software Foundation, either version 3 of - * the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - **/ - - -#pragma once - -#include "BasicApplicationContext.h" - -#include - -#if ORTHANC_ENABLE_SDL == 1 -# include // Necessary to avoid undefined reference to `SDL_main' -#endif - -namespace OrthancStone -{ - class IBasicApplication : public boost::noncopyable - { - public: - virtual ~IBasicApplication() - { - } - - virtual void DeclareCommandLineOptions(boost::program_options::options_description& options) = 0; - - virtual std::string GetTitle() const = 0; - - virtual void Initialize(BasicApplicationContext& context, - IStatusBar& statusBar, - const boost::program_options::variables_map& parameters) = 0; - - virtual void Finalize() = 0; - -#if ORTHANC_ENABLE_SDL == 1 - static int ExecuteWithSdl(IBasicApplication& application, - int argc, - char* argv[]); -#endif - }; -} diff -r fe4befe03935 -r d6136a7e914d Applications/IStoneApplication.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Applications/IStoneApplication.h Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,74 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "StoneApplicationContext.h" +#include +#include "../Framework/Viewport/WidgetViewport.h" +#include "json/json.h" +#include "Commands/ICommand.h" +#include "Commands/BaseCommandBuilder.h" + + +namespace OrthancStone +{ +#if ORTHANC_ENABLE_QT==1 + class QStoneMainWindow; +#endif + + // a StoneApplication is an application that can actually be executed + // in multiple environments. i.e: it can run natively integrated in a QtApplication + // or it can be executed as part of a WebPage when compiled into WebAssembly. + class IStoneApplication : public boost::noncopyable + { + protected: + StoneApplicationContext* context_; + + public: + virtual ~IStoneApplication() + { + } + + virtual void DeclareStartupOptions(boost::program_options::options_description& options) = 0; + virtual void Initialize(StoneApplicationContext* context, + IStatusBar& statusBar, + const boost::program_options::variables_map& parameters) = 0; +#if ORTHANC_ENABLE_WASM==1 + virtual void InitializeWasm() {} // specific initialization when the app is running in WebAssembly. This is called after the other Initialize() +#endif +#if ORTHANC_ENABLE_QT==1 + virtual QStoneMainWindow* CreateQtMainWindow() = 0; +#endif + + virtual std::string GetTitle() const = 0; + virtual IWidget* GetCentralWidget() = 0; + + virtual void Finalize() = 0; + + virtual BaseCommandBuilder& GetCommandBuilder() = 0; + + virtual void ExecuteCommand(ICommand& command) + { + } + }; + +} diff -r fe4befe03935 -r d6136a7e914d Applications/Qt/QCairoWidget.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Applications/Qt/QCairoWidget.cpp Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,183 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + +#include "QCairoWidget.h" + +#include +#include + +#include + +QCairoWidget::QCairoWidget(QWidget *parent) : + QWidget(parent), + context_(NULL) +{ + setFocusPolicy(Qt::StrongFocus); // catch keyPressEvents +} + +QCairoWidget::~QCairoWidget() +{ +} + +void QCairoWidget::SetContext(OrthancStone::NativeStoneApplicationContext& context) +{ + context_ = &context; + context_->GetCentralViewport().Register(*this); // get notified each time the content of the central viewport changes +} + +void QCairoWidget::paintEvent(QPaintEvent* /*event*/) +{ + QPainter painter(this); + + if (image_.get() != NULL && context_ != NULL) + { + OrthancStone::NativeStoneApplicationContext::GlobalMutexLocker locker(*context_); + OrthancStone::IViewport& viewport = context_->GetCentralViewport(); + Orthanc::ImageAccessor a; + surface_.GetWriteableAccessor(a); + viewport.Render(a); + painter.drawImage(0, 0, *image_); + } + else + { + painter.fillRect(rect(), Qt::red); + } +} + +OrthancStone::KeyboardModifiers GetKeyboardModifiers(QInputEvent* event) +{ + Qt::KeyboardModifiers qtModifiers = event->modifiers(); + int stoneModifiers = static_cast(OrthancStone::KeyboardModifiers_None); + if ((qtModifiers & Qt::AltModifier) != 0) + { + stoneModifiers |= static_cast(OrthancStone::KeyboardModifiers_Alt); + } + if ((qtModifiers & Qt::ControlModifier) != 0) + { + stoneModifiers |= static_cast(OrthancStone::KeyboardModifiers_Control); + } + if ((qtModifiers & Qt::ShiftModifier) != 0) + { + stoneModifiers |= static_cast(OrthancStone::KeyboardModifiers_Shift); + } + return static_cast(stoneModifiers); +} + +void QCairoWidget::mousePressEvent(QMouseEvent* event) +{ + OrthancStone::KeyboardModifiers stoneModifiers = GetKeyboardModifiers(event); + + OrthancStone::MouseButton button; + + switch (event->button()) + { + case Qt::LeftButton: + button = OrthancStone::MouseButton_Left; + break; + + case Qt::RightButton: + button = OrthancStone::MouseButton_Right; + break; + + case Qt::MiddleButton: + button = OrthancStone::MouseButton_Middle; + break; + + default: + return; // Unsupported button + } + context_->GetCentralViewport().MouseDown(button, event->pos().x(), event->pos().y(), stoneModifiers); +} + + +void QCairoWidget::mouseReleaseEvent(QMouseEvent* /*eventNotUsed*/) +{ + context_->GetCentralViewport().MouseLeave(); +} + + +void QCairoWidget::mouseMoveEvent(QMouseEvent* event) +{ + context_->GetCentralViewport().MouseMove(event->pos().x(), event->pos().y()); +} + + +void QCairoWidget::wheelEvent(QWheelEvent * event) +{ + OrthancStone::KeyboardModifiers stoneModifiers = GetKeyboardModifiers(event); + + if (event->orientation() == Qt::Vertical) + { + if (event->delta() < 0) // TODO: compare direction with SDL and make sure we send the same directions + { + context_->GetCentralViewport().MouseWheel(OrthancStone::MouseWheelDirection_Up, event->pos().x(), event->pos().y(), stoneModifiers); + } + else + { + context_->GetCentralViewport().MouseWheel(OrthancStone::MouseWheelDirection_Down, event->pos().x(), event->pos().y(), stoneModifiers); + } + } +} + +void QCairoWidget::keyPressEvent(QKeyEvent *event) +{ + using namespace OrthancStone; + + OrthancStone::KeyboardModifiers stoneModifiers = GetKeyboardModifiers(event); + + OrthancStone::KeyboardKeys keyType = OrthancStone::KeyboardKeys_Generic; + char keyChar = event->text()[0].toLatin1(); + +#define CASE_QT_KEY_TO_ORTHANC(qt, o) case qt: keyType = o; break; + if (keyChar == 0) + { + switch (event->key()) + { + CASE_QT_KEY_TO_ORTHANC(Qt::Key_Up, KeyboardKeys_Up); + CASE_QT_KEY_TO_ORTHANC(Qt::Key_Down, KeyboardKeys_Down); + CASE_QT_KEY_TO_ORTHANC(Qt::Key_Left, KeyboardKeys_Left); + CASE_QT_KEY_TO_ORTHANC(Qt::Key_Right, KeyboardKeys_Right); + default: + break; + } + } + context_->GetCentralViewport().KeyPressed(keyType, keyChar, stoneModifiers); +} + + +void QCairoWidget::resizeEvent(QResizeEvent* event) +{ + grabGesture(Qt::PanGesture); + QWidget::resizeEvent(event); + + if (event) + { + surface_.SetSize(event->size().width(), event->size().height()); + + image_.reset(new QImage(reinterpret_cast(surface_.GetBuffer()), + event->size().width(), + event->size().height(), + surface_.GetPitch(), + QImage::Format_RGB32)); + + context_->GetCentralViewport().SetSize(event->size().width(), event->size().height()); + + } +} diff -r fe4befe03935 -r d6136a7e914d Applications/Qt/QCairoWidget.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Applications/Qt/QCairoWidget.h Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,75 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + +#pragma once + +#include "../../Framework/Widgets/CairoWidget.h" +#include "../../Applications/Generic/NativeStoneApplicationContext.h" +#include "../../Framework/Viewport/CairoSurface.h" + +#include +#include +#include +#include + +class QCairoWidget : public QWidget, public OrthancStone::IViewport::IObserver +{ + Q_OBJECT + +private: + std::auto_ptr image_; + OrthancStone::CairoSurface surface_; + OrthancStone::NativeStoneApplicationContext* context_; + +protected: + virtual void paintEvent(QPaintEvent *event); + + virtual void resizeEvent(QResizeEvent *event); + + virtual void mouseMoveEvent(QMouseEvent *event); + + virtual void mousePressEvent(QMouseEvent *event); + + virtual void mouseReleaseEvent(QMouseEvent *event); + + virtual void wheelEvent(QWheelEvent *event); + + virtual void keyPressEvent(QKeyEvent *event); + +public: + explicit QCairoWidget(QWidget *parent); + + virtual ~QCairoWidget(); + + void SetContext(OrthancStone::NativeStoneApplicationContext& context); + + virtual void OnViewportContentChanged(const OrthancStone::IViewport& /*sceneNotUsed*/) + { + update(); // schedule a repaint (handled by Qt) + emit ContentChanged(); + } + +signals: + + void ContentChanged(); + +public slots: + +}; diff -r fe4befe03935 -r d6136a7e914d Applications/Qt/QStoneMainWindow.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Applications/Qt/QStoneMainWindow.cpp Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,41 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + +#include "QStoneMainWindow.h" + +namespace OrthancStone +{ + + QStoneMainWindow::QStoneMainWindow(NativeStoneApplicationContext& context, QWidget *parent) : + QMainWindow(parent), + context_(context) + { + } + + void QStoneMainWindow::SetCentralStoneWidget(QCairoWidget *centralWidget) + { + cairoCentralWidget_ = centralWidget; + cairoCentralWidget_->SetContext(context_); + } + + QStoneMainWindow::~QStoneMainWindow() + { + } +} diff -r fe4befe03935 -r d6136a7e914d Applications/Qt/QStoneMainWindow.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Applications/Qt/QStoneMainWindow.h Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,45 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ +#pragma once + +#include + +#include "QCairoWidget.h" +#include "../Generic/NativeStoneApplicationContext.h" + +namespace OrthancStone +{ + class QStoneMainWindow : public QMainWindow + { + Q_OBJECT + + private: + OrthancStone::NativeStoneApplicationContext& context_; + QCairoWidget *cairoCentralWidget_; + + protected: // you must inherit this class + QStoneMainWindow(NativeStoneApplicationContext& context, QWidget *parent = 0); + void SetCentralStoneWidget(QCairoWidget* centralWidget); + public: + virtual ~QStoneMainWindow(); + + }; + +} diff -r fe4befe03935 -r d6136a7e914d Applications/Qt/QtStoneApplicationRunner.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Applications/Qt/QtStoneApplicationRunner.cpp Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,67 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#if ORTHANC_ENABLE_QT != 1 +#error this file shall be included only with the ORTHANC_ENABLE_QT set to 1 +#endif + +#include "QtStoneApplicationRunner.h" +#include +#include + +#include "../../Framework/Toolbox/MessagingToolbox.h" + +#include +#include +#include +#include +#include "../../Platforms/Generic/OracleWebService.h" + + +namespace OrthancStone +{ + void QtStoneApplicationRunner::Initialize() + { + } + + void QtStoneApplicationRunner::DeclareCommandLineOptions(boost::program_options::options_description& options) + { + } + + void QtStoneApplicationRunner::Run(NativeStoneApplicationContext& context, const std::string& title, int argc, char* argv[]) + { + context.Start(); + + QApplication qtApplication(argc, argv); + window_.reset(application_.CreateQtMainWindow()); + + window_->show(); + qtApplication.exec(); + + context.Stop(); + } + + void QtStoneApplicationRunner::Finalize() + { + } + + +} diff -r fe4befe03935 -r d6136a7e914d Applications/Qt/QtStoneApplicationRunner.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Applications/Qt/QtStoneApplicationRunner.h Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,54 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "../Generic/NativeStoneApplicationRunner.h" +#include "QStoneMainWindow.h" + +#if ORTHANC_ENABLE_QT != 1 +#error this file shall be included only with the ORTHANC_ENABLE_QT set to 1 +#endif + +namespace OrthancStone +{ + class QtStoneApplicationRunner : public NativeStoneApplicationRunner + { + protected: + std::auto_ptr window_; + + public: + QtStoneApplicationRunner(MessageBroker& broker, + IStoneApplication& application) + : NativeStoneApplicationRunner(broker, application) + { + } + + + virtual void Initialize(); + + virtual void DeclareCommandLineOptions(boost::program_options::options_description& options); + virtual void ParseCommandLineOptions(const boost::program_options::variables_map& parameters) {} + virtual void Run(NativeStoneApplicationContext& context, const std::string& title, int argc, char* argv[]); + virtual void Finalize(); + }; + +} diff -r fe4befe03935 -r d6136a7e914d Applications/Samples/CMakeLists.txt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Applications/Samples/CMakeLists.txt Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,222 @@ +# Usage (Linux): +# to build the WASM samples +# source ~/Downloads/emsdk/emsdk_env.sh && cmake -DCMAKE_TOOLCHAIN_FILE=${EMSCRIPTEN}/cmake/Modules/Platform/Emscripten.cmake -DCMAKE_BUILD_TYPE=Release -DSTONE_SOURCES_DIR=$currentDir/../../../orthanc-stone -DORTHANC_FRAMEWORK_SOURCE=path -DORTHANC_FRAMEWORK_ROOT=$currentDir/../../../orthanc -DALLOW_DOWNLOADS=ON .. -DENABLE_WASM=ON +# to build the Qt samples + +cmake_minimum_required(VERSION 2.8.3) +project(OrthancStone) + +include(../../Resources/CMake/OrthancStoneParameters.cmake) + +#set(ENABLE_DCMTK ON) + +set(ENABLE_SDL OFF CACHE BOOL "Target SDL Native application") +set(ENABLE_QT OFF CACHE BOOL "Target Qt Native application") +set(ENABLE_WASM OFF CACHE BOOL "Target WASM application") + +if (ENABLE_WASM) + ##################################################################### + ## Configuration of the Emscripten compiler for WebAssembly target + ##################################################################### + + set(WASM_FLAGS "-s WASM=1") + set(WASM_MODULE_NAME "StoneFrameworkModule" CACHE STRING "Name of the WebAssembly module") + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${WASM_FLAGS}") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${WASM_FLAGS}") + set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} --js-library ${STONE_SOURCES_DIR}/Applications/Samples/samples-library.js --js-library ${STONE_SOURCES_DIR}/Platforms/Wasm/WasmWebService.js --js-library ${STONE_SOURCES_DIR}/Platforms/Wasm/default-library.js -s EXTRA_EXPORTED_RUNTIME_METHODS='[\"ccall\", \"cwrap\"]'") + + # Handling of memory + #set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s ALLOW_MEMORY_GROWTH=1") # Resize + #set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s TOTAL_MEMORY=536870912") # 512MB + set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s EXPORT_NAME='\"${WASM_MODULE_NAME}\"' -s ALLOW_MEMORY_GROWTH=1 -s TOTAL_MEMORY=536870912 -s TOTAL_STACK=128000000") # 512MB + resize + #set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s ALLOW_MEMORY_GROWTH=1 -s TOTAL_MEMORY=1073741824") # 1GB + resize + + # To debug exceptions + #set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s DEMANGLE_SUPPORT=1 -s ASSERTIONS=2") + + add_definitions(-DORTHANC_ENABLE_WASM=1) + set(ORTHANC_SANDBOXED ON) + +elseif (ENABLE_QT OR ENABLE_SDL) + + set(ENABLE_NATIVE ON) + set(ORTHANC_SANDBOXED OFF) + set(ENABLE_CRYPTO_OPTIONS ON) + set(ENABLE_GOOGLE_TEST ON) + set(ENABLE_WEB_CLIENT ON) + +endif() + +##################################################################### +## Configuration for Orthanc +##################################################################### + +# include(../../Resources/CMake/Version.cmake) + +if (ORTHANC_STONE_VERSION STREQUAL "mainline") + set(ORTHANC_FRAMEWORK_VERSION "mainline") + set(ORTHANC_FRAMEWORK_DEFAULT_SOURCE "hg") +else() + set(ORTHANC_FRAMEWORK_VERSION "1.4.1") + set(ORTHANC_FRAMEWORK_DEFAULT_SOURCE "web") +endif() + +set(ORTHANC_FRAMEWORK_SOURCE "${ORTHANC_FRAMEWORK_DEFAULT_SOURCE}" CACHE STRING "Source of the Orthanc source code (can be \"hg\", \"archive\", \"web\" or \"path\")") +set(ORTHANC_FRAMEWORK_ARCHIVE "" CACHE STRING "Path to the Orthanc archive, if ORTHANC_FRAMEWORK_SOURCE is \"archive\"") +set(ORTHANC_FRAMEWORK_ROOT "" CACHE STRING "Path to the Orthanc source directory, if ORTHANC_FRAMEWORK_SOURCE is \"path\"") + + +##################################################################### +## Build a static library containing the Orthanc Stone framework +##################################################################### + + +LIST(APPEND ORTHANC_BOOST_COMPONENTS program_options) + +include(../../Resources/CMake/OrthancStoneConfiguration.cmake) + +add_library(OrthancStone STATIC + ${ORTHANC_STONE_SOURCES} + ) + +##################################################################### +## Build all the sample applications +##################################################################### + +include_directories(${ORTHANC_STONE_ROOT}) + +# files common to all samples +list(APPEND SAMPLE_APPLICATIONS_SOURCES + ${ORTHANC_STONE_ROOT}/Applications/Samples/SampleInteractor.h + ${ORTHANC_STONE_ROOT}/Applications/Samples/SampleApplicationBase.h + ) + +if (ENABLE_QT) + list(APPEND SAMPLE_APPLICATIONS_SOURCES + ${ORTHANC_STONE_ROOT}/Applications/Samples/Qt/SampleQtApplicationRunner.h + ${ORTHANC_STONE_ROOT}/Applications/Samples/Qt/SampleMainWindow.cpp + ${ORTHANC_STONE_ROOT}/Applications/Samples/Qt/SampleMainWindowWithButtons.cpp + ) + + ORTHANC_QT_WRAP_UI(SAMPLE_APPLICATIONS_SOURCES + ${ORTHANC_STONE_ROOT}/Applications/Samples/Qt/SampleMainWindow.ui + ${ORTHANC_STONE_ROOT}/Applications/Samples/Qt/SampleMainWindowWithButtons.ui + ) + + ORTHANC_QT_WRAP_CPP(SAMPLE_APPLICATIONS_SOURCES + ${ORTHANC_STONE_ROOT}/Applications/Qt/QCairoWidget.h + ${ORTHANC_STONE_ROOT}/Applications/Qt/QStoneMainWindow.h + ${ORTHANC_STONE_ROOT}/Applications/Samples/Qt/SampleMainWindow.h + ${ORTHANC_STONE_ROOT}/Applications/Samples/Qt/SampleMainWindowWithButtons.h + ) +endif() + +if (ENABLE_NATIVE) + list(APPEND SAMPLE_APPLICATIONS_SOURCES + ${ORTHANC_STONE_ROOT}/Applications/Samples/SampleMainNative.cpp + ) + +elseif (ENABLE_WASM) + + list(APPEND SAMPLE_APPLICATIONS_SOURCES + ${ORTHANC_STONE_ROOT}/Applications/Samples/SampleMainWasm.cpp + ${STONE_WASM_SOURCES} + ) +endif() + + +macro(BuildSingleFileSample Target Header Sample) + add_executable(${Target} + ${ORTHANC_STONE_ROOT}/Applications/Samples/${Header} + ${SAMPLE_APPLICATIONS_SOURCES} + ) + set_target_properties(${Target} PROPERTIES COMPILE_DEFINITIONS ORTHANC_STONE_SAMPLE=${Sample}) + target_link_libraries(${Target} OrthancStone) +endmacro() + +#BuildSingleFileSample(OrthancStoneEmpty EmptyApplication.h 1) +#BuildSingleFileSample(OrthancStoneTestPattern TestPatternApplication.h 2) +BuildSingleFileSample(OrthancStoneSingleFrame SingleFrameApplication.h 3) +#BuildSingleFileSample(OrthancStoneSingleVolume SingleVolumeApplication.h 4) +#BuildSingleFileSample(OrthancStoneBasicPetCtFusion 5) +#BuildSingleFileSample(OrthancStoneSynchronizedSeries 6) +#BuildSingleFileSample(OrthancStoneLayoutPetCtFusion 7) +BuildSingleFileSample(OrthancStoneSimpleViewerSingleFile SimpleViewerApplicationSingleFile.h 8) # we keep that one just as a sample before we convert another sample to this pattern +BuildSingleFileSample(OrthancStoneSingleFrameEditor SingleFrameEditorApplication.h 9) + +##### SimpleViewer sample (Qt and WASM only) ####### + +if (ENABLE_QT OR ENABLE_WASM) + + if (ENABLE_QT) + list(APPEND SIMPLE_VIEWER_APPLICATION_SOURCES + ${ORTHANC_STONE_ROOT}/Applications/Samples/SimpleViewer/Qt/SimpleViewerMainWindow.cpp + ${ORTHANC_STONE_ROOT}/Applications/Samples/SimpleViewer/Qt/SimpleViewerMainWindow.ui + ${ORTHANC_STONE_ROOT}/Applications/Samples/SimpleViewer/Qt/mainQt.cpp + ) + + ORTHANC_QT_WRAP_UI(SIMPLE_VIEWER_APPLICATION_SOURCES + ${ORTHANC_STONE_ROOT}/Applications/Samples/SimpleViewer/Qt/SimpleViewerMainWindow.ui + ) + + ORTHANC_QT_WRAP_CPP(SIMPLE_VIEWER_APPLICATION_SOURCES + ${ORTHANC_STONE_ROOT}/Applications/Qt/QCairoWidget.h + ${ORTHANC_STONE_ROOT}/Applications/Qt/QStoneMainWindow.h + ${ORTHANC_STONE_ROOT}/Applications/Samples/SimpleViewer/Qt/SimpleViewerMainWindow.h + ) + +elseif (ENABLE_WASM) + list(APPEND SIMPLE_VIEWER_APPLICATION_SOURCES + ${ORTHANC_STONE_ROOT}/Applications/Samples/SimpleViewer/Wasm/mainWasm.cpp + ${ORTHANC_STONE_ROOT}/Applications/Samples/SimpleViewer/Wasm/SimpleViewerWasmApplicationAdapter.cpp + ${STONE_WASM_SOURCES} + ) + endif() + + add_executable(OrthancStoneSimpleViewer + ${ORTHANC_STONE_ROOT}/Applications/Samples/SimpleViewer/SimpleViewerApplication.cpp + ${ORTHANC_STONE_ROOT}/Applications/Samples/SimpleViewer/ThumbnailInteractor.cpp + ${ORTHANC_STONE_ROOT}/Applications/Samples/SimpleViewer/MainWidgetInteractor.cpp + ${ORTHANC_STONE_ROOT}/Applications/Samples/SimpleViewer/AppStatus.h + ${ORTHANC_STONE_ROOT}/Applications/Samples/SimpleViewer/Messages.h + ${SIMPLE_VIEWER_APPLICATION_SOURCES} + ) + target_link_libraries(OrthancStoneSimpleViewer OrthancStone) + +endif() + +##################################################################### +## Build the unit tests +##################################################################### + +if (ENABLE_NATIVE) + add_executable(UnitTests + ${GOOGLE_TEST_SOURCES} + ${ORTHANC_STONE_ROOT}/UnitTestsSources/TestCommands.cpp + ${ORTHANC_STONE_ROOT}/UnitTestsSources/TestExceptions.cpp + ${ORTHANC_STONE_ROOT}/UnitTestsSources/TestMessageBroker.cpp + ${ORTHANC_STONE_ROOT}/UnitTestsSources/TestMessageBroker2.cpp + ${ORTHANC_STONE_ROOT}/UnitTestsSources/UnitTestsMain.cpp + ) + + target_link_libraries(UnitTests OrthancStone) +endif() + +##################################################################### +## Generate the documentation if Doxygen is present +##################################################################### + +find_package(Doxygen) +if (DOXYGEN_FOUND) + configure_file( + ${ORTHANC_STONE_ROOT}/Resources/OrthancStone.doxygen + ${CMAKE_CURRENT_BINARY_DIR}/OrthancStone.doxygen + @ONLY) + + add_custom_target(doc + ${DOXYGEN_EXECUTABLE} ${CMAKE_CURRENT_BINARY_DIR}/OrthancStone.doxygen + COMMENT "Generating documentation with Doxygen" VERBATIM + ) +else() + message("Doxygen not found. The documentation will not be built.") +endif() diff -r fe4befe03935 -r d6136a7e914d Applications/Samples/EmptyApplication.h --- a/Applications/Samples/EmptyApplication.h Mon Nov 05 10:04:56 2018 +0100 +++ b/Applications/Samples/EmptyApplication.h Mon Nov 05 10:06:18 2018 +0100 @@ -32,7 +32,7 @@ class EmptyApplication : public SampleApplicationBase { public: - virtual void DeclareCommandLineOptions(boost::program_options::options_description& options) + virtual void DeclareStartupOptions(boost::program_options::options_description& options) { boost::program_options::options_description generic("Sample options"); generic.add_options() @@ -44,15 +44,14 @@ options.add(generic); } - virtual void Initialize(BasicApplicationContext& context, - IStatusBar& statusBar, + virtual void Initialize(IStatusBar& statusBar, const boost::program_options::variables_map& parameters) { int red = parameters["red"].as(); int green = parameters["green"].as(); int blue = parameters["blue"].as(); - context.SetCentralWidget(new EmptyWidget(red, green, blue)); + context_->SetCentralWidget(new EmptyWidget(red, green, blue)); } }; } diff -r fe4befe03935 -r d6136a7e914d Applications/Samples/LayoutPetCtFusionApplication.h --- a/Applications/Samples/LayoutPetCtFusionApplication.h Mon Nov 05 10:04:56 2018 +0100 +++ b/Applications/Samples/LayoutPetCtFusionApplication.h Mon Nov 05 10:06:18 2018 +0100 @@ -89,7 +89,7 @@ { if (key == 's') { - that_.SetDefaultView(); + that_.FitContent(); } } }; @@ -109,11 +109,11 @@ LayeredSceneWidget* fusionSagittal_; - void SetDefaultView() + void FitContent() { - petAxial_->SetDefaultView(); - petCoronal_->SetDefaultView(); - petSagittal_->SetDefaultView(); + petAxial_->FitContent(); + petCoronal_->FitContent(); + petSagittal_->FitContent(); } @@ -361,7 +361,7 @@ virtual void NotifySizeChange(const WorldSceneWidget& source, ViewportGeometry& view) { - view.SetDefaultView(); + view.FitContent(); } virtual void NotifyViewChange(const WorldSceneWidget& source, diff -r fe4befe03935 -r d6136a7e914d Applications/Samples/Qt/SampleMainWindow.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Applications/Samples/Qt/SampleMainWindow.cpp Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,50 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + +#include "SampleMainWindow.h" + +/** + * Don't use "ui_MainWindow.h" instead of below, as + * this makes CMake unable to detect when the UI file changes. + **/ +#include +#include "../../Applications/Samples/SampleApplicationBase.h" + +namespace OrthancStone +{ + namespace Samples + { + + SampleMainWindow::SampleMainWindow(OrthancStone::NativeStoneApplicationContext& context, OrthancStone::Samples::SampleSingleCanvasApplicationBase& stoneSampleApplication, QWidget *parent) : + QStoneMainWindow(context, parent), + ui_(new Ui::SampleMainWindow), + stoneSampleApplication_(stoneSampleApplication) + { + ui_->setupUi(this); + SetCentralStoneWidget(ui_->cairoCentralWidget); + } + + SampleMainWindow::~SampleMainWindow() + { + delete ui_; + } + + } +} diff -r fe4befe03935 -r d6136a7e914d Applications/Samples/Qt/SampleMainWindow.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Applications/Samples/Qt/SampleMainWindow.h Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,50 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ +#pragma once + +#include "../../Qt/QCairoWidget.h" +#include "../../Qt/QStoneMainWindow.h" + +namespace Ui +{ + class SampleMainWindow; +} + +namespace OrthancStone +{ + namespace Samples + { + + class SampleSingleCanvasApplicationBase; + + class SampleMainWindow : public QStoneMainWindow + { + Q_OBJECT + + private: + Ui::SampleMainWindow* ui_; + SampleSingleCanvasApplicationBase& stoneSampleApplication_; + + public: + explicit SampleMainWindow(OrthancStone::NativeStoneApplicationContext& context, SampleSingleCanvasApplicationBase& stoneSampleApplication, QWidget *parent = 0); + ~SampleMainWindow(); + }; + } +} diff -r fe4befe03935 -r d6136a7e914d Applications/Samples/Qt/SampleMainWindow.ui --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Applications/Samples/Qt/SampleMainWindow.ui Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,84 @@ + + + SampleMainWindow + + + + 0 + 0 + 903 + 634 + + + + + 500 + 300 + + + + + 500 + 300 + + + + Stone of Orthanc + + + Qt::LeftToRight + + + + + 0 + 0 + + + + Qt::LeftToRight + + + + QLayout::SetDefaultConstraint + + + + + + 0 + 500 + + + + + + + + + + 0 + 0 + 903 + 22 + + + + + Test + + + + + + + + + QCairoWidget + QGraphicsView +
QCairoWidget.h
+
+
+ + +
diff -r fe4befe03935 -r d6136a7e914d Applications/Samples/Qt/SampleMainWindowWithButtons.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Applications/Samples/Qt/SampleMainWindowWithButtons.cpp Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,93 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + +#include "SampleMainWindow.h" + +/** + * Don't use "ui_MainWindow.h" instead of below, as + * this makes CMake unable to detect when the UI file changes. + **/ +#include +#include "../../Applications/Samples/SampleApplicationBase.h" + +namespace OrthancStone +{ + namespace Samples + { + + SampleMainWindowWithButtons::SampleMainWindowWithButtons(OrthancStone::NativeStoneApplicationContext& context, OrthancStone::Samples::SampleSingleCanvasWithButtonsApplicationBase& stoneSampleApplication, QWidget *parent) : + QStoneMainWindow(context, parent), + ui_(new Ui::SampleMainWindowWithButtons), + stoneSampleApplication_(stoneSampleApplication) + { + ui_->setupUi(this); + SetCentralStoneWidget(ui_->cairoCentralWidget); + +#if QT_VERSION >= 0x050000 + connect(ui_->toolButton1, &QToolButton::clicked, this, &SampleMainWindowWithButtons::tool1Clicked); + connect(ui_->toolButton2, &QToolButton::clicked, this, &SampleMainWindowWithButtons::tool2Clicked); + connect(ui_->pushButton1, &QPushButton::clicked, this, &SampleMainWindowWithButtons::pushButton1Clicked); + connect(ui_->pushButton1, &QPushButton::clicked, this, &SampleMainWindowWithButtons::pushButton2Clicked); +#else + connect(ui_->toolButton1, SIGNAL(clicked()), this, SLOT(tool1Clicked())); + connect(ui_->toolButton2, SIGNAL(clicked()), this, SLOT(tool2Clicked())); + connect(ui_->pushButton1, SIGNAL(clicked()), this, SLOT(pushButton1Clicked())); + connect(ui_->pushButton1, SIGNAL(clicked()), this, SLOT(pushButton2Clicked())); +#endif + + std::string pushButton1Name; + std::string pushButton2Name; + std::string tool1Name; + std::string tool2Name; + stoneSampleApplication_.GetButtonNames(pushButton1Name, pushButton2Name, tool1Name, tool2Name); + + ui_->toolButton1->setText(QString::fromStdString(tool1Name)); + ui_->toolButton2->setText(QString::fromStdString(tool2Name)); + ui_->pushButton1->setText(QString::fromStdString(pushButton1Name)); + ui_->pushButton2->setText(QString::fromStdString(pushButton2Name)); + } + + SampleMainWindowWithButtons::~SampleMainWindowWithButtons() + { + delete ui_; + } + + void SampleMainWindowWithButtons::tool1Clicked() + { + stoneSampleApplication_.OnTool1Clicked(); + } + + void SampleMainWindowWithButtons::tool2Clicked() + { + stoneSampleApplication_.OnTool2Clicked(); + } + + void SampleMainWindowWithButtons::pushButton1Clicked() + { + stoneSampleApplication_.OnPushButton1Clicked(); + } + + void SampleMainWindowWithButtons::pushButton2Clicked() + { + stoneSampleApplication_.OnPushButton2Clicked(); + } + + } +} diff -r fe4befe03935 -r d6136a7e914d Applications/Samples/Qt/SampleMainWindowWithButtons.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Applications/Samples/Qt/SampleMainWindowWithButtons.h Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,56 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ +#pragma once + +#include "../../Qt/QCairoWidget.h" +#include "../../Qt/QStoneMainWindow.h" + +namespace Ui +{ + class SampleMainWindowWithButtons; +} + +namespace OrthancStone +{ + namespace Samples + { + + class SampleSingleCanvasWithButtonsApplicationBase; + + class SampleMainWindowWithButtons : public QStoneMainWindow + { + Q_OBJECT + + private: + Ui::SampleMainWindowWithButtons* ui_; + SampleSingleCanvasWithButtonsApplicationBase& stoneSampleApplication_; + + public: + explicit SampleMainWindowWithButtons(OrthancStone::NativeStoneApplicationContext& context, SampleSingleCanvasWithButtonsApplicationBase& stoneSampleApplication, QWidget *parent = 0); + ~SampleMainWindowWithButtons(); + + private slots: + void tool1Clicked(); + void tool2Clicked(); + void pushButton1Clicked(); + void pushButton2Clicked(); + }; + } +} diff -r fe4befe03935 -r d6136a7e914d Applications/Samples/Qt/SampleMainWindowWithButtons.ui --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Applications/Samples/Qt/SampleMainWindowWithButtons.ui Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,130 @@ + + + SampleMainWindowWithButtons + + + + 0 + 0 + 903 + 634 + + + + + 500 + 300 + + + + + 500 + 300 + + + + Stone of Orthanc + + + Qt::LeftToRight + + + + + 0 + 0 + + + + Qt::LeftToRight + + + + QLayout::SetDefaultConstraint + + + + + + 0 + 500 + + + + + + + + + 0 + 100 + + + + + 16777215 + 100 + + + + + + + tool1 + + + + + + + tool2 + + + + + + + action1 + + + + + + + action2 + + + + + + + + + + + + 0 + 0 + 903 + 22 + + + + + Test + + + + + + + + + QCairoWidget + QGraphicsView +
QCairoWidget.h
+
+
+ + +
diff -r fe4befe03935 -r d6136a7e914d Applications/Samples/Qt/SampleQtApplicationRunner.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Applications/Samples/Qt/SampleQtApplicationRunner.h Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,50 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "../../Qt/QtStoneApplicationRunner.h" + +#if ORTHANC_ENABLE_QT != 1 +#error this file shall be included only with the ORTHANC_ENABLE_QT set to 1 +#endif + +namespace OrthancStone +{ + namespace Samples + { + class SampleQtApplicationRunner : public OrthancStone::QtStoneApplicationRunner + { + protected: + virtual void InitializeMainWindow(OrthancStone::NativeStoneApplicationContext& context) + { + window_.reset(application_.CreateQtMainWindow()); + } + public: + SampleQtApplicationRunner(MessageBroker& broker, + SampleApplicationBase& application) + : OrthancStone::QtStoneApplicationRunner(broker, application) + { + } + + }; + } +} diff -r fe4befe03935 -r d6136a7e914d Applications/Samples/SampleApplicationBase.h --- a/Applications/Samples/SampleApplicationBase.h Mon Nov 05 10:04:56 2018 +0100 +++ b/Applications/Samples/SampleApplicationBase.h Mon Nov 05 10:06:18 2018 +0100 @@ -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 . **/ @@ -21,27 +21,100 @@ #pragma once -#include "../IBasicApplication.h" +#include "../../Applications/IStoneApplication.h" +#include "../../Framework/Widgets/WorldSceneWidget.h" + +#if ORTHANC_ENABLE_WASM==1 +#include "../../Platforms/Wasm/WasmPlatformApplicationAdapter.h" +#include "../../Platforms/Wasm/Defaults.h" +#endif + +#if ORTHANC_ENABLE_QT==1 +#include "Qt/SampleMainWindow.h" +#include "Qt/SampleMainWindowWithButtons.h" +#endif namespace OrthancStone { namespace Samples { - class SampleApplicationBase : public IBasicApplication + class SampleApplicationBase : public IStoneApplication { + protected: + BaseCommandBuilder commandBuilder_; + WorldSceneWidget* mainWidget_; // ownership is transfered to the application context + public: + virtual void Initialize(StoneApplicationContext* context, + IStatusBar& statusBar, + const boost::program_options::variables_map& parameters) + { + } + virtual std::string GetTitle() const { return "Stone of Orthanc - Sample"; } - virtual void DeclareCommandLineOptions(boost::program_options::options_description& options) + virtual BaseCommandBuilder& GetCommandBuilder() {return commandBuilder_;} + + virtual void Finalize() {} + virtual IWidget* GetCentralWidget() {return mainWidget_;} + +#if ORTHANC_ENABLE_WASM==1 + // default implementations for a single canvas named "canvas" in the HTML and an emtpy WasmApplicationAdapter + + virtual void InitializeWasm() { + AttachWidgetToWasmViewport("canvas", mainWidget_); } - virtual void Finalize() + virtual WasmPlatformApplicationAdapter* CreateWasmApplicationAdapter(MessageBroker& broker) { + return new WasmPlatformApplicationAdapter(broker, *this); } +#endif + + }; + + // this application actually works in Qt and WASM + class SampleSingleCanvasWithButtonsApplicationBase : public SampleApplicationBase + { +public: + virtual void OnPushButton1Clicked() {} + virtual void OnPushButton2Clicked() {} + virtual void OnTool1Clicked() {} + virtual void OnTool2Clicked() {} + + virtual void GetButtonNames(std::string& pushButton1, + std::string& pushButton2, + std::string& tool1, + std::string& tool2 + ) { + pushButton1 = "action1"; + pushButton2 = "action2"; + tool1 = "tool1"; + tool2 = "tool2"; + } + +#if ORTHANC_ENABLE_QT==1 + virtual QStoneMainWindow* CreateQtMainWindow() { + return new SampleMainWindowWithButtons(dynamic_cast(*context_), *this); + } +#endif + + }; + + // this application actually works in SDL and WASM + class SampleSingleCanvasApplicationBase : public SampleApplicationBase + { +public: + +#if ORTHANC_ENABLE_QT==1 + virtual QStoneMainWindow* CreateQtMainWindow() { + return new SampleMainWindow(dynamic_cast(*context_), *this); + } +#endif }; } } diff -r fe4befe03935 -r d6136a7e914d Applications/Samples/SampleList.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Applications/Samples/SampleList.h Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,41 @@ +// The macro "ORTHANC_STONE_SAMPLE" must be set by the CMake script + +#if ORTHANC_STONE_SAMPLE == 1 +#include "EmptyApplication.h" +typedef OrthancStone::Samples::EmptyApplication SampleApplication; + +#elif ORTHANC_STONE_SAMPLE == 2 +#include "TestPatternApplication.h" +typedef OrthancStone::Samples::TestPatternApplication SampleApplication; + +#elif ORTHANC_STONE_SAMPLE == 3 +#include "SingleFrameApplication.h" +typedef OrthancStone::Samples::SingleFrameApplication SampleApplication; + +#elif ORTHANC_STONE_SAMPLE == 4 +#include "SingleVolumeApplication.h" +typedef OrthancStone::Samples::SingleVolumeApplication SampleApplication; + +#elif ORTHANC_STONE_SAMPLE == 5 +#include "BasicPetCtFusionApplication.h" +typedef OrthancStone::Samples::BasicPetCtFusionApplication SampleApplication; + +#elif ORTHANC_STONE_SAMPLE == 6 +#include "SynchronizedSeriesApplication.h" +typedef OrthancStone::Samples::SynchronizedSeriesApplication SampleApplication; + +#elif ORTHANC_STONE_SAMPLE == 7 +#include "LayoutPetCtFusionApplication.h" +typedef OrthancStone::Samples::LayoutPetCtFusionApplication SampleApplication; + +#elif ORTHANC_STONE_SAMPLE == 8 +#include "SimpleViewerApplicationSingleFile.h" +typedef OrthancStone::Samples::SimpleViewerApplication SampleApplication; + +#elif ORTHANC_STONE_SAMPLE == 9 +#include "SingleFrameEditorApplication.h" +typedef OrthancStone::Samples::SingleFrameEditorApplication SampleApplication; + +#else +#error Please set the ORTHANC_STONE_SAMPLE macro +#endif diff -r fe4befe03935 -r d6136a7e914d Applications/Samples/SampleMainNative.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Applications/Samples/SampleMainNative.cpp Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,44 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#include "SampleList.h" +#if ORTHANC_ENABLE_SDL==1 +#include "../Sdl/SdlStoneApplicationRunner.h" +#endif +#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); + +#if ORTHANC_ENABLE_SDL==1 + OrthancStone::SdlStoneApplicationRunner sdlApplicationRunner(broker, sampleStoneApplication); + return sdlApplicationRunner.Execute(argc, argv); +#endif +#if ORTHANC_ENABLE_QT==1 + OrthancStone::Samples::SampleQtApplicationRunner qtAppRunner(broker, sampleStoneApplication); + return qtAppRunner.Execute(argc, argv); +#endif +} diff -r fe4befe03935 -r d6136a7e914d Applications/Samples/SampleMainSdl.cpp --- a/Applications/Samples/SampleMainSdl.cpp Mon Nov 05 10:04:56 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,62 +0,0 @@ -/** - * Stone of Orthanc - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2018 Osimis S.A., Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU Affero General Public License - * as published by the Free Software Foundation, either version 3 of - * the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - **/ - - -// The macro "ORTHANC_STONE_SAMPLE" must be set by the CMake script - -#if ORTHANC_STONE_SAMPLE == 1 -#include "EmptyApplication.h" -typedef OrthancStone::Samples::EmptyApplication Application; - -#elif ORTHANC_STONE_SAMPLE == 2 -#include "TestPatternApplication.h" -typedef OrthancStone::Samples::TestPatternApplication Application; - -#elif ORTHANC_STONE_SAMPLE == 3 -#include "SingleFrameApplication.h" -typedef OrthancStone::Samples::SingleFrameApplication Application; - -#elif ORTHANC_STONE_SAMPLE == 4 -#include "SingleVolumeApplication.h" -typedef OrthancStone::Samples::SingleVolumeApplication Application; - -#elif ORTHANC_STONE_SAMPLE == 5 -#include "BasicPetCtFusionApplication.h" -typedef OrthancStone::Samples::BasicPetCtFusionApplication Application; - -#elif ORTHANC_STONE_SAMPLE == 6 -#include "SynchronizedSeriesApplication.h" -typedef OrthancStone::Samples::SynchronizedSeriesApplication Application; - -#elif ORTHANC_STONE_SAMPLE == 7 -#include "LayoutPetCtFusionApplication.h" -typedef OrthancStone::Samples::LayoutPetCtFusionApplication Application; - -#else -#error Please set the ORTHANC_STONE_SAMPLE macro -#endif - - -int main(int argc, char* argv[]) -{ - Application application; - - return OrthancStone::IBasicApplication::ExecuteWithSdl(application, argc, argv); -} diff -r fe4befe03935 -r d6136a7e914d Applications/Samples/SampleMainWasm.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Applications/Samples/SampleMainWasm.cpp Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,37 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + +#include "Platforms/Wasm/WasmWebService.h" +#include "Platforms/Wasm/WasmViewport.h" + +#include + +#include "SampleList.h" + + +OrthancStone::IStoneApplication* CreateUserApplication(OrthancStone::MessageBroker& broker) +{ + return new SampleApplication(broker); +} + +OrthancStone::WasmPlatformApplicationAdapter* CreateWasmApplicationAdapter(OrthancStone::MessageBroker& broker, OrthancStone::IStoneApplication* application) +{ + return dynamic_cast(application)->CreateWasmApplicationAdapter(broker); +} \ No newline at end of file diff -r fe4befe03935 -r d6136a7e914d Applications/Samples/SimpleViewer/AppStatus.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Applications/Samples/SimpleViewer/AppStatus.h Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,27 @@ +#pragma once + +#include + + +namespace SimpleViewer +{ + struct AppStatus + { + std::string patientId; + std::string studyDescription; + std::string currentInstanceIdInMainViewport; + // note: if you add members here, update the serialization code below and deserialization in simple-viewer.ts -> onAppStatusUpdated() + + + AppStatus() + { + } + + void ToJson(Json::Value &output) const + { + output["patientId"] = patientId; + output["studyDescription"] = studyDescription; + output["currentInstanceIdInMainViewport"] = currentInstanceIdInMainViewport; + } + }; +} diff -r fe4befe03935 -r d6136a7e914d Applications/Samples/SimpleViewer/MainWidgetInteractor.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Applications/Samples/SimpleViewer/MainWidgetInteractor.cpp Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,111 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + +#include "MainWidgetInteractor.h" + +#include "SimpleViewerApplication.h" + +namespace SimpleViewer { + + IWorldSceneMouseTracker* MainWidgetInteractor::CreateMouseTracker(WorldSceneWidget& widget, + const ViewportGeometry& view, + MouseButton button, + KeyboardModifiers modifiers, + int viewportX, + int viewportY, + double x, + double y, + IStatusBar* statusBar) + { + if (button == MouseButton_Left) + { + if (application_.GetCurrentTool() == SimpleViewerApplication::Tools_LineMeasure) + { + return new LineMeasureTracker(statusBar, dynamic_cast(widget).GetSlice(), + x, y, 255, 0, 0, application_.GetFont()); + } + else if (application_.GetCurrentTool() == SimpleViewerApplication::Tools_CircleMeasure) + { + return new CircleMeasureTracker(statusBar, dynamic_cast(widget).GetSlice(), + x, y, 255, 0, 0, application_.GetFont()); + } + else if (application_.GetCurrentTool() == SimpleViewerApplication::Tools_Crop) + { + // TODO + } + else if (application_.GetCurrentTool() == SimpleViewerApplication::Tools_Windowing) + { + // TODO + } + else if (application_.GetCurrentTool() == SimpleViewerApplication::Tools_Zoom) + { + // TODO + } + else if (application_.GetCurrentTool() == SimpleViewerApplication::Tools_Pan) + { + // TODO + } + + } + return NULL; + } + + void MainWidgetInteractor::MouseOver(CairoContext& context, + WorldSceneWidget& widget, + const ViewportGeometry& view, + double x, + double y, + IStatusBar* statusBar) + { + if (statusBar != NULL) + { + Vector p = dynamic_cast(widget).GetSlice().MapSliceToWorldCoordinates(x, y); + + char buf[64]; + sprintf(buf, "X = %.02f Y = %.02f Z = %.02f (in cm)", + p[0] / 10.0, p[1] / 10.0, p[2] / 10.0); + statusBar->SetMessage(buf); + } + } + + void MainWidgetInteractor::MouseWheel(WorldSceneWidget& widget, + MouseWheelDirection direction, + KeyboardModifiers modifiers, + IStatusBar* statusBar) + { + } + + void MainWidgetInteractor::KeyPressed(WorldSceneWidget& widget, + KeyboardKeys key, + char keyChar, + KeyboardModifiers modifiers, + IStatusBar* statusBar) + { + switch (keyChar) + { + case 's': + widget.FitContent(); + break; + + default: + break; + } + } +} diff -r fe4befe03935 -r d6136a7e914d Applications/Samples/SimpleViewer/MainWidgetInteractor.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Applications/Samples/SimpleViewer/MainWidgetInteractor.h Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,73 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "Framework/Widgets/IWorldSceneInteractor.h" + +using namespace OrthancStone; + +namespace SimpleViewer { + + class SimpleViewerApplication; + + class MainWidgetInteractor : public IWorldSceneInteractor + { + private: + SimpleViewerApplication& application_; + + public: + MainWidgetInteractor(SimpleViewerApplication& application) : + application_(application) + { + } + + virtual IWorldSceneMouseTracker* CreateMouseTracker(WorldSceneWidget& widget, + const ViewportGeometry& view, + MouseButton button, + KeyboardModifiers modifiers, + int viewportX, + int viewportY, + double x, + double y, + IStatusBar* statusBar); + + virtual void MouseOver(CairoContext& context, + WorldSceneWidget& widget, + const ViewportGeometry& view, + double x, + double y, + IStatusBar* statusBar); + + virtual void MouseWheel(WorldSceneWidget& widget, + MouseWheelDirection direction, + KeyboardModifiers modifiers, + IStatusBar* statusBar); + + virtual void KeyPressed(WorldSceneWidget& widget, + KeyboardKeys key, + char keyChar, + KeyboardModifiers modifiers, + IStatusBar* statusBar); + }; + + +} diff -r fe4befe03935 -r d6136a7e914d Applications/Samples/SimpleViewer/Messages.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Applications/Samples/SimpleViewer/Messages.h Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,13 @@ +#pragma once + +#include "Framework/Messages/MessageType.h" + +namespace SimpleViewer +{ + enum SimpleViewerMessageType + { + SimpleViewerMessageType_First = OrthancStone::MessageType_CustomMessage, + SimpleViewerMessageType_AppStatusUpdated + + }; +} diff -r fe4befe03935 -r d6136a7e914d Applications/Samples/SimpleViewer/Qt/SimpleViewerMainWindow.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Applications/Samples/SimpleViewer/Qt/SimpleViewerMainWindow.cpp Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,106 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + +#include "SimpleViewerMainWindow.h" + +/** + * Don't use "ui_MainWindow.h" instead of below, as + * this makes CMake unable to detect when the UI file changes. + **/ +#include +#include "../SimpleViewerApplication.h" + +namespace SimpleViewer +{ + + SimpleViewerMainWindow::SimpleViewerMainWindow(OrthancStone::NativeStoneApplicationContext& context, SimpleViewerApplication& stoneApplication, QWidget *parent) : + QStoneMainWindow(context, parent), + ui_(new Ui::SimpleViewerMainWindow), + stoneApplication_(stoneApplication) + { + ui_->setupUi(this); + SetCentralStoneWidget(ui_->cairoCentralWidget); + +#if QT_VERSION >= 0x050000 + connect(ui_->toolButtonCrop, &QToolButton::clicked, this, &SimpleViewerMainWindow::cropClicked); + connect(ui_->pushButtonUndoCrop, &QToolButton::clicked, this, &SimpleViewerMainWindow::undoCropClicked); + connect(ui_->toolButtonLine, &QToolButton::clicked, this, &SimpleViewerMainWindow::lineClicked); + connect(ui_->toolButtonCircle, &QToolButton::clicked, this, &SimpleViewerMainWindow::circleClicked); + connect(ui_->toolButtonWindowing, &QToolButton::clicked, this, &SimpleViewerMainWindow::windowingClicked); + connect(ui_->pushButtonRotate, &QPushButton::clicked, this, &SimpleViewerMainWindow::rotateClicked); + connect(ui_->pushButtonInvert, &QPushButton::clicked, this, &SimpleViewerMainWindow::invertClicked); +#else + connect(ui_->toolButtonCrop, SIGNAL(clicked()), this, SLOT(cropClicked())); + connect(ui_->toolButtonLine, SIGNAL(clicked()), this, SLOT(lineClicked())); + connect(ui_->toolButtonCircle, SIGNAL(clicked()), this, SLOT(circleClicked())); + connect(ui_->toolButtonWindowing, SIGNAL(clicked()), this, SLOT(windowingClicked())); + connect(ui_->pushButtonUndoCrop, SIGNAL(clicked()), this, SLOT(undoCropClicked())); + connect(ui_->pushButtonRotate, SIGNAL(clicked()), this, SLOT(rotateClicked())); + connect(ui_->pushButtonInvert, SIGNAL(clicked()), this, SLOT(invertClicked())); +#endif + } + + SimpleViewerMainWindow::~SimpleViewerMainWindow() + { + delete ui_; + } + + void SimpleViewerMainWindow::cropClicked() + { + GenericNoArgCommand command("selectTool:crop"); + stoneApplication_.ExecuteCommand(command); + } + + void SimpleViewerMainWindow::undoCropClicked() + { + GenericNoArgCommand command("action:undo-crop"); + stoneApplication_.ExecuteCommand(command); + } + + void SimpleViewerMainWindow::lineClicked() + { + GenericNoArgCommand command("selectTool:line-measure"); + stoneApplication_.ExecuteCommand(command); + } + + void SimpleViewerMainWindow::circleClicked() + { + GenericNoArgCommand command("selectTool:circle-measure"); + stoneApplication_.ExecuteCommand(command); + } + + void SimpleViewerMainWindow::windowingClicked() + { + GenericNoArgCommand command("selectTool:windowing"); + stoneApplication_.ExecuteCommand(command); + } + + void SimpleViewerMainWindow::rotateClicked() + { + GenericNoArgCommand command("action:rotate"); + stoneApplication_.ExecuteCommand(command); + } + + void SimpleViewerMainWindow::invertClicked() + { + GenericNoArgCommand command("action:invert"); + stoneApplication_.ExecuteCommand(command); + } +} diff -r fe4befe03935 -r d6136a7e914d Applications/Samples/SimpleViewer/Qt/SimpleViewerMainWindow.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Applications/Samples/SimpleViewer/Qt/SimpleViewerMainWindow.h Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,57 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ +#pragma once + +#include +#include + +namespace Ui +{ + class SimpleViewerMainWindow; +} + +using namespace OrthancStone; + +namespace SimpleViewer +{ + class SimpleViewerApplication; + + class SimpleViewerMainWindow : public QStoneMainWindow + { + Q_OBJECT + + private: + Ui::SimpleViewerMainWindow* ui_; + SimpleViewerApplication& stoneApplication_; + + public: + explicit SimpleViewerMainWindow(OrthancStone::NativeStoneApplicationContext& context, SimpleViewerApplication& stoneApplication, QWidget *parent = 0); + ~SimpleViewerMainWindow(); + + private slots: + void cropClicked(); + void undoCropClicked(); + void rotateClicked(); + void windowingClicked(); + void lineClicked(); + void circleClicked(); + void invertClicked(); + }; +} diff -r fe4befe03935 -r d6136a7e914d Applications/Samples/SimpleViewer/Qt/SimpleViewerMainWindow.ui --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Applications/Samples/SimpleViewer/Qt/SimpleViewerMainWindow.ui Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,151 @@ + + + SimpleViewerMainWindow + + + + 0 + 0 + 903 + 634 + + + + + 500 + 300 + + + + + 500 + 300 + + + + Stone of Orthanc + + + Qt::LeftToRight + + + + + 0 + 0 + + + + Qt::LeftToRight + + + + QLayout::SetDefaultConstraint + + + + + + 0 + 500 + + + + + + + + + 0 + 100 + + + + + 16777215 + 100 + + + + + + + windowing + + + + + + + crop + + + + + + + undo crop + + + + + + + line + + + + + + + circle + + + + + + + rotate + + + + + + + invert + + + + + + + + + + + + 0 + 0 + 903 + 22 + + + + + Test + + + + + + + + + QCairoWidget + QGraphicsView +
QCairoWidget.h
+
+
+ + +
diff -r fe4befe03935 -r d6136a7e914d Applications/Samples/SimpleViewer/Qt/mainQt.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Applications/Samples/SimpleViewer/Qt/mainQt.cpp Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,14 @@ +#include "Applications/Qt/QtStoneApplicationRunner.h" + +#include "../SimpleViewerApplication.h" +#include "Framework/Messages/MessageBroker.h" + + +int main(int argc, char* argv[]) +{ + OrthancStone::MessageBroker broker; + SimpleViewer::SimpleViewerApplication stoneApplication(broker); + + OrthancStone::QtStoneApplicationRunner qtAppRunner(broker, stoneApplication); + return qtAppRunner.Execute(argc, argv); +} diff -r fe4befe03935 -r d6136a7e914d Applications/Samples/SimpleViewer/SimpleViewerApplication.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Applications/Samples/SimpleViewer/SimpleViewerApplication.cpp Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,242 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#include "SimpleViewerApplication.h" + +#if ORTHANC_ENABLE_QT==1 +#include "Qt/SimpleViewerMainWindow.h" +#endif + +#if ORTHANC_ENABLE_WASM==1 +#include +#endif + +namespace SimpleViewer { + + void SimpleViewerApplication::Initialize(StoneApplicationContext* context, + IStatusBar& statusBar, + const boost::program_options::variables_map& parameters) + { + using namespace OrthancStone; + + context_ = context; + statusBar_ = &statusBar; + + {// initialize viewports and layout + mainLayout_ = new LayoutWidget("main-layout"); + mainLayout_->SetPadding(10); + mainLayout_->SetBackgroundCleared(true); + mainLayout_->SetBackgroundColor(0, 0, 0); + mainLayout_->SetHorizontal(); + + thumbnailsLayout_ = new LayoutWidget("thumbnail-layout"); + thumbnailsLayout_->SetPadding(10); + thumbnailsLayout_->SetBackgroundCleared(true); + thumbnailsLayout_->SetBackgroundColor(50, 50, 50); + thumbnailsLayout_->SetVertical(); + + mainWidget_ = new LayerWidget(IObserver::broker_, "main-viewport"); + //mainWidget_->RegisterObserver(*this); + + // hierarchy + mainLayout_->AddWidget(thumbnailsLayout_); + mainLayout_->AddWidget(mainWidget_); + + orthancApiClient_.reset(new OrthancApiClient(IObserver::broker_, context_->GetWebService())); + + // sources + smartLoader_.reset(new SmartLoader(IObserver::broker_, *orthancApiClient_)); + smartLoader_->SetImageQuality(SliceImageQuality_FullPam); + + mainLayout_->SetTransmitMouseOver(true); + mainWidgetInteractor_.reset(new MainWidgetInteractor(*this)); + mainWidget_->SetInteractor(*mainWidgetInteractor_); + thumbnailInteractor_.reset(new ThumbnailInteractor(*this)); + } + + statusBar.SetMessage("Use the key \"s\" to reinitialize the layout"); + statusBar.SetMessage("Use the key \"n\" to go to next image in the main viewport"); + + + if (parameters.count("studyId") < 1) + { + LOG(WARNING) << "The study ID is missing, will take the first studyId found in Orthanc"; + orthancApiClient_->GetJsonAsync("/studies", new Callable(*this, &SimpleViewerApplication::OnStudyListReceived)); + } + else + { + SelectStudy(parameters["studyId"].as()); + } + } + + + void SimpleViewerApplication::DeclareStartupOptions(boost::program_options::options_description& options) + { + boost::program_options::options_description generic("Sample options"); + generic.add_options() + ("studyId", boost::program_options::value(), + "Orthanc ID of the study") + ; + + options.add(generic); + } + + void SimpleViewerApplication::OnStudyListReceived(const OrthancApiClient::JsonResponseReadyMessage& message) + { + const Json::Value& response = message.Response; + + if (response.isArray() && response.size() > 1) + { + SelectStudy(response[0].asString()); + } + } + void SimpleViewerApplication::OnStudyReceived(const OrthancApiClient::JsonResponseReadyMessage& message) + { + const Json::Value& response = message.Response; + + if (response.isObject() && response["Series"].isArray()) + { + for (size_t i=0; i < response["Series"].size(); i++) + { + orthancApiClient_->GetJsonAsync("/series/" + response["Series"][(int)i].asString(), new Callable(*this, &SimpleViewerApplication::OnSeriesReceived)); + } + } + } + + void SimpleViewerApplication::OnSeriesReceived(const OrthancApiClient::JsonResponseReadyMessage& message) + { + const Json::Value& response = message.Response; + + if (response.isObject() && response["Instances"].isArray() && response["Instances"].size() > 0) + { + // keep track of all instances IDs + const std::string& seriesId = response["ID"].asString(); + seriesTags_[seriesId] = response; + instancesIdsPerSeriesId_[seriesId] = std::vector(); + for (size_t i = 0; i < response["Instances"].size(); i++) + { + const std::string& instanceId = response["Instances"][static_cast(i)].asString(); + instancesIdsPerSeriesId_[seriesId].push_back(instanceId); + } + + // load the first instance in the thumbnail + LoadThumbnailForSeries(seriesId, instancesIdsPerSeriesId_[seriesId][0]); + + // if this is the first thumbnail loaded, load the first instance in the mainWidget + if (mainWidget_->GetLayerCount() == 0) + { + smartLoader_->SetFrameInWidget(*mainWidget_, 0, instancesIdsPerSeriesId_[seriesId][0], 0); + } + } + } + + void SimpleViewerApplication::LoadThumbnailForSeries(const std::string& seriesId, const std::string& instanceId) + { + LOG(INFO) << "Loading thumbnail for series " << seriesId; + LayerWidget* thumbnailWidget = new LayerWidget(IObserver::broker_, "thumbnail-series-" + seriesId); + thumbnails_.push_back(thumbnailWidget); + thumbnailsLayout_->AddWidget(thumbnailWidget); + thumbnailWidget->RegisterObserverCallback(new Callable(*this, &SimpleViewerApplication::OnWidgetGeometryChanged)); + smartLoader_->SetFrameInWidget(*thumbnailWidget, 0, instanceId, 0); + thumbnailWidget->SetInteractor(*thumbnailInteractor_); + } + + void SimpleViewerApplication::SelectStudy(const std::string& studyId) + { + orthancApiClient_->GetJsonAsync("/studies/" + studyId, new Callable(*this, &SimpleViewerApplication::OnStudyReceived)); + } + + void SimpleViewerApplication::OnWidgetGeometryChanged(const LayerWidget::GeometryChangedMessage& message) + { + message.origin_.FitContent(); + } + + void SimpleViewerApplication::SelectSeriesInMainViewport(const std::string& seriesId) + { + smartLoader_->SetFrameInWidget(*mainWidget_, 0, instancesIdsPerSeriesId_[seriesId][0], 0); + } + + + + void SimpleViewerApplication::ExecuteCommand(ICommand& command) + { + statusBar_->SetMessage("received command: " + std::string(command.GetName())); + if (command.GetName() == "selectTool:circle-measure") + { + SelectTool(Tools_CircleMeasure); + } + else if (command.GetName() == "selectTool:line-measure") + { + SelectTool(Tools_LineMeasure); + } + else if (command.GetName() == "selectTool:crop") + { + SelectTool(Tools_Crop); + } + else if (command.GetName() == "selectTool:windowing") + { + SelectTool(Tools_Windowing); + } + else if (command.GetName() == "action:rotate") + { + ExecuteAction(Actions_Rotate); + } + else if (command.GetName() == "action:undo-crop") + { + ExecuteAction(Actions_UndoCrop); + } + else if (command.GetName() == "action:invert") + { + ExecuteAction(Actions_Invert); + } + else + { + command.Execute(); + } + } + + void SimpleViewerApplication::ExecuteAction(SimpleViewerApplication::Actions action) + { + // TODO + } + + void SimpleViewerApplication::SelectTool(SimpleViewerApplication::Tools tool) + { + currentTool_ = tool; + } + +#if ORTHANC_ENABLE_QT==1 + QStoneMainWindow* SimpleViewerApplication::CreateQtMainWindow() + { + return new SimpleViewerMainWindow(dynamic_cast(*context_), *this); + } +#endif + +#if ORTHANC_ENABLE_WASM==1 + void SimpleViewerApplication::InitializeWasm() { + + AttachWidgetToWasmViewport("canvasThumbnails", thumbnailsLayout_); + AttachWidgetToWasmViewport("canvasMain", mainWidget_); + } +#endif + + +} diff -r fe4befe03935 -r d6136a7e914d Applications/Samples/SimpleViewer/SimpleViewerApplication.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Applications/Samples/SimpleViewer/SimpleViewerApplication.h Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,176 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "Applications/IStoneApplication.h" + +#include "Framework/Layers/OrthancFrameLayerSource.h" +#include "Framework/Layers/CircleMeasureTracker.h" +#include "Framework/Layers/LineMeasureTracker.h" +#include "Framework/Widgets/LayerWidget.h" +#include "Framework/Widgets/LayoutWidget.h" +#include "Framework/Messages/IObserver.h" +#include "Framework/SmartLoader.h" + +#if ORTHANC_ENABLE_WASM==1 +#include "Platforms/Wasm/WasmPlatformApplicationAdapter.h" +#include "Platforms/Wasm/Defaults.h" +#endif + +#if ORTHANC_ENABLE_QT==1 +#include "Qt/SimpleViewerMainWindow.h" +#endif + +#include +#include + +#include "ThumbnailInteractor.h" +#include "MainWidgetInteractor.h" +#include "AppStatus.h" +#include "Messages.h" + +using namespace OrthancStone; + + +namespace SimpleViewer +{ + + class SimpleViewerApplication : + public IStoneApplication, + public IObserver, + public IObservable + { + public: + + struct StatusUpdatedMessage : public BaseMessage + { + const AppStatus& status_; + + StatusUpdatedMessage(const AppStatus& status) + : BaseMessage(), + status_(status) + { + } + }; + + enum Tools { + Tools_LineMeasure, + Tools_CircleMeasure, + Tools_Crop, + Tools_Windowing, + Tools_Zoom, + Tools_Pan + }; + + enum Actions { + Actions_Rotate, + Actions_Invert, + Actions_UndoCrop + }; + + private: + Tools currentTool_; + std::unique_ptr mainWidgetInteractor_; + std::unique_ptr thumbnailInteractor_; + LayoutWidget* mainLayout_; + LayoutWidget* thumbnailsLayout_; + LayerWidget* mainWidget_; + std::vector thumbnails_; + std::map > instancesIdsPerSeriesId_; + std::map seriesTags_; + BaseCommandBuilder commandBuilder_; + + unsigned int currentInstanceIndex_; + OrthancStone::WidgetViewport* wasmViewport1_; + OrthancStone::WidgetViewport* wasmViewport2_; + + IStatusBar* statusBar_; + std::unique_ptr smartLoader_; + std::unique_ptr orthancApiClient_; + + Orthanc::Font font_; + + public: + SimpleViewerApplication(MessageBroker& broker) : + IObserver(broker), + IObservable(broker), + currentTool_(Tools_LineMeasure), + mainLayout_(NULL), + currentInstanceIndex_(0), + wasmViewport1_(NULL), + wasmViewport2_(NULL) + { + font_.LoadFromResource(Orthanc::EmbeddedResources::FONT_UBUNTU_MONO_BOLD_16); + } + + virtual void Finalize() {} + virtual IWidget* GetCentralWidget() {return mainLayout_;} + + virtual void DeclareStartupOptions(boost::program_options::options_description& options); + virtual void Initialize(StoneApplicationContext* context, + IStatusBar& statusBar, + const boost::program_options::variables_map& parameters); + + void OnStudyListReceived(const OrthancApiClient::JsonResponseReadyMessage& message); + + void OnStudyReceived(const OrthancApiClient::JsonResponseReadyMessage& message); + + void OnSeriesReceived(const OrthancApiClient::JsonResponseReadyMessage& message); + + void LoadThumbnailForSeries(const std::string& seriesId, const std::string& instanceId); + + void SelectStudy(const std::string& studyId); + + void OnWidgetGeometryChanged(const LayerWidget::GeometryChangedMessage& message); + + void SelectSeriesInMainViewport(const std::string& seriesId); + + void SelectTool(Tools tool); + + Tools GetCurrentTool() const + { + return currentTool_; + } + + const Orthanc::Font& GetFont() const + { + return font_; + } + + void ExecuteAction(Actions action); + + virtual std::string GetTitle() const {return "SimpleViewer";} + virtual void ExecuteCommand(ICommand& command); + virtual BaseCommandBuilder& GetCommandBuilder() {return commandBuilder_;} + + +#if ORTHANC_ENABLE_WASM==1 + virtual void InitializeWasm(); +#endif + +#if ORTHANC_ENABLE_QT==1 + virtual QStoneMainWindow* CreateQtMainWindow(); +#endif + }; + + +} diff -r fe4befe03935 -r d6136a7e914d Applications/Samples/SimpleViewer/ThumbnailInteractor.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Applications/Samples/SimpleViewer/ThumbnailInteractor.cpp Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,45 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + +#include "ThumbnailInteractor.h" + +#include "SimpleViewerApplication.h" + +namespace SimpleViewer { + + IWorldSceneMouseTracker* ThumbnailInteractor::CreateMouseTracker(WorldSceneWidget& widget, + const ViewportGeometry& view, + MouseButton button, + KeyboardModifiers modifiers, + int viewportX, + int viewportY, + double x, + double y, + IStatusBar* statusBar) + { + if (button == MouseButton_Left) + { + statusBar->SetMessage("selected thumbnail " + widget.GetName()); + std::string seriesId = widget.GetName().substr(strlen("thumbnail-series-")); + application_.SelectSeriesInMainViewport(seriesId); + } + return NULL; + } +} diff -r fe4befe03935 -r d6136a7e914d Applications/Samples/SimpleViewer/ThumbnailInteractor.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Applications/Samples/SimpleViewer/ThumbnailInteractor.h Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,76 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "Framework/Widgets/IWorldSceneInteractor.h" + +using namespace OrthancStone; + +namespace SimpleViewer { + + class SimpleViewerApplication; + + class ThumbnailInteractor : public IWorldSceneInteractor + { + private: + SimpleViewerApplication& application_; + public: + ThumbnailInteractor(SimpleViewerApplication& application) : + application_(application) + { + } + + virtual IWorldSceneMouseTracker* CreateMouseTracker(WorldSceneWidget& widget, + const ViewportGeometry& view, + MouseButton button, + KeyboardModifiers modifiers, + int viewportX, + int viewportY, + double x, + double y, + IStatusBar* statusBar); + + virtual void MouseOver(CairoContext& context, + WorldSceneWidget& widget, + const ViewportGeometry& view, + double x, + double y, + IStatusBar* statusBar) + {} + + virtual void MouseWheel(WorldSceneWidget& widget, + MouseWheelDirection direction, + KeyboardModifiers modifiers, + IStatusBar* statusBar) + {} + + virtual void KeyPressed(WorldSceneWidget& widget, + KeyboardKeys key, + char keyChar, + KeyboardModifiers modifiers, + IStatusBar* statusBar) + {} + + }; + + +} diff -r fe4befe03935 -r d6136a7e914d Applications/Samples/SimpleViewer/Wasm/SimpleViewerWasmApplicationAdapter.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Applications/Samples/SimpleViewer/Wasm/SimpleViewerWasmApplicationAdapter.cpp Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,51 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + +#include "SimpleViewerWasmApplicationAdapter.h" + +namespace SimpleViewer +{ + + SimpleViewerWasmApplicationAdapter::SimpleViewerWasmApplicationAdapter(MessageBroker &broker, SimpleViewerApplication &application) + : WasmPlatformApplicationAdapter(broker, application), + viewerApplication_(application) + { + application.RegisterObserverCallback(new Callable(*this, &SimpleViewerWasmApplicationAdapter::OnStatusUpdated)); + } + + void SimpleViewerWasmApplicationAdapter::OnStatusUpdated(const SimpleViewerApplication::StatusUpdatedMessage &message) + { + Json::Value statusJson; + message.status_.ToJson(statusJson); + + Json::Value event; + event["event"] = "appStatusUpdated"; + event["data"] = statusJson; + + Json::StreamWriterBuilder builder; + std::unique_ptr writer(builder.newStreamWriter()); + std::ostringstream outputStr; + + writer->write(event, &outputStr); + + NotifyStatusUpdateFromCppToWeb(outputStr.str()); + } + +} // namespace SimpleViewer \ No newline at end of file diff -r fe4befe03935 -r d6136a7e914d Applications/Samples/SimpleViewer/Wasm/SimpleViewerWasmApplicationAdapter.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Applications/Samples/SimpleViewer/Wasm/SimpleViewerWasmApplicationAdapter.h Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,43 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + +#pragma once + +#include +#include +#include + +#include "../SimpleViewerApplication.h" + +namespace SimpleViewer { + + class SimpleViewerWasmApplicationAdapter : public WasmPlatformApplicationAdapter + { + SimpleViewerApplication& viewerApplication_; + + public: + SimpleViewerWasmApplicationAdapter(MessageBroker& broker, SimpleViewerApplication& application); + + private: + void OnStatusUpdated(const SimpleViewerApplication::StatusUpdatedMessage& message); + + }; + +} \ No newline at end of file diff -r fe4befe03935 -r d6136a7e914d Applications/Samples/SimpleViewer/Wasm/mainWasm.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Applications/Samples/SimpleViewer/Wasm/mainWasm.cpp Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,38 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + +#include "Platforms/Wasm/WasmWebService.h" +#include "Platforms/Wasm/WasmViewport.h" + +#include + +#include "../SimpleViewerApplication.h" +#include "SimpleViewerWasmApplicationAdapter.h" + + +OrthancStone::IStoneApplication* CreateUserApplication(OrthancStone::MessageBroker& broker) { + + return new SimpleViewer::SimpleViewerApplication(broker); +} + +OrthancStone::WasmPlatformApplicationAdapter* CreateWasmApplicationAdapter(OrthancStone::MessageBroker& broker, IStoneApplication* application) +{ + return new SimpleViewer::SimpleViewerWasmApplicationAdapter(broker, *(dynamic_cast(application))); +} \ No newline at end of file diff -r fe4befe03935 -r d6136a7e914d Applications/Samples/SimpleViewer/Wasm/simple-viewer.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Applications/Samples/SimpleViewer/Wasm/simple-viewer.html Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,39 @@ + + + + + + + + + + + + Simple Viewer + + + + +
+ + +
+
+ + + + + + + + + +
+ + + + \ No newline at end of file diff -r fe4befe03935 -r d6136a7e914d Applications/Samples/SimpleViewer/Wasm/simple-viewer.ts --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Applications/Samples/SimpleViewer/Wasm/simple-viewer.ts Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,75 @@ +/// + +InitializeWasmApplication("OrthancStoneSimpleViewer", "/orthanc"); + +function SelectTool(toolName: string) { + var command = { + command: "selectTool:" + toolName, + commandType: "generic-no-arg-command", + args: { + } + }; + SendMessageToStoneApplication(JSON.stringify(command)); + +} + +function PerformAction(actionName: string) { + var command = { + command: "action:" + actionName, + commandType: "generic-no-arg-command", + args: { + } + }; + SendMessageToStoneApplication(JSON.stringify(command)); +} + +class SimpleViewerUI { + + private _labelPatientId: HTMLSpanElement; + private _labelStudyDescription: HTMLSpanElement; + + public constructor() { + // install "SelectTool" handlers + document.querySelectorAll("[tool-selector]").forEach((e) => { + console.log(e); + (e as HTMLButtonElement).addEventListener("click", () => { + console.log(e); + SelectTool(e.attributes["tool-selector"].value); + }); + }); + + // install "PerformAction" handlers + document.querySelectorAll("[action-trigger]").forEach((e) => { + (e as HTMLButtonElement).addEventListener("click", () => { + PerformAction(e.attributes["action-trigger"].value); + }); + }); + + // connect all ui elements to members + this._labelPatientId = document.getElementById("label-patient-id") as HTMLSpanElement; + this._labelStudyDescription = document.getElementById("label-study-description") as HTMLSpanElement; + } + + public onAppStatusUpdated(status: any) { + this._labelPatientId.innerText = status["patientId"]; + this._labelStudyDescription.innerText = status["studyDescription"]; + // this.highlighThumbnail(status["currentInstanceIdInMainViewport"]); + } + +} + +var ui = new SimpleViewerUI(); + +// this method is called "from the C++ code" when the StoneApplication is updated. +// it can be used to update the UI of the application +function UpdateWebApplication(statusUpdateMessageString: string) { + console.log("updating web application: ", statusUpdateMessageString); + let statusUpdateMessage = JSON.parse(statusUpdateMessageString); + + if ("event" in statusUpdateMessage) { + let eventName = statusUpdateMessage["event"]; + if (eventName == "appStatusUpdated") { + ui.onAppStatusUpdated(statusUpdateMessage["data"]); + } + } +} diff -r fe4befe03935 -r d6136a7e914d Applications/Samples/SimpleViewer/Wasm/styles.css --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Applications/Samples/SimpleViewer/Wasm/styles.css Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,54 @@ +html, body { + width: 100%; + height: 100%; + margin: 0px; + border: 0; + overflow: hidden; /* Disable scrollbars */ + display: block; /* No floating content on sides */ + background-color: black; + color: white; + font-family: Arial, Helvetica, sans-serif; +} + +canvas { + left:0px; + top:0px; +} + +#canvas-group { + padding:5px; + background-color: grey; +} + +#status-group { + padding:5px; +} + +#worklist-group { + padding:5px; +} + +.vsol-button { + height: 40px; +} + +#thumbnails-group ul li { + display: inline; + list-style: none; +} + +.thumbnail { + width: 100px; + height: 100px; + padding: 3px; +} + +.thumbnail-selected { + border-width: 1px; + border-color: red; + border-style: solid; +} + +#template-thumbnail-li { + display: none !important; +} \ No newline at end of file diff -r fe4befe03935 -r d6136a7e914d Applications/Samples/SimpleViewer/Wasm/tsconfig-simple-viewer.json --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Applications/Samples/SimpleViewer/Wasm/tsconfig-simple-viewer.json Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,9 @@ +{ + "extends" : "../../Web/tsconfig-samples", + "compilerOptions": { + "outFile": "../../build-web/simple-viewer/app-simple-viewer.js" + }, + "include" : [ + "simple-viewer.ts" + ] +} \ No newline at end of file diff -r fe4befe03935 -r d6136a7e914d Applications/Samples/SimpleViewerApplicationSingleFile.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Applications/Samples/SimpleViewerApplicationSingleFile.h Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,440 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "SampleApplicationBase.h" + +#include "../../Framework/Layers/OrthancFrameLayerSource.h" +#include "../../Framework/Layers/CircleMeasureTracker.h" +#include "../../Framework/Layers/LineMeasureTracker.h" +#include "../../Framework/Widgets/LayerWidget.h" +#include "../../Framework/Widgets/LayoutWidget.h" +#include "../../Framework/Messages/IObserver.h" +#include "../../Framework/SmartLoader.h" + +#if ORTHANC_ENABLE_WASM==1 +#include "../../Platforms/Wasm/WasmPlatformApplicationAdapter.h" +#include "../../Platforms/Wasm/Defaults.h" +#endif + +#include +#include + +namespace OrthancStone +{ + namespace Samples + { + class SimpleViewerApplication : + public SampleSingleCanvasWithButtonsApplicationBase, + public IObserver + { + private: + class ThumbnailInteractor : public IWorldSceneInteractor + { + private: + SimpleViewerApplication& application_; + public: + ThumbnailInteractor(SimpleViewerApplication& application) : + application_(application) + { + } + + virtual IWorldSceneMouseTracker* CreateMouseTracker(WorldSceneWidget& widget, + const ViewportGeometry& view, + MouseButton button, + KeyboardModifiers modifiers, + int viewportX, + int viewportY, + double x, + double y, + IStatusBar* statusBar) + { + if (button == MouseButton_Left) + { + statusBar->SetMessage("selected thumbnail " + widget.GetName()); + std::string seriesId = widget.GetName().substr(strlen("thumbnail-series-")); + application_.SelectSeriesInMainViewport(seriesId); + } + return NULL; + } + virtual void MouseOver(CairoContext& context, + WorldSceneWidget& widget, + const ViewportGeometry& view, + double x, + double y, + IStatusBar* statusBar) + {} + + virtual void MouseWheel(WorldSceneWidget& widget, + MouseWheelDirection direction, + KeyboardModifiers modifiers, + IStatusBar* statusBar) + {} + + virtual void KeyPressed(WorldSceneWidget& widget, + KeyboardKeys key, + char keyChar, + KeyboardModifiers modifiers, + IStatusBar* statusBar) + {} + + }; + + class MainWidgetInteractor : public IWorldSceneInteractor + { + private: + SimpleViewerApplication& application_; + + public: + MainWidgetInteractor(SimpleViewerApplication& application) : + application_(application) + { + } + + virtual IWorldSceneMouseTracker* CreateMouseTracker(WorldSceneWidget& widget, + const ViewportGeometry& view, + MouseButton button, + KeyboardModifiers modifiers, + int viewportX, + int viewportY, + double x, + double y, + IStatusBar* statusBar) + { + if (button == MouseButton_Left) + { + if (application_.currentTool_ == Tools_LineMeasure) + { + return new LineMeasureTracker(statusBar, dynamic_cast(widget).GetSlice(), + x, y, 255, 0, 0, application_.GetFont()); + } + else if (application_.currentTool_ == Tools_CircleMeasure) + { + return new CircleMeasureTracker(statusBar, dynamic_cast(widget).GetSlice(), + x, y, 255, 0, 0, application_.GetFont()); + } + } + return NULL; + } + + virtual void MouseOver(CairoContext& context, + WorldSceneWidget& widget, + const ViewportGeometry& view, + double x, + double y, + IStatusBar* statusBar) + { + if (statusBar != NULL) + { + Vector p = dynamic_cast(widget).GetSlice().MapSliceToWorldCoordinates(x, y); + + char buf[64]; + sprintf(buf, "X = %.02f Y = %.02f Z = %.02f (in cm)", + p[0] / 10.0, p[1] / 10.0, p[2] / 10.0); + statusBar->SetMessage(buf); + } + } + + virtual void MouseWheel(WorldSceneWidget& widget, + MouseWheelDirection direction, + KeyboardModifiers modifiers, + IStatusBar* statusBar) + { + } + + virtual void KeyPressed(WorldSceneWidget& widget, + KeyboardKeys key, + char keyChar, + KeyboardModifiers modifiers, + IStatusBar* statusBar) + { + switch (keyChar) + { + case 's': + widget.FitContent(); + break; + + case 'l': + application_.currentTool_ = Tools_LineMeasure; + break; + + case 'c': + application_.currentTool_ = Tools_CircleMeasure; + break; + + default: + break; + } + } + }; + + +#if ORTHANC_ENABLE_WASM==1 + class SimpleViewerApplicationAdapter : public WasmPlatformApplicationAdapter + { + SimpleViewerApplication& viewerApplication_; + + public: + SimpleViewerApplicationAdapter(MessageBroker& broker, SimpleViewerApplication& application) + : WasmPlatformApplicationAdapter(broker, application), + viewerApplication_(application) + { + + } + + virtual void HandleMessageFromWeb(std::string& output, const std::string& input) { + if (input == "select-tool:line-measure") + { + viewerApplication_.currentTool_ = Tools_LineMeasure; + NotifyStatusUpdateFromCppToWeb("currentTool=line-measure"); + } + else if (input == "select-tool:circle-measure") + { + viewerApplication_.currentTool_ = Tools_CircleMeasure; + NotifyStatusUpdateFromCppToWeb("currentTool=circle-measure"); + } + + output = "ok"; + } + + virtual void NotifyStatusUpdateFromCppToWeb(const std::string& statusUpdateMessage) { + UpdateStoneApplicationStatusFromCpp(statusUpdateMessage.c_str()); + } + + }; +#endif + enum Tools { + Tools_LineMeasure, + Tools_CircleMeasure + }; + + Tools currentTool_; + std::auto_ptr mainWidgetInteractor_; + std::auto_ptr thumbnailInteractor_; + LayoutWidget* mainLayout_; + LayoutWidget* thumbnailsLayout_; + std::vector thumbnails_; + + std::map > instancesIdsPerSeriesId_; + std::map seriesTags_; + + unsigned int currentInstanceIndex_; + OrthancStone::WidgetViewport* wasmViewport1_; + OrthancStone::WidgetViewport* wasmViewport2_; + + IStatusBar* statusBar_; + std::auto_ptr smartLoader_; + std::auto_ptr orthancApiClient_; + + Orthanc::Font font_; + + public: + SimpleViewerApplication(MessageBroker& broker) : + IObserver(broker), + currentTool_(Tools_LineMeasure), + mainLayout_(NULL), + currentInstanceIndex_(0), + wasmViewport1_(NULL), + wasmViewport2_(NULL) + { + font_.LoadFromResource(Orthanc::EmbeddedResources::FONT_UBUNTU_MONO_BOLD_16); +// DeclareIgnoredMessage(MessageType_Widget_ContentChanged); + } + + virtual void DeclareStartupOptions(boost::program_options::options_description& options) + { + boost::program_options::options_description generic("Sample options"); + generic.add_options() + ("studyId", boost::program_options::value(), + "Orthanc ID of the study") + ; + + options.add(generic); + } + + virtual void Initialize(StoneApplicationContext* context, + IStatusBar& statusBar, + const boost::program_options::variables_map& parameters) + { + using namespace OrthancStone; + + context_ = context; + statusBar_ = &statusBar; + + {// initialize viewports and layout + mainLayout_ = new LayoutWidget("main-layout"); + mainLayout_->SetPadding(10); + mainLayout_->SetBackgroundCleared(true); + mainLayout_->SetBackgroundColor(0, 0, 0); + mainLayout_->SetHorizontal(); + + thumbnailsLayout_ = new LayoutWidget("thumbnail-layout"); + thumbnailsLayout_->SetPadding(10); + thumbnailsLayout_->SetBackgroundCleared(true); + thumbnailsLayout_->SetBackgroundColor(50, 50, 50); + thumbnailsLayout_->SetVertical(); + + mainWidget_ = new LayerWidget(broker_, "main-viewport"); + //mainWidget_->RegisterObserver(*this); + + // hierarchy + mainLayout_->AddWidget(thumbnailsLayout_); + mainLayout_->AddWidget(mainWidget_); + + orthancApiClient_.reset(new OrthancApiClient(IObserver::broker_, context_->GetWebService())); + + // sources + smartLoader_.reset(new SmartLoader(IObserver::broker_, *orthancApiClient_)); + smartLoader_->SetImageQuality(SliceImageQuality_FullPam); + + mainLayout_->SetTransmitMouseOver(true); + mainWidgetInteractor_.reset(new MainWidgetInteractor(*this)); + mainWidget_->SetInteractor(*mainWidgetInteractor_); + thumbnailInteractor_.reset(new ThumbnailInteractor(*this)); + } + + statusBar.SetMessage("Use the key \"s\" to reinitialize the layout"); + statusBar.SetMessage("Use the key \"n\" to go to next image in the main viewport"); + + + if (parameters.count("studyId") < 1) + { + LOG(WARNING) << "The study ID is missing, will take the first studyId found in Orthanc"; + orthancApiClient_->GetJsonAsync("/studies", new Callable(*this, &SimpleViewerApplication::OnStudyListReceived)); + } + else + { + SelectStudy(parameters["studyId"].as()); + } + } + + void OnStudyListReceived(const OrthancApiClient::JsonResponseReadyMessage& message) + { + const Json::Value& response = message.Response; + + if (response.isArray() && response.size() > 1) + { + SelectStudy(response[0].asString()); + } + } + void OnStudyReceived(const OrthancApiClient::JsonResponseReadyMessage& message) + { + const Json::Value& response = message.Response; + + if (response.isObject() && response["Series"].isArray()) + { + for (size_t i=0; i < response["Series"].size(); i++) + { + orthancApiClient_->GetJsonAsync("/series/" + response["Series"][(int)i].asString(), new Callable(*this, &SimpleViewerApplication::OnSeriesReceived)); + } + } + } + + void OnSeriesReceived(const OrthancApiClient::JsonResponseReadyMessage& message) + { + const Json::Value& response = message.Response; + + if (response.isObject() && response["Instances"].isArray() && response["Instances"].size() > 0) + { + // keep track of all instances IDs + const std::string& seriesId = response["ID"].asString(); + seriesTags_[seriesId] = response; + instancesIdsPerSeriesId_[seriesId] = std::vector(); + for (size_t i = 0; i < response["Instances"].size(); i++) + { + const std::string& instanceId = response["Instances"][static_cast(i)].asString(); + instancesIdsPerSeriesId_[seriesId].push_back(instanceId); + } + + // load the first instance in the thumbnail + LoadThumbnailForSeries(seriesId, instancesIdsPerSeriesId_[seriesId][0]); + + // if this is the first thumbnail loaded, load the first instance in the mainWidget + LayerWidget& widget = *dynamic_cast(mainWidget_); + if (widget.GetLayerCount() == 0) + { + smartLoader_->SetFrameInWidget(widget, 0, instancesIdsPerSeriesId_[seriesId][0], 0); + } + } + } + + void LoadThumbnailForSeries(const std::string& seriesId, const std::string& instanceId) + { + LOG(INFO) << "Loading thumbnail for series " << seriesId; + LayerWidget* thumbnailWidget = new LayerWidget(IObserver::broker_, "thumbnail-series-" + seriesId); + thumbnails_.push_back(thumbnailWidget); + thumbnailsLayout_->AddWidget(thumbnailWidget); + thumbnailWidget->RegisterObserverCallback(new Callable(*this, &SimpleViewerApplication::OnWidgetGeometryChanged)); + smartLoader_->SetFrameInWidget(*thumbnailWidget, 0, instanceId, 0); + thumbnailWidget->SetInteractor(*thumbnailInteractor_); + } + + void SelectStudy(const std::string& studyId) + { + orthancApiClient_->GetJsonAsync("/studies/" + studyId, new Callable(*this, &SimpleViewerApplication::OnStudyReceived)); + } + + void OnWidgetGeometryChanged(const LayerWidget::GeometryChangedMessage& message) + { + message.origin_.FitContent(); + } + + void SelectSeriesInMainViewport(const std::string& seriesId) + { + LayerWidget& widget = *dynamic_cast(mainWidget_); + smartLoader_->SetFrameInWidget(widget, 0, instancesIdsPerSeriesId_[seriesId][0], 0); + } + + const Orthanc::Font& GetFont() const + { + return font_; + } + + virtual void OnPushButton1Clicked() {} + virtual void OnPushButton2Clicked() {} + virtual void OnTool1Clicked() { currentTool_ = Tools_LineMeasure;} + virtual void OnTool2Clicked() { currentTool_ = Tools_CircleMeasure;} + + virtual void GetButtonNames(std::string& pushButton1, + std::string& pushButton2, + std::string& tool1, + std::string& tool2 + ) { + tool1 = "line"; + tool2 = "circle"; + pushButton1 = "action1"; + pushButton2 = "action2"; + } + +#if ORTHANC_ENABLE_WASM==1 + virtual void InitializeWasm() { + + AttachWidgetToWasmViewport("canvas", thumbnailsLayout_); + AttachWidgetToWasmViewport("canvas2", mainWidget_); + } +#endif + + }; + + + } +} diff -r fe4befe03935 -r d6136a7e914d Applications/Samples/SingleFrameApplication.h --- a/Applications/Samples/SingleFrameApplication.h Mon Nov 05 10:04:56 2018 +0100 +++ b/Applications/Samples/SingleFrameApplication.h Mon Nov 05 10:06:18 2018 +0100 @@ -33,8 +33,8 @@ namespace Samples { class SingleFrameApplication : - public SampleApplicationBase, - private ILayerSource::IObserver + public SampleSingleCanvasApplicationBase, + public IObserver { private: class Interactor : public IWorldSceneInteractor @@ -51,6 +51,9 @@ virtual IWorldSceneMouseTracker* CreateMouseTracker(WorldSceneWidget& widget, const ViewportGeometry& view, MouseButton button, + KeyboardModifiers modifiers, + int viewportX, + int viewportY, double x, double y, IStatusBar* statusBar) @@ -99,14 +102,15 @@ } virtual void KeyPressed(WorldSceneWidget& widget, - char key, + KeyboardKeys key, + char keyChar, KeyboardModifiers modifiers, IStatusBar* statusBar) { - switch (key) + switch (keyChar) { case 's': - widget.SetDefaultView(); + widget.FitContent(); break; default: @@ -138,6 +142,12 @@ } } } + + + LayerWidget& GetMainWidget() + { + return *dynamic_cast(mainWidget_); + } void SetSlice(size_t index) @@ -148,7 +158,7 @@ slice_ = index; #if 1 - widget_->SetSlice(source_->GetSlice(slice_).GetGeometry()); + GetMainWidget().SetSlice(source_->GetSlice(slice_).GetGeometry()); #else // TEST for scene extents - Rotate the axes double a = 15.0 / 180.0 * M_PI; @@ -169,52 +179,33 @@ } - virtual void NotifyGeometryReady(const ILayerSource& source) + void OnMainWidgetGeometryReady(const ILayerSource::GeometryReadyMessage& message) { // Once the geometry of the series is downloaded from Orthanc, - // display its first slice, and adapt the viewport to fit this + // display its middle slice, and adapt the viewport to fit this // slice - if (source_ == &source) + if (source_ == &message.origin_) { SetSlice(source_->GetSliceCount() / 2); } - widget_->SetDefaultView(); - } - - virtual void NotifyGeometryError(const ILayerSource& source) - { + GetMainWidget().FitContent(); } - virtual void NotifyContentChange(const ILayerSource& source) - { - } + std::auto_ptr mainWidgetInteractor_; + std::auto_ptr orthancApiClient_; + const OrthancFrameLayerSource* source_; + unsigned int slice_; - virtual void NotifySliceChange(const ILayerSource& source, - const Slice& slice) - { - } - - virtual void NotifyLayerReady(std::auto_ptr& layer, - const ILayerSource& source, - const CoordinateSystem3D& slice, - bool isError) - { - } - - LayerWidget* widget_; - const OrthancFrameLayerSource* source_; - unsigned int slice_; - public: - SingleFrameApplication() : - widget_(NULL), + SingleFrameApplication(MessageBroker& broker) : + IObserver(broker), source_(NULL), slice_(0) { } - virtual void DeclareCommandLineOptions(boost::program_options::options_description& options) + virtual void DeclareStartupOptions(boost::program_options::options_description& options) { boost::program_options::options_description generic("Sample options"); generic.add_options() @@ -229,12 +220,14 @@ options.add(generic); } - virtual void Initialize(BasicApplicationContext& context, + virtual void Initialize(StoneApplicationContext* context, IStatusBar& statusBar, const boost::program_options::variables_map& parameters) { using namespace OrthancStone; + context_ = context; + statusBar.SetMessage("Use the key \"s\" to reinitialize the layout"); if (parameters.count("instance") != 1) @@ -246,17 +239,14 @@ std::string instance = parameters["instance"].as(); int frame = parameters["frame"].as(); - std::auto_ptr widget(new LayerWidget); + orthancApiClient_.reset(new OrthancApiClient(IObserver::broker_, context_->GetWebService())); + mainWidget_ = new LayerWidget(broker_, "main-widget"); -#if 1 - std::auto_ptr layer - (new OrthancFrameLayerSource(context.GetWebService())); - //layer->SetImageQuality(SliceImageQuality_Jpeg50); + std::auto_ptr layer(new OrthancFrameLayerSource(broker_, *orthancApiClient_)); + source_ = layer.get(); layer->LoadFrame(instance, frame); - //layer->LoadSeries("6f1b492a-e181e200-44e51840-ef8db55e-af529ab6"); - layer->Register(*this); - source_ = layer.get(); - widget->AddLayer(layer.release()); + layer->RegisterObserverCallback(new Callable(*this, &SingleFrameApplication::OnMainWidgetGeometryReady)); + GetMainWidget().AddLayer(layer.release()); RenderStyle s; @@ -265,53 +255,14 @@ s.interpolation_ = ImageInterpolation_Bilinear; } - //s.drawGrid_ = true; - widget->SetLayerStyle(0, s); -#else - // 0178023P** - // Extent of the CT layer: (-35.068 -20.368) => (34.932 49.632) - std::auto_ptr ct; - ct.reset(new OrthancFrameLayerSource(context.GetWebService())); - //ct->LoadInstance("c804a1a2-142545c9-33b32fe2-3df4cec0-a2bea6d6", 0); - //ct->LoadInstance("4bd4304f-47478948-71b24af2-51f4f1bc-275b6c1b", 0); // BAD SLICE - //ct->SetImageQuality(SliceImageQuality_Jpeg50); - ct->LoadSeries("dd069910-4f090474-7d2bba07-e5c10783-f9e4fb1d"); - - ct->Register(*this); - widget->AddLayer(ct.release()); - - std::auto_ptr pet; - pet.reset(new OrthancFrameLayerSource(context.GetWebService())); - //pet->LoadInstance("a1c4dc6b-255d27f0-88069875-8daed730-2f5ee5c6", 0); - pet->LoadSeries("aabad2e7-80702b5d-e599d26c-4f13398e-38d58a9e"); - pet->Register(*this); - source_ = pet.get(); - widget->AddLayer(pet.release()); + GetMainWidget().SetLayerStyle(0, s); + GetMainWidget().SetTransmitMouseOver(true); - { - RenderStyle s; - //s.drawGrid_ = true; - s.alpha_ = 1; - widget->SetLayerStyle(0, s); - } - - { - RenderStyle s; - //s.drawGrid_ = true; - s.SetColor(255, 0, 0); // Draw missing PET layer in red - s.alpha_ = 0.5; - s.applyLut_ = true; - s.lut_ = Orthanc::EmbeddedResources::COLORMAP_JET; - s.interpolation_ = ImageInterpolation_Bilinear; - widget->SetLayerStyle(1, s); - } -#endif - - widget_ = widget.get(); - widget_->SetTransmitMouseOver(true); - widget_->SetInteractor(context.AddInteractor(new Interactor(*this))); - context.SetCentralWidget(widget.release()); + mainWidgetInteractor_.reset(new Interactor(*this)); + GetMainWidget().SetInteractor(*mainWidgetInteractor_); } }; + + } } diff -r fe4befe03935 -r d6136a7e914d Applications/Samples/SingleFrameEditorApplication.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Applications/Samples/SingleFrameEditorApplication.h Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,2922 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "SampleApplicationBase.h" + +#include "../../Framework/Toolbox/ImageGeometry.h" +#include "../../Framework/Toolbox/OrthancApiClient.h" +#include "../../Framework/Toolbox/DicomFrameConverter.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define EXPORT_USING_PAM 1 + + +#include + +namespace OrthancStone +{ + static Matrix CreateOffsetMatrix(double dx, + double dy) + { + Matrix m = LinearAlgebra::IdentityMatrix(3); + m(0, 2) = dx; + m(1, 2) = dy; + return m; + } + + + static Matrix CreateScalingMatrix(double sx, + double sy) + { + Matrix m = LinearAlgebra::IdentityMatrix(3); + m(0, 0) = sx; + m(1, 1) = sy; + return m; + } + + + static Matrix CreateRotationMatrix(double angle) + { + Matrix m; + const double v[] = { cos(angle), -sin(angle), 0, + sin(angle), cos(angle), 0, + 0, 0, 1 }; + LinearAlgebra::FillMatrix(m, 3, 3, v); + return m; + } + + + class BitmapStack : + public IObserver, + public IObservable + { + public: + typedef OriginMessage GeometryChangedMessage; + typedef OriginMessage ContentChangedMessage; + + + enum Corner + { + Corner_TopLeft, + Corner_TopRight, + Corner_BottomLeft, + Corner_BottomRight + }; + + + + class Bitmap : public boost::noncopyable + { + private: + size_t index_; + bool hasSize_; + unsigned int width_; + unsigned int height_; + bool hasCrop_; + unsigned int cropX_; + unsigned int cropY_; + unsigned int cropWidth_; + unsigned int cropHeight_; + Matrix transform_; + Matrix transformInverse_; + double pixelSpacingX_; + double pixelSpacingY_; + double panX_; + double panY_; + double angle_; + bool resizeable_; + + + protected: + const Matrix& GetTransform() const + { + return transform_; + } + + + private: + static void ApplyTransform(double& x /* inout */, + double& y /* inout */, + const Matrix& transform) + { + Vector p; + LinearAlgebra::AssignVector(p, x, y, 1); + + Vector q = LinearAlgebra::Product(transform, p); + + if (!LinearAlgebra::IsNear(q[2], 1.0)) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + else + { + x = q[0]; + y = q[1]; + } + } + + + void UpdateTransform() + { + transform_ = CreateScalingMatrix(pixelSpacingX_, pixelSpacingY_); + + double centerX, centerY; + GetCenter(centerX, centerY); + + transform_ = LinearAlgebra::Product( + CreateOffsetMatrix(panX_ + centerX, panY_ + centerY), + CreateRotationMatrix(angle_), + CreateOffsetMatrix(-centerX, -centerY), + transform_); + + LinearAlgebra::InvertMatrix(transformInverse_, transform_); + } + + + void AddToExtent(Extent2D& extent, + double x, + double y) const + { + ApplyTransform(x, y, transform_); + extent.AddPoint(x, y); + } + + + void GetCornerInternal(double& x, + double& y, + Corner corner, + unsigned int cropX, + unsigned int cropY, + unsigned int cropWidth, + unsigned int cropHeight) const + { + double dx = static_cast(cropX); + double dy = static_cast(cropY); + double dwidth = static_cast(cropWidth); + double dheight = static_cast(cropHeight); + + switch (corner) + { + case Corner_TopLeft: + x = dx; + y = dy; + break; + + case Corner_TopRight: + x = dx + dwidth; + y = dy; + break; + + case Corner_BottomLeft: + x = dx; + y = dy + dheight; + break; + + case Corner_BottomRight: + x = dx + dwidth; + y = dy + dheight; + break; + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + + ApplyTransform(x, y, transform_); + } + + + public: + Bitmap(size_t index) : + index_(index), + hasSize_(false), + width_(0), + height_(0), + hasCrop_(false), + pixelSpacingX_(1), + pixelSpacingY_(1), + panX_(0), + panY_(0), + angle_(0), + resizeable_(false) + { + UpdateTransform(); + } + + virtual ~Bitmap() + { + } + + size_t GetIndex() const + { + return index_; + } + + void ResetCrop() + { + hasCrop_ = false; + } + + void SetCrop(unsigned int x, + unsigned int y, + unsigned int width, + unsigned int height) + { + if (!hasSize_) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + + if (x + width > width_ || + y + height > height_) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + + hasCrop_ = true; + cropX_ = x; + cropY_ = y; + cropWidth_ = width; + cropHeight_ = height; + + UpdateTransform(); + } + + void GetCrop(unsigned int& x, + unsigned int& y, + unsigned int& width, + unsigned int& height) const + { + if (hasCrop_) + { + x = cropX_; + y = cropY_; + width = cropWidth_; + height = cropHeight_; + } + else + { + x = 0; + y = 0; + width = width_; + height = height_; + } + } + + void SetAngle(double angle) + { + angle_ = angle; + UpdateTransform(); + } + + double GetAngle() const + { + return angle_; + } + + void SetSize(unsigned int width, + unsigned int height) + { + if (hasSize_ && + (width != width_ || + height != height_)) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageSize); + } + + hasSize_ = true; + width_ = width; + height_ = height; + + UpdateTransform(); + } + + + unsigned int GetWidth() const + { + return width_; + } + + + unsigned int GetHeight() const + { + return height_; + } + + + void CheckSize(unsigned int width, + unsigned int height) + { + if (hasSize_ && + (width != width_ || + height != height_)) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageSize); + } + } + + + Extent2D GetExtent() const + { + Extent2D extent; + + unsigned int x, y, width, height; + GetCrop(x, y, width, height); + + double dx = static_cast(x); + double dy = static_cast(y); + double dwidth = static_cast(width); + double dheight = static_cast(height); + + AddToExtent(extent, dx, dy); + AddToExtent(extent, dx + dwidth, dy); + AddToExtent(extent, dx, dy + dheight); + AddToExtent(extent, dx + dwidth, dy + dheight); + + return extent; + } + + + bool Contains(double x, + double y) const + { + ApplyTransform(x, y, transformInverse_); + + unsigned int cropX, cropY, cropWidth, cropHeight; + GetCrop(cropX, cropY, cropWidth, cropHeight); + + return (x >= cropX && x <= cropX + cropWidth && + y >= cropY && y <= cropY + cropHeight); + } + + + bool GetPixel(unsigned int& imageX, + unsigned int& imageY, + double sceneX, + double sceneY) const + { + if (width_ == 0 || + height_ == 0) + { + return false; + } + else + { + ApplyTransform(sceneX, sceneY, transformInverse_); + + int x = static_cast(std::floor(sceneX)); + int y = static_cast(std::floor(sceneY)); + + if (x < 0) + { + imageX = 0; + } + else if (x >= static_cast(width_)) + { + imageX = width_; + } + else + { + imageX = static_cast(x); + } + + if (y < 0) + { + imageY = 0; + } + else if (y >= static_cast(height_)) + { + imageY = height_; + } + else + { + imageY = static_cast(y); + } + + return true; + } + } + + + void SetPan(double x, + double y) + { + panX_ = x; + panY_ = y; + UpdateTransform(); + } + + + void SetPixelSpacing(double x, + double y) + { + pixelSpacingX_ = x; + pixelSpacingY_ = y; + UpdateTransform(); + } + + double GetPixelSpacingX() const + { + return pixelSpacingX_; + } + + double GetPixelSpacingY() const + { + return pixelSpacingY_; + } + + double GetPanX() const + { + return panX_; + } + + double GetPanY() const + { + return panY_; + } + + void GetCenter(double& centerX, + double& centerY) const + { + centerX = static_cast(width_) / 2.0; + centerY = static_cast(height_) / 2.0; + ApplyTransform(centerX, centerY, transform_); + } + + + void DrawBorders(CairoContext& context, + double zoom) + { + unsigned int cx, cy, width, height; + GetCrop(cx, cy, width, height); + + double dx = static_cast(cx); + double dy = static_cast(cy); + double dwidth = static_cast(width); + double dheight = static_cast(height); + + cairo_t* cr = context.GetObject(); + cairo_set_line_width(cr, 2.0 / zoom); + + double x, y; + x = dx; + y = dy; + ApplyTransform(x, y, transform_); + cairo_move_to(cr, x, y); + + x = dx + dwidth; + y = dy; + ApplyTransform(x, y, transform_); + cairo_line_to(cr, x, y); + + x = dx + dwidth; + y = dy + dheight; + ApplyTransform(x, y, transform_); + cairo_line_to(cr, x, y); + + x = dx; + y = dy + dheight; + ApplyTransform(x, y, transform_); + cairo_line_to(cr, x, y); + + x = dx; + y = dy; + ApplyTransform(x, y, transform_); + cairo_line_to(cr, x, y); + + cairo_stroke(cr); + } + + + static double Square(double x) + { + return x * x; + } + + + void GetCorner(double& x /* out */, + double& y /* out */, + Corner corner) const + { + unsigned int cropX, cropY, cropWidth, cropHeight; + GetCrop(cropX, cropY, cropWidth, cropHeight); + GetCornerInternal(x, y, corner, cropX, cropY, cropWidth, cropHeight); + } + + + bool LookupCorner(Corner& corner /* out */, + double x, + double y, + double zoom, + double viewportDistance) const + { + static const Corner CORNERS[] = { + Corner_TopLeft, + Corner_TopRight, + Corner_BottomLeft, + Corner_BottomRight + }; + + unsigned int cropX, cropY, cropWidth, cropHeight; + GetCrop(cropX, cropY, cropWidth, cropHeight); + + double threshold = Square(viewportDistance / zoom); + + for (size_t i = 0; i < 4; i++) + { + double cx, cy; + GetCornerInternal(cx, cy, CORNERS[i], cropX, cropY, cropWidth, cropHeight); + + double d = Square(cx - x) + Square(cy - y); + + if (d <= threshold) + { + corner = CORNERS[i]; + return true; + } + } + + return false; + } + + bool IsResizeable() const + { + return resizeable_; + } + + void SetResizeable(bool resizeable) + { + resizeable_ = resizeable; + } + + virtual bool GetDefaultWindowing(float& center, + float& width) const + { + return false; + } + + virtual void Render(Orthanc::ImageAccessor& buffer, + const Matrix& viewTransform, + ImageInterpolation interpolation) const = 0; + + virtual bool GetRange(float& minValue, + float& maxValue) const = 0; + }; + + + class BitmapAccessor : public boost::noncopyable + { + private: + BitmapStack& stack_; + size_t index_; + Bitmap* bitmap_; + + public: + BitmapAccessor(BitmapStack& stack, + size_t index) : + stack_(stack), + index_(index) + { + Bitmaps::iterator bitmap = stack.bitmaps_.find(index); + if (bitmap == stack.bitmaps_.end()) + { + bitmap_ = NULL; + } + else + { + assert(bitmap->second != NULL); + bitmap_ = bitmap->second; + } + } + + BitmapAccessor(BitmapStack& stack, + double x, + double y) : + stack_(stack), + index_(0) // Dummy initialization + { + if (stack.LookupBitmap(index_, x, y)) + { + Bitmaps::iterator bitmap = stack.bitmaps_.find(index_); + + if (bitmap == stack.bitmaps_.end()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + else + { + assert(bitmap->second != NULL); + bitmap_ = bitmap->second; + } + } + else + { + bitmap_ = NULL; + } + } + + void Invalidate() + { + bitmap_ = NULL; + } + + bool IsValid() const + { + return bitmap_ != NULL; + } + + BitmapStack& GetStack() const + { + if (IsValid()) + { + return stack_; + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + } + + size_t GetIndex() const + { + if (IsValid()) + { + return index_; + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + } + + Bitmap& GetBitmap() const + { + if (IsValid()) + { + return *bitmap_; + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + } + }; + + + class AlphaBitmap : public Bitmap + { + private: + const BitmapStack& stack_; + std::auto_ptr alpha_; // Grayscale8 + bool useWindowing_; + float foreground_; + + public: + AlphaBitmap(size_t index, + const BitmapStack& stack) : + Bitmap(index), + stack_(stack), + useWindowing_(true), + foreground_(0) + { + } + + + void SetForegroundValue(float foreground) + { + useWindowing_ = false; + foreground_ = foreground; + } + + + void SetAlpha(Orthanc::ImageAccessor* image) + { + std::auto_ptr raii(image); + + if (image == NULL) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); + } + + if (image->GetFormat() != Orthanc::PixelFormat_Grayscale8) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageFormat); + } + + SetSize(image->GetWidth(), image->GetHeight()); + alpha_ = raii; + } + + + void LoadText(const Orthanc::Font& font, + const std::string& utf8) + { + SetAlpha(font.RenderAlpha(utf8)); + } + + + virtual void Render(Orthanc::ImageAccessor& buffer, + const Matrix& viewTransform, + ImageInterpolation interpolation) const + { + if (buffer.GetFormat() != Orthanc::PixelFormat_Float32) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageFormat); + } + + unsigned int cropX, cropY, cropWidth, cropHeight; + GetCrop(cropX, cropY, cropWidth, cropHeight); + + Matrix m = LinearAlgebra::Product(viewTransform, + GetTransform(), + CreateOffsetMatrix(cropX, cropY)); + + Orthanc::ImageAccessor cropped; + alpha_->GetRegion(cropped, cropX, cropY, cropWidth, cropHeight); + + Orthanc::Image tmp(Orthanc::PixelFormat_Grayscale8, buffer.GetWidth(), buffer.GetHeight(), false); + ApplyProjectiveTransform(tmp, cropped, m, interpolation, true /* clear */); + + // Blit + const unsigned int width = buffer.GetWidth(); + const unsigned int height = buffer.GetHeight(); + + float value = foreground_; + + if (useWindowing_) + { + float center, width; + if (stack_.GetWindowing(center, width)) + { + value = center + width / 2.0f; + } + } + + for (unsigned int y = 0; y < height; y++) + { + float *q = reinterpret_cast(buffer.GetRow(y)); + const uint8_t *p = reinterpret_cast(tmp.GetRow(y)); + + for (unsigned int x = 0; x < width; x++, p++, q++) + { + float a = static_cast(*p) / 255.0f; + + *q = (a * value + (1.0f - a) * (*q)); + } + } + } + + virtual bool GetRange(float& minValue, + float& maxValue) const + { + if (useWindowing_) + { + return false; + } + else + { + minValue = 0; + maxValue = 0; + + if (foreground_ < 0) + { + minValue = foreground_; + } + + if (foreground_ > 0) + { + maxValue = foreground_; + } + + return true; + } + } + }; + + + + private: + class DicomBitmap : public Bitmap + { + private: + std::auto_ptr source_; // Content of PixelData + std::auto_ptr converter_; + std::auto_ptr converted_; // Float32 + + static OrthancPlugins::DicomTag ConvertTag(const Orthanc::DicomTag& tag) + { + return OrthancPlugins::DicomTag(tag.GetGroup(), tag.GetElement()); + } + + + void ApplyConverter() + { + if (source_.get() != NULL && + converter_.get() != NULL) + { + converted_.reset(converter_->ConvertFrame(*source_)); + } + } + + public: + DicomBitmap(size_t index) : + Bitmap(index) + { + } + + void SetDicomTags(const OrthancPlugins::FullOrthancDataset& dataset) + { + converter_.reset(new DicomFrameConverter); + converter_->ReadParameters(dataset); + ApplyConverter(); + + std::string tmp; + Vector pixelSpacing; + + if (dataset.GetStringValue(tmp, ConvertTag(Orthanc::DICOM_TAG_PIXEL_SPACING)) && + LinearAlgebra::ParseVector(pixelSpacing, tmp) && + pixelSpacing.size() == 2) + { + SetPixelSpacing(pixelSpacing[0], pixelSpacing[1]); + } + + //SetPan(-0.5 * GetPixelSpacingX(), -0.5 * GetPixelSpacingY()); + + OrthancPlugins::DicomDatasetReader reader(dataset); + + unsigned int width, height; + if (!reader.GetUnsignedIntegerValue(width, ConvertTag(Orthanc::DICOM_TAG_COLUMNS)) || + !reader.GetUnsignedIntegerValue(height, ConvertTag(Orthanc::DICOM_TAG_ROWS))) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat); + } + else + { + SetSize(width, height); + } + } + + + void SetSourceImage(Orthanc::ImageAccessor* image) // Takes ownership + { + std::auto_ptr raii(image); + + if (image == NULL) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); + } + + SetSize(image->GetWidth(), image->GetHeight()); + + source_ = raii; + ApplyConverter(); + } + + + virtual void Render(Orthanc::ImageAccessor& buffer, + const Matrix& viewTransform, + ImageInterpolation interpolation) const + { + if (converted_.get() != NULL) + { + if (converted_->GetFormat() != Orthanc::PixelFormat_Float32) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + + unsigned int cropX, cropY, cropWidth, cropHeight; + GetCrop(cropX, cropY, cropWidth, cropHeight); + + Matrix m = LinearAlgebra::Product(viewTransform, + GetTransform(), + CreateOffsetMatrix(cropX, cropY)); + + Orthanc::ImageAccessor cropped; + converted_->GetRegion(cropped, cropX, cropY, cropWidth, cropHeight); + + ApplyProjectiveTransform(buffer, cropped, m, interpolation, false); + } + } + + + virtual bool GetDefaultWindowing(float& center, + float& width) const + { + if (converter_.get() != NULL && + converter_->HasDefaultWindow()) + { + center = static_cast(converter_->GetDefaultWindowCenter()); + width = static_cast(converter_->GetDefaultWindowWidth()); + return true; + } + else + { + return false; + } + } + + + virtual bool GetRange(float& minValue, + float& maxValue) const + { + if (converted_.get() != NULL) + { + if (converted_->GetFormat() != Orthanc::PixelFormat_Float32) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + + Orthanc::ImageProcessing::GetMinMaxFloatValue(minValue, maxValue, *converted_); + return true; + } + else + { + return false; + } + } + }; + + + + + typedef std::map Bitmaps; + + OrthancApiClient& orthanc_; + size_t countBitmaps_; + bool hasWindowing_; + float windowingCenter_; + float windowingWidth_; + Bitmaps bitmaps_; + bool hasSelection_; + size_t selectedBitmap_; + + public: + BitmapStack(MessageBroker& broker, + OrthancApiClient& orthanc) : + IObserver(broker), + IObservable(broker), + orthanc_(orthanc), + countBitmaps_(0), + hasWindowing_(false), + windowingCenter_(0), // Dummy initialization + windowingWidth_(0), // Dummy initialization + hasSelection_(false), + selectedBitmap_(0) // Dummy initialization + { + } + + + void Unselect() + { + hasSelection_ = false; + } + + + void Select(size_t bitmap) + { + hasSelection_ = true; + selectedBitmap_ = bitmap; + } + + + bool GetSelectedBitmap(size_t& bitmap) const + { + if (hasSelection_) + { + bitmap = selectedBitmap_; + return true; + } + else + { + return false; + } + } + + + virtual ~BitmapStack() + { + for (Bitmaps::iterator it = bitmaps_.begin(); it != bitmaps_.end(); it++) + { + assert(it->second != NULL); + delete it->second; + } + } + + + bool GetWindowing(float& center, + float& width) const + { + if (hasWindowing_) + { + center = windowingCenter_; + width = windowingWidth_; + return true; + } + else + { + return false; + } + } + + + void GetWindowingWithDefault(float& center, + float& width) const + { + if (!GetWindowing(center, width)) + { + center = 128; + width = 256; + } + } + + + void SetWindowing(float center, + float width) + + { + hasWindowing_ = true; + windowingCenter_ = center; + windowingWidth_ = width; + + //EmitMessage(ContentChangedMessage(*this)); + } + + + Bitmap& LoadText(const Orthanc::Font& font, + const std::string& utf8) + { + size_t bitmap = countBitmaps_++; + + std::auto_ptr alpha(new AlphaBitmap(bitmap, *this)); + alpha->LoadText(font, utf8); + + AlphaBitmap* ptr = alpha.get(); + bitmaps_[bitmap] = alpha.release(); + + return *ptr; + } + + + Bitmap& LoadTestBlock(unsigned int width, + unsigned int height) + { + size_t bitmap = countBitmaps_++; + + std::auto_ptr alpha(new AlphaBitmap(bitmap, *this)); + + std::auto_ptr block(new Orthanc::Image(Orthanc::PixelFormat_Grayscale8, width, height, false)); + + for (unsigned int padding = 0; + (width > 2 * padding) && (height > 2 * padding); + padding++) + { + uint8_t color; + if (255 > 10 * padding) + { + color = 255 - 10 * padding; + } + else + { + color = 0; + } + + Orthanc::ImageAccessor region; + block->GetRegion(region, padding, padding, width - 2 * padding, height - 2 * padding); + Orthanc::ImageProcessing::Set(region, color); + } + + alpha->SetAlpha(block.release()); + + AlphaBitmap* ptr = alpha.get(); + bitmaps_[bitmap] = alpha.release(); + + return *ptr; + } + + + Bitmap& LoadFrame(const std::string& instance, + unsigned int frame, + bool httpCompression) + { + size_t bitmap = countBitmaps_++; + + bitmaps_[bitmap] = new DicomBitmap(bitmap); + + { + IWebService::Headers headers; + std::string uri = "/instances/" + instance + "/tags"; + orthanc_.GetBinaryAsync(uri, headers, + new Callable + (*this, &BitmapStack::OnTagsReceived), NULL, + new Orthanc::SingleValueObject(bitmap)); + } + + { + IWebService::Headers headers; + headers["Accept"] = "image/x-portable-arbitrarymap"; + + if (httpCompression) + { + headers["Accept-Encoding"] = "gzip"; + } + + std::string uri = "/instances/" + instance + "/frames/" + boost::lexical_cast(frame) + "/image-uint16"; + orthanc_.GetBinaryAsync(uri, headers, + new Callable + (*this, &BitmapStack::OnFrameReceived), NULL, + new Orthanc::SingleValueObject(bitmap)); + } + + return *bitmaps_[bitmap]; + } + + + void OnTagsReceived(const OrthancApiClient::BinaryResponseReadyMessage& message) + { + size_t index = dynamic_cast*>(message.Payload.get())->GetValue(); + + LOG(INFO) << "JSON received: " << message.Uri.c_str() + << " (" << message.AnswerSize << " bytes) for bitmap " << index; + + Bitmaps::iterator bitmap = bitmaps_.find(index); + if (bitmap != bitmaps_.end()) + { + assert(bitmap->second != NULL); + + OrthancPlugins::FullOrthancDataset dicom(message.Answer, message.AnswerSize); + dynamic_cast(bitmap->second)->SetDicomTags(dicom); + + float c, w; + if (!hasWindowing_ && + bitmap->second->GetDefaultWindowing(c, w)) + { + hasWindowing_ = true; + windowingCenter_ = c; + windowingWidth_ = w; + } + + EmitMessage(GeometryChangedMessage(*this)); + } + } + + + void OnFrameReceived(const OrthancApiClient::BinaryResponseReadyMessage& message) + { + size_t index = dynamic_cast*>(message.Payload.get())->GetValue(); + + LOG(INFO) << "DICOM frame received: " << message.Uri.c_str() + << " (" << message.AnswerSize << " bytes) for bitmap " << index; + + Bitmaps::iterator bitmap = bitmaps_.find(index); + if (bitmap != bitmaps_.end()) + { + assert(bitmap->second != NULL); + + std::string content; + if (message.AnswerSize > 0) + { + content.assign(reinterpret_cast(message.Answer), message.AnswerSize); + } + + std::auto_ptr reader(new Orthanc::PamReader); + reader->ReadFromMemory(content); + dynamic_cast(bitmap->second)->SetSourceImage(reader.release()); + + EmitMessage(ContentChangedMessage(*this)); + } + } + + + Extent2D GetSceneExtent() const + { + Extent2D extent; + + for (Bitmaps::const_iterator it = bitmaps_.begin(); + it != bitmaps_.end(); ++it) + { + assert(it->second != NULL); + extent.Union(it->second->GetExtent()); + } + + return extent; + } + + + void Render(Orthanc::ImageAccessor& buffer, + const Matrix& viewTransform, + ImageInterpolation interpolation) const + { + Orthanc::ImageProcessing::Set(buffer, 0); + + // Render layers in the background-to-foreground order + for (size_t index = 0; index < countBitmaps_; index++) + { + Bitmaps::const_iterator it = bitmaps_.find(index); + if (it != bitmaps_.end()) + { + assert(it->second != NULL); + it->second->Render(buffer, viewTransform, interpolation); + } + } + } + + + bool LookupBitmap(size_t& index /* out */, + double x, + double y) const + { + // Render layers in the foreground-to-background order + for (size_t i = countBitmaps_; i > 0; i--) + { + index = i - 1; + Bitmaps::const_iterator it = bitmaps_.find(index); + if (it != bitmaps_.end()) + { + assert(it->second != NULL); + if (it->second->Contains(x, y)) + { + return true; + } + } + } + + return false; + } + + void DrawControls(CairoContext& context, + double zoom) + { + if (hasSelection_) + { + Bitmaps::const_iterator bitmap = bitmaps_.find(selectedBitmap_); + + if (bitmap != bitmaps_.end()) + { + context.SetSourceColor(255, 0, 0); + //view.ApplyTransform(context); + bitmap->second->DrawBorders(context, zoom); + } + } + } + + + void GetRange(float& minValue, + float& maxValue) const + { + bool first = true; + + for (Bitmaps::const_iterator it = bitmaps_.begin(); + it != bitmaps_.end(); it++) + { + assert(it->second != NULL); + + float a, b; + if (it->second->GetRange(a, b)) + { + if (first) + { + minValue = a; + maxValue = b; + first = false; + } + else + { + minValue = std::min(a, minValue); + maxValue = std::max(b, maxValue); + } + } + } + + if (first) + { + minValue = 0; + maxValue = 0; + } + } + }; + + + class UndoRedoStack : public boost::noncopyable + { + public: + class ICommand : public boost::noncopyable + { + public: + virtual ~ICommand() + { + } + + virtual void Undo() const = 0; + + virtual void Redo() const = 0; + }; + + private: + typedef std::list Stack; + + Stack stack_; + Stack::iterator current_; + + void Clear(Stack::iterator from) + { + for (Stack::iterator it = from; it != stack_.end(); ++it) + { + assert(*it != NULL); + delete *it; + } + + stack_.erase(from, stack_.end()); + } + + public: + UndoRedoStack() : + current_(stack_.end()) + { + } + + ~UndoRedoStack() + { + Clear(stack_.begin()); + } + + void Add(ICommand* command) + { + if (command == NULL) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); + } + + Clear(current_); + + stack_.push_back(command); + current_ = stack_.end(); + } + + void Undo() + { + if (current_ != stack_.begin()) + { + --current_; + + assert(*current_ != NULL); + (*current_)->Undo(); + } + } + + void Redo() + { + if (current_ != stack_.end()) + { + assert(*current_ != NULL); + (*current_)->Redo(); + + ++current_; + } + } + }; + + + class BitmapCommandBase : public UndoRedoStack::ICommand + { + private: + BitmapStack& stack_; + size_t bitmap_; + + protected: + virtual void UndoInternal(BitmapStack::Bitmap& bitmap) const = 0; + + virtual void RedoInternal(BitmapStack::Bitmap& bitmap) const = 0; + + public: + BitmapCommandBase(BitmapStack& stack, + size_t bitmap) : + stack_(stack), + bitmap_(bitmap) + { + } + + BitmapCommandBase(const BitmapStack::BitmapAccessor& accessor) : + stack_(accessor.GetStack()), + bitmap_(accessor.GetIndex()) + { + } + + virtual void Undo() const + { + BitmapStack::BitmapAccessor accessor(stack_, bitmap_); + + if (accessor.IsValid()) + { + UndoInternal(accessor.GetBitmap()); + } + } + + virtual void Redo() const + { + BitmapStack::BitmapAccessor accessor(stack_, bitmap_); + + if (accessor.IsValid()) + { + RedoInternal(accessor.GetBitmap()); + } + } + }; + + + class RotateBitmapTracker : public IWorldSceneMouseTracker + { + private: + UndoRedoStack& undoRedoStack_; + BitmapStack::BitmapAccessor accessor_; + double centerX_; + double centerY_; + double originalAngle_; + double clickAngle_; + bool roundAngles_; + + bool ComputeAngle(double& angle /* out */, + double sceneX, + double sceneY) const + { + Vector u; + LinearAlgebra::AssignVector(u, sceneX - centerX_, sceneY - centerY_); + + double nu = boost::numeric::ublas::norm_2(u); + + if (!LinearAlgebra::IsCloseToZero(nu)) + { + u /= nu; + angle = atan2(u[1], u[0]); + return true; + } + else + { + return false; + } + } + + + class UndoRedoCommand : public BitmapCommandBase + { + private: + double sourceAngle_; + double targetAngle_; + + static int ToDegrees(double angle) + { + return static_cast(round(angle * 180.0 / boost::math::constants::pi())); + } + + protected: + virtual void UndoInternal(BitmapStack::Bitmap& bitmap) const + { + LOG(INFO) << "Undo - Set angle to " << ToDegrees(sourceAngle_) << " degrees"; + bitmap.SetAngle(sourceAngle_); + } + + virtual void RedoInternal(BitmapStack::Bitmap& bitmap) const + { + LOG(INFO) << "Redo - Set angle to " << ToDegrees(sourceAngle_) << " degrees"; + bitmap.SetAngle(targetAngle_); + } + + public: + UndoRedoCommand(const RotateBitmapTracker& tracker) : + BitmapCommandBase(tracker.accessor_), + sourceAngle_(tracker.originalAngle_), + targetAngle_(tracker.accessor_.GetBitmap().GetAngle()) + { + } + }; + + + public: + RotateBitmapTracker(UndoRedoStack& undoRedoStack, + BitmapStack& stack, + const ViewportGeometry& view, + size_t bitmap, + double x, + double y, + bool roundAngles) : + undoRedoStack_(undoRedoStack), + accessor_(stack, bitmap), + roundAngles_(roundAngles) + { + if (accessor_.IsValid()) + { + accessor_.GetBitmap().GetCenter(centerX_, centerY_); + originalAngle_ = accessor_.GetBitmap().GetAngle(); + + double sceneX, sceneY; + view.MapDisplayToScene(sceneX, sceneY, x, y); + + if (!ComputeAngle(clickAngle_, x, y)) + { + accessor_.Invalidate(); + } + } + } + + virtual bool HasRender() const + { + return false; + } + + virtual void Render(CairoContext& context, + double zoom) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + + virtual void MouseUp() + { + if (accessor_.IsValid()) + { + undoRedoStack_.Add(new UndoRedoCommand(*this)); + } + } + + virtual void MouseMove(int displayX, + int displayY, + double sceneX, + double sceneY) + { + static const double ROUND_ANGLE = 15.0 / 180.0 * boost::math::constants::pi(); + + double angle; + + if (accessor_.IsValid() && + ComputeAngle(angle, sceneX, sceneY)) + { + angle = angle - clickAngle_ + originalAngle_; + + if (roundAngles_) + { + angle = round(angle / ROUND_ANGLE) * ROUND_ANGLE; + } + + accessor_.GetBitmap().SetAngle(angle); + } + } + }; + + + class MoveBitmapTracker : public IWorldSceneMouseTracker + { + private: + UndoRedoStack& undoRedoStack_; + BitmapStack::BitmapAccessor accessor_; + double clickX_; + double clickY_; + double panX_; + double panY_; + bool oneAxis_; + + class UndoRedoCommand : public BitmapCommandBase + { + private: + double sourceX_; + double sourceY_; + double targetX_; + double targetY_; + + protected: + virtual void UndoInternal(BitmapStack::Bitmap& bitmap) const + { + bitmap.SetPan(sourceX_, sourceY_); + } + + virtual void RedoInternal(BitmapStack::Bitmap& bitmap) const + { + bitmap.SetPan(targetX_, targetY_); + } + + public: + UndoRedoCommand(const MoveBitmapTracker& tracker) : + BitmapCommandBase(tracker.accessor_), + sourceX_(tracker.panX_), + sourceY_(tracker.panY_), + targetX_(tracker.accessor_.GetBitmap().GetPanX()), + targetY_(tracker.accessor_.GetBitmap().GetPanY()) + { + } + }; + + + public: + MoveBitmapTracker(UndoRedoStack& undoRedoStack, + BitmapStack& stack, + size_t bitmap, + double x, + double y, + bool oneAxis) : + undoRedoStack_(undoRedoStack), + accessor_(stack, bitmap), + clickX_(x), + clickY_(y), + oneAxis_(oneAxis) + { + if (accessor_.IsValid()) + { + panX_ = accessor_.GetBitmap().GetPanX(); + panY_ = accessor_.GetBitmap().GetPanY(); + } + } + + virtual bool HasRender() const + { + return false; + } + + virtual void Render(CairoContext& context, + double zoom) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + + virtual void MouseUp() + { + if (accessor_.IsValid()) + { + undoRedoStack_.Add(new UndoRedoCommand(*this)); + } + } + + virtual void MouseMove(int displayX, + int displayY, + double sceneX, + double sceneY) + { + if (accessor_.IsValid()) + { + double dx = sceneX - clickX_; + double dy = sceneY - clickY_; + + if (oneAxis_) + { + if (fabs(dx) > fabs(dy)) + { + accessor_.GetBitmap().SetPan(dx + panX_, panY_); + } + else + { + accessor_.GetBitmap().SetPan(panX_, dy + panY_); + } + } + else + { + accessor_.GetBitmap().SetPan(dx + panX_, dy + panY_); + } + } + } + }; + + + class CropBitmapTracker : public IWorldSceneMouseTracker + { + private: + UndoRedoStack& undoRedoStack_; + BitmapStack::BitmapAccessor accessor_; + BitmapStack::Corner corner_; + unsigned int cropX_; + unsigned int cropY_; + unsigned int cropWidth_; + unsigned int cropHeight_; + + class UndoRedoCommand : public BitmapCommandBase + { + private: + unsigned int sourceCropX_; + unsigned int sourceCropY_; + unsigned int sourceCropWidth_; + unsigned int sourceCropHeight_; + unsigned int targetCropX_; + unsigned int targetCropY_; + unsigned int targetCropWidth_; + unsigned int targetCropHeight_; + + protected: + virtual void UndoInternal(BitmapStack::Bitmap& bitmap) const + { + bitmap.SetCrop(sourceCropX_, sourceCropY_, sourceCropWidth_, sourceCropHeight_); + } + + virtual void RedoInternal(BitmapStack::Bitmap& bitmap) const + { + bitmap.SetCrop(targetCropX_, targetCropY_, targetCropWidth_, targetCropHeight_); + } + + public: + UndoRedoCommand(const CropBitmapTracker& tracker) : + BitmapCommandBase(tracker.accessor_), + sourceCropX_(tracker.cropX_), + sourceCropY_(tracker.cropY_), + sourceCropWidth_(tracker.cropWidth_), + sourceCropHeight_(tracker.cropHeight_) + { + tracker.accessor_.GetBitmap().GetCrop(targetCropX_, targetCropY_, + targetCropWidth_, targetCropHeight_); + } + }; + + + public: + CropBitmapTracker(UndoRedoStack& undoRedoStack, + BitmapStack& stack, + const ViewportGeometry& view, + size_t bitmap, + double x, + double y, + BitmapStack::Corner corner) : + undoRedoStack_(undoRedoStack), + accessor_(stack, bitmap), + corner_(corner) + { + if (accessor_.IsValid()) + { + accessor_.GetBitmap().GetCrop(cropX_, cropY_, cropWidth_, cropHeight_); + } + } + + virtual bool HasRender() const + { + return false; + } + + virtual void Render(CairoContext& context, + double zoom) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + + virtual void MouseUp() + { + if (accessor_.IsValid()) + { + undoRedoStack_.Add(new UndoRedoCommand(*this)); + } + } + + virtual void MouseMove(int displayX, + int displayY, + double sceneX, + double sceneY) + { + if (accessor_.IsValid()) + { + unsigned int x, y; + + BitmapStack::Bitmap& bitmap = accessor_.GetBitmap(); + if (bitmap.GetPixel(x, y, sceneX, sceneY)) + { + unsigned int targetX, targetWidth; + + if (corner_ == BitmapStack::Corner_TopLeft || + corner_ == BitmapStack::Corner_BottomLeft) + { + targetX = std::min(x, cropX_ + cropWidth_); + targetWidth = cropX_ + cropWidth_ - targetX; + } + else + { + targetX = cropX_; + targetWidth = std::max(x, cropX_) - cropX_; + } + + unsigned int targetY, targetHeight; + + if (corner_ == BitmapStack::Corner_TopLeft || + corner_ == BitmapStack::Corner_TopRight) + { + targetY = std::min(y, cropY_ + cropHeight_); + targetHeight = cropY_ + cropHeight_ - targetY; + } + else + { + targetY = cropY_; + targetHeight = std::max(y, cropY_) - cropY_; + } + + bitmap.SetCrop(targetX, targetY, targetWidth, targetHeight); + } + } + } + }; + + + class ResizeBitmapTracker : public IWorldSceneMouseTracker + { + private: + UndoRedoStack& undoRedoStack_; + BitmapStack::BitmapAccessor accessor_; + bool roundScaling_; + double originalSpacingX_; + double originalSpacingY_; + double originalPanX_; + double originalPanY_; + BitmapStack::Corner oppositeCorner_; + double oppositeX_; + double oppositeY_; + double baseScaling_; + + static double ComputeDistance(double x1, + double y1, + double x2, + double y2) + { + double dx = x1 - x2; + double dy = y1 - y2; + return sqrt(dx * dx + dy * dy); + } + + class UndoRedoCommand : public BitmapCommandBase + { + private: + double sourceSpacingX_; + double sourceSpacingY_; + double sourcePanX_; + double sourcePanY_; + double targetSpacingX_; + double targetSpacingY_; + double targetPanX_; + double targetPanY_; + + protected: + virtual void UndoInternal(BitmapStack::Bitmap& bitmap) const + { + bitmap.SetPixelSpacing(sourceSpacingX_, sourceSpacingY_); + bitmap.SetPan(sourcePanX_, sourcePanY_); + } + + virtual void RedoInternal(BitmapStack::Bitmap& bitmap) const + { + bitmap.SetPixelSpacing(targetSpacingX_, targetSpacingY_); + bitmap.SetPan(targetPanX_, targetPanY_); + } + + public: + UndoRedoCommand(const ResizeBitmapTracker& tracker) : + BitmapCommandBase(tracker.accessor_), + sourceSpacingX_(tracker.originalSpacingX_), + sourceSpacingY_(tracker.originalSpacingY_), + sourcePanX_(tracker.originalPanX_), + sourcePanY_(tracker.originalPanY_), + targetSpacingX_(tracker.accessor_.GetBitmap().GetPixelSpacingX()), + targetSpacingY_(tracker.accessor_.GetBitmap().GetPixelSpacingY()), + targetPanX_(tracker.accessor_.GetBitmap().GetPanX()), + targetPanY_(tracker.accessor_.GetBitmap().GetPanY()) + { + } + }; + + + public: + ResizeBitmapTracker(UndoRedoStack& undoRedoStack, + BitmapStack& stack, + size_t bitmap, + double x, + double y, + BitmapStack::Corner corner, + bool roundScaling) : + undoRedoStack_(undoRedoStack), + accessor_(stack, bitmap), + roundScaling_(roundScaling) + { + if (accessor_.IsValid() && + accessor_.GetBitmap().IsResizeable()) + { + originalSpacingX_ = accessor_.GetBitmap().GetPixelSpacingX(); + originalSpacingY_ = accessor_.GetBitmap().GetPixelSpacingY(); + originalPanX_ = accessor_.GetBitmap().GetPanX(); + originalPanY_ = accessor_.GetBitmap().GetPanY(); + + switch (corner) + { + case BitmapStack::Corner_TopLeft: + oppositeCorner_ = BitmapStack::Corner_BottomRight; + break; + + case BitmapStack::Corner_TopRight: + oppositeCorner_ = BitmapStack::Corner_BottomLeft; + break; + + case BitmapStack::Corner_BottomLeft: + oppositeCorner_ = BitmapStack::Corner_TopRight; + break; + + case BitmapStack::Corner_BottomRight: + oppositeCorner_ = BitmapStack::Corner_TopLeft; + break; + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + + accessor_.GetBitmap().GetCorner(oppositeX_, oppositeY_, oppositeCorner_); + + double d = ComputeDistance(x, y, oppositeX_, oppositeY_); + if (d >= std::numeric_limits::epsilon()) + { + baseScaling_ = 1.0 / d; + } + else + { + // Avoid division by zero in extreme cases + accessor_.Invalidate(); + } + } + } + + virtual bool HasRender() const + { + return false; + } + + virtual void Render(CairoContext& context, + double zoom) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + + virtual void MouseUp() + { + if (accessor_.IsValid() && + accessor_.GetBitmap().IsResizeable()) + { + undoRedoStack_.Add(new UndoRedoCommand(*this)); + } + } + + virtual void MouseMove(int displayX, + int displayY, + double sceneX, + double sceneY) + { + static const double ROUND_SCALING = 0.1; + + if (accessor_.IsValid() && + accessor_.GetBitmap().IsResizeable()) + { + double scaling = ComputeDistance(oppositeX_, oppositeY_, sceneX, sceneY) * baseScaling_; + + if (roundScaling_) + { + scaling = round(scaling / ROUND_SCALING) * ROUND_SCALING; + } + + BitmapStack::Bitmap& bitmap = accessor_.GetBitmap(); + bitmap.SetPixelSpacing(scaling * originalSpacingX_, + scaling * originalSpacingY_); + + // Keep the opposite corner at a fixed location + double ox, oy; + bitmap.GetCorner(ox, oy, oppositeCorner_); + bitmap.SetPan(bitmap.GetPanX() + oppositeX_ - ox, + bitmap.GetPanY() + oppositeY_ - oy); + } + } + }; + + + class WindowingTracker : public IWorldSceneMouseTracker + { + public: + enum Action + { + Action_IncreaseWidth, + Action_DecreaseWidth, + Action_IncreaseCenter, + Action_DecreaseCenter + }; + + private: + UndoRedoStack& undoRedoStack_; + BitmapStack& stack_; + int clickX_; + int clickY_; + Action leftAction_; + Action rightAction_; + Action upAction_; + Action downAction_; + float strength_; + float sourceCenter_; + float sourceWidth_; + + static void ComputeAxisEffect(int& deltaCenter, + int& deltaWidth, + int delta, + Action actionNegative, + Action actionPositive) + { + if (delta < 0) + { + switch (actionNegative) + { + case Action_IncreaseWidth: + deltaWidth = -delta; + break; + + case Action_DecreaseWidth: + deltaWidth = delta; + break; + + case Action_IncreaseCenter: + deltaCenter = -delta; + break; + + case Action_DecreaseCenter: + deltaCenter = delta; + break; + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + } + else if (delta > 0) + { + switch (actionPositive) + { + case Action_IncreaseWidth: + deltaWidth = delta; + break; + + case Action_DecreaseWidth: + deltaWidth = -delta; + break; + + case Action_IncreaseCenter: + deltaCenter = delta; + break; + + case Action_DecreaseCenter: + deltaCenter = -delta; + break; + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + } + } + + + class UndoRedoCommand : public UndoRedoStack::ICommand + { + private: + BitmapStack& stack_; + float sourceCenter_; + float sourceWidth_; + float targetCenter_; + float targetWidth_; + + public: + UndoRedoCommand(const WindowingTracker& tracker) : + stack_(tracker.stack_), + sourceCenter_(tracker.sourceCenter_), + sourceWidth_(tracker.sourceWidth_) + { + stack_.GetWindowingWithDefault(targetCenter_, targetWidth_); + } + + virtual void Undo() const + { + stack_.SetWindowing(sourceCenter_, sourceWidth_); + } + + virtual void Redo() const + { + stack_.SetWindowing(targetCenter_, targetWidth_); + } + }; + + + public: + WindowingTracker(UndoRedoStack& undoRedoStack, + BitmapStack& stack, + int x, + int y, + Action leftAction, + Action rightAction, + Action upAction, + Action downAction) : + undoRedoStack_(undoRedoStack), + stack_(stack), + clickX_(x), + clickY_(y), + leftAction_(leftAction), + rightAction_(rightAction), + upAction_(upAction), + downAction_(downAction) + { + stack_.GetWindowingWithDefault(sourceCenter_, sourceWidth_); + + float minValue, maxValue; + stack.GetRange(minValue, maxValue); + + assert(minValue <= maxValue); + + float tmp; + + float delta = (maxValue - minValue); + if (delta <= 1) + { + tmp = 0; + } + else + { + tmp = log2(delta); + } + + strength_ = tmp - 7; + if (strength_ < 1) + { + strength_ = 1; + } + } + + virtual bool HasRender() const + { + return false; + } + + virtual void Render(CairoContext& context, + double zoom) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + + virtual void MouseUp() + { + undoRedoStack_.Add(new UndoRedoCommand(*this)); + } + + + virtual void MouseMove(int displayX, + int displayY, + double sceneX, + double sceneY) + { + // https://bitbucket.org/osimis/osimis-webviewer-plugin/src/master/frontend/src/app/viewport/image-plugins/windowing-viewport-tool.class.js + + static const float SCALE = 1.0; + + int deltaCenter = 0; + int deltaWidth = 0; + + ComputeAxisEffect(deltaCenter, deltaWidth, displayX - clickX_, leftAction_, rightAction_); + ComputeAxisEffect(deltaCenter, deltaWidth, displayY - clickY_, upAction_, downAction_); + + float newCenter = sourceCenter_ + (deltaCenter / SCALE * strength_); + float newWidth = sourceWidth_ + (deltaWidth / SCALE * strength_); + stack_.SetWindowing(newCenter, newWidth); + } + }; + + + class BitmapStackWidget : + public WorldSceneWidget, + public IObservable, + public IObserver + { + private: + BitmapStack& stack_; + std::auto_ptr floatBuffer_; + std::auto_ptr cairoBuffer_; + bool invert_; + ImageInterpolation interpolation_; + + virtual bool RenderInternal(unsigned int width, + unsigned int height, + ImageInterpolation interpolation) + { + float windowCenter, windowWidth; + stack_.GetWindowingWithDefault(windowCenter, windowWidth); + + float x0 = windowCenter - windowWidth / 2.0f; + float x1 = windowCenter + windowWidth / 2.0f; + + if (windowWidth <= 0.001f) // Avoid division by zero at (*) + { + return false; + } + else + { + if (floatBuffer_.get() == NULL || + floatBuffer_->GetWidth() != width || + floatBuffer_->GetHeight() != height) + { + floatBuffer_.reset(new Orthanc::Image(Orthanc::PixelFormat_Float32, width, height, false)); + } + + if (cairoBuffer_.get() == NULL || + cairoBuffer_->GetWidth() != width || + cairoBuffer_->GetHeight() != height) + { + cairoBuffer_.reset(new CairoSurface(width, height)); + } + + stack_.Render(*floatBuffer_, GetView().GetMatrix(), interpolation); + + // Conversion from Float32 to BGRA32 (cairo). Very similar to + // GrayscaleFrameRenderer => TODO MERGE? + + Orthanc::ImageAccessor target; + cairoBuffer_->GetWriteableAccessor(target); + + float scaling = 255.0f / (x1 - x0); + + for (unsigned int y = 0; y < height; y++) + { + const float* p = reinterpret_cast(floatBuffer_->GetConstRow(y)); + uint8_t* q = reinterpret_cast(target.GetRow(y)); + + for (unsigned int x = 0; x < width; x++, p++, q += 4) + { + uint8_t v = 0; + if (*p >= x1) + { + v = 255; + } + else if (*p <= x0) + { + v = 0; + } + else + { + // https://en.wikipedia.org/wiki/Linear_interpolation + v = static_cast(scaling * (*p - x0)); // (*) + } + + if (invert_) + { + v = 255 - v; + } + + q[0] = v; + q[1] = v; + q[2] = v; + q[3] = 255; + } + } + + return true; + } + } + + + protected: + virtual Extent2D GetSceneExtent() + { + return stack_.GetSceneExtent(); + } + + virtual bool RenderScene(CairoContext& context, + const ViewportGeometry& view) + { + cairo_t* cr = context.GetObject(); + + if (RenderInternal(context.GetWidth(), context.GetHeight(), interpolation_)) + { + // https://www.cairographics.org/FAQ/#paint_from_a_surface + cairo_save(cr); + cairo_identity_matrix(cr); + cairo_set_source_surface(cr, cairoBuffer_->GetObject(), 0, 0); + cairo_paint(cr); + cairo_restore(cr); + } + else + { + // https://www.cairographics.org/FAQ/#clear_a_surface + context.SetSourceColor(0, 0, 0); + cairo_paint(cr); + } + + stack_.DrawControls(context, view.GetZoom()); + + return true; + } + + public: + BitmapStackWidget(MessageBroker& broker, + BitmapStack& stack, + const std::string& name) : + WorldSceneWidget(name), + IObservable(broker), + IObserver(broker), + stack_(stack), + invert_(false), + interpolation_(ImageInterpolation_Nearest) + { + stack.RegisterObserverCallback(new Callable(*this, &BitmapStackWidget::OnGeometryChanged)); + stack.RegisterObserverCallback(new Callable(*this, &BitmapStackWidget::OnContentChanged)); + } + + BitmapStack& GetStack() const + { + return stack_; + } + + void OnGeometryChanged(const BitmapStack::GeometryChangedMessage& message) + { + LOG(INFO) << "Geometry has changed"; + FitContent(); + } + + void OnContentChanged(const BitmapStack::ContentChangedMessage& message) + { + LOG(INFO) << "Content has changed"; + NotifyContentChanged(); + } + + void SetInvert(bool invert) + { + if (invert_ != invert) + { + invert_ = invert; + NotifyContentChanged(); + } + } + + void SwitchInvert() + { + invert_ = !invert_; + NotifyContentChanged(); + } + + bool IsInvert() const + { + return invert_; + } + + void SetInterpolation(ImageInterpolation interpolation) + { + if (interpolation_ != interpolation) + { + interpolation_ = interpolation; + NotifyContentChanged(); + } + } + + ImageInterpolation GetInterpolation() const + { + return interpolation_; + } + }; + + + class BitmapStackInteractor : + public IWorldSceneInteractor, + public IObserver + { + private: + enum Tool + { + Tool_Move, + Tool_Rotate, + Tool_Crop, + Tool_Resize, + Tool_Windowing + }; + + + UndoRedoStack undoRedoStack_; + Tool tool_; + OrthancApiClient *orthanc_; + + + static double GetHandleSize() + { + return 10.0; + } + + + static BitmapStackWidget& GetWidget(WorldSceneWidget& widget) + { + return dynamic_cast(widget); + } + + + static BitmapStack& GetStack(WorldSceneWidget& widget) + { + return GetWidget(widget).GetStack(); + } + + + public: + BitmapStackInteractor(MessageBroker& broker) : + IObserver(broker), + tool_(Tool_Move), + orthanc_(NULL) + { + } + + virtual IWorldSceneMouseTracker* CreateMouseTracker(WorldSceneWidget& widget, + const ViewportGeometry& view, + MouseButton button, + KeyboardModifiers modifiers, + int viewportX, + int viewportY, + double x, + double y, + IStatusBar* statusBar) + { + if (button == MouseButton_Left) + { + size_t selected; + + if (tool_ == Tool_Windowing) + { + return new WindowingTracker(undoRedoStack_, GetStack(widget), + viewportX, viewportY, + WindowingTracker::Action_DecreaseWidth, + WindowingTracker::Action_IncreaseWidth, + WindowingTracker::Action_DecreaseCenter, + WindowingTracker::Action_IncreaseCenter); + } + else if (!GetStack(widget).GetSelectedBitmap(selected)) + { + size_t bitmap; + if (GetStack(widget).LookupBitmap(bitmap, x, y)) + { + LOG(INFO) << "Click on bitmap " << bitmap; + GetStack(widget).Select(bitmap); + } + + return NULL; + } + else if (tool_ == Tool_Crop || + tool_ == Tool_Resize) + { + BitmapStack::BitmapAccessor accessor(GetStack(widget), selected); + BitmapStack::Corner corner; + if (accessor.GetBitmap().LookupCorner(corner, x, y, view.GetZoom(), GetHandleSize())) + { + switch (tool_) + { + case Tool_Crop: + return new CropBitmapTracker(undoRedoStack_, GetStack(widget), view, selected, x, y, corner); + + case Tool_Resize: + return new ResizeBitmapTracker(undoRedoStack_, GetStack(widget), selected, x, y, corner, + (modifiers & KeyboardModifiers_Shift)); + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + } + else + { + size_t bitmap; + + if (!GetStack(widget).LookupBitmap(bitmap, x, y) || + bitmap != selected) + { + GetStack(widget).Unselect(); + } + + return NULL; + } + } + else + { + size_t bitmap; + + if (GetStack(widget).LookupBitmap(bitmap, x, y) && + bitmap == selected) + { + switch (tool_) + { + case Tool_Move: + return new MoveBitmapTracker(undoRedoStack_, GetStack(widget), bitmap, x, y, + (modifiers & KeyboardModifiers_Shift)); + + case Tool_Rotate: + return new RotateBitmapTracker(undoRedoStack_, GetStack(widget), view, bitmap, x, y, + (modifiers & KeyboardModifiers_Shift)); + + default: + break; + } + + return NULL; + } + else + { + LOG(INFO) << "Click out of any bitmap"; + GetStack(widget).Unselect(); + return NULL; + } + } + } + else + { + return NULL; + } + } + + virtual void MouseOver(CairoContext& context, + WorldSceneWidget& widget, + const ViewportGeometry& view, + double x, + double y, + IStatusBar* statusBar) + { +#if 0 + if (statusBar != NULL) + { + char buf[64]; + sprintf(buf, "X = %.02f Y = %.02f (in cm)", x / 10.0, y / 10.0); + statusBar->SetMessage(buf); + } +#endif + + size_t selected; + if (GetStack(widget).GetSelectedBitmap(selected) && + (tool_ == Tool_Crop || + tool_ == Tool_Resize)) + { + BitmapStack::BitmapAccessor accessor(GetStack(widget), selected); + + BitmapStack::Corner corner; + if (accessor.GetBitmap().LookupCorner(corner, x, y, view.GetZoom(), GetHandleSize())) + { + accessor.GetBitmap().GetCorner(x, y, corner); + + double z = 1.0 / view.GetZoom(); + + context.SetSourceColor(255, 0, 0); + cairo_t* cr = context.GetObject(); + cairo_set_line_width(cr, 2.0 * z); + cairo_move_to(cr, x - GetHandleSize() * z, y - GetHandleSize() * z); + cairo_line_to(cr, x + GetHandleSize() * z, y - GetHandleSize() * z); + cairo_line_to(cr, x + GetHandleSize() * z, y + GetHandleSize() * z); + cairo_line_to(cr, x - GetHandleSize() * z, y + GetHandleSize() * z); + cairo_line_to(cr, x - GetHandleSize() * z, y - GetHandleSize() * z); + cairo_stroke(cr); + } + } + } + + virtual void MouseWheel(WorldSceneWidget& widget, + MouseWheelDirection direction, + KeyboardModifiers modifiers, + IStatusBar* statusBar) + { + } + + virtual void KeyPressed(WorldSceneWidget& widget, + KeyboardKeys key, + char keyChar, + KeyboardModifiers modifiers, + IStatusBar* statusBar) + { + switch (keyChar) + { + case 'a': + widget.FitContent(); + break; + + case 'c': + tool_ = Tool_Crop; + break; + + case 'e': + { + Orthanc::DicomMap tags; + + // Minimal set of tags to generate a valid CR image + tags.SetValue(Orthanc::DICOM_TAG_ACCESSION_NUMBER, "NOPE", false); + tags.SetValue(Orthanc::DICOM_TAG_BODY_PART_EXAMINED, "PELVIS", false); + tags.SetValue(Orthanc::DICOM_TAG_INSTANCE_NUMBER, "1", false); + //tags.SetValue(Orthanc::DICOM_TAG_LATERALITY, "", false); + tags.SetValue(Orthanc::DICOM_TAG_MANUFACTURER, "OSIMIS", false); + tags.SetValue(Orthanc::DICOM_TAG_MODALITY, "CR", false); + tags.SetValue(Orthanc::DICOM_TAG_PATIENT_BIRTH_DATE, "20000101", false); + tags.SetValue(Orthanc::DICOM_TAG_PATIENT_ID, "hello", false); + tags.SetValue(Orthanc::DICOM_TAG_PATIENT_NAME, "HELLO^WORLD", false); + tags.SetValue(Orthanc::DICOM_TAG_PATIENT_ORIENTATION, "", false); + tags.SetValue(Orthanc::DICOM_TAG_PATIENT_SEX, "M", false); + tags.SetValue(Orthanc::DICOM_TAG_REFERRING_PHYSICIAN_NAME, "HOUSE^MD", false); + tags.SetValue(Orthanc::DICOM_TAG_SERIES_NUMBER, "1", false); + tags.SetValue(Orthanc::DICOM_TAG_SOP_CLASS_UID, "1.2.840.10008.5.1.4.1.1.1", false); + tags.SetValue(Orthanc::DICOM_TAG_STUDY_ID, "STUDY", false); + tags.SetValue(Orthanc::DICOM_TAG_VIEW_POSITION, "", false); + + Export(GetWidget(widget), 0.1, 0.1, tags); + break; + } + + case 'i': + GetWidget(widget).SwitchInvert(); + break; + + case 'm': + tool_ = Tool_Move; + break; + + case 'n': + { + switch (GetWidget(widget).GetInterpolation()) + { + case ImageInterpolation_Nearest: + LOG(INFO) << "Switching to bilinear interpolation"; + GetWidget(widget).SetInterpolation(ImageInterpolation_Bilinear); + break; + + case ImageInterpolation_Bilinear: + LOG(INFO) << "Switching to nearest neighbor interpolation"; + GetWidget(widget).SetInterpolation(ImageInterpolation_Nearest); + break; + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + + break; + } + + case 'r': + tool_ = Tool_Rotate; + break; + + case 's': + tool_ = Tool_Resize; + break; + + case 'w': + tool_ = Tool_Windowing; + break; + + case 'y': + if (modifiers & KeyboardModifiers_Control) + { + undoRedoStack_.Redo(); + widget.NotifyContentChanged(); + } + break; + + case 'z': + if (modifiers & KeyboardModifiers_Control) + { + undoRedoStack_.Undo(); + widget.NotifyContentChanged(); + } + break; + + default: + break; + } + } + + + void SetOrthanc(OrthancApiClient& orthanc) + { + orthanc_ = &orthanc; + } + + + void Export(const BitmapStackWidget& widget, + double pixelSpacingX, + double pixelSpacingY, + const Orthanc::DicomMap& dicom) + { + if (pixelSpacingX <= 0 || + pixelSpacingY <= 0) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + + if (orthanc_ == NULL) + { + return; + } + + LOG(WARNING) << "Exporting DICOM"; + + Extent2D extent = widget.GetStack().GetSceneExtent(); + + int w = std::ceil(extent.GetWidth() / pixelSpacingX); + int h = std::ceil(extent.GetHeight() / pixelSpacingY); + + if (w < 0 || h < 0) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + + Orthanc::Image layers(Orthanc::PixelFormat_Float32, + static_cast(w), + static_cast(h), false); + + Matrix view = LinearAlgebra::Product( + CreateScalingMatrix(1.0 / pixelSpacingX, 1.0 / pixelSpacingY), + CreateOffsetMatrix(-extent.GetX1(), -extent.GetY1())); + + widget.GetStack().Render(layers, view, widget.GetInterpolation()); + + Orthanc::Image rendered(Orthanc::PixelFormat_Grayscale16, + layers.GetWidth(), layers.GetHeight(), false); + Orthanc::ImageProcessing::Convert(rendered, layers); + + std::string base64; + + { + std::string content; + +#if EXPORT_USING_PAM == 1 + { + Orthanc::PamWriter writer; + writer.WriteToMemory(content, rendered); + } +#else + { + Orthanc::PngWriter writer; + writer.WriteToMemory(content, rendered); + } +#endif + + Orthanc::Toolbox::EncodeBase64(base64, content); + } + + std::set tags; + dicom.GetTags(tags); + + Json::Value json = Json::objectValue; + json["Tags"] = Json::objectValue; + + for (std::set::const_iterator + tag = tags.begin(); tag != tags.end(); ++tag) + { + const Orthanc::DicomValue& value = dicom.GetValue(*tag); + if (!value.IsNull() && + !value.IsBinary()) + { + json["Tags"][tag->Format()] = value.GetContent(); + } + } + + json["Tags"][Orthanc::DICOM_TAG_PHOTOMETRIC_INTERPRETATION.Format()] = + (widget.IsInvert() ? "MONOCHROME1" : "MONOCHROME2"); + + + // WARNING: The order of PixelSpacing is Y/X + char buf[32]; + sprintf(buf, "%0.8f\\%0.8f", pixelSpacingY, pixelSpacingX); + + json["Tags"][Orthanc::DICOM_TAG_PIXEL_SPACING.Format()] = buf; + + float center, width; + if (widget.GetStack().GetWindowing(center, width)) + { + json["Tags"][Orthanc::DICOM_TAG_WINDOW_CENTER.Format()] = + boost::lexical_cast(lroundf(center)); + + json["Tags"][Orthanc::DICOM_TAG_WINDOW_WIDTH.Format()] = + boost::lexical_cast(lroundf(width)); + } + +#if EXPORT_USING_PAM == 1 + json["Content"] = "data:" + std::string(Orthanc::MIME_PAM) + ";base64," + base64; +#else + json["Content"] = "data:" + std::string(Orthanc::MIME_PNG) + ";base64," + base64; +#endif + + orthanc_->PostJsonAsyncExpectJson( + "/tools/create-dicom", json, + new Callable + (*this, &BitmapStackInteractor::OnDicomExported), + NULL, NULL); + } + + + void OnDicomExported(const OrthancApiClient::JsonResponseReadyMessage& message) + { + LOG(WARNING) << "DICOM export was successful:" + << message.Response.toStyledString(); + } + }; + + + + namespace Samples + { + class SingleFrameEditorApplication : + public SampleSingleCanvasApplicationBase, + public IObserver + { + private: + std::auto_ptr orthancApiClient_; + std::auto_ptr stack_; + BitmapStackInteractor interactor_; + + public: + SingleFrameEditorApplication(MessageBroker& broker) : + IObserver(broker), + interactor_(broker) + { + } + + virtual void DeclareStartupOptions(boost::program_options::options_description& options) + { + boost::program_options::options_description generic("Sample options"); + generic.add_options() + ("instance", boost::program_options::value(), + "Orthanc ID of the instance") + ("frame", boost::program_options::value()->default_value(0), + "Number of the frame, for multi-frame DICOM instances") + ; + + options.add(generic); + } + + virtual void Initialize(StoneApplicationContext* context, + IStatusBar& statusBar, + const boost::program_options::variables_map& parameters) + { + using namespace OrthancStone; + + context_ = context; + + statusBar.SetMessage("Use the key \"a\" to reinitialize the layout"); + statusBar.SetMessage("Use the key \"c\" to crop"); + statusBar.SetMessage("Use the key \"e\" to export DICOM to the Orthanc server"); + statusBar.SetMessage("Use the key \"f\" to switch full screen"); + statusBar.SetMessage("Use the key \"i\" to invert contrast"); + statusBar.SetMessage("Use the key \"m\" to move objects"); + statusBar.SetMessage("Use the key \"n\" to switch between nearest neighbor and bilinear interpolation"); + statusBar.SetMessage("Use the key \"r\" to rotate objects"); + statusBar.SetMessage("Use the key \"s\" to resize objects (not applicable to DICOM bitmaps)"); + statusBar.SetMessage("Use the key \"w\" to change windowing"); + + statusBar.SetMessage("Use the key \"ctrl-z\" to undo action"); + statusBar.SetMessage("Use the key \"ctrl-y\" to redo action"); + + if (parameters.count("instance") != 1) + { + LOG(ERROR) << "The instance ID is missing"; + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + + std::string instance = parameters["instance"].as(); + int frame = parameters["frame"].as(); + + orthancApiClient_.reset(new OrthancApiClient(IObserver::broker_, context_->GetWebService())); + interactor_.SetOrthanc(*orthancApiClient_); + + Orthanc::FontRegistry fonts; + fonts.AddFromResource(Orthanc::EmbeddedResources::FONT_UBUNTU_MONO_BOLD_16); + + stack_.reset(new BitmapStack(IObserver::broker_, *orthancApiClient_)); + stack_->LoadFrame(instance, frame, false); //.SetPan(200, 0); + //stack_->LoadFrame("61f3143e-96f34791-ad6bbb8d-62559e75-45943e1b", 0, false); + + { + BitmapStack::Bitmap& bitmap = stack_->LoadText(fonts.GetFont(0), "Hello\nworld"); + //dynamic_cast(bitmap).SetForegroundValue(256); + dynamic_cast(bitmap).SetResizeable(true); + } + + { + BitmapStack::Bitmap& bitmap = stack_->LoadTestBlock(100, 50); + //dynamic_cast(bitmap).SetForegroundValue(256); + dynamic_cast(bitmap).SetResizeable(true); + dynamic_cast(bitmap).SetPan(0, 200); + } + + + mainWidget_ = new BitmapStackWidget(IObserver::broker_, *stack_, "main-widget"); + mainWidget_->SetTransmitMouseOver(true); + mainWidget_->SetInteractor(interactor_); + + //stack_->SetWindowing(128, 256); + } + }; + } +} diff -r fe4befe03935 -r d6136a7e914d Applications/Samples/SingleVolumeApplication.h --- a/Applications/Samples/SingleVolumeApplication.h Mon Nov 05 10:04:56 2018 +0100 +++ b/Applications/Samples/SingleVolumeApplication.h Mon Nov 05 10:06:18 2018 +0100 @@ -89,7 +89,7 @@ public: - virtual void DeclareCommandLineOptions(boost::program_options::options_description& options) + virtual void DeclareStartupOptions(boost::program_options::options_description& options) { boost::program_options::options_description generic("Sample options"); generic.add_options() @@ -108,8 +108,7 @@ options.add(generic); } - virtual void Initialize(BasicApplicationContext& context, - IStatusBar& statusBar, + virtual void Initialize(IStatusBar& statusBar, const boost::program_options::variables_map& parameters) { using namespace OrthancStone; @@ -147,8 +146,8 @@ throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); } - unsigned int threads = parameters["threads"].as(); - bool reverse = parameters["reverse"].as(); + //unsigned int threads = parameters["threads"].as(); + //bool reverse = parameters["reverse"].as(); std::string tmp = parameters["projection"].as(); Orthanc::Toolbox::ToLowerCase(tmp); @@ -187,8 +186,8 @@ widget->AddLayer(new VolumeImageSource(*volume)); - context.AddInteractor(new Interactor(*volume, *widget, projection, 0)); - context.AddSlicedVolume(volume.release()); + context_->AddInteractor(new Interactor(*volume, *widget, projection, 0)); + context_->AddSlicedVolume(volume.release()); if (1) { @@ -208,14 +207,14 @@ widget->SetLayerStyle(0, s); } #else - std::auto_ptr ct(new OrthancVolumeImage(context.GetWebService(), false)); + std::auto_ptr ct(new OrthancVolumeImage(context_->GetWebService(), false)); //ct->ScheduleLoadSeries("15a6f44a-ac7b88fe-19c462d9-dddd918e-b01550d8"); // 0178023P //ct->ScheduleLoadSeries("dd069910-4f090474-7d2bba07-e5c10783-f9e4fb1d"); //ct->ScheduleLoadSeries("a04ecf01-79b2fc33-58239f7e-ad9db983-28e81afa"); // IBA //ct->ScheduleLoadSeries("03677739-1d8bca40-db1daf59-d74ff548-7f6fc9c0"); // 0522c0001 TCIA ct->ScheduleLoadSeries("295e8a13-dfed1320-ba6aebb2-9a13e20f-1b3eb953"); // Captain - std::auto_ptr pet(new OrthancVolumeImage(context.GetWebService(), true)); + std::auto_ptr pet(new OrthancVolumeImage(context_->GetWebService(), true)); //pet->ScheduleLoadSeries("48d2997f-8e25cd81-dd715b64-bd79cdcc-e8fcee53"); // 0178023P //pet->ScheduleLoadSeries("aabad2e7-80702b5d-e599d26c-4f13398e-38d58a9e"); //pet->ScheduleLoadInstance("830a69ff-8e4b5ee3-b7f966c8-bccc20fb-d322dceb"); // IBA 1 @@ -225,7 +224,7 @@ pet->ScheduleLoadInstance("f080888c-0ab7528a-f7d9c28c-84980eb1-ff3b0ae6"); // Captain 1 //pet->ScheduleLoadInstance("4f78055b-6499a2c5-1e089290-394acc05-3ec781c1"); // Captain 2 - std::auto_ptr rtStruct(new StructureSetLoader(context.GetWebService())); + std::auto_ptr rtStruct(new StructureSetLoader(context_->GetWebService())); //rtStruct->ScheduleLoadInstance("c2ebc17b-6b3548db-5e5da170-b8ecab71-ea03add3"); // 0178023P //rtStruct->ScheduleLoadInstance("54460695-ba3885ee-ddf61ac0-f028e31d-a6e474d9"); // IBA //rtStruct->ScheduleLoadInstance("17cd032b-ad92a438-ca05f06a-f9e96668-7e3e9e20"); // 0522c0001 TCIA @@ -235,12 +234,12 @@ widget->AddLayer(new VolumeImageSource(*pet)); widget->AddLayer(new DicomStructureSetRendererFactory(*rtStruct)); - context.AddInteractor(new Interactor(*pet, *widget, projection, 1)); - //context.AddInteractor(new VolumeImageInteractor(*ct, *widget, projection)); + context_->AddInteractor(new Interactor(*pet, *widget, projection, 1)); + //context_->AddInteractor(new VolumeImageInteractor(*ct, *widget, projection)); - context.AddSlicedVolume(ct.release()); - context.AddSlicedVolume(pet.release()); - context.AddVolumeLoader(rtStruct.release()); + context_->AddSlicedVolume(ct.release()); + context_->AddSlicedVolume(pet.release()); + context_->AddVolumeLoader(rtStruct.release()); { RenderStyle s; @@ -272,7 +271,7 @@ statusBar.SetMessage("Use the keys \"c\" to draw circles"); widget->SetTransmitMouseOver(true); - context.SetCentralWidget(widget.release()); + context_->SetCentralWidget(widget.release()); } }; } diff -r fe4befe03935 -r d6136a7e914d Applications/Samples/TestPatternApplication.h --- a/Applications/Samples/TestPatternApplication.h Mon Nov 05 10:04:56 2018 +0100 +++ b/Applications/Samples/TestPatternApplication.h Mon Nov 05 10:06:18 2018 +0100 @@ -34,7 +34,7 @@ class TestPatternApplication : public SampleApplicationBase { public: - virtual void DeclareCommandLineOptions(boost::program_options::options_description& options) + virtual void DeclareStartupOptions(boost::program_options::options_description& options) { boost::program_options::options_description generic("Sample options"); generic.add_options() @@ -44,8 +44,7 @@ options.add(generic); } - virtual void Initialize(BasicApplicationContext& context, - IStatusBar& statusBar, + virtual void Initialize(IStatusBar& statusBar, const boost::program_options::variables_map& parameters) { using namespace OrthancStone; @@ -56,8 +55,8 @@ layout->AddWidget(new TestCairoWidget(parameters["animate"].as())); layout->AddWidget(new TestWorldSceneWidget(parameters["animate"].as())); - context.SetCentralWidget(layout.release()); - context.SetUpdateDelay(25); // If animation, update the content each 25ms + context_->SetCentralWidget(layout.release()); + context_->SetUpdateDelay(25); // If animation, update the content each 25ms } }; } diff -r fe4befe03935 -r d6136a7e914d Applications/Samples/Web/index.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Applications/Samples/Web/index.html Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,24 @@ + + + + + + + + + + + + Wasm Samples + + + + + + + \ No newline at end of file diff -r fe4befe03935 -r d6136a7e914d Applications/Samples/Web/samples-styles.css diff -r fe4befe03935 -r d6136a7e914d Applications/Samples/Web/simple-viewer-single-file.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Applications/Samples/Web/simple-viewer-single-file.html Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,34 @@ + + + + + + + + + + + + Simple Viewer + + + + +
+ + +
+
+ line + circle + + +
+ + + + \ No newline at end of file diff -r fe4befe03935 -r d6136a7e914d Applications/Samples/Web/simple-viewer-single-file.ts --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Applications/Samples/Web/simple-viewer-single-file.ts Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,51 @@ +/// + +InitializeWasmApplication("OrthancStoneSimpleViewerSingleFile", "/orthanc"); + +function SelectTool(toolName: string) { + var command = { + command: "selectTool", + args: { + toolName: toolName + } + }; + SendMessageToStoneApplication(JSON.stringify(command)); + +} + +function PerformAction(commandName: string) { + var command = { + command: commandName, + commandType: "simple", + args: {} + }; + SendMessageToStoneApplication(JSON.stringify(command)); +} + +//initializes the buttons +//----------------------- +// install "SelectTool" handlers +document.querySelectorAll("[tool-selector]").forEach((e) => { + console.log(e); + (e as HTMLInputElement).addEventListener("click", () => { + console.log(e); + SelectTool(e.attributes["tool-selector"].value); + }); +}); + +// install "PerformAction" handlers +document.querySelectorAll("[action-trigger]").forEach((e) => { + (e as HTMLInputElement).addEventListener("click", () => { + PerformAction(e.attributes["action-trigger"].value); + }); +}); + +// this method is called "from the C++ code" when the StoneApplication is updated. +// it can be used to update the UI of the application +function UpdateWebApplication(statusUpdateMessage: string) { + console.log(statusUpdateMessage); + + if (statusUpdateMessage.startsWith("series-description=")) { + document.getElementById("series-description").innerText = statusUpdateMessage.split("=")[1]; + } +} diff -r fe4befe03935 -r d6136a7e914d Applications/Samples/Web/simple-viewer-single-file.tsconfig.json --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Applications/Samples/Web/simple-viewer-single-file.tsconfig.json Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,9 @@ +{ + "extends" : "./tsconfig-samples", + "compilerOptions": { + "outFile": "../build-web/app-simple-viewer-single-file.js" + }, + "include" : [ + "simple-viewer-single-file.ts" + ] +} \ No newline at end of file diff -r fe4befe03935 -r d6136a7e914d Applications/Samples/Web/single-frame-editor.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Applications/Samples/Web/single-frame-editor.html Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,22 @@ + + + + + + + + + + + + Simple Viewer + + + +
+ +
+ + + + \ No newline at end of file diff -r fe4befe03935 -r d6136a7e914d Applications/Samples/Web/single-frame-editor.ts --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Applications/Samples/Web/single-frame-editor.ts Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,3 @@ +/// + +InitializeWasmApplication("OrthancStoneSingleFrameEditor", "/orthanc"); diff -r fe4befe03935 -r d6136a7e914d Applications/Samples/Web/single-frame-editor.tsconfig.json --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Applications/Samples/Web/single-frame-editor.tsconfig.json Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,9 @@ +{ + "extends" : "./tsconfig-samples", + "compilerOptions": { + "outFile": "../build-web/app-single-frame-editor.js" + }, + "include" : [ + "single-frame-editor.ts" + ] +} \ No newline at end of file diff -r fe4befe03935 -r d6136a7e914d Applications/Samples/Web/single-frame.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Applications/Samples/Web/single-frame.html Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,22 @@ + + + + + + + + + + + + Simple Viewer + + + +
+ +
+ + + + \ No newline at end of file diff -r fe4befe03935 -r d6136a7e914d Applications/Samples/Web/single-frame.ts --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Applications/Samples/Web/single-frame.ts Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,3 @@ +/// + +InitializeWasmApplication("OrthancStoneSingleFrame", "/orthanc"); diff -r fe4befe03935 -r d6136a7e914d Applications/Samples/Web/single-frame.tsconfig.json --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Applications/Samples/Web/single-frame.tsconfig.json Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,9 @@ +{ + "extends" : "./tsconfig-samples", + "compilerOptions": { + "outFile": "../build-web/app-single-frame.js" + }, + "include" : [ + "single-frame.ts" + ] +} \ No newline at end of file diff -r fe4befe03935 -r d6136a7e914d Applications/Samples/Web/tsconfig-samples.json --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Applications/Samples/Web/tsconfig-samples.json Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,11 @@ +{ + "extends" : "../../../Platforms/Wasm/tsconfig-stone", + "compilerOptions": { + "sourceMap": false, + "lib" : [ + "es2017", + "dom", + "dom.iterable" + ] + } +} diff -r fe4befe03935 -r d6136a7e914d Applications/Samples/build-wasm.sh --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Applications/Samples/build-wasm.sh Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,17 @@ +#!/bin/bash + +set -e + +currentDir=$(pwd) +samplesRootDir=$(pwd) + +mkdir -p $samplesRootDir/build-wasm +cd $samplesRootDir/build-wasm + +source ~/Downloads/emsdk/emsdk_env.sh +cmake -DCMAKE_TOOLCHAIN_FILE=${EMSCRIPTEN}/cmake/Modules/Platform/Emscripten.cmake -DCMAKE_BUILD_TYPE=Release -DSTONE_SOURCES_DIR=$currentDir/../../../orthanc-stone -DORTHANC_FRAMEWORK_SOURCE=path -DORTHANC_FRAMEWORK_ROOT=$currentDir/../../../orthanc -DALLOW_DOWNLOADS=ON .. -DENABLE_WASM=ON +make -j 5 + +echo "-- building the web application -- " +cd $currentDir +./build-web.sh \ No newline at end of file diff -r fe4befe03935 -r d6136a7e914d Applications/Samples/build-web.sh --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Applications/Samples/build-web.sh Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,43 @@ +#!/bin/bash + +set -e + +# this script currently assumes that the wasm code has been built on its side and is availabie in build-wasm/ + +currentDir=$(pwd) +samplesRootDir=$(pwd) + +outputDir=$samplesRootDir/build-web/ +mkdir -p $outputDir + +# files used by all single files samples +cp $samplesRootDir/Web/index.html $outputDir +cp $samplesRootDir/Web/samples-styles.css $outputDir + +# build simple-viewer-single-file (obsolete project) +cp $samplesRootDir/Web/simple-viewer-single-file.html $outputDir +tsc --allowJs --project $samplesRootDir/Web/simple-viewer-single-file.tsconfig.json +cp $currentDir/build-wasm/OrthancStoneSimpleViewerSingleFile.js $outputDir +cp $currentDir/build-wasm/OrthancStoneSimpleViewerSingleFile.wasm $outputDir + +# build single-frame +cp $samplesRootDir/Web/single-frame.html $outputDir +tsc --allowJs --project $samplesRootDir/Web/single-frame.tsconfig.json +cp $currentDir/build-wasm/OrthancStoneSingleFrame.js $outputDir +cp $currentDir/build-wasm/OrthancStoneSingleFrame.wasm $outputDir + +# build single-frame-editor +cp $samplesRootDir/Web/single-frame-editor.html $outputDir +tsc --allowJs --project $samplesRootDir/Web/single-frame-editor.tsconfig.json +cp $currentDir/build-wasm/OrthancStoneSingleFrameEditor.js $outputDir +cp $currentDir/build-wasm/OrthancStoneSingleFrameEditor.wasm $outputDir + +# build simple-viewer project +mkdir -p $outputDir/simple-viewer/ +cp $samplesRootDir/SimpleViewer/Wasm/simple-viewer.html $outputDir/simple-viewer/ +cp $samplesRootDir/SimpleViewer/Wasm/styles.css $outputDir/simple-viewer/ +tsc --allowJs --project $samplesRootDir/SimpleViewer/Wasm/tsconfig-simple-viewer.json +cp $currentDir/build-wasm/OrthancStoneSimpleViewer.js $outputDir/simple-viewer/ +cp $currentDir/build-wasm/OrthancStoneSimpleViewer.wasm $outputDir/simple-viewer/ + +cd $currentDir diff -r fe4befe03935 -r d6136a7e914d Applications/Samples/nginx.local.conf --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Applications/Samples/nginx.local.conf Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,44 @@ +# Local config to serve the WASM samples static files and reverse proxy Orthanc. +# Uses port 9977 instead of 80. + +# `events` section is mandatory +events { + worker_connections 1024; # Default: 1024 +} + +http { + + # prevent nginx sync issues on OSX + proxy_buffering off; + + server { + listen 9977 default_server; + client_max_body_size 4G; + + # location may have to be adjusted depending on your OS and nginx install + include /etc/nginx/mime.types; + # if not in your system mime.types, add this line to support WASM: + # types { + # application/wasm wasm; + # } + + # serve WASM static files + root build-web/; + location / { + } + + # reverse proxy orthanc + location /orthanc/ { + rewrite /orthanc(.*) $1 break; + proxy_pass http://127.0.0.1:8042; + proxy_set_header Host $http_host; + proxy_set_header my-auth-header good-token; + proxy_request_buffering off; + proxy_max_temp_file_size 0; + client_max_body_size 0; + } + + + } + +} diff -r fe4befe03935 -r d6136a7e914d Applications/Samples/samples-library.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Applications/Samples/samples-library.js Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,8 @@ +// this file contains the JS method you want to expose to C++ code + +// mergeInto(LibraryManager.library, { +// ScheduleRedraw: function() { +// ScheduleRedraw(); +// } +// }); + \ No newline at end of file diff -r fe4befe03935 -r d6136a7e914d Applications/Samples/tsconfig-stone.json --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Applications/Samples/tsconfig-stone.json Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,7 @@ +{ + "include" : [ + "../../Platforms/Wasm/stone-framework-loader.ts", + "../../Platforms/Wasm/wasm-application-runner.ts", + "../../Platforms/Wasm/wasm-viewport.ts" + ] +} diff -r fe4befe03935 -r d6136a7e914d Applications/Sdl/SdlCairoSurface.cpp diff -r fe4befe03935 -r d6136a7e914d Applications/Sdl/SdlEngine.cpp --- a/Applications/Sdl/SdlEngine.cpp Mon Nov 05 10:04:56 2018 +0100 +++ b/Applications/Sdl/SdlEngine.cpp Mon Nov 05 10:06:18 2018 +0100 @@ -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 . **/ @@ -29,21 +29,20 @@ namespace OrthancStone { - void SdlEngine::SetSize(BasicApplicationContext::ViewportLocker& locker, - unsigned int width, + void SdlEngine::SetSize(unsigned int width, unsigned int height) { - locker.GetViewport().SetSize(width, height); + context_.GetCentralViewport().SetSize(width, height); surface_.SetSize(width, height); } - + void SdlEngine::RenderFrame() { if (viewportChanged_) { - BasicApplicationContext::ViewportLocker locker(context_); - surface_.Render(locker.GetViewport()); + NativeStoneApplicationContext::GlobalMutexLocker locker(context_); + surface_.Render(context_.GetCentralViewport()); viewportChanged_ = false; } @@ -99,7 +98,7 @@ SdlEngine::SdlEngine(SdlWindow& window, - BasicApplicationContext& context) : + NativeStoneApplicationContext& context) : window_(window), context_(context), surface_(window), @@ -119,9 +118,9 @@ const uint8_t* keyboardState = SDL_GetKeyboardState(&scancodeCount); { - BasicApplicationContext::ViewportLocker locker(context_); - SetSize(locker, window_.GetWidth(), window_.GetHeight()); - locker.GetViewport().SetDefaultView(); + NativeStoneApplicationContext::GlobalMutexLocker locker(context_); + SetSize(window_.GetWidth(), window_.GetHeight()); + context_.GetCentralViewport().FitContent(); } bool stop = false; @@ -134,9 +133,9 @@ while (!stop && SDL_PollEvent(&event)) { - BasicApplicationContext::ViewportLocker locker(context_); + NativeStoneApplicationContext::GlobalMutexLocker locker(context_); - if (event.type == SDL_QUIT) + if (event.type == SDL_QUIT) { stop = true; break; @@ -147,48 +146,48 @@ switch (event.button.button) { - case SDL_BUTTON_LEFT: - locker.GetViewport().MouseDown(MouseButton_Left, event.button.x, event.button.y, modifiers); - break; + case SDL_BUTTON_LEFT: + context_.GetCentralViewport().MouseDown(MouseButton_Left, event.button.x, event.button.y, modifiers); + break; - case SDL_BUTTON_RIGHT: - locker.GetViewport().MouseDown(MouseButton_Right, event.button.x, event.button.y, modifiers); - break; + case SDL_BUTTON_RIGHT: + context_.GetCentralViewport().MouseDown(MouseButton_Right, event.button.x, event.button.y, modifiers); + break; - case SDL_BUTTON_MIDDLE: - locker.GetViewport().MouseDown(MouseButton_Middle, event.button.x, event.button.y, modifiers); - break; + case SDL_BUTTON_MIDDLE: + context_.GetCentralViewport().MouseDown(MouseButton_Middle, event.button.x, event.button.y, modifiers); + break; - default: - break; + default: + break; } } else if (event.type == SDL_MOUSEMOTION) { - locker.GetViewport().MouseMove(event.button.x, event.button.y); + context_.GetCentralViewport().MouseMove(event.button.x, event.button.y); } else if (event.type == SDL_MOUSEBUTTONUP) { - locker.GetViewport().MouseUp(); + context_.GetCentralViewport().MouseUp(); } else if (event.type == SDL_WINDOWEVENT) { switch (event.window.event) { - case SDL_WINDOWEVENT_LEAVE: - locker.GetViewport().MouseLeave(); - break; + case SDL_WINDOWEVENT_LEAVE: + context_.GetCentralViewport().MouseLeave(); + break; - case SDL_WINDOWEVENT_ENTER: - locker.GetViewport().MouseEnter(); - break; + case SDL_WINDOWEVENT_ENTER: + context_.GetCentralViewport().MouseEnter(); + break; - case SDL_WINDOWEVENT_SIZE_CHANGED: - SetSize(locker, event.window.data1, event.window.data2); - break; + case SDL_WINDOWEVENT_SIZE_CHANGED: + SetSize(event.window.data1, event.window.data2); + break; - default: - break; + default: + break; } } else if (event.type == SDL_MOUSEWHEEL) @@ -200,11 +199,11 @@ if (event.wheel.y > 0) { - locker.GetViewport().MouseWheel(MouseWheelDirection_Up, x, y, modifiers); + context_.GetCentralViewport().MouseWheel(MouseWheelDirection_Up, x, y, modifiers); } else if (event.wheel.y < 0) { - locker.GetViewport().MouseWheel(MouseWheelDirection_Down, x, y, modifiers); + context_.GetCentralViewport().MouseWheel(MouseWheelDirection_Down, x, y, modifiers); } } else if (event.type == SDL_KEYDOWN && @@ -214,53 +213,61 @@ switch (event.key.keysym.sym) { - case SDLK_a: locker.GetViewport().KeyPressed('a', modifiers); break; - case SDLK_b: locker.GetViewport().KeyPressed('b', modifiers); break; - case SDLK_c: locker.GetViewport().KeyPressed('c', modifiers); break; - case SDLK_d: locker.GetViewport().KeyPressed('d', modifiers); break; - case SDLK_e: locker.GetViewport().KeyPressed('e', modifiers); break; - case SDLK_f: window_.ToggleMaximize(); break; - case SDLK_g: locker.GetViewport().KeyPressed('g', modifiers); break; - case SDLK_h: locker.GetViewport().KeyPressed('h', modifiers); break; - case SDLK_i: locker.GetViewport().KeyPressed('i', modifiers); break; - case SDLK_j: locker.GetViewport().KeyPressed('j', modifiers); break; - case SDLK_k: locker.GetViewport().KeyPressed('k', modifiers); break; - case SDLK_l: locker.GetViewport().KeyPressed('l', modifiers); break; - case SDLK_m: locker.GetViewport().KeyPressed('m', modifiers); break; - case SDLK_n: locker.GetViewport().KeyPressed('n', modifiers); break; - case SDLK_o: locker.GetViewport().KeyPressed('o', modifiers); break; - case SDLK_p: locker.GetViewport().KeyPressed('p', modifiers); break; - case SDLK_q: stop = true; break; - case SDLK_r: locker.GetViewport().KeyPressed('r', modifiers); break; - case SDLK_s: locker.GetViewport().KeyPressed('s', modifiers); break; - case SDLK_t: locker.GetViewport().KeyPressed('t', modifiers); break; - case SDLK_u: locker.GetViewport().KeyPressed('u', modifiers); break; - case SDLK_v: locker.GetViewport().KeyPressed('v', modifiers); break; - case SDLK_w: locker.GetViewport().KeyPressed('w', modifiers); break; - case SDLK_x: locker.GetViewport().KeyPressed('x', modifiers); break; - case SDLK_y: locker.GetViewport().KeyPressed('y', modifiers); break; - case SDLK_z: locker.GetViewport().KeyPressed('z', modifiers); break; - case SDLK_KP_0: locker.GetViewport().KeyPressed('0', modifiers); break; - case SDLK_KP_1: locker.GetViewport().KeyPressed('1', modifiers); break; - case SDLK_KP_2: locker.GetViewport().KeyPressed('2', modifiers); break; - case SDLK_KP_3: locker.GetViewport().KeyPressed('3', modifiers); break; - case SDLK_KP_4: locker.GetViewport().KeyPressed('4', modifiers); break; - case SDLK_KP_5: locker.GetViewport().KeyPressed('5', modifiers); break; - case SDLK_KP_6: locker.GetViewport().KeyPressed('6', modifiers); break; - case SDLK_KP_7: locker.GetViewport().KeyPressed('7', modifiers); break; - case SDLK_KP_8: locker.GetViewport().KeyPressed('8', modifiers); break; - case SDLK_KP_9: locker.GetViewport().KeyPressed('9', modifiers); break; + case SDLK_a: context_.GetCentralViewport().KeyPressed(KeyboardKeys_Generic, 'a', modifiers); break; + case SDLK_b: context_.GetCentralViewport().KeyPressed(KeyboardKeys_Generic, 'b', modifiers); break; + case SDLK_c: context_.GetCentralViewport().KeyPressed(KeyboardKeys_Generic, 'c', modifiers); break; + case SDLK_d: context_.GetCentralViewport().KeyPressed(KeyboardKeys_Generic, 'd', modifiers); break; + case SDLK_e: context_.GetCentralViewport().KeyPressed(KeyboardKeys_Generic, 'e', modifiers); break; + case SDLK_f: window_.ToggleMaximize(); break; + case SDLK_g: context_.GetCentralViewport().KeyPressed(KeyboardKeys_Generic, 'g', modifiers); break; + case SDLK_h: context_.GetCentralViewport().KeyPressed(KeyboardKeys_Generic, 'h', modifiers); break; + case SDLK_i: context_.GetCentralViewport().KeyPressed(KeyboardKeys_Generic, 'i', modifiers); break; + case SDLK_j: context_.GetCentralViewport().KeyPressed(KeyboardKeys_Generic, 'j', modifiers); break; + case SDLK_k: context_.GetCentralViewport().KeyPressed(KeyboardKeys_Generic, 'k', modifiers); break; + case SDLK_l: context_.GetCentralViewport().KeyPressed(KeyboardKeys_Generic, 'l', modifiers); break; + case SDLK_m: context_.GetCentralViewport().KeyPressed(KeyboardKeys_Generic, 'm', modifiers); break; + case SDLK_n: context_.GetCentralViewport().KeyPressed(KeyboardKeys_Generic, 'n', modifiers); break; + case SDLK_o: context_.GetCentralViewport().KeyPressed(KeyboardKeys_Generic, 'o', modifiers); break; + case SDLK_p: context_.GetCentralViewport().KeyPressed(KeyboardKeys_Generic, 'p', modifiers); break; + case SDLK_q: stop = true; break; + case SDLK_r: context_.GetCentralViewport().KeyPressed(KeyboardKeys_Generic, 'r', modifiers); break; + case SDLK_s: context_.GetCentralViewport().KeyPressed(KeyboardKeys_Generic, 's', modifiers); break; + case SDLK_t: context_.GetCentralViewport().KeyPressed(KeyboardKeys_Generic, 't', modifiers); break; + case SDLK_u: context_.GetCentralViewport().KeyPressed(KeyboardKeys_Generic, 'u', modifiers); break; + case SDLK_v: context_.GetCentralViewport().KeyPressed(KeyboardKeys_Generic, 'v', modifiers); break; + case SDLK_w: context_.GetCentralViewport().KeyPressed(KeyboardKeys_Generic, 'w', modifiers); break; + case SDLK_x: context_.GetCentralViewport().KeyPressed(KeyboardKeys_Generic, 'x', modifiers); break; + case SDLK_y: context_.GetCentralViewport().KeyPressed(KeyboardKeys_Generic, 'y', modifiers); break; + case SDLK_z: context_.GetCentralViewport().KeyPressed(KeyboardKeys_Generic, 'z', modifiers); break; + case SDLK_KP_0: context_.GetCentralViewport().KeyPressed(KeyboardKeys_Generic, '0', modifiers); break; + case SDLK_KP_1: context_.GetCentralViewport().KeyPressed(KeyboardKeys_Generic, '1', modifiers); break; + case SDLK_KP_2: context_.GetCentralViewport().KeyPressed(KeyboardKeys_Generic, '2', modifiers); break; + case SDLK_KP_3: context_.GetCentralViewport().KeyPressed(KeyboardKeys_Generic, '3', modifiers); break; + case SDLK_KP_4: context_.GetCentralViewport().KeyPressed(KeyboardKeys_Generic, '4', modifiers); break; + case SDLK_KP_5: context_.GetCentralViewport().KeyPressed(KeyboardKeys_Generic, '5', modifiers); break; + case SDLK_KP_6: context_.GetCentralViewport().KeyPressed(KeyboardKeys_Generic, '6', modifiers); break; + case SDLK_KP_7: context_.GetCentralViewport().KeyPressed(KeyboardKeys_Generic, '7', modifiers); break; + case SDLK_KP_8: context_.GetCentralViewport().KeyPressed(KeyboardKeys_Generic, '8', modifiers); break; + case SDLK_KP_9: context_.GetCentralViewport().KeyPressed(KeyboardKeys_Generic, '9', modifiers); break; - case SDLK_PLUS: - case SDLK_KP_PLUS: - locker.GetViewport().KeyPressed('+', modifiers); break; + case SDLK_PLUS: + case SDLK_KP_PLUS: + context_.GetCentralViewport().KeyPressed(KeyboardKeys_Generic, '+', modifiers); break; + + case SDLK_MINUS: + case SDLK_KP_MINUS: + context_.GetCentralViewport().KeyPressed(KeyboardKeys_Generic, '-', modifiers); break; - case SDLK_MINUS: - case SDLK_KP_MINUS: - locker.GetViewport().KeyPressed('-', modifiers); break; - - default: - break; + case SDLK_RIGHT: + context_.GetCentralViewport().KeyPressed(KeyboardKeys_Right, 0, modifiers); break; + case SDLK_LEFT: + context_.GetCentralViewport().KeyPressed(KeyboardKeys_Left, 0, modifiers); break; + case SDLK_UP: + context_.GetCentralViewport().KeyPressed(KeyboardKeys_Up, 0, modifiers); break; + case SDLK_DOWN: + context_.GetCentralViewport().KeyPressed(KeyboardKeys_Down, 0, modifiers); break; + default: + break; } } } diff -r fe4befe03935 -r d6136a7e914d Applications/Sdl/SdlEngine.h --- a/Applications/Sdl/SdlEngine.h Mon Nov 05 10:04:56 2018 +0100 +++ b/Applications/Sdl/SdlEngine.h Mon Nov 05 10:06:18 2018 +0100 @@ -24,7 +24,7 @@ #if ORTHANC_ENABLE_SDL == 1 #include "SdlCairoSurface.h" -#include "../BasicApplicationContext.h" +#include "../Generic/NativeStoneApplicationContext.h" namespace OrthancStone { @@ -32,12 +32,11 @@ { private: SdlWindow& window_; - BasicApplicationContext& context_; + NativeStoneApplicationContext& context_; SdlCairoSurface surface_; bool viewportChanged_; - void SetSize(BasicApplicationContext::ViewportLocker& locker, - unsigned int width, + void SetSize(unsigned int width, unsigned int height); void RenderFrame(); @@ -47,11 +46,11 @@ public: SdlEngine(SdlWindow& window, - BasicApplicationContext& context); + NativeStoneApplicationContext& context); virtual ~SdlEngine(); - virtual void NotifyChange(const IViewport& viewport) + virtual void OnViewportContentChanged(const IViewport& viewport) { viewportChanged_ = true; } diff -r fe4befe03935 -r d6136a7e914d Applications/Sdl/SdlStoneApplicationRunner.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Applications/Sdl/SdlStoneApplicationRunner.cpp Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,127 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#if ORTHANC_ENABLE_SDL != 1 +#error this file shall be included only with the ORTHANC_ENABLE_SDL set to 1 +#endif + +#include "SdlStoneApplicationRunner.h" +#include + +#include "../../Framework/Toolbox/MessagingToolbox.h" +#include "SdlEngine.h" + +#include +#include +#include +#include +#include "../../Platforms/Generic/OracleWebService.h" + +namespace OrthancStone +{ + void SdlStoneApplicationRunner::Initialize() + { + SdlWindow::GlobalInitialize(); + } + + void SdlStoneApplicationRunner::DeclareCommandLineOptions(boost::program_options::options_description& options) + { + boost::program_options::options_description sdl("SDL options"); + sdl.add_options() + ("width", boost::program_options::value()->default_value(1024), "Initial width of the SDL window") + ("height", boost::program_options::value()->default_value(768), "Initial height of the SDL window") + ("opengl", boost::program_options::value()->default_value(true), "Enable OpenGL in SDL") + ; + + options.add(sdl); + } + + void SdlStoneApplicationRunner::ParseCommandLineOptions(const boost::program_options::variables_map& parameters) + { + if (!parameters.count("width") || + !parameters.count("height") || + !parameters.count("opengl")) + { + LOG(ERROR) << "Parameter \"width\", \"height\" or \"opengl\" is missing"; + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + + int w = parameters["width"].as(); + int h = parameters["height"].as(); + if (w <= 0 || h <= 0) + { + LOG(ERROR) << "Parameters \"width\" and \"height\" must be positive"; + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + + width_ = static_cast(w); + height_ = static_cast(h); + LOG(WARNING) << "Initial display size: " << width_ << "x" << height_; + + enableOpenGl_ = parameters["opengl"].as(); + if (enableOpenGl_) + { + LOG(WARNING) << "OpenGL is enabled, disable it with option \"--opengl=off\" if the application crashes"; + } + else + { + LOG(WARNING) << "OpenGL is disabled, enable it with option \"--opengl=on\" for best performance"; + } + + } + + void SdlStoneApplicationRunner::Run(NativeStoneApplicationContext& context, const std::string& title, int argc, char* argv[]) + { + /************************************************************** + * Run the application inside a SDL window + **************************************************************/ + + LOG(WARNING) << "Starting the application"; + + SdlWindow window(title.c_str(), width_, height_, enableOpenGl_); + SdlEngine sdl(window, context); + + { + NativeStoneApplicationContext::GlobalMutexLocker locker(context); + context.GetCentralViewport().Register(sdl); // (*) + } + + context.Start(); + sdl.Run(); + + LOG(WARNING) << "Stopping the application"; + + // Don't move the "Stop()" command below out of the block, + // otherwise the application might crash, because the + // "SdlEngine" is an observer of the viewport (*) and the + // update thread started by "context.Start()" would call a + // destructed object (the "SdlEngine" is deleted with the + // lexical scope). + context.Stop(); + } + + void SdlStoneApplicationRunner::Finalize() + { + SdlWindow::GlobalFinalize(); + } + + +} diff -r fe4befe03935 -r d6136a7e914d Applications/Sdl/SdlStoneApplicationRunner.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Applications/Sdl/SdlStoneApplicationRunner.h Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,53 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "../Generic/NativeStoneApplicationRunner.h" + +#if ORTHANC_ENABLE_SDL != 1 +#error this file shall be included only with the ORTHANC_ENABLE_SDL set to 1 +#endif + +#include // Necessary to avoid undefined reference to `SDL_main' + +namespace OrthancStone +{ + class SdlStoneApplicationRunner : public NativeStoneApplicationRunner + { + unsigned int width_; + unsigned int height_; + bool enableOpenGl_; + public: + SdlStoneApplicationRunner(MessageBroker& broker, + IStoneApplication& application) + : NativeStoneApplicationRunner(broker, application) + { + } + + virtual void Initialize(); + virtual void DeclareCommandLineOptions(boost::program_options::options_description& options); + virtual void Run(NativeStoneApplicationContext& context, const std::string& title, int argc, char* argv[]); + virtual void ParseCommandLineOptions(const boost::program_options::variables_map& parameters); + virtual void Finalize(); + }; + +} diff -r fe4befe03935 -r d6136a7e914d Applications/StoneApplicationContext.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Applications/StoneApplicationContext.cpp Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,26 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#include "StoneApplicationContext.h" + +namespace OrthancStone +{ +} diff -r fe4befe03935 -r d6136a7e914d Applications/StoneApplicationContext.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Applications/StoneApplicationContext.h Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,56 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "../Framework/Toolbox/IWebService.h" +#include "../Framework/Viewport/WidgetViewport.h" + +#include + +namespace OrthancStone +{ + // a StoneApplicationContext contains the services that a StoneApplication + // uses and that depends on the environment in which the Application executes. + // I.e, the StoneApplicationContext provides a WebService interface such that + // the StoneApplication can perform HTTP requests. In a WASM environment, + // the WebService is provided by the browser while, in a native environment, + // the WebService is provided by the OracleWebService (a C++ Http client) + class StoneApplicationContext : public boost::noncopyable + { + + protected: + IWebService* webService_; + public: + StoneApplicationContext() + : webService_(NULL) + { + } + + IWebService& GetWebService() {return *webService_;} + void SetWebService(IWebService& webService) + { + webService_ = &webService; + } + + virtual ~StoneApplicationContext() {} + }; +} diff -r fe4befe03935 -r d6136a7e914d Applications/Wasm/StartupParametersBuilder.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Applications/Wasm/StartupParametersBuilder.cpp Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,43 @@ +#include "StartupParametersBuilder.h" + +namespace OrthancStone +{ + void StartupParametersBuilder::Clear() { + startupParameters_.clear(); + } + + void StartupParametersBuilder::SetStartupParameter(const char* name, const char* value) { + startupParameters_.push_back(std::make_tuple(name, value)); + } + + void StartupParametersBuilder::GetStartupParameters(boost::program_options::variables_map& parameters, const boost::program_options::options_description& options) { + + const char* argv[startupParameters_.size() + 1]; + int argCounter = 0; + argv[0] = "Toto.exe"; + argCounter++; + + std::string cmdLine = ""; + for (StartupParameters::const_iterator it = startupParameters_.begin(); it != startupParameters_.end(); it++) { + char* arg = new char[128]; + snprintf(arg, 128, "--%s=%s", std::get<0>(*it).c_str(), std::get<1>(*it).c_str()); + argv[argCounter] = arg; + cmdLine = cmdLine + " --" + std::get<0>(*it) + "=" + std::get<1>(*it); + argCounter++; + } + + printf("simulated cmdLine = %s\n", cmdLine.c_str()); + + try + { + boost::program_options::store(boost::program_options::command_line_parser(argCounter, argv). + options(options).run(), parameters); + boost::program_options::notify(parameters); + } + catch (boost::program_options::error& e) + { + printf("Error while parsing the command-line arguments: %s\n", e.what()); + } + + } +} \ No newline at end of file diff -r fe4befe03935 -r d6136a7e914d Applications/Wasm/StartupParametersBuilder.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Applications/Wasm/StartupParametersBuilder.h Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,50 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include +#include + +#if ORTHANC_ENABLE_SDL == 1 +#error this file shall be included only with the ORTHANC_ENABLE_SDL set to 0 +#endif + +namespace OrthancStone +{ + // This class is used to generate boost program options from a dico. + // In a Wasm context, startup options are passed as URI arguments that + // are then passed to this class as a dico. + // This class regenerates a fake command-line and parses it to produce + // the same output as if the app was started at command-line. + class StartupParametersBuilder + { + typedef std::list> StartupParameters; + StartupParameters startupParameters_; + + public: + + void Clear(); + void SetStartupParameter(const char* name, const char* value); + void GetStartupParameters(boost::program_options::variables_map& parameters_, const boost::program_options::options_description& options); + }; + +} diff -r fe4befe03935 -r d6136a7e914d Framework/Layers/CircleMeasureTracker.cpp --- a/Framework/Layers/CircleMeasureTracker.cpp Mon Nov 05 10:04:56 2018 +0100 +++ b/Framework/Layers/CircleMeasureTracker.cpp Mon Nov 05 10:06:18 2018 +0100 @@ -24,8 +24,6 @@ #include "CircleMeasureTracker.h" -#include "../Viewport/CairoFont.h" - #include namespace OrthancStone @@ -37,14 +35,14 @@ uint8_t red, uint8_t green, uint8_t blue, - unsigned int fontSize) : + const Orthanc::Font& font) : statusBar_(statusBar), slice_(slice), x1_(x), y1_(y), x2_(x), y2_(y), - fontSize_(fontSize) + font_(font) { color_[0] = red; color_[1] = green; @@ -73,12 +71,7 @@ cairo_stroke(cr); cairo_restore(cr); - if (fontSize_ != 0) - { - cairo_move_to(cr, x, y); - CairoFont font("sans-serif", CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_NORMAL); - font.Draw(context, FormatRadius(), static_cast(fontSize_) / zoom); - } + context.DrawText(font_, FormatRadius(), x, y, BitmapAnchor_Center); } @@ -97,7 +90,9 @@ return buf; } - void CircleMeasureTracker::MouseMove(double x, + void CircleMeasureTracker::MouseMove(int displayX, + int displayY, + double x, double y) { x2_ = x; diff -r fe4befe03935 -r d6136a7e914d Framework/Layers/CircleMeasureTracker.h --- a/Framework/Layers/CircleMeasureTracker.h Mon Nov 05 10:04:56 2018 +0100 +++ b/Framework/Layers/CircleMeasureTracker.h Mon Nov 05 10:06:18 2018 +0100 @@ -26,19 +26,21 @@ #include "../Viewport/IStatusBar.h" #include "../Toolbox/CoordinateSystem3D.h" +#include + namespace OrthancStone { class CircleMeasureTracker : public IWorldSceneMouseTracker { private: - IStatusBar* statusBar_; - CoordinateSystem3D slice_; - double x1_; - double y1_; - double x2_; - double y2_; - uint8_t color_[3]; - unsigned int fontSize_; + IStatusBar* statusBar_; + CoordinateSystem3D slice_; + double x1_; + double y1_; + double x2_; + double y2_; + uint8_t color_[3]; + const Orthanc::Font& font_; public: CircleMeasureTracker(IStatusBar* statusBar, @@ -48,8 +50,13 @@ uint8_t red, uint8_t green, uint8_t blue, - unsigned int fontSize); + const Orthanc::Font& font); + virtual bool HasRender() const + { + return true; + } + virtual void Render(CairoContext& context, double zoom); @@ -62,7 +69,9 @@ // Possibly create a new landmark "volume" with the circle in subclasses } - virtual void MouseMove(double x, + virtual void MouseMove(int displayX, + int displayY, + double x, double y); }; } diff -r fe4befe03935 -r d6136a7e914d Framework/Layers/ColorFrameRenderer.cpp diff -r fe4befe03935 -r d6136a7e914d Framework/Layers/DicomStructureSetRendererFactory.h --- a/Framework/Layers/DicomStructureSetRendererFactory.h Mon Nov 05 10:04:56 2018 +0100 +++ b/Framework/Layers/DicomStructureSetRendererFactory.h Mon Nov 05 10:06:18 2018 +0100 @@ -37,21 +37,22 @@ { LayerSourceBase::NotifyGeometryReady(); } - + virtual void NotifyGeometryError(const IVolumeLoader& loader) { LayerSourceBase::NotifyGeometryError(); } - + virtual void NotifyContentChange(const IVolumeLoader& loader) { LayerSourceBase::NotifyContentChange(); } - + StructureSetLoader& loader_; public: - DicomStructureSetRendererFactory(StructureSetLoader& loader) : + DicomStructureSetRendererFactory(MessageBroker& broker, StructureSetLoader& loader) : + LayerSourceBase(broker), loader_(loader) { loader_.Register(*this); diff -r fe4befe03935 -r d6136a7e914d Framework/Layers/GrayscaleFrameRenderer.cpp --- a/Framework/Layers/GrayscaleFrameRenderer.cpp Mon Nov 05 10:04:56 2018 +0100 +++ b/Framework/Layers/GrayscaleFrameRenderer.cpp Mon Nov 05 10:06:18 2018 +0100 @@ -27,6 +27,8 @@ { CairoSurface* GrayscaleFrameRenderer::GenerateDisplay(const RenderStyle& style) { + assert(frame_->GetFormat() == Orthanc::PixelFormat_Float32); + std::auto_ptr result; float windowCenter, windowWidth; @@ -82,7 +84,7 @@ v = static_cast(255.0f * (*p - x0) / (x1 - x0)); } - if (style.reverse_) + if (style.reverse_ ^ (photometric_ == Orthanc::PhotometricInterpretation_Monochrome1)) { v = 255 - v; } @@ -119,14 +121,15 @@ FrameRenderer(frameSlice, pixelSpacingX, pixelSpacingY, isFullQuality), frame_(frame), defaultWindowCenter_(converter.GetDefaultWindowCenter()), - defaultWindowWidth_(converter.GetDefaultWindowWidth()) + defaultWindowWidth_(converter.GetDefaultWindowWidth()), + photometric_(converter.GetPhotometricInterpretation()) { if (frame == NULL) { throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); } - converter.ConvertFrame(frame_); + converter.ConvertFrameInplace(frame_); assert(frame_.get() != NULL); if (frame_->GetFormat() != Orthanc::PixelFormat_Float32) diff -r fe4befe03935 -r d6136a7e914d Framework/Layers/GrayscaleFrameRenderer.h --- a/Framework/Layers/GrayscaleFrameRenderer.h Mon Nov 05 10:04:56 2018 +0100 +++ b/Framework/Layers/GrayscaleFrameRenderer.h Mon Nov 05 10:06:18 2018 +0100 @@ -32,6 +32,7 @@ std::auto_ptr frame_; // In Float32 float defaultWindowCenter_; float defaultWindowWidth_; + Orthanc::PhotometricInterpretation photometric_; protected: virtual CairoSurface* GenerateDisplay(const RenderStyle& style); diff -r fe4befe03935 -r d6136a7e914d Framework/Layers/ILayerSource.h --- a/Framework/Layers/ILayerSource.h Mon Nov 05 10:04:56 2018 +0100 +++ b/Framework/Layers/ILayerSource.h Mon Nov 05 10:06:18 2018 +0100 @@ -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 . **/ @@ -23,48 +23,77 @@ #include "ILayerRenderer.h" #include "../Toolbox/Slice.h" +#include "../../Framework/Messages/IObservable.h" +#include "../../Framework/Messages/IMessage.h" +#include "Core/Images/Image.h" +#include namespace OrthancStone { - class ILayerSource : public boost::noncopyable + class ILayerSource : public IObservable { public: - class IObserver : public boost::noncopyable + + typedef OriginMessage GeometryReadyMessage; + typedef OriginMessage GeometryErrorMessage; + typedef OriginMessage ContentChangedMessage; + + struct SliceChangedMessage : public OriginMessage { - public: - virtual ~IObserver() + const Slice& slice_; + SliceChangedMessage(ILayerSource& origin, const Slice& slice) + : OriginMessage(origin), + slice_(slice) { } + }; - // Triggered as soon as the source has enough information to - // answer to "GetExtent()" - virtual void NotifyGeometryReady(const ILayerSource& source) = 0; - - virtual void NotifyGeometryError(const ILayerSource& source) = 0; - - // Triggered if the content of several slices in the source - // volume has changed - virtual void NotifyContentChange(const ILayerSource& source) = 0; + struct LayerReadyMessage : public OriginMessage + { + std::auto_ptr& renderer_; + const CoordinateSystem3D& slice_; + bool isError_; - // Triggered if the content of some individual slice in the - // source volume has changed - virtual void NotifySliceChange(const ILayerSource& source, - const Slice& slice) = 0; - - // The layer must be deleted by the observer that releases the - // std::auto_ptr - virtual void NotifyLayerReady(std::auto_ptr& layer, - const ILayerSource& source, - const CoordinateSystem3D& slice, - bool isError) = 0; // TODO Shouldn't this be separate as NotifyLayerError? + LayerReadyMessage(ILayerSource& origin, + std::auto_ptr& layer, + const CoordinateSystem3D& slice, + bool isError // TODO Shouldn't this be separate as NotifyLayerError? + ) + : OriginMessage(origin), + renderer_(layer), + slice_(slice), + isError_(isError) + { + } + }; + + struct ImageReadyMessage : public OriginMessage + { + boost::shared_ptr image_; + SliceImageQuality imageQuality_; + const Slice& slice_; + + ImageReadyMessage(ILayerSource& origin, + boost::shared_ptr image, + SliceImageQuality imageQuality, + const Slice& slice + ) + : OriginMessage(origin), + image_(image), + imageQuality_(imageQuality), + slice_(slice) + { + } }; + ILayerSource(MessageBroker& broker) + : IObservable(broker) + {} + virtual ~ILayerSource() { } - virtual void Register(IObserver& observer) = 0; - virtual bool GetExtent(std::vector& points, const CoordinateSystem3D& viewportSlice) = 0; diff -r fe4befe03935 -r d6136a7e914d Framework/Layers/LayerSourceBase.cpp --- a/Framework/Layers/LayerSourceBase.cpp Mon Nov 05 10:04:56 2018 +0100 +++ b/Framework/Layers/LayerSourceBase.cpp Mon Nov 05 10:06:18 2018 +0100 @@ -25,63 +25,39 @@ namespace OrthancStone { - namespace - { - class LayerReadyFunctor : public boost::noncopyable - { - private: - std::auto_ptr layer_; - const CoordinateSystem3D& slice_; - bool isError_; - - public: - LayerReadyFunctor(ILayerRenderer* layer, - const CoordinateSystem3D& slice, - bool isError) : - layer_(layer), - slice_(slice), - isError_(isError) - { - } - - void operator() (ILayerSource::IObserver& observer, - const ILayerSource& source) - { - observer.NotifyLayerReady(layer_, source, slice_, isError_); - } - }; - } - void LayerSourceBase::NotifyGeometryReady() { - observers_.Apply(*this, &IObserver::NotifyGeometryReady); + EmitMessage(ILayerSource::GeometryReadyMessage(*this)); } void LayerSourceBase::NotifyGeometryError() { - observers_.Apply(*this, &IObserver::NotifyGeometryError); - } + EmitMessage(ILayerSource::GeometryErrorMessage(*this)); + } void LayerSourceBase::NotifyContentChange() { - observers_.Apply(*this, &IObserver::NotifyContentChange); + EmitMessage(ILayerSource::ContentChangedMessage(*this)); } void LayerSourceBase::NotifySliceChange(const Slice& slice) { - observers_.Apply(*this, &IObserver::NotifySliceChange, slice); + EmitMessage(ILayerSource::SliceChangedMessage(*this, slice)); } void LayerSourceBase::NotifyLayerReady(ILayerRenderer* layer, const CoordinateSystem3D& slice, bool isError) { - LayerReadyFunctor functor(layer, slice, isError); - observers_.Notify(*this, functor); + std::auto_ptr renderer(layer); + EmitMessage(ILayerSource::LayerReadyMessage(*this, renderer, slice, isError)); } - void LayerSourceBase::Register(IObserver& observer) + void LayerSourceBase::NotifyImageReady(boost::shared_ptr image, + SliceImageQuality imageQuality, + const Slice& slice) { - observers_.Register(observer); + EmitMessage(ILayerSource::ImageReadyMessage(*this, image, imageQuality, slice)); } + } diff -r fe4befe03935 -r d6136a7e914d Framework/Layers/LayerSourceBase.h --- a/Framework/Layers/LayerSourceBase.h Mon Nov 05 10:04:56 2018 +0100 +++ b/Framework/Layers/LayerSourceBase.h Mon Nov 05 10:06:18 2018 +0100 @@ -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 . **/ @@ -26,13 +26,10 @@ namespace OrthancStone { + class SmartLoader; + class LayerSourceBase : public ILayerSource { - private: - typedef ObserversRegistry Observers; - - Observers observers_; - protected: void NotifyGeometryReady(); @@ -46,7 +43,15 @@ const CoordinateSystem3D& slice, bool isError); - public: - virtual void Register(IObserver& observer); + void NotifyImageReady(boost::shared_ptr image, + SliceImageQuality imageQuality, + const Slice& slice); + + LayerSourceBase(MessageBroker& broker) + : ILayerSource(broker) + { + } + + friend class SmartLoader; }; } diff -r fe4befe03935 -r d6136a7e914d Framework/Layers/LineMeasureTracker.cpp --- a/Framework/Layers/LineMeasureTracker.cpp Mon Nov 05 10:04:56 2018 +0100 +++ b/Framework/Layers/LineMeasureTracker.cpp Mon Nov 05 10:06:18 2018 +0100 @@ -21,8 +21,6 @@ #include "LineMeasureTracker.h" -#include "../Viewport/CairoFont.h" - #include namespace OrthancStone @@ -34,14 +32,14 @@ uint8_t red, uint8_t green, uint8_t blue, - unsigned int fontSize) : + const Orthanc::Font& font) : statusBar_(statusBar), slice_(slice), x1_(x), y1_(y), x2_(x), y2_(y), - fontSize_(fontSize) + font_(font) { color_[0] = red; color_[1] = green; @@ -60,11 +58,13 @@ cairo_line_to(cr, x2_, y2_); cairo_stroke(cr); - if (fontSize_ != 0) + if (y2_ - y1_ < 0) { - cairo_move_to(cr, x2_, y2_ - static_cast(fontSize_) / zoom); - CairoFont font("sans-serif", CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_NORMAL); - font.Draw(context, FormatLength(), static_cast(fontSize_) / zoom); + context.DrawText(font_, FormatLength(), x2_, y2_ - 5, BitmapAnchor_BottomCenter); + } + else + { + context.DrawText(font_, FormatLength(), x2_, y2_ + 5, BitmapAnchor_TopCenter); } } @@ -84,7 +84,9 @@ return buf; } - void LineMeasureTracker::MouseMove(double x, + void LineMeasureTracker::MouseMove(int displayX, + int displayY, + double x, double y) { x2_ = x; diff -r fe4befe03935 -r d6136a7e914d Framework/Layers/LineMeasureTracker.h --- a/Framework/Layers/LineMeasureTracker.h Mon Nov 05 10:04:56 2018 +0100 +++ b/Framework/Layers/LineMeasureTracker.h Mon Nov 05 10:06:18 2018 +0100 @@ -31,14 +31,15 @@ class LineMeasureTracker : public IWorldSceneMouseTracker { private: - IStatusBar* statusBar_; - CoordinateSystem3D slice_; - double x1_; - double y1_; - double x2_; - double y2_; - uint8_t color_[3]; - unsigned int fontSize_; + IStatusBar* statusBar_; + CoordinateSystem3D slice_; + double x1_; + double y1_; + double x2_; + double y2_; + uint8_t color_[3]; + unsigned int fontSize_; + const Orthanc::Font& font_; public: LineMeasureTracker(IStatusBar* statusBar, @@ -48,8 +49,13 @@ uint8_t red, uint8_t green, uint8_t blue, - unsigned int fontSize); - + const Orthanc::Font& font); + + virtual bool HasRender() const + { + return true; + } + virtual void Render(CairoContext& context, double zoom); @@ -62,7 +68,9 @@ // Possibly create a new landmark "volume" with the line in subclasses } - virtual void MouseMove(double x, + virtual void MouseMove(int displayX, + int displayY, + double x, double y); }; } diff -r fe4befe03935 -r d6136a7e914d Framework/Layers/OrthancFrameLayerSource.cpp --- a/Framework/Layers/OrthancFrameLayerSource.cpp Mon Nov 05 10:04:56 2018 +0100 +++ b/Framework/Layers/OrthancFrameLayerSource.cpp Mon Nov 05 10:06:18 2018 +0100 @@ -31,9 +31,10 @@ namespace OrthancStone { - void OrthancFrameLayerSource::NotifyGeometryReady(const OrthancSlicesLoader& loader) + + void OrthancFrameLayerSource::OnSliceGeometryReady(const OrthancSlicesLoader::SliceGeometryReadyMessage& message) { - if (loader.GetSliceCount() > 0) + if (message.origin_.GetSliceCount() > 0) { LayerSourceBase::NotifyGeometryReady(); } @@ -43,35 +44,41 @@ } } - void OrthancFrameLayerSource::NotifyGeometryError(const OrthancSlicesLoader& loader) + void OrthancFrameLayerSource::OnSliceGeometryError(const OrthancSlicesLoader::SliceGeometryErrorMessage& message) { LayerSourceBase::NotifyGeometryError(); } - void OrthancFrameLayerSource::NotifySliceImageReady(const OrthancSlicesLoader& loader, - unsigned int sliceIndex, - const Slice& slice, - std::auto_ptr& image, - SliceImageQuality quality) + void OrthancFrameLayerSource::OnSliceImageReady(const OrthancSlicesLoader::SliceImageReadyMessage& message) { - bool isFull = (quality == SliceImageQuality_Full); - LayerSourceBase::NotifyLayerReady(FrameRenderer::CreateRenderer(image.release(), slice, isFull), - slice.GetGeometry(), false); + // first notify that the image is ready (targeted to, i.e: an image cache) + LayerSourceBase::NotifyImageReady(message.image_, message.effectiveQuality_, message.slice_); + + // then notify that the layer is ready for render + bool isFull = (message.effectiveQuality_ == SliceImageQuality_FullPng || message.effectiveQuality_ == SliceImageQuality_FullPam); + std::auto_ptr accessor(new Orthanc::ImageAccessor()); + message.image_->GetReadOnlyAccessor(*accessor); + + LayerSourceBase::NotifyLayerReady(FrameRenderer::CreateRenderer(accessor.release(), message.slice_, isFull), + message.slice_.GetGeometry(), false); + } - void OrthancFrameLayerSource::NotifySliceImageError(const OrthancSlicesLoader& loader, - unsigned int sliceIndex, - const Slice& slice, - SliceImageQuality quality) + void OrthancFrameLayerSource::OnSliceImageError(const OrthancSlicesLoader::SliceImageErrorMessage& message) { - LayerSourceBase::NotifyLayerReady(NULL, slice.GetGeometry(), true); + LayerSourceBase::NotifyLayerReady(NULL, message.slice_.GetGeometry(), true); } - - OrthancFrameLayerSource::OrthancFrameLayerSource(IWebService& orthanc) : - loader_(*this, orthanc), - quality_(SliceImageQuality_Full) + OrthancFrameLayerSource::OrthancFrameLayerSource(MessageBroker& broker, OrthancApiClient& orthanc) : + LayerSourceBase(broker), + IObserver(broker), + loader_(broker, orthanc), + quality_(SliceImageQuality_FullPng) { + loader_.RegisterObserverCallback(new Callable(*this, &OrthancFrameLayerSource::OnSliceGeometryReady)); + loader_.RegisterObserverCallback(new Callable(*this, &OrthancFrameLayerSource::OnSliceGeometryError)); + loader_.RegisterObserverCallback(new Callable(*this, &OrthancFrameLayerSource::OnSliceImageReady)); + loader_.RegisterObserverCallback(new Callable(*this, &OrthancFrameLayerSource::OnSliceImageError)); } @@ -98,6 +105,7 @@ const CoordinateSystem3D& viewportSlice) { size_t index; + if (loader_.IsGeometryReady() && loader_.LookupSlice(index, viewportSlice)) { @@ -115,17 +123,10 @@ { size_t index; - if (loader_.IsGeometryReady()) + if (loader_.IsGeometryReady() && + loader_.LookupSlice(index, viewportSlice)) { - if (loader_.LookupSlice(index, viewportSlice)) - { - loader_.ScheduleLoadSliceImage(index, quality_); - } - else - { - Slice slice; - LayerSourceBase::NotifyLayerReady(NULL, slice.GetGeometry(), true); - } + loader_.ScheduleLoadSliceImage(index, quality_); } } } diff -r fe4befe03935 -r d6136a7e914d Framework/Layers/OrthancFrameLayerSource.h --- a/Framework/Layers/OrthancFrameLayerSource.h Mon Nov 05 10:04:56 2018 +0100 +++ b/Framework/Layers/OrthancFrameLayerSource.h Mon Nov 05 10:06:18 2018 +0100 @@ -24,34 +24,24 @@ #include "LayerSourceBase.h" #include "../Toolbox/IWebService.h" #include "../Toolbox/OrthancSlicesLoader.h" +#include "../Toolbox/OrthancApiClient.h" namespace OrthancStone { + // this class is in charge of loading a Frame. + // once it's been loaded (first the geometry and then the image), + // messages are sent to observers so they can use it class OrthancFrameLayerSource : public LayerSourceBase, - private OrthancSlicesLoader::ICallback + public IObserver + //private OrthancSlicesLoader::ISliceLoaderObserver { private: OrthancSlicesLoader loader_; SliceImageQuality quality_; - virtual void NotifyGeometryReady(const OrthancSlicesLoader& loader); - - virtual void NotifyGeometryError(const OrthancSlicesLoader& loader); - - virtual void NotifySliceImageReady(const OrthancSlicesLoader& loader, - unsigned int sliceIndex, - const Slice& slice, - std::auto_ptr& image, - SliceImageQuality quality); - - virtual void NotifySliceImageError(const OrthancSlicesLoader& loader, - unsigned int sliceIndex, - const Slice& slice, - SliceImageQuality quality); - public: - OrthancFrameLayerSource(IWebService& orthanc); + OrthancFrameLayerSource(MessageBroker& broker, OrthancApiClient& orthanc); void LoadSeries(const std::string& seriesId); @@ -65,6 +55,11 @@ quality_ = quality; } + SliceImageQuality GetImageQuality() const + { + return quality_; + } + size_t GetSliceCount() const { return loader_.GetSliceCount(); @@ -79,5 +74,11 @@ const CoordinateSystem3D& viewportSlice); virtual void ScheduleLayerCreation(const CoordinateSystem3D& viewportSlice); + +protected: + void OnSliceGeometryReady(const OrthancSlicesLoader::SliceGeometryReadyMessage& message); + void OnSliceGeometryError(const OrthancSlicesLoader::SliceGeometryErrorMessage& message); + void OnSliceImageReady(const OrthancSlicesLoader::SliceImageReadyMessage& message); + void OnSliceImageError(const OrthancSlicesLoader::SliceImageErrorMessage& message); }; } diff -r fe4befe03935 -r d6136a7e914d Framework/Messages/ICallable.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Messages/ICallable.h Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,92 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "IMessage.h" + +#include + +namespace OrthancStone { + + class IObserver; + + // 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 + class ICallable : public boost::noncopyable + { + public: + virtual ~ICallable() + { + } + + virtual void Apply(const IMessage& message) = 0; + + virtual MessageType GetMessageType() const = 0; + virtual IObserver* GetObserver() const = 0; + }; + + template + class MessageHandler: public ICallable + { + }; + + + template + class Callable : public MessageHandler + { + private: + typedef void (TObserver::* MemberFunction) (const TMessage&); + + TObserver& observer_; + MemberFunction function_; + + public: + Callable(TObserver& observer, + MemberFunction function) : + observer_(observer), + function_(function) + { + } + + void ApplyInternal(const TMessage& message) + { + (observer_.*function_) (message); + } + + virtual void Apply(const IMessage& message) + { + ApplyInternal(dynamic_cast(message)); + } + + virtual MessageType GetMessageType() const + { + return static_cast(TMessage::Type); + } + + virtual IObserver* GetObserver() const + { + return &observer_; + } + }; +} diff -r fe4befe03935 -r d6136a7e914d Framework/Messages/IMessage.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Messages/IMessage.h Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,87 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "MessageType.h" + +#include + +namespace OrthancStone { + + + // base message that are exchanged between IObservable and IObserver + struct IMessage : public boost::noncopyable + { + int messageType_; + protected: + IMessage(const int& messageType) + : messageType_(messageType) + {} + public: + virtual ~IMessage() {} + + virtual int GetType() const {return messageType_;} + }; + + + // base class to derive from to implement your own messages + // it handles the message type for you + template + struct BaseMessage : public IMessage + { + enum + { + Type = type + }; + + BaseMessage() + : IMessage(static_cast(Type)) + {} + }; + + // simple message implementation when no payload is needed + // sample usage: + // typedef NoPayloadMessage GeometryReadyMessage; + template + struct NoPayloadMessage : public BaseMessage + { + NoPayloadMessage() + : BaseMessage() + {} + + }; + + // simple message implementation when no payload is needed but the origin is required + // sample usage: + // typedef OriginMessage SliceGeometryErrorMessage; + template + struct OriginMessage : public BaseMessage + { + TOrigin& origin_; + OriginMessage(TOrigin& origin) + : BaseMessage(), + origin_(origin) + {} + + }; + +} diff -r fe4befe03935 -r d6136a7e914d Framework/Messages/IObservable.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Messages/IObservable.h Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,110 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include +#include +#include +#include +#include + + +#include "MessageBroker.h" +#include "MessageType.h" +#include "ICallable.h" +#include "IObserver.h" +#include "MessageForwarder.h" + +namespace OrthancStone { + + + class IObservable : public boost::noncopyable + { + protected: + MessageBroker& broker_; + + typedef std::map > Callables; + Callables callables_; + + typedef std::set Forwarders; + Forwarders forwarders_; + + public: + + IObservable(MessageBroker& broker) + : broker_(broker) + { + } + virtual ~IObservable() + { + // delete all callables (this will also unregister them from the broker) + for (Callables::const_iterator it = callables_.begin(); + it != callables_.end(); ++it) + { + for (std::set::const_iterator + it2 = it->second.begin(); it2 != it->second.end(); ++it2) + { + 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(*fw)); + } + } + + void RegisterObserverCallback(ICallable* callable) + { + MessageType messageType = callable->GetMessageType(); + + callables_[messageType].insert(callable); + } + + void EmitMessage(const IMessage& message) + { + Callables::const_iterator found = callables_.find(message.GetType()); + + if (found != callables_.end()) + { + for (std::set::const_iterator + it = found->second.begin(); it != found->second.end(); ++it) + { + if (broker_.IsActive((*it)->GetObserver())) + { + (*it)->Apply(message); + } + } + } + } + + void RegisterForwarder(IMessageForwarder* forwarder) + { + forwarders_.insert(forwarder); + } + + }; + +} diff -r fe4befe03935 -r d6136a7e914d Framework/Messages/IObserver.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Messages/IObserver.h Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,51 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "MessageBroker.h" +#include "IMessage.h" +#include +#include + +namespace OrthancStone { + + class IObservable; + + class IObserver : public boost::noncopyable + { + protected: + MessageBroker& broker_; + + public: + IObserver(MessageBroker& broker) + : broker_(broker) + { + broker_.Register(*this); + } + + virtual ~IObserver() + { + broker_.Unregister(*this); + } + }; + +} diff -r fe4befe03935 -r d6136a7e914d Framework/Messages/MessageBroker.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Messages/MessageBroker.h Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,60 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "boost/noncopyable.hpp" +#include + +namespace OrthancStone +{ + class IObserver; + class IObservable; + + /* + * This is a central message broker. It keeps track of all observers and knows + * when an observer is deleted. + * This way, it can prevent an observable to send a message to a deleted observer. + */ + class MessageBroker : public boost::noncopyable + { + + std::set activeObservers_; // the list of observers that are currently alive (that have not been deleted) + + public: + + void Register(IObserver& observer) + { + activeObservers_.insert(&observer); + } + + void Unregister(IObserver& observer) + { + activeObservers_.erase(&observer); + } + + bool IsActive(IObserver* observer) + { + return activeObservers_.find(observer) != activeObservers_.end(); + } + }; + +} diff -r fe4befe03935 -r d6136a7e914d Framework/Messages/MessageForwarder.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Messages/MessageForwarder.cpp Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,38 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#include "MessageForwarder.h" + +#include "IObservable.h" + +namespace OrthancStone +{ + + void IMessageForwarder::ForwardMessageInternal(const IMessage& message) + { + emitter_.EmitMessage(message); + } + + void IMessageForwarder::RegisterForwarderInEmitter() + { + emitter_.RegisterForwarder(this); + } +} diff -r fe4befe03935 -r d6136a7e914d Framework/Messages/MessageForwarder.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Messages/MessageForwarder.h Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,87 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "ICallable.h" +#include "IObserver.h" + +#include + +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(broker, *this) // where this is B + * + * in C: + * B.RegisterObserverCallback(new Callable(*this, &B::MyCallback)) // where this is C + */ + template + class MessageForwarder : public IMessageForwarder, public Callable, TMessage> + { + public: + MessageForwarder(MessageBroker& broker, + IObservable& emitter // the object that will emit the messages to forward + ) + : IMessageForwarder(broker, emitter), + Callable, TMessage>(*this, &MessageForwarder::ForwardMessage) + { + RegisterForwarderInEmitter(); + } + +protected: + void ForwardMessage(const TMessage& message) + { + ForwardMessageInternal(message); + } + + }; +} diff -r fe4befe03935 -r d6136a7e914d Framework/Messages/MessageType.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Messages/MessageType.h Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,59 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + +#pragma once + +namespace OrthancStone { + + enum MessageType + { + MessageType_Widget_GeometryChanged, + MessageType_Widget_ContentChanged, + + MessageType_LayerSource_GeometryReady, // instance tags have been loaded + MessageType_LayerSource_GeometryError, + MessageType_LayerSource_ContentChanged, + MessageType_LayerSource_SliceChanged, + MessageType_LayerSource_ImageReady, // instance pixels data have been loaded + MessageType_LayerSource_LayerReady, // layer is ready to be rendered + + MessageType_SliceLoader_GeometryReady, + MessageType_SliceLoader_GeometryError, + MessageType_SliceLoader_ImageReady, + MessageType_SliceLoader_ImageError, + + MessageType_HttpRequestSuccess, + MessageType_HttpRequestError, + + MessageType_OrthancApi_InternalGetJsonResponseReady, + MessageType_OrthancApi_InternalGetJsonResponseError, + + MessageType_OrthancApi_GenericGetJson_Ready, + MessageType_OrthancApi_GenericGetBinary_Ready, + MessageType_OrthancApi_GenericHttpError_Ready, + MessageType_OrthancApi_GenericEmptyResponse_Ready, + + // used in unit tests only + MessageType_Test1, + MessageType_Test2, + + MessageType_CustomMessage // Custom messages ids ust be greater than this (this one must remain in last position) + }; +} diff -r fe4befe03935 -r d6136a7e914d Framework/Messages/Promise.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Messages/Promise.h Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,88 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "MessageBroker.h" +#include "ICallable.h" +#include "IMessage.h" + +#include +#include + +namespace OrthancStone { + + class Promise : public boost::noncopyable + { + protected: + MessageBroker& broker_; + + std::auto_ptr successCallable_; + std::auto_ptr failureCallable_; + + public: + Promise(MessageBroker& broker) + : broker_(broker) + { + } + + void Success(const IMessage& message) + { + // check the target is still alive in the broker + if (broker_.IsActive(successCallable_->GetObserver())) + { + successCallable_->Apply(message); + } + } + + void Failure(const IMessage& message) + { + // check the target is still alive in the broker + if (broker_.IsActive(failureCallable_->GetObserver())) + { + failureCallable_->Apply(message); + } + } + + Promise& Then(ICallable* successCallable) + { + if (successCallable_.get() != NULL) + { + // TODO: throw throw new "Promise may only have a single success target" + } + successCallable_.reset(successCallable); + return *this; + } + + Promise& Else(ICallable* failureCallable) + { + if (failureCallable_.get() != NULL) + { + // TODO: throw throw new "Promise may only have a single failure target" + } + failureCallable_.reset(failureCallable); + return *this; + } + + }; + + +} diff -r fe4befe03935 -r d6136a7e914d Framework/SmartLoader.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/SmartLoader.cpp Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,272 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#include "SmartLoader.h" +#include "Layers/OrthancFrameLayerSource.h" +#include "Messages/MessageForwarder.h" +#include "Core/Images/Image.h" +#include "Framework/Widgets/LayerWidget.h" +#include "Framework/StoneException.h" +#include "Framework/Layers/FrameRenderer.h" +#include "Core/Logging.h" + +namespace OrthancStone +{ + enum CachedSliceStatus + { + CachedSliceStatus_ScheduledToLoad, + CachedSliceStatus_GeometryLoaded, + CachedSliceStatus_ImageLoaded + }; + + class SmartLoader::CachedSlice : public LayerSourceBase + { + public: + unsigned int sliceIndex_; + std::auto_ptr slice_; + boost::shared_ptr image_; + SliceImageQuality effectiveQuality_; + CachedSliceStatus status_; + + public: + CachedSlice(MessageBroker& broker) : + LayerSourceBase(broker) + { + } + + virtual ~CachedSlice() + { + } + + virtual bool GetExtent(std::vector& points, + const CoordinateSystem3D& viewportSlice) + { + // TODO: viewportSlice is not used !!!! + slice_->GetExtent(points); + return true; + } + + virtual void ScheduleLayerCreation(const CoordinateSystem3D& viewportSlice) + { + // TODO: viewportSlice is not used !!!! + + // it has already been loaded -> trigger the "layer ready" message immediately otherwise, do nothing now. The LayerReady will be triggered + // once the LayerSource is ready + if (status_ == CachedSliceStatus_ImageLoaded) + { + LOG(WARNING) << "ScheduleLayerCreation for CachedSlice (image is loaded): " << slice_->GetOrthancInstanceId(); + bool isFull = (effectiveQuality_ == SliceImageQuality_FullPng || effectiveQuality_ == SliceImageQuality_FullPam); + std::auto_ptr accessor(new Orthanc::ImageAccessor()); + image_->GetReadOnlyAccessor(*accessor); + LayerSourceBase::NotifyLayerReady(FrameRenderer::CreateRenderer(accessor.release(), *slice_, isFull), + slice_->GetGeometry(), false); + } + else + { + LOG(WARNING) << "ScheduleLayerCreation for CachedSlice (image is not loaded yet): " << slice_->GetOrthancInstanceId(); + } + } + + CachedSlice* Clone() const + { + CachedSlice* output = new CachedSlice(broker_); + output->sliceIndex_ = sliceIndex_; + output->slice_.reset(slice_->Clone()); + output->image_ = image_; + output->effectiveQuality_ = effectiveQuality_; + output->status_ = status_; + + return output; + } + + }; + + + SmartLoader::SmartLoader(MessageBroker& broker, + OrthancApiClient& orthancApiClient) : + IObservable(broker), + IObserver(broker), + imageQuality_(SliceImageQuality_FullPam), + orthancApiClient_(orthancApiClient) + { + } + + void SmartLoader::SetFrameInWidget(LayerWidget& layerWidget, + size_t layerIndex, + const std::string& instanceId, + unsigned int frame) + { + // TODO: check if this frame has already been loaded or is already being loaded. + // - if already loaded: create a "clone" that will emit the GeometryReady/ImageReady messages "immediately" + // (it can not be immediate because Observers needs to register first and this is done after this method returns) + // - if currently loading, we need to return an object that will observe the existing LayerSource and forward + // the messages to its observables + // in both cases, we must be carefull about objects lifecycle !!! + + std::auto_ptr layerSource; + std::string sliceKeyId = instanceId + ":" + boost::lexical_cast(frame); + SmartLoader::CachedSlice* cachedSlice = NULL; + + if (cachedSlices_.find(sliceKeyId) != cachedSlices_.end()) // && cachedSlices_[sliceKeyId]->status_ == CachedSliceStatus_Loaded) + { + layerSource.reset(cachedSlices_[sliceKeyId]->Clone()); + cachedSlice = dynamic_cast(layerSource.get()); + } + else + { + layerSource.reset(new OrthancFrameLayerSource(IObserver::broker_, orthancApiClient_)); + dynamic_cast(layerSource.get())->SetImageQuality(imageQuality_); + layerSource->RegisterObserverCallback(new Callable(*this, &SmartLoader::OnLayerGeometryReady)); + layerSource->RegisterObserverCallback(new Callable(*this, &SmartLoader::OnImageReady)); + layerSource->RegisterObserverCallback(new Callable(*this, &SmartLoader::OnLayerReady)); + dynamic_cast(layerSource.get())->LoadFrame(instanceId, frame); + } + + // make sure that the widget registers the events before we trigger them + if (layerWidget.GetLayerCount() == layerIndex) + { + layerWidget.AddLayer(layerSource.release()); + } + else if (layerWidget.GetLayerCount() > layerIndex) + { + layerWidget.ReplaceLayer(layerIndex, layerSource.release()); + } + else + { + throw StoneException(ErrorCode_CanOnlyAddOneLayerAtATime); + } + + if (cachedSlice != NULL) + { + cachedSlice->NotifyGeometryReady(); + } + + } + + void SmartLoader::PreloadSlice(const std::string instanceId, + unsigned int frame) + { + // TODO: reactivate -> need to be able to ScheduleLayerLoading in ILayerSource without calling ScheduleLayerCreation + return; + // TODO: check if it is already in the cache + + + + // create the slice in the cache with "empty" data + boost::shared_ptr cachedSlice(new CachedSlice(IObserver::broker_)); + cachedSlice->slice_.reset(new Slice(instanceId, frame)); + cachedSlice->status_ = CachedSliceStatus_ScheduledToLoad; + std::string sliceKeyId = instanceId + ":" + boost::lexical_cast(frame); + + LOG(WARNING) << "Will preload: " << sliceKeyId; + + cachedSlices_[sliceKeyId] = boost::shared_ptr(cachedSlice); + + std::auto_ptr layerSource(new OrthancFrameLayerSource(IObserver::broker_, orthancApiClient_)); + + dynamic_cast(layerSource.get())->SetImageQuality(imageQuality_); + layerSource->RegisterObserverCallback(new Callable(*this, &SmartLoader::OnLayerGeometryReady)); + layerSource->RegisterObserverCallback(new Callable(*this, &SmartLoader::OnImageReady)); + layerSource->RegisterObserverCallback(new Callable(*this, &SmartLoader::OnLayerReady)); + dynamic_cast(layerSource.get())->LoadFrame(instanceId, frame); + + // keep a ref to the LayerSource until the slice is fully loaded and saved to cache + preloadingInstances_[sliceKeyId] = boost::shared_ptr(layerSource.release()); + } + + +// void PreloadStudy(const std::string studyId) +// { +// /* TODO */ +// } + +// void PreloadSeries(const std::string seriesId) +// { +// /* TODO */ +// } + + + void SmartLoader::OnLayerGeometryReady(const ILayerSource::GeometryReadyMessage& message) + { + OrthancFrameLayerSource& source = dynamic_cast(message.origin_); + + // save/replace the slice in cache + const Slice& slice = source.GetSlice(0); // TODO handle GetSliceCount() + std::string sliceKeyId = (slice.GetOrthancInstanceId() + ":" + + boost::lexical_cast(slice.GetFrame())); + + LOG(WARNING) << "Geometry ready: " << sliceKeyId; + + boost::shared_ptr cachedSlice(new CachedSlice(IObserver::broker_)); + cachedSlice->slice_.reset(slice.Clone()); + cachedSlice->effectiveQuality_ = source.GetImageQuality(); + cachedSlice->status_ = CachedSliceStatus_GeometryLoaded; + + cachedSlices_[sliceKeyId] = boost::shared_ptr(cachedSlice); + + // re-emit original Layer message to observers + EmitMessage(message); + } + + + void SmartLoader::OnImageReady(const ILayerSource::ImageReadyMessage& message) + { + OrthancFrameLayerSource& source = dynamic_cast(message.origin_); + + // save/replace the slice in cache + const Slice& slice = source.GetSlice(0); // TODO handle GetSliceCount() ? + std::string sliceKeyId = (slice.GetOrthancInstanceId() + ":" + + boost::lexical_cast(slice.GetFrame())); + + LOG(WARNING) << "Image ready: " << sliceKeyId; + + boost::shared_ptr cachedSlice(new CachedSlice(IObserver::broker_)); + cachedSlice->image_ = message.image_; + cachedSlice->effectiveQuality_ = message.imageQuality_; + cachedSlice->slice_.reset(message.slice_.Clone()); + cachedSlice->status_ = CachedSliceStatus_ImageLoaded; + + cachedSlices_[sliceKeyId] = cachedSlice; + + // re-emit original Layer message to observers + EmitMessage(message); + } + + + void SmartLoader::OnLayerReady(const ILayerSource::LayerReadyMessage& message) + { + OrthancFrameLayerSource& source = dynamic_cast(message.origin_); + const Slice& slice = source.GetSlice(0); // TODO handle GetSliceCount() ? + std::string sliceKeyId = (slice.GetOrthancInstanceId() + ":" + + boost::lexical_cast(slice.GetFrame())); + + LOG(WARNING) << "Layer ready: " << sliceKeyId; + + // remove the slice from the preloading slices now that it has been fully loaded and it is referenced in the cache + if (preloadingInstances_.find(sliceKeyId) != preloadingInstances_.end()) + { + preloadingInstances_.erase(sliceKeyId); + } + + // re-emit original Layer message to observers + EmitMessage(message); + } +} diff -r fe4befe03935 -r d6136a7e914d Framework/SmartLoader.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/SmartLoader.h Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,67 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#pragma once +#include + +#include "Layers/ILayerSource.h" +#include "Messages/IObservable.h" +#include "Toolbox/OrthancApiClient.h" + +namespace OrthancStone +{ + class LayerWidget; + + class SmartLoader : public IObservable, public IObserver + { + class CachedSlice; + + protected: + typedef std::map > CachedSlices; + CachedSlices cachedSlices_; + + typedef std::map > PreloadingInstances; + PreloadingInstances preloadingInstances_; + + SliceImageQuality imageQuality_; + OrthancApiClient& orthancApiClient_; + + public: + SmartLoader(MessageBroker& broker, OrthancApiClient& orthancApiClient); // TODO: add maxPreloadStorageSizeInBytes + +// void PreloadStudy(const std::string studyId); +// void PreloadSeries(const std::string seriesId); + void PreloadSlice(const std::string instanceId, unsigned int frame); + + void SetImageQuality(SliceImageQuality imageQuality) { imageQuality_ = imageQuality; } + + void SetFrameInWidget(LayerWidget& layerWidget, size_t layerIndex, const std::string& instanceId, unsigned int frame); + + void GetFirstInstanceIdForSeries(std::string& output, const std::string& seriesId); + + private: + void OnLayerGeometryReady(const ILayerSource::GeometryReadyMessage& message); + void OnImageReady(const ILayerSource::ImageReadyMessage& message); + void OnLayerReady(const ILayerSource::LayerReadyMessage& message); + + }; + +} diff -r fe4befe03935 -r d6136a7e914d Framework/StoneEnumerations.cpp --- a/Framework/StoneEnumerations.cpp Mon Nov 05 10:04:56 2018 +0100 +++ b/Framework/StoneEnumerations.cpp Mon Nov 05 10:06:18 2018 +0100 @@ -72,4 +72,66 @@ throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); } } + + + void ComputeAnchorTranslation(double& deltaX, + double& deltaY, + BitmapAnchor anchor, + unsigned int bitmapWidth, + unsigned int bitmapHeight) + { + double dw = static_cast(bitmapWidth); + double dh = static_cast(bitmapHeight); + + switch (anchor) + { + case BitmapAnchor_TopLeft: + deltaX = 0; + deltaY = 0; + break; + + case BitmapAnchor_TopCenter: + deltaX = -dw / 2.0; + deltaY = 0; + break; + + case BitmapAnchor_TopRight: + deltaX = -dw; + deltaY = 0; + break; + + case BitmapAnchor_CenterLeft: + deltaX = 0; + deltaY = -dh / 2.0; + break; + + case BitmapAnchor_Center: + deltaX = -dw / 2.0; + deltaY = -dh / 2.0; + break; + + case BitmapAnchor_CenterRight: + deltaX = -dw; + deltaY = -dh / 2.0; + break; + + case BitmapAnchor_BottomLeft: + deltaX = 0; + deltaY = -dh; + break; + + case BitmapAnchor_BottomCenter: + deltaX = -dw / 2.0; + deltaY = -dh; + break; + + case BitmapAnchor_BottomRight: + deltaX = -dw; + deltaY = -dh; + break; + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + } } diff -r fe4befe03935 -r d6136a7e914d Framework/StoneEnumerations.h --- a/Framework/StoneEnumerations.h Mon Nov 05 10:04:56 2018 +0100 +++ b/Framework/StoneEnumerations.h Mon Nov 05 10:06:18 2018 +0100 @@ -75,12 +75,26 @@ KeyboardModifiers_Alt = (1 << 2) }; + enum KeyboardKeys + { + KeyboardKeys_Generic = 0, + + // let's use the same ids as in javascript to avoid some conversion in WASM: https://css-tricks.com/snippets/javascript/javascript-keycodes/ + KeyboardKeys_Left = 37, + KeyboardKeys_Up = 38, + KeyboardKeys_Right = 39, + KeyboardKeys_Down = 40 + }; + enum SliceImageQuality { - SliceImageQuality_Full, + 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_Jpeg95, + + SliceImageQuality_InternalRaw // downloads the raw pixels data as they are stored in the DICOM file (internal use only) }; enum SopClassUid @@ -88,6 +102,19 @@ SopClassUid_RTDose }; + enum BitmapAnchor + { + BitmapAnchor_BottomLeft, + BitmapAnchor_BottomCenter, + BitmapAnchor_BottomRight, + BitmapAnchor_CenterLeft, + BitmapAnchor_Center, + BitmapAnchor_CenterRight, + BitmapAnchor_TopLeft, + BitmapAnchor_TopCenter, + BitmapAnchor_TopRight + }; + bool StringToSopClassUid(SopClassUid& result, const std::string& source); @@ -96,4 +123,10 @@ ImageWindowing windowing, float defaultCenter, float defaultWidth); + + void ComputeAnchorTranslation(double& deltaX /* out */, + double& deltaY /* out */, + BitmapAnchor anchor, + unsigned int bitmapWidth, + unsigned int bitmapHeight); } diff -r fe4befe03935 -r d6136a7e914d Framework/StoneException.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/StoneException.h Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,115 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "Core/OrthancException.h" +#include + +namespace OrthancStone +{ + enum ErrorCode + { + ErrorCode_Success, + ErrorCode_OrthancError, // this StoneException is actually an OrthancException with an Orthanc error code + ErrorCode_ApplicationException, // this StoneException is specific to an application (and should have its own internal error code) + ErrorCode_NotImplemented, // case not implemented + + ErrorCode_PromiseSingleSuccessHandler, // a Promise can only have a single success handler + ErrorCode_PromiseSingleFailureHandler, // a Promise can only have a single failure handler + + ErrorCode_CanOnlyAddOneLayerAtATime, + ErrorCode_CommandJsonInvalidFormat, + ErrorCode_Last + }; + + + + class StoneException + { + protected: + OrthancStone::ErrorCode errorCode_; + + public: + explicit StoneException(ErrorCode errorCode) : + errorCode_(errorCode) + { + } + + ErrorCode GetErrorCode() const + { + return errorCode_; + } + + virtual const char* What() const + { + return "TODO: EnumerationToString for StoneException"; + } + }; + + class StoneOrthancException : public StoneException + { + protected: + Orthanc::OrthancException& orthancException_; + + public: + explicit StoneOrthancException(Orthanc::OrthancException& orthancException) : + StoneException(ErrorCode_OrthancError), + orthancException_(orthancException) + { + } + + Orthanc::ErrorCode GetOrthancErrorCode() const + { + return orthancException_.GetErrorCode(); + } + + virtual const char* What() const + { + return orthancException_.What(); + } + }; + + class StoneApplicationException : public StoneException + { + protected: + int applicationErrorCode_; + + public: + explicit StoneApplicationException(int applicationErrorCode) : + StoneException(ErrorCode_ApplicationException), + applicationErrorCode_(applicationErrorCode) + { + } + + int GetApplicationErrorCode() const + { + return applicationErrorCode_; + } + + virtual const char* What() const + { + return boost::lexical_cast(applicationErrorCode_).c_str(); + } + }; + +} + diff -r fe4befe03935 -r d6136a7e914d Framework/Toolbox/DicomFrameConverter.cpp --- a/Framework/Toolbox/DicomFrameConverter.cpp Mon Nov 05 10:04:56 2018 +0100 +++ b/Framework/Toolbox/DicomFrameConverter.cpp Mon Nov 05 10:06:18 2018 +0100 @@ -30,6 +30,19 @@ namespace OrthancStone { + static const Orthanc::DicomTag IMAGE_TAGS[] = + { + Orthanc::DICOM_TAG_BITS_STORED, + Orthanc::DICOM_TAG_DOSE_GRID_SCALING, + Orthanc::DICOM_TAG_PHOTOMETRIC_INTERPRETATION, + Orthanc::DICOM_TAG_PIXEL_REPRESENTATION, + Orthanc::DICOM_TAG_RESCALE_INTERCEPT, + Orthanc::DICOM_TAG_RESCALE_SLOPE, + Orthanc::DICOM_TAG_WINDOW_CENTER, + Orthanc::DICOM_TAG_WINDOW_WIDTH + }; + + void DicomFrameConverter::SetDefaultParameters() { isSigned_ = true; @@ -37,6 +50,7 @@ hasRescale_ = false; rescaleIntercept_ = 0; rescaleSlope_ = 1; + hasDefaultWindow_ = false; defaultWindowCenter_ = 128; defaultWindowWidth_ = 256; expectedPixelFormat_ = Orthanc::PixelFormat_Grayscale16; @@ -53,6 +67,7 @@ c.size() > 0 && w.size() > 0) { + hasDefaultWindow_ = true; defaultWindowCenter_ = static_cast(c[0]); defaultWindowWidth_ = static_cast(w[0]); } @@ -113,6 +128,8 @@ // Type 1 tag, must be present throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat); } + + photometric_ = Orthanc::StringToPhotometricInterpretation(photometric.c_str()); isColor_ = (photometric != "MONOCHROME1" && photometric != "MONOCHROME2"); @@ -137,8 +154,27 @@ } } + + void DicomFrameConverter::ReadParameters(const OrthancPlugins::IDicomDataset& dicom) + { + Orthanc::DicomMap converted; - void DicomFrameConverter::ConvertFrame(std::auto_ptr& source) const + for (size_t i = 0; i < sizeof(IMAGE_TAGS) / sizeof(Orthanc::DicomTag); i++) + { + OrthancPlugins::DicomTag tag(IMAGE_TAGS[i].GetGroup(), IMAGE_TAGS[i].GetElement()); + + std::string value; + if (dicom.GetStringValue(value, tag)) + { + converted.SetValue(IMAGE_TAGS[i], value, false); + } + } + + ReadParameters(converted); + } + + + void DicomFrameConverter::ConvertFrameInplace(std::auto_ptr& source) const { assert(sizeof(float) == 4); @@ -147,7 +183,24 @@ throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); } - Orthanc::PixelFormat sourceFormat = source->GetFormat(); + if (source->GetFormat() == GetExpectedPixelFormat() && + source->GetFormat() == Orthanc::PixelFormat_RGB24) + { + // No conversion has to be done, check out (*) + return; + } + else + { + source.reset(ConvertFrame(*source)); + } + } + + + Orthanc::ImageAccessor* DicomFrameConverter::ConvertFrame(const Orthanc::ImageAccessor& source) const + { + assert(sizeof(float) == 4); + + Orthanc::PixelFormat sourceFormat = source.GetFormat(); if (sourceFormat != GetExpectedPixelFormat()) { @@ -156,27 +209,32 @@ if (sourceFormat == Orthanc::PixelFormat_RGB24) { - // No conversion has to be done - return; + // This is the case of a color image. No conversion has to be done (*) + std::auto_ptr converted(new Orthanc::Image(Orthanc::PixelFormat_RGB24, + source.GetWidth(), + source.GetHeight(), + false)); + Orthanc::ImageProcessing::Copy(*converted, source); + return converted.release(); } - - assert(sourceFormat == Orthanc::PixelFormat_Grayscale16 || - sourceFormat == Orthanc::PixelFormat_Grayscale32 || - sourceFormat == Orthanc::PixelFormat_SignedGrayscale16); + else + { + assert(sourceFormat == Orthanc::PixelFormat_Grayscale16 || + sourceFormat == Orthanc::PixelFormat_Grayscale32 || + sourceFormat == Orthanc::PixelFormat_SignedGrayscale16); - // This is the case of a grayscale frame. Convert it to Float32. - std::auto_ptr converted(new Orthanc::Image(Orthanc::PixelFormat_Float32, - source->GetWidth(), - source->GetHeight(), - false)); - Orthanc::ImageProcessing::Convert(*converted, *source); + // This is the case of a grayscale frame. Convert it to Float32. + std::auto_ptr converted(new Orthanc::Image(Orthanc::PixelFormat_Float32, + source.GetWidth(), + source.GetHeight(), + false)); + Orthanc::ImageProcessing::Convert(*converted, source); - source.reset(NULL); // We don't need the source frame anymore - - // Correct rescale slope/intercept if need be - ApplyRescale(*converted, sourceFormat != Orthanc::PixelFormat_Grayscale32); + // Correct rescale slope/intercept if need be + ApplyRescale(*converted, sourceFormat != Orthanc::PixelFormat_Grayscale32); - source = converted; + return converted.release(); + } } diff -r fe4befe03935 -r d6136a7e914d Framework/Toolbox/DicomFrameConverter.h --- a/Framework/Toolbox/DicomFrameConverter.h Mon Nov 05 10:04:56 2018 +0100 +++ b/Framework/Toolbox/DicomFrameConverter.h Mon Nov 05 10:06:18 2018 +0100 @@ -21,6 +21,7 @@ #pragma once +#include #include #include @@ -43,10 +44,12 @@ bool hasRescale_; double rescaleIntercept_; double rescaleSlope_; + bool hasDefaultWindow_; double defaultWindowCenter_; double defaultWindowWidth_; - - Orthanc::PixelFormat expectedPixelFormat_; + + Orthanc::PhotometricInterpretation photometric_; + Orthanc::PixelFormat expectedPixelFormat_; void SetDefaultParameters(); @@ -61,8 +64,20 @@ return expectedPixelFormat_; } + Orthanc::PhotometricInterpretation GetPhotometricInterpretation() const + { + return photometric_; + } + void ReadParameters(const Orthanc::DicomMap& dicom); + void ReadParameters(const OrthancPlugins::IDicomDataset& dicom); + + bool HasDefaultWindow() const + { + return hasDefaultWindow_; + } + double GetDefaultWindowCenter() const { return defaultWindowCenter_; @@ -83,7 +98,9 @@ return rescaleSlope_; } - void ConvertFrame(std::auto_ptr& source) const; + void ConvertFrameInplace(std::auto_ptr& source) const; + + Orthanc::ImageAccessor* ConvertFrame(const Orthanc::ImageAccessor& source) const; void ApplyRescale(Orthanc::ImageAccessor& image, bool useDouble) const; diff -r fe4befe03935 -r d6136a7e914d Framework/Toolbox/GeometryToolbox.cpp --- a/Framework/Toolbox/GeometryToolbox.cpp Mon Nov 05 10:04:56 2018 +0100 +++ b/Framework/Toolbox/GeometryToolbox.cpp Mon Nov 05 10:06:18 2018 +0100 @@ -260,8 +260,9 @@ } else { - spacingX = v[0]; - spacingY = v[1]; + // WARNING: X/Y are swapped (Y comes first) + spacingX = v[1]; + spacingY = v[0]; } } else diff -r fe4befe03935 -r d6136a7e914d Framework/Toolbox/IWebService.h --- a/Framework/Toolbox/IWebService.h Mon Nov 05 10:04:56 2018 +0100 +++ b/Framework/Toolbox/IWebService.h Mon Nov 05 10:06:18 2018 +0100 @@ -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 . **/ @@ -22,41 +22,86 @@ #pragma once #include - +#include "../../Framework/Messages/IObserver.h" +#include "../../Framework/Messages/ICallable.h" #include +#include +#include namespace OrthancStone { - class IWebService : public boost::noncopyable + // 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 + // also implements an asynchronous interface: you must schedule a request + // and you'll be notified when the response/error is ready. + class IWebService { + protected: + MessageBroker& broker_; public: - class ICallback : public boost::noncopyable - { - public: - virtual ~ICallback() - { - } + typedef std::map Headers; - virtual void NotifyError(const std::string& uri, - Orthanc::IDynamicObject* payload) = 0; + struct HttpRequestSuccessMessage: public BaseMessage + { + const std::string& uri_; + const void* answer_; + size_t answerSize_; + Orthanc::IDynamicObject* payload_; + HttpRequestSuccessMessage(const std::string& uri, + const void* answer, + size_t answerSize, + Orthanc::IDynamicObject* payload) + : BaseMessage(), + uri_(uri), + answer_(answer), + answerSize_(answerSize), + payload_(payload) + {} + }; - virtual void NotifySuccess(const std::string& uri, - const void* answer, - size_t answerSize, - Orthanc::IDynamicObject* payload) = 0; + struct HttpRequestErrorMessage: public BaseMessage + { + const std::string& uri_; + Orthanc::IDynamicObject* payload_; + HttpRequestErrorMessage(const std::string& uri, + Orthanc::IDynamicObject* payload) + : BaseMessage(), + uri_(uri), + payload_(payload) + {} }; - + + + + IWebService(MessageBroker& broker) + : broker_(broker) + {} + virtual ~IWebService() { } - virtual void ScheduleGetRequest(ICallback& callback, - const std::string& uri, - Orthanc::IDynamicObject* payload) = 0; + virtual void GetAsync(const std::string& uri, + const Headers& headers, + Orthanc::IDynamicObject* payload, + MessageHandler* successCallback, + MessageHandler* failureCallback = NULL, + unsigned int timeoutInSeconds = 60) = 0; - virtual void SchedulePostRequest(ICallback& callback, - const std::string& uri, - const std::string& body, - Orthanc::IDynamicObject* payload) = 0; + virtual void PostAsync(const std::string& uri, + const Headers& headers, + const std::string& body, + Orthanc::IDynamicObject* payload, + MessageHandler* successCallback, + MessageHandler* failureCallback = NULL, + unsigned int timeoutInSeconds = 60) = 0; + + virtual void DeleteAsync(const std::string& uri, + const Headers& headers, + Orthanc::IDynamicObject* payload, + MessageHandler* successCallback, + MessageHandler* failureCallback = NULL, + unsigned int timeoutInSeconds = 60) = 0; }; } diff -r fe4befe03935 -r d6136a7e914d Framework/Toolbox/ImageGeometry.cpp --- a/Framework/Toolbox/ImageGeometry.cpp Mon Nov 05 10:04:56 2018 +0100 +++ b/Framework/Toolbox/ImageGeometry.cpp Mon Nov 05 10:06:18 2018 +0100 @@ -152,10 +152,6 @@ { *p = value; } - else - { - Reader::Traits::SetZero(*p); - } if (HasOffsetX) { @@ -174,7 +170,8 @@ ImageInterpolation Interpolation> static void ApplyAffineInternal(Orthanc::ImageAccessor& target, const Orthanc::ImageAccessor& source, - const Matrix& a) + const Matrix& a, + bool clear) { assert(target.GetFormat() == Format && source.GetFormat() == Format); @@ -182,13 +179,16 @@ typedef SubpixelReader Reader; typedef typename Reader::PixelType PixelType; - if (Format == Orthanc::PixelFormat_RGB24) + if (clear) { - Orthanc::ImageProcessing::Set(target, 0, 0, 0, 255); - } - else - { - Orthanc::ImageProcessing::Set(target, 0); + if (Format == Orthanc::PixelFormat_RGB24) + { + Orthanc::ImageProcessing::Set(target, 0, 0, 0, 255); + } + else + { + Orthanc::ImageProcessing::Set(target, 0); + } } Matrix inva; @@ -260,7 +260,8 @@ double a21, double a22, double b2, - ImageInterpolation interpolation) + ImageInterpolation interpolation, + bool clear) { if (source.GetFormat() != target.GetFormat()) { @@ -292,12 +293,12 @@ { case ImageInterpolation_Nearest: ApplyAffineInternal(target, source, a); + ImageInterpolation_Nearest>(target, source, a, clear); break; case ImageInterpolation_Bilinear: ApplyAffineInternal(target, source, a); + ImageInterpolation_Bilinear>(target, source, a, clear); break; default: @@ -310,12 +311,12 @@ { case ImageInterpolation_Nearest: ApplyAffineInternal(target, source, a); + ImageInterpolation_Nearest>(target, source, a, clear); break; case ImageInterpolation_Bilinear: ApplyAffineInternal(target, source, a); + ImageInterpolation_Bilinear>(target, source, a, clear); break; default: @@ -328,12 +329,30 @@ { case ImageInterpolation_Nearest: ApplyAffineInternal(target, source, a); + ImageInterpolation_Nearest>(target, source, a, clear); break; case ImageInterpolation_Bilinear: ApplyAffineInternal(target, source, a); + ImageInterpolation_Bilinear>(target, source, a, clear); + break; + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); + } + break; + + case Orthanc::PixelFormat_Float32: + switch (interpolation) + { + case ImageInterpolation_Nearest: + ApplyAffineInternal(target, source, a, clear); + break; + + case ImageInterpolation_Bilinear: + ApplyAffineInternal(target, source, a, clear); break; default: @@ -346,7 +365,7 @@ { case ImageInterpolation_Nearest: ApplyAffineInternal(target, source, a); + ImageInterpolation_Nearest>(target, source, a, clear); break; default: @@ -412,10 +431,6 @@ { reader.GetValue(*p, sourceX, sourceY); } - else - { - Reader::Traits::SetZero(*p); - } p++; } @@ -429,7 +444,8 @@ void ApplyProjectiveTransform(Orthanc::ImageAccessor& target, const Orthanc::ImageAccessor& source, const Matrix& a, - ImageInterpolation interpolation) + ImageInterpolation interpolation, + bool clear) { if (source.GetFormat() != target.GetFormat()) { @@ -463,18 +479,21 @@ ApplyAffineTransform(target, source, a(0, 0) / w, a(0, 1) / w, a(0, 2) / w, a(1, 0) / w, a(1, 1) / w, a(1, 2) / w, - interpolation); + interpolation, clear); return; } } - if (target.GetFormat() == Orthanc::PixelFormat_RGB24) + if (clear) { - Orthanc::ImageProcessing::Set(target, 0, 0, 0, 255); - } - else - { - Orthanc::ImageProcessing::Set(target, 0); + if (target.GetFormat() == Orthanc::PixelFormat_RGB24) + { + Orthanc::ImageProcessing::Set(target, 0, 0, 0, 255); + } + else + { + Orthanc::ImageProcessing::Set(target, 0); + } } Matrix inva; @@ -539,6 +558,24 @@ } break; + case Orthanc::PixelFormat_Float32: + switch (interpolation) + { + case ImageInterpolation_Nearest: + ApplyProjectiveInternal(target, source, a, inva); + break; + + case ImageInterpolation_Bilinear: + ApplyProjectiveInternal(target, source, a, inva); + break; + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); + } + break; + case Orthanc::PixelFormat_RGB24: switch (interpolation) { diff -r fe4befe03935 -r d6136a7e914d Framework/Toolbox/ImageGeometry.h --- a/Framework/Toolbox/ImageGeometry.h Mon Nov 05 10:04:56 2018 +0100 +++ b/Framework/Toolbox/ImageGeometry.h Mon Nov 05 10:06:18 2018 +0100 @@ -50,10 +50,12 @@ double a21, double a22, double b2, - ImageInterpolation interpolation); + ImageInterpolation interpolation, + bool clear); void ApplyProjectiveTransform(Orthanc::ImageAccessor& target, const Orthanc::ImageAccessor& source, const Matrix& a, - ImageInterpolation interpolation); + ImageInterpolation interpolation, + bool clear); } diff -r fe4befe03935 -r d6136a7e914d Framework/Toolbox/MessagingToolbox.cpp --- a/Framework/Toolbox/MessagingToolbox.cpp Mon Nov 05 10:04:56 2018 +0100 +++ b/Framework/Toolbox/MessagingToolbox.cpp Mon Nov 05 10:06:18 2018 +0100 @@ -31,6 +31,7 @@ #include #include +#include namespace OrthancStone { @@ -114,6 +115,12 @@ target); } + void JsonToString(std::string& target, + const Json::Value& source) + { + Json::FastWriter writer; + target = writer.write(source); + } static void ParseJsonException(Json::Value& target, const std::string& source) diff -r fe4befe03935 -r d6136a7e914d Framework/Toolbox/MessagingToolbox.h --- a/Framework/Toolbox/MessagingToolbox.h Mon Nov 05 10:04:56 2018 +0100 +++ b/Framework/Toolbox/MessagingToolbox.h Mon Nov 05 10:06:18 2018 +0100 @@ -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 . **/ @@ -38,6 +38,10 @@ const void* content, size_t size); + void JsonToString(std::string& target, + const Json::Value& source); + + void RestApiGet(Json::Value& target, OrthancPlugins::IOrthancConnection& orthanc, const std::string& uri); diff -r fe4befe03935 -r d6136a7e914d Framework/Toolbox/OrthancApiClient.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Toolbox/OrthancApiClient.cpp Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,232 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + +#include "OrthancApiClient.h" + +#include "MessagingToolbox.h" +#include +#include "Framework/Toolbox/MessagingToolbox.h" + +namespace OrthancStone { + + OrthancApiClient::OrthancApiClient(MessageBroker &broker, IWebService &orthanc) + : IObservable(broker), + orthanc_(orthanc) + { + } + + // performs the translation between IWebService messages and OrthancApiClient messages + // TODO: handle destruction of this object (with shared_ptr ?::delete_later ???) + class HttpResponseToJsonConverter : public IObserver, IObservable + { + private: + std::auto_ptr > orthancApiSuccessCallback_; + std::auto_ptr > orthancApiFailureCallback_; + + public: + HttpResponseToJsonConverter(MessageBroker& broker, + MessageHandler* orthancApiSuccessCallback, + MessageHandler* orthancApiFailureCallback) + : IObserver(broker), + IObservable(broker), + orthancApiSuccessCallback_(orthancApiSuccessCallback), + orthancApiFailureCallback_(orthancApiFailureCallback) + { + } + + void ConvertResponseToJson(const IWebService::HttpRequestSuccessMessage& message) + { + Json::Value response; + if (MessagingToolbox::ParseJson(response, message.answer_, message.answerSize_)) + { + if (orthancApiSuccessCallback_.get() != NULL) + { + orthancApiSuccessCallback_->Apply(OrthancApiClient::JsonResponseReadyMessage(message.uri_, response, message.payload_)); + } + } + else if (orthancApiFailureCallback_.get() != NULL) + { + orthancApiFailureCallback_->Apply(OrthancApiClient::HttpErrorMessage(message.uri_, message.payload_)); + } + + delete this; // hack untill we find someone to take ownership of this object (https://isocpp.org/wiki/faq/freestore-mgmt#delete-this) + } + + void ConvertError(const IWebService::HttpRequestErrorMessage& message) + { + if (orthancApiFailureCallback_.get() != NULL) + { + orthancApiFailureCallback_->Apply(OrthancApiClient::HttpErrorMessage(message.uri_)); + } + + delete this; // hack untill we find someone to take ownership of this object (https://isocpp.org/wiki/faq/freestore-mgmt#delete-this) + } + }; + + // performs the translation between IWebService messages and OrthancApiClient messages + // TODO: handle destruction of this object (with shared_ptr ?::delete_later ???) + class HttpResponseToBinaryConverter : public IObserver, IObservable + { + private: + std::auto_ptr > orthancApiSuccessCallback_; + std::auto_ptr > orthancApiFailureCallback_; + + public: + HttpResponseToBinaryConverter(MessageBroker& broker, + MessageHandler* orthancApiSuccessCallback, + MessageHandler* orthancApiFailureCallback) + : IObserver(broker), + IObservable(broker), + orthancApiSuccessCallback_(orthancApiSuccessCallback), + orthancApiFailureCallback_(orthancApiFailureCallback) + { + } + + void ConvertResponseToBinary(const IWebService::HttpRequestSuccessMessage& message) + { + if (orthancApiSuccessCallback_.get() != NULL) + { + orthancApiSuccessCallback_->Apply(OrthancApiClient::BinaryResponseReadyMessage(message.uri_, message.answer_, message.answerSize_, message.payload_)); + } + else if (orthancApiFailureCallback_.get() != NULL) + { + orthancApiFailureCallback_->Apply(OrthancApiClient::HttpErrorMessage(message.uri_, message.payload_)); + } + + delete this; // hack untill we find someone to take ownership of this object (https://isocpp.org/wiki/faq/freestore-mgmt#delete-this) + } + + void ConvertError(const IWebService::HttpRequestErrorMessage& message) + { + if (orthancApiFailureCallback_.get() != NULL) + { + orthancApiFailureCallback_->Apply(OrthancApiClient::HttpErrorMessage(message.uri_)); + } + + delete this; // hack untill we find someone to take ownership of this object (https://isocpp.org/wiki/faq/freestore-mgmt#delete-this) + } + }; + + // performs the translation between IWebService messages and OrthancApiClient messages + // TODO: handle destruction of this object (with shared_ptr ?::delete_later ???) + class HttpResponseToEmptyConverter : public IObserver, IObservable + { + private: + std::auto_ptr > orthancApiSuccessCallback_; + std::auto_ptr > orthancApiFailureCallback_; + + public: + HttpResponseToEmptyConverter(MessageBroker& broker, + MessageHandler* orthancApiSuccessCallback, + MessageHandler* orthancApiFailureCallback) + : IObserver(broker), + IObservable(broker), + orthancApiSuccessCallback_(orthancApiSuccessCallback), + orthancApiFailureCallback_(orthancApiFailureCallback) + { + } + + void ConvertResponseToEmpty(const IWebService::HttpRequestSuccessMessage& message) + { + if (orthancApiSuccessCallback_.get() != NULL) + { + orthancApiSuccessCallback_->Apply(OrthancApiClient::EmptyResponseReadyMessage(message.uri_, message.payload_)); + } + else if (orthancApiFailureCallback_.get() != NULL) + { + orthancApiFailureCallback_->Apply(OrthancApiClient::HttpErrorMessage(message.uri_, message.payload_)); + } + + delete this; // hack untill we find someone to take ownership of this object (https://isocpp.org/wiki/faq/freestore-mgmt#delete-this) + } + + void ConvertError(const IWebService::HttpRequestErrorMessage& message) + { + if (orthancApiFailureCallback_.get() != NULL) + { + orthancApiFailureCallback_->Apply(OrthancApiClient::HttpErrorMessage(message.uri_)); + } + + delete this; // hack untill we find someone to take ownership of this object (https://isocpp.org/wiki/faq/freestore-mgmt#delete-this) + } + }; + + + void OrthancApiClient::GetJsonAsync(const std::string& uri, + MessageHandler* successCallback, + MessageHandler* failureCallback, + Orthanc::IDynamicObject* payload) + { + HttpResponseToJsonConverter* converter = new HttpResponseToJsonConverter(broker_, successCallback, failureCallback); // it is currently deleting itself after being used + orthanc_.GetAsync(uri, IWebService::Headers(), payload, + new Callable(*converter, &HttpResponseToJsonConverter::ConvertResponseToJson), + new Callable(*converter, &HttpResponseToJsonConverter::ConvertError)); + + } + + void OrthancApiClient::GetBinaryAsync(const std::string& uri, + const IWebService::Headers& headers, + MessageHandler* successCallback, + MessageHandler* failureCallback, + Orthanc::IDynamicObject* payload) + { + HttpResponseToBinaryConverter* converter = new HttpResponseToBinaryConverter(broker_, successCallback, failureCallback); // it is currently deleting itself after being used + orthanc_.GetAsync(uri, headers, payload, + new Callable(*converter, &HttpResponseToBinaryConverter::ConvertResponseToBinary), + new Callable(*converter, &HttpResponseToBinaryConverter::ConvertError)); + } + + void OrthancApiClient::PostBinaryAsyncExpectJson(const std::string& uri, + const std::string& body, + MessageHandler* successCallback, + MessageHandler* failureCallback, + Orthanc::IDynamicObject* payload) + { + HttpResponseToJsonConverter* converter = new HttpResponseToJsonConverter(broker_, successCallback, failureCallback); // it is currently deleting itself after being used + orthanc_.PostAsync(uri, IWebService::Headers(), body, payload, + new Callable(*converter, &HttpResponseToJsonConverter::ConvertResponseToJson), + new Callable(*converter, &HttpResponseToJsonConverter::ConvertError)); + + } + + void OrthancApiClient::PostJsonAsyncExpectJson(const std::string& uri, + const Json::Value& data, + MessageHandler* successCallback, + MessageHandler* failureCallback, + Orthanc::IDynamicObject* payload) + { + std::string body; + MessagingToolbox::JsonToString(body, data); + return PostBinaryAsyncExpectJson(uri, body, successCallback, failureCallback, payload); + } + + void OrthancApiClient::DeleteAsync(const std::string& uri, + MessageHandler* successCallback, + MessageHandler* failureCallback, + Orthanc::IDynamicObject* payload) + { + HttpResponseToEmptyConverter* converter = new HttpResponseToEmptyConverter(broker_, successCallback, failureCallback); // it is currently deleting itself after being used + orthanc_.DeleteAsync(uri, IWebService::Headers(), payload, + new Callable(*converter, &HttpResponseToEmptyConverter::ConvertResponseToEmpty), + new Callable(*converter, &HttpResponseToEmptyConverter::ConvertError)); + } + + +} diff -r fe4befe03935 -r d6136a7e914d Framework/Toolbox/OrthancApiClient.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Toolbox/OrthancApiClient.h Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,167 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include +#include + +#include "IWebService.h" +#include "../Messages/IObservable.h" +#include "../Messages/Promise.h" + +namespace OrthancStone +{ + class OrthancApiClient: + public IObservable + { + public: + + struct JsonResponseReadyMessage : public BaseMessage + { + Json::Value Response; + std::string Uri; + std::auto_ptr Payload; + + JsonResponseReadyMessage(const std::string& uri, + const Json::Value& response, + Orthanc::IDynamicObject* payload = NULL) + : BaseMessage(), + Response(response), + Uri(uri), + Payload(payload) + { + } + }; + + struct EmptyResponseReadyMessage : public BaseMessage + { + std::string Uri; + std::auto_ptr Payload; + + EmptyResponseReadyMessage(const std::string& uri, + Orthanc::IDynamicObject* payload = NULL) + : BaseMessage(), + Uri(uri), + Payload(payload) + { + } + }; + + struct HttpErrorMessage : public BaseMessage + { + std::string Uri; + std::auto_ptr Payload; + + HttpErrorMessage(const std::string& uri, + Orthanc::IDynamicObject* payload = NULL) + : BaseMessage(), + Uri(uri), + Payload(payload) + { + } + }; + + struct BinaryResponseReadyMessage : public BaseMessage + { + const void* Answer; + size_t AnswerSize; + std::string Uri; + std::auto_ptr Payload; + + BinaryResponseReadyMessage(const std::string& uri, + const void* answer, + size_t answerSize, + Orthanc::IDynamicObject* payload = NULL) + : BaseMessage(), + Answer(answer), + AnswerSize(answerSize), + Uri(uri), + Payload(payload) + { + } + }; + + + + public: + + enum Mode + { + Mode_GetJson + }; + + protected: + IWebService& orthanc_; + + public: + OrthancApiClient(MessageBroker& broker, + IWebService& orthanc); + virtual ~OrthancApiClient() {} + + // schedule a GET request expecting a JSON response. + void GetJsonAsync(const std::string& uri, + MessageHandler* successCallback, + MessageHandler* failureCallback = NULL, + Orthanc::IDynamicObject* payload = NULL); + + // schedule a GET request expecting a binary response. + void GetBinaryAsync(const std::string& uri, + const std::string& contentType, + MessageHandler* successCallback, + MessageHandler* failureCallback = NULL, + Orthanc::IDynamicObject* payload = NULL) + { + IWebService::Headers headers; + headers["Accept"] = contentType; + GetBinaryAsync(uri, headers, successCallback, failureCallback, payload); + } + + // schedule a GET request expecting a binary response. + void GetBinaryAsync(const std::string& uri, + const IWebService::Headers& headers, + MessageHandler* successCallback, + MessageHandler* failureCallback = NULL, + Orthanc::IDynamicObject* payload = NULL); + + // schedule a POST request expecting a JSON response. + void PostBinaryAsyncExpectJson(const std::string& uri, + const std::string& body, + MessageHandler* successCallback, + MessageHandler* failureCallback = NULL, + Orthanc::IDynamicObject* payload = NULL); + + // schedule a POST request expecting a JSON response. + void PostJsonAsyncExpectJson(const std::string& uri, + const Json::Value& data, + MessageHandler* successCallback, + MessageHandler* failureCallback = NULL, + Orthanc::IDynamicObject* payload = NULL); + + // schedule a DELETE request expecting an empty response. + void DeleteAsync(const std::string& uri, + MessageHandler* successCallback, + MessageHandler* failureCallback = NULL, + Orthanc::IDynamicObject* payload = NULL); + + + }; +} diff -r fe4befe03935 -r d6136a7e914d Framework/Toolbox/OrthancSlicesLoader.cpp --- a/Framework/Toolbox/OrthancSlicesLoader.cpp Mon Nov 05 10:04:56 2018 +0100 +++ b/Framework/Toolbox/OrthancSlicesLoader.cpp Mon Nov 05 10:06:18 2018 +0100 @@ -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 . **/ @@ -29,6 +29,7 @@ #include #include #include +#include #include #include #include @@ -47,10 +48,10 @@ static std::string base64_decode(const std::string &in) { std::string out; - + std::vector T(256,-1); - for (int i=0; i<64; i++) T["ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"[i]] = i; - + for (int i=0; i<64; i++) T["ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"[i]] = i; + int val=0, valb=-8; for (size_t i = 0; i < in.size(); i++) { unsigned char c = in[i]; @@ -117,18 +118,13 @@ assert(mode_ == Mode_FrameGeometry); return frame_; } - + const std::string& GetInstanceId() const { assert(mode_ == Mode_FrameGeometry || mode_ == Mode_InstanceGeometry); return instanceId_; } - - static Operation* DownloadSeriesGeometry() - { - return new Operation(Mode_SeriesGeometry); - } static Operation* DownloadInstanceGeometry(const std::string& instanceId) { @@ -163,103 +159,21 @@ std::auto_ptr tmp(new Operation(Mode_LoadRawImage)); tmp->sliceIndex_ = sliceIndex; tmp->slice_ = &slice; - tmp->quality_ = SliceImageQuality_Full; + tmp->quality_ = SliceImageQuality_InternalRaw; return tmp.release(); } - }; - - - class OrthancSlicesLoader::WebCallback : public IWebService::ICallback - { - private: - OrthancSlicesLoader& that_; - - public: - WebCallback(OrthancSlicesLoader& that) : - that_(that) - { - } - virtual void NotifySuccess(const std::string& uri, - const void* answer, - size_t answerSize, - Orthanc::IDynamicObject* payload) + static Operation* DownloadDicomFile(const Slice& slice) { - std::auto_ptr operation(dynamic_cast(payload)); - - switch (operation->GetMode()) - { - case Mode_SeriesGeometry: - that_.ParseSeriesGeometry(answer, answerSize); - break; - - case Mode_InstanceGeometry: - that_.ParseInstanceGeometry(operation->GetInstanceId(), answer, answerSize); - break; - - case Mode_FrameGeometry: - that_.ParseFrameGeometry(operation->GetInstanceId(), - operation->GetFrame(), answer, answerSize); - break; - - case Mode_LoadImage: - switch (operation->GetQuality()) - { - case SliceImageQuality_Full: - that_.ParseSliceImagePng(*operation, answer, answerSize); - break; - - case SliceImageQuality_Jpeg50: - case SliceImageQuality_Jpeg90: - case SliceImageQuality_Jpeg95: - that_.ParseSliceImageJpeg(*operation, answer, answerSize); - break; - - default: - throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); - } - - break; - - case Mode_LoadRawImage: - that_.ParseSliceRawImage(*operation, answer, answerSize); - break; - - default: - throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); - } + std::auto_ptr tmp(new Operation(Mode_LoadDicomFile)); + tmp->slice_ = &slice; + return tmp.release(); } - virtual void NotifyError(const std::string& uri, - Orthanc::IDynamicObject* payload) - { - std::auto_ptr operation(dynamic_cast(payload)); - LOG(ERROR) << "Cannot download " << uri; - - switch (operation->GetMode()) - { - case Mode_FrameGeometry: - case Mode_SeriesGeometry: - that_.userCallback_.NotifyGeometryError(that_); - that_.state_ = State_Error; - break; - - case Mode_LoadImage: - that_.userCallback_.NotifySliceImageError(that_, operation->GetSliceIndex(), - operation->GetSlice(), - operation->GetQuality()); - break; - - default: - throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); - } - } }; - - void OrthancSlicesLoader::NotifySliceImageSuccess(const Operation& operation, - std::auto_ptr& image) const + boost::shared_ptr image) { if (image.get() == NULL) { @@ -267,19 +181,19 @@ } else { - userCallback_.NotifySliceImageReady - (*this, operation.GetSliceIndex(), operation.GetSlice(), image, operation.GetQuality()); + OrthancSlicesLoader::SliceImageReadyMessage msg(operation.GetSliceIndex(), operation.GetSlice(), image, operation.GetQuality()); + EmitMessage(msg); } } - + - void OrthancSlicesLoader::NotifySliceImageError(const Operation& operation) const + void OrthancSlicesLoader::NotifySliceImageError(const Operation& operation) { - userCallback_.NotifySliceImageError - (*this, operation.GetSliceIndex(), operation.GetSlice(), operation.GetQuality()); + OrthancSlicesLoader::SliceImageErrorMessage msg(operation.GetSliceIndex(), operation.GetSlice(), operation.GetQuality()); + EmitMessage(msg); } - - + + void OrthancSlicesLoader::SortAndFinalizeSlices() { bool ok = false; @@ -295,41 +209,44 @@ ok = true; } } - + state_ = State_GeometryReady; - + if (ok) { LOG(INFO) << "Loaded a series with " << slices_.GetSliceCount() << " slice(s)"; - userCallback_.NotifyGeometryReady(*this); + EmitMessage(SliceGeometryReadyMessage(*this)); } else { LOG(ERROR) << "This series is empty"; - userCallback_.NotifyGeometryError(*this); + EmitMessage(SliceGeometryErrorMessage(*this)); } } + + void OrthancSlicesLoader::OnGeometryError(const OrthancApiClient::HttpErrorMessage& message) + { + EmitMessage(SliceGeometryErrorMessage(*this)); + state_ = State_Error; + } - - void OrthancSlicesLoader::ParseSeriesGeometry(const void* answer, - size_t size) + void OrthancSlicesLoader::OnSliceImageError(const OrthancApiClient::HttpErrorMessage& message) { - Json::Value series; - if (!MessagingToolbox::ParseJson(series, answer, size) || - series.type() != Json::objectValue) - { - userCallback_.NotifyGeometryError(*this); - return; - } + NotifySliceImageError(dynamic_cast(*(message.Payload))); + state_ = State_Error; + } + void OrthancSlicesLoader::ParseSeriesGeometry(const OrthancApiClient::JsonResponseReadyMessage& message) + { + Json::Value series = message.Response; Json::Value::Members instances = series.getMemberNames(); - + slices_.Reserve(instances.size()); - + for (size_t i = 0; i < instances.size(); i++) { OrthancPlugins::FullOrthancDataset dataset(series[instances[i]]); - + Orthanc::DicomMap dicom; MessagingToolbox::ConvertDataset(dicom, dataset); @@ -338,7 +255,7 @@ { frames = 1; } - + for (unsigned int frame = 0; frame < frames; frame++) { std::auto_ptr slice(new Slice); @@ -352,28 +269,20 @@ } } } - + SortAndFinalizeSlices(); } - - - void OrthancSlicesLoader::ParseInstanceGeometry(const std::string& instanceId, - const void* answer, - size_t size) + + void OrthancSlicesLoader::ParseInstanceGeometry(const OrthancApiClient::JsonResponseReadyMessage& message) { - Json::Value tags; - if (!MessagingToolbox::ParseJson(tags, answer, size) || - tags.type() != Json::objectValue) - { - userCallback_.NotifyGeometryError(*this); - return; - } + Json::Value tags = message.Response; + const std::string& instanceId = dynamic_cast(message.Payload.get())->GetInstanceId(); OrthancPlugins::FullOrthancDataset dataset(tags); - + Orthanc::DicomMap dicom; MessagingToolbox::ConvertDataset(dicom, dataset); - + unsigned int frames; if (!dicom.ParseUnsignedInteger32(frames, Orthanc::DICOM_TAG_NUMBER_OF_FRAMES)) { @@ -381,7 +290,7 @@ } LOG(INFO) << "Instance " << instanceId << " contains " << frames << " frame(s)"; - + for (unsigned int frame = 0; frame < frames; frame++) { std::auto_ptr slice(new Slice); @@ -392,60 +301,92 @@ else { LOG(WARNING) << "Skipping invalid multi-frame instance " << instanceId; - userCallback_.NotifyGeometryError(*this); + EmitMessage(SliceGeometryErrorMessage(*this)); return; } } - + SortAndFinalizeSlices(); } - - - void OrthancSlicesLoader::ParseFrameGeometry(const std::string& instanceId, - unsigned int frame, - const void* answer, - size_t size) + + + void OrthancSlicesLoader::ParseFrameGeometry(const OrthancApiClient::JsonResponseReadyMessage& message) { - Json::Value tags; - if (!MessagingToolbox::ParseJson(tags, answer, size) || - tags.type() != Json::objectValue) - { - userCallback_.NotifyGeometryError(*this); - return; - } + Json::Value tags = message.Response; + const std::string& instanceId = dynamic_cast(message.Payload.get())->GetInstanceId(); + unsigned int frame = dynamic_cast(message.Payload.get())->GetFrame(); OrthancPlugins::FullOrthancDataset dataset(tags); - + state_ = State_GeometryReady; - + Orthanc::DicomMap dicom; MessagingToolbox::ConvertDataset(dicom, dataset); - + std::auto_ptr slice(new Slice); if (slice->ParseOrthancFrame(dicom, instanceId, frame)) { - LOG(INFO) << "Loaded instance " << instanceId; + LOG(INFO) << "Loaded instance geometry " << instanceId; slices_.AddSlice(slice.release()); - userCallback_.NotifyGeometryReady(*this); + EmitMessage(SliceGeometryReadyMessage(*this)); } else { LOG(WARNING) << "Skipping invalid instance " << instanceId; - userCallback_.NotifyGeometryError(*this); + EmitMessage(SliceGeometryErrorMessage(*this)); } } - + + + void OrthancSlicesLoader::ParseSliceImagePng(const OrthancApiClient::BinaryResponseReadyMessage& message) + { + const Operation& operation = dynamic_cast(*message.Payload.get()); + boost::shared_ptr image; + + try + { + image.reset(new Orthanc::PngReader); + dynamic_cast(*image).ReadFromMemory(message.Answer, message.AnswerSize); + } + catch (Orthanc::OrthancException&) + { + NotifySliceImageError(operation); + return; + } + + if (image->GetWidth() != operation.GetSlice().GetWidth() || + image->GetHeight() != operation.GetSlice().GetHeight()) + { + NotifySliceImageError(operation); + return; + } - void OrthancSlicesLoader::ParseSliceImagePng(const Operation& operation, - const void* answer, - size_t size) + if (operation.GetSlice().GetConverter().GetExpectedPixelFormat() == + Orthanc::PixelFormat_SignedGrayscale16) + { + if (image->GetFormat() == Orthanc::PixelFormat_Grayscale16) + { + image->SetFormat(Orthanc::PixelFormat_SignedGrayscale16); + } + else + { + NotifySliceImageError(operation); + return; + } + } + + NotifySliceImageSuccess(operation, image); + } + + void OrthancSlicesLoader::ParseSliceImagePam(const OrthancApiClient::BinaryResponseReadyMessage& message) { - std::auto_ptr image; + const Operation& operation = dynamic_cast(*message.Payload.get()); + boost::shared_ptr image; try { - image.reset(new Orthanc::PngReader); - dynamic_cast(*image).ReadFromMemory(answer, size); + image.reset(new Orthanc::PamReader); + dynamic_cast(*image).ReadFromMemory(std::string(reinterpret_cast(message.Answer), message.AnswerSize)); } catch (Orthanc::OrthancException&) { @@ -459,7 +400,7 @@ NotifySliceImageError(operation); return; } - + if (operation.GetSlice().GetConverter().GetExpectedPixelFormat() == Orthanc::PixelFormat_SignedGrayscale16) { @@ -475,23 +416,22 @@ } NotifySliceImageSuccess(operation, image); - } + } + - - void OrthancSlicesLoader::ParseSliceImageJpeg(const Operation& operation, - const void* answer, - size_t size) + void OrthancSlicesLoader::ParseSliceImageJpeg(const OrthancApiClient::JsonResponseReadyMessage& message) { - Json::Value encoded; - if (!MessagingToolbox::ParseJson(encoded, answer, size) || - encoded.type() != Json::objectValue || + const Operation& operation = dynamic_cast(*message.Payload.get()); + + Json::Value encoded = message.Response; + if (encoded.type() != Json::objectValue || !encoded.isMember("Orthanc") || encoded["Orthanc"].type() != Json::objectValue) { NotifySliceImageError(operation); return; } - + Json::Value& info = encoded["Orthanc"]; if (!info.isMember("PixelData") || !info.isMember("Stretched") || @@ -504,30 +444,30 @@ NotifySliceImageError(operation); return; } - + bool isSigned = false; bool isStretched = info["Stretched"].asBool(); - + if (info.isMember("IsSigned")) { if (info["IsSigned"].type() != Json::booleanValue) { NotifySliceImageError(operation); return; - } + } else { isSigned = info["IsSigned"].asBool(); } } - - std::auto_ptr reader; - + + boost::shared_ptr reader; + { std::string jpeg; //Orthanc::Toolbox::DecodeBase64(jpeg, info["PixelData"].asString()); jpeg = base64_decode(info["PixelData"].asString()); - + try { reader.reset(new Orthanc::JpegReader); @@ -539,10 +479,10 @@ return; } } - + Orthanc::PixelFormat expectedFormat = - operation.GetSlice().GetConverter().GetExpectedPixelFormat(); - + operation.GetSlice().GetConverter().GetExpectedPixelFormat(); + if (reader->GetFormat() == Orthanc::PixelFormat_RGB24) // This is a color image { if (expectedFormat != Orthanc::PixelFormat_RGB24) @@ -550,7 +490,7 @@ NotifySliceImageError(operation); return; } - + if (isSigned || isStretched) { NotifySliceImageError(operation); @@ -562,13 +502,13 @@ return; } } - + if (reader->GetFormat() != Orthanc::PixelFormat_Grayscale8) { NotifySliceImageError(operation); return; } - + if (!isStretched) { if (expectedFormat != reader->GetFormat()) @@ -582,10 +522,10 @@ return; } } - + int32_t stretchLow = 0; int32_t stretchHigh = 0; - + if (!info.isMember("StretchLow") || !info.isMember("StretchHigh") || info["StretchLow"].type() != Json::intValue || @@ -594,10 +534,10 @@ NotifySliceImageError(operation); return; } - + stretchLow = info["StretchLow"].asInt(); stretchHigh = info["StretchHigh"].asInt(); - + if (stretchLow < -32768 || stretchHigh > 65535 || (stretchLow < 0 && stretchHigh > 32767)) @@ -606,22 +546,22 @@ NotifySliceImageError(operation); return; } - + // Decode a grayscale JPEG 8bpp image coming from the Web viewer - std::auto_ptr image - (new Orthanc::Image(expectedFormat, reader->GetWidth(), reader->GetHeight(), false)); - + boost::shared_ptr image + (new Orthanc::Image(expectedFormat, reader->GetWidth(), reader->GetHeight(), false)); + Orthanc::ImageProcessing::Convert(*image, *reader); - reader.reset(NULL); - + reader.reset(); + float scaling = static_cast(stretchHigh - stretchLow) / 255.0f; - + if (!LinearAlgebra::IsCloseToZero(scaling)) { float offset = static_cast(stretchLow) / scaling; Orthanc::ImageProcessing::ShiftScale(*image, offset, scaling, true); } - + NotifySliceImageSuccess(operation, image); } @@ -641,25 +581,23 @@ { throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageFormat); } - + buffer_.swap(buffer); // The source buffer is now empty - + void* data = (buffer_.empty() ? NULL : &buffer_[0]); - + AssignWritable(format, width, height, Orthanc::GetBytesPerPixel(format) * width, data); } }; - - void OrthancSlicesLoader::ParseSliceRawImage(const Operation& operation, - const void* answer, - size_t size) + void OrthancSlicesLoader::ParseSliceRawImage(const OrthancApiClient::BinaryResponseReadyMessage& message) { + const Operation& operation = dynamic_cast(*message.Payload.get()); Orthanc::GzipCompressor compressor; - + std::string raw; - compressor.Uncompress(raw, answer, size); + compressor.Uncompress(raw, message.Answer, message.AnswerSize); const Orthanc::DicomImageInformation& info = operation.GetSlice().GetImageInformation(); @@ -673,10 +611,10 @@ { // This is the case of RT-DOSE (uint32_t values) - std::auto_ptr image - (new StringImage(Orthanc::PixelFormat_Grayscale32, info.GetWidth(), - info.GetHeight(), raw)); - + boost::shared_ptr image + (new StringImage(Orthanc::PixelFormat_Grayscale32, info.GetWidth(), + info.GetHeight(), raw)); + // TODO - Only for big endian for (unsigned int y = 0; y < image->GetHeight(); y++) { @@ -686,7 +624,7 @@ *p = le32toh(*p); } } - + NotifySliceImageSuccess(operation, image); } else if (info.GetBitsAllocated() == 16 && @@ -697,31 +635,31 @@ info.GetPhotometricInterpretation() == Orthanc::PhotometricInterpretation_Monochrome2 && raw.size() == info.GetWidth() * info.GetHeight() * 2) { - std::auto_ptr image - (new StringImage(Orthanc::PixelFormat_Grayscale16, info.GetWidth(), - info.GetHeight(), raw)); - + boost::shared_ptr image + (new StringImage(Orthanc::PixelFormat_Grayscale16, info.GetWidth(), + info.GetHeight(), raw)); + // TODO - Big endian ? - + NotifySliceImageSuccess(operation, image); } else { throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); } - + } - - - OrthancSlicesLoader::OrthancSlicesLoader(ICallback& callback, - IWebService& orthanc) : - webCallback_(new WebCallback(*this)), - userCallback_(callback), + + + OrthancSlicesLoader::OrthancSlicesLoader(MessageBroker& broker, + OrthancApiClient& orthanc) : + IObservable(broker), + IObserver(broker), orthanc_(orthanc), state_(State_Initialization) { } - + void OrthancSlicesLoader::ScheduleLoadSeries(const std::string& seriesId) { @@ -732,12 +670,13 @@ else { state_ = State_LoadingGeometry; - std::string uri = "/series/" + seriesId + "/instances-tags"; - orthanc_.ScheduleGetRequest(*webCallback_, uri, Operation::DownloadSeriesGeometry()); + orthanc_.GetJsonAsync("/series/" + seriesId + "/instances-tags", + new Callable(*this, &OrthancSlicesLoader::ParseSeriesGeometry), + new Callable(*this, &OrthancSlicesLoader::OnGeometryError), + NULL); } } - - + void OrthancSlicesLoader::ScheduleLoadInstance(const std::string& instanceId) { if (state_ != State_Initialization) @@ -747,16 +686,17 @@ else { state_ = State_LoadingGeometry; - + // Tag "3004-000c" is "Grid Frame Offset Vector", which is // mandatory to read RT DOSE, but is too long to be returned by default - std::string uri = "/instances/" + instanceId + "/tags?ignore-length=3004-000c"; - orthanc_.ScheduleGetRequest - (*webCallback_, uri, Operation::DownloadInstanceGeometry(instanceId)); + orthanc_.GetJsonAsync("/instances/" + instanceId + "/tags?ignore-length=3004-000c", + new Callable(*this, &OrthancSlicesLoader::ParseInstanceGeometry), + new Callable(*this, &OrthancSlicesLoader::OnGeometryError), + Operation::DownloadInstanceGeometry(instanceId)); } } - + void OrthancSlicesLoader::ScheduleLoadFrame(const std::string& instanceId, unsigned int frame) { @@ -767,29 +707,31 @@ else { state_ = State_LoadingGeometry; - std::string uri = "/instances/" + instanceId + "/tags"; - orthanc_.ScheduleGetRequest - (*webCallback_, uri, Operation::DownloadFrameGeometry(instanceId, frame)); + + orthanc_.GetJsonAsync("/instances/" + instanceId + "/tags", + new Callable(*this, &OrthancSlicesLoader::ParseFrameGeometry), + new Callable(*this, &OrthancSlicesLoader::OnGeometryError), + Operation::DownloadFrameGeometry(instanceId, frame)); } } - + bool OrthancSlicesLoader::IsGeometryReady() const { return state_ == State_GeometryReady; } - - + + size_t OrthancSlicesLoader::GetSliceCount() const { if (state_ != State_GeometryReady) { throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); } - + return slices_.GetSliceCount(); } - + const Slice& OrthancSlicesLoader::GetSlice(size_t index) const { @@ -797,11 +739,11 @@ { throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); } - + return slices_.GetSlice(index); } - + bool OrthancSlicesLoader::LookupSlice(size_t& index, const CoordinateSystem3D& plane) const { @@ -809,76 +751,111 @@ { throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); } - + return slices_.LookupSlice(index, plane); } - + void OrthancSlicesLoader::ScheduleSliceImagePng(const Slice& slice, size_t index) { - std::string uri = ("/instances/" + slice.GetOrthancInstanceId() + "/frames/" + + std::string uri = ("/instances/" + slice.GetOrthancInstanceId() + "/frames/" + + boost::lexical_cast(slice.GetFrame())); + + switch (slice.GetConverter().GetExpectedPixelFormat()) + { + case Orthanc::PixelFormat_RGB24: + uri += "/preview"; + break; + + case Orthanc::PixelFormat_Grayscale16: + uri += "/image-uint16"; + break; + + case Orthanc::PixelFormat_SignedGrayscale16: + uri += "/image-int16"; + break; + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + + orthanc_.GetBinaryAsync(uri, "image/png", + new Callable(*this, &OrthancSlicesLoader::ParseSliceImagePng), + new Callable(*this, &OrthancSlicesLoader::OnSliceImageError), + Operation::DownloadSliceImage(index, slice, SliceImageQuality_FullPng)); + } + + void OrthancSlicesLoader::ScheduleSliceImagePam(const Slice& slice, + size_t index) + { + std::string uri = ("/instances/" + slice.GetOrthancInstanceId() + "/frames/" + boost::lexical_cast(slice.GetFrame())); switch (slice.GetConverter().GetExpectedPixelFormat()) { - case Orthanc::PixelFormat_RGB24: - uri += "/preview"; - break; + case Orthanc::PixelFormat_RGB24: + uri += "/preview"; + break; - case Orthanc::PixelFormat_Grayscale16: - uri += "/image-uint16"; - break; + case Orthanc::PixelFormat_Grayscale16: + uri += "/image-uint16"; + break; - case Orthanc::PixelFormat_SignedGrayscale16: - uri += "/image-int16"; - break; + case Orthanc::PixelFormat_SignedGrayscale16: + uri += "/image-int16"; + break; - default: - throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); } - orthanc_.ScheduleGetRequest(*webCallback_, uri, - Operation::DownloadSliceImage(index, slice, SliceImageQuality_Full)); + orthanc_.GetBinaryAsync(uri, "image/x-portable-arbitrarymap", + new Callable(*this, &OrthancSlicesLoader::ParseSliceImagePam), + new Callable(*this, &OrthancSlicesLoader::OnSliceImageError), + Operation::DownloadSliceImage(index, slice, SliceImageQuality_FullPam)); } + void OrthancSlicesLoader::ScheduleSliceImageJpeg(const Slice& slice, size_t index, SliceImageQuality quality) { unsigned int value; - + switch (quality) { - case SliceImageQuality_Jpeg50: - value = 50; - break; - - case SliceImageQuality_Jpeg90: - value = 90; - break; - - case SliceImageQuality_Jpeg95: - value = 95; - break; + case SliceImageQuality_Jpeg50: + value = 50; + break; - default: - throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + case SliceImageQuality_Jpeg90: + value = 90; + break; + + case SliceImageQuality_Jpeg95: + value = 95; + break; + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); } // This requires the official Web viewer plugin to be installed! - std::string uri = ("/web-viewer/instances/jpeg" + - boost::lexical_cast(value) + - "-" + slice.GetOrthancInstanceId() + "_" + + std::string uri = ("/web-viewer/instances/jpeg" + + boost::lexical_cast(value) + + "-" + slice.GetOrthancInstanceId() + "_" + boost::lexical_cast(slice.GetFrame())); - - orthanc_.ScheduleGetRequest(*webCallback_, uri, - Operation::DownloadSliceImage(index, slice, quality)); + + orthanc_.GetJsonAsync(uri, + new Callable(*this, &OrthancSlicesLoader::ParseSliceImageJpeg), + new Callable(*this, &OrthancSlicesLoader::OnSliceImageError), + Operation::DownloadSliceImage(index, slice, quality)); } - - - + + + void OrthancSlicesLoader::ScheduleLoadSliceImage(size_t index, SliceImageQuality quality) { @@ -886,26 +863,31 @@ { throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); } - + const Slice& slice = GetSlice(index); - + if (slice.HasOrthancDecoding()) { - if (quality == SliceImageQuality_Full) + switch (quality) { + case SliceImageQuality_FullPng: ScheduleSliceImagePng(slice, index); - } - else - { + break; + case SliceImageQuality_FullPam: + ScheduleSliceImagePam(slice, index); + break; + default: ScheduleSliceImageJpeg(slice, index, quality); } } else { - std::string uri = ("/instances/" + slice.GetOrthancInstanceId() + "/frames/" + + std::string uri = ("/instances/" + slice.GetOrthancInstanceId() + "/frames/" + boost::lexical_cast(slice.GetFrame()) + "/raw.gz"); - orthanc_.ScheduleGetRequest(*webCallback_, uri, - Operation::DownloadSliceRawImage(index, slice)); + orthanc_.GetBinaryAsync(uri, IWebService::Headers(), + new Callable(*this, &OrthancSlicesLoader::ParseSliceRawImage), + new Callable(*this, &OrthancSlicesLoader::OnSliceImageError), + Operation::DownloadSliceRawImage(index, slice)); } } } diff -r fe4befe03935 -r d6136a7e914d Framework/Toolbox/OrthancSlicesLoader.h --- a/Framework/Toolbox/OrthancSlicesLoader.h Mon Nov 05 10:04:56 2018 +0100 +++ b/Framework/Toolbox/OrthancSlicesLoader.h Mon Nov 05 10:06:18 2018 +0100 @@ -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 . **/ @@ -24,35 +24,56 @@ #include "IWebService.h" #include "SlicesSorter.h" #include "../StoneEnumerations.h" +#include "../Messages/IObservable.h" +#include +#include "OrthancApiClient.h" +#include "Core/Images/Image.h" -#include namespace OrthancStone { - class OrthancSlicesLoader : public boost::noncopyable + class OrthancSlicesLoader : public IObservable, public IObserver { public: - class ICallback : public boost::noncopyable + + typedef OriginMessage SliceGeometryReadyMessage; + typedef OriginMessage SliceGeometryErrorMessage; + + struct SliceImageReadyMessage : public BaseMessage { - public: - virtual ~ICallback() + unsigned int sliceIndex_; + const Slice& slice_; + boost::shared_ptr image_; + SliceImageQuality effectiveQuality_; + + SliceImageReadyMessage(unsigned int sliceIndex, + const Slice& slice, + boost::shared_ptr image, + SliceImageQuality effectiveQuality) + : BaseMessage(), + sliceIndex_(sliceIndex), + slice_(slice), + image_(image), + effectiveQuality_(effectiveQuality) { } + }; - virtual void NotifyGeometryReady(const OrthancSlicesLoader& loader) = 0; - - virtual void NotifyGeometryError(const OrthancSlicesLoader& loader) = 0; + struct SliceImageErrorMessage : public BaseMessage + { + const Slice& slice_; + unsigned int sliceIndex_; + SliceImageQuality effectiveQuality_; - virtual void NotifySliceImageReady(const OrthancSlicesLoader& loader, - unsigned int sliceIndex, - const Slice& slice, - std::auto_ptr& image, - SliceImageQuality effectiveQuality) = 0; - - virtual void NotifySliceImageError(const OrthancSlicesLoader& loader, - unsigned int sliceIndex, - const Slice& slice, - SliceImageQuality quality) = 0; + SliceImageErrorMessage(unsigned int sliceIndex, + const Slice& slice, + SliceImageQuality effectiveQuality) + : BaseMessage(), + slice_(slice), + sliceIndex_(sliceIndex), + effectiveQuality_(effectiveQuality) + { + } }; private: @@ -70,51 +91,44 @@ Mode_InstanceGeometry, Mode_FrameGeometry, Mode_LoadImage, - Mode_LoadRawImage + Mode_LoadRawImage, + Mode_LoadDicomFile }; class Operation; - class WebCallback; - boost::shared_ptr webCallback_; // This is a PImpl pattern - - ICallback& userCallback_; - IWebService& orthanc_; + OrthancApiClient& orthanc_; State state_; SlicesSorter slices_; void NotifySliceImageSuccess(const Operation& operation, - std::auto_ptr& image) const; - - void NotifySliceImageError(const Operation& operation) const; - - void ParseSeriesGeometry(const void* answer, - size_t size); + boost::shared_ptr image); + + void NotifySliceImageError(const Operation& operation); - void ParseInstanceGeometry(const std::string& instanceId, - const void* answer, - size_t size); + void OnGeometryError(const OrthancApiClient::HttpErrorMessage& message); + void OnSliceImageError(const OrthancApiClient::HttpErrorMessage& message); + + void ParseSeriesGeometry(const OrthancApiClient::JsonResponseReadyMessage& message); - void ParseFrameGeometry(const std::string& instanceId, - unsigned int frame, - const void* answer, - size_t size); + void ParseInstanceGeometry(const OrthancApiClient::JsonResponseReadyMessage& message); + + void ParseFrameGeometry(const OrthancApiClient::JsonResponseReadyMessage& message); + + void ParseSliceImagePng(const OrthancApiClient::BinaryResponseReadyMessage& message); - void ParseSliceImagePng(const Operation& operation, - const void* answer, - size_t size); + void ParseSliceImagePam(const OrthancApiClient::BinaryResponseReadyMessage& message); - void ParseSliceImageJpeg(const Operation& operation, - const void* answer, - size_t size); + void ParseSliceImageJpeg(const OrthancApiClient::JsonResponseReadyMessage& message); - void ParseSliceRawImage(const Operation& operation, - const void* answer, - size_t size); + void ParseSliceRawImage(const OrthancApiClient::BinaryResponseReadyMessage& message); void ScheduleSliceImagePng(const Slice& slice, size_t index); - + + void ScheduleSliceImagePam(const Slice& slice, + size_t index); + void ScheduleSliceImageJpeg(const Slice& slice, size_t index, SliceImageQuality quality); @@ -122,8 +136,9 @@ void SortAndFinalizeSlices(); public: - OrthancSlicesLoader(ICallback& callback, - IWebService& orthanc); + OrthancSlicesLoader(MessageBroker& broker, + //ISliceLoaderObserver& callback, + OrthancApiClient& orthancApi); void ScheduleLoadSeries(const std::string& seriesId); @@ -143,5 +158,7 @@ void ScheduleLoadSliceImage(size_t index, SliceImageQuality requestedQuality); + + }; } diff -r fe4befe03935 -r d6136a7e914d Framework/Toolbox/ShearWarpProjectiveTransform.cpp --- a/Framework/Toolbox/ShearWarpProjectiveTransform.cpp Mon Nov 05 10:04:56 2018 +0100 +++ b/Framework/Toolbox/ShearWarpProjectiveTransform.cpp Mon Nov 05 10:06:18 2018 +0100 @@ -476,7 +476,7 @@ ApplyAffineTransform(*intermediate, reader.GetAccessor(), a11, 0, b1, 0, a22, b2, - shearInterpolation); + shearInterpolation, true); } @@ -582,7 +582,7 @@ } // (5.b) Apply the projective transform to the image - ApplyProjectiveTransform(target, *intermediate, warp, warpInterpolation); + ApplyProjectiveTransform(target, *intermediate, warp, warpInterpolation, true); } diff -r fe4befe03935 -r d6136a7e914d Framework/Toolbox/Slice.cpp --- a/Framework/Toolbox/Slice.cpp Mon Nov 05 10:04:56 2018 +0100 +++ b/Framework/Toolbox/Slice.cpp Mon Nov 05 10:06:18 2018 +0100 @@ -45,6 +45,28 @@ return false; } } + + Slice* Slice::Clone() const + { + std::auto_ptr target(new Slice()); + + target->type_ = type_; + target->orthancInstanceId_ = orthancInstanceId_; + target->sopClassUid_ = sopClassUid_; + target->frame_ = frame_; + target->frameCount_ = frameCount_; + target->geometry_ = geometry_; + target->pixelSpacingX_ = pixelSpacingX_; + target->pixelSpacingY_ = pixelSpacingY_; + target->thickness_ = thickness_; + target->width_ = width_; + target->height_ = height_; + target->converter_ = converter_; + if (imageInformation_.get() != NULL) + target->imageInformation_.reset(imageInformation_->Clone()); + + return target.release(); + } bool Slice::ComputeRTDoseGeometry(const Orthanc::DicomMap& dataset, unsigned int frame) diff -r fe4befe03935 -r d6136a7e914d Framework/Toolbox/Slice.h --- a/Framework/Toolbox/Slice.h Mon Nov 05 10:04:56 2018 +0100 +++ b/Framework/Toolbox/Slice.h Mon Nov 05 10:06:18 2018 +0100 @@ -61,6 +61,16 @@ public: Slice() : type_(Type_Invalid) + { + } + + + // this constructor is used to reference, i.e, a slice that is being loaded + Slice(const std::string& orthancInstanceId, + unsigned int frame) : + type_(Type_Invalid), + orthancInstanceId_(orthancInstanceId), + frame_(frame) { } @@ -136,5 +146,7 @@ void GetExtent(std::vector& points) const; const Orthanc::DicomImageInformation& GetImageInformation() const; + + Slice* Clone() const; }; } diff -r fe4befe03935 -r d6136a7e914d Framework/Toolbox/ViewportGeometry.cpp --- a/Framework/Toolbox/ViewportGeometry.cpp Mon Nov 05 10:04:56 2018 +0100 +++ b/Framework/Toolbox/ViewportGeometry.cpp Mon Nov 05 10:06:18 2018 +0100 @@ -121,7 +121,19 @@ } - void ViewportGeometry::SetDefaultView() + void ViewportGeometry::MapPixelCenterToScene(double& sceneX, + double& sceneY, + int x, + int y) const + { + // Take the center of the pixel + MapDisplayToScene(sceneX, sceneY, + static_cast(x) + 0.5, + static_cast(y) + 0.5); + } + + + void ViewportGeometry::FitContent() { if (width_ > 0 && height_ > 0 && @@ -167,4 +179,22 @@ zoom_ = zoom; ComputeTransform(); } + + + Matrix ViewportGeometry::GetMatrix() const + { + Matrix m(3, 3); + + m(0, 0) = transform_.xx; + m(0, 1) = transform_.xy; + m(0, 2) = transform_.x0; + m(1, 0) = transform_.yx; + m(1, 1) = transform_.yy; + m(1, 2) = transform_.y0; + m(2, 0) = 0; + m(2, 1) = 0; + m(2, 2) = 1; + + return m; + } } diff -r fe4befe03935 -r d6136a7e914d Framework/Toolbox/ViewportGeometry.h --- a/Framework/Toolbox/ViewportGeometry.h Mon Nov 05 10:04:56 2018 +0100 +++ b/Framework/Toolbox/ViewportGeometry.h Mon Nov 05 10:06:18 2018 +0100 @@ -22,7 +22,8 @@ #pragma once #include "../Viewport/CairoContext.h" -#include "../Toolbox/Extent2D.h" +#include "Extent2D.h" +#include "LinearAlgebra.h" namespace OrthancStone { @@ -63,6 +64,11 @@ double x, double y) const; + void MapPixelCenterToScene(double& sceneX /* out */, + double& sceneY /* out */, + int x, + int y) const; + void MapSceneToDisplay(int& displayX /* out */, int& displayY /* out */, double x, @@ -83,7 +89,7 @@ return zoom_; } - void SetDefaultView(); + void FitContent(); void ApplyTransform(CairoContext& context) const; @@ -94,5 +100,7 @@ double y); void SetZoom(double zoom); + + Matrix GetMatrix() const; }; } diff -r fe4befe03935 -r d6136a7e914d Framework/Viewport/CairoContext.cpp --- a/Framework/Viewport/CairoContext.cpp Mon Nov 05 10:04:56 2018 +0100 +++ b/Framework/Viewport/CairoContext.cpp Mon Nov 05 10:06:18 2018 +0100 @@ -24,9 +24,12 @@ #include #include + namespace OrthancStone { - CairoContext::CairoContext(CairoSurface& surface) + CairoContext::CairoContext(CairoSurface& surface) : + width_(surface.GetWidth()), + height_(surface.GetHeight()) { context_ = cairo_create(surface.GetObject()); if (!context_) @@ -56,4 +59,88 @@ static_cast(green) / 255.0f, static_cast(blue) / 255.0f); } + + + class CairoContext::AlphaSurface : public boost::noncopyable + { + private: + cairo_surface_t *surface_; + + public: + AlphaSurface(unsigned int width, + unsigned int height) + { + surface_ = cairo_image_surface_create(CAIRO_FORMAT_A8, width, height); + + if (!surface_) + { + // Should never occur + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + + if (cairo_surface_status(surface_) != CAIRO_STATUS_SUCCESS) + { + LOG(ERROR) << "Cannot create a Cairo surface"; + cairo_surface_destroy(surface_); + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + } + + ~AlphaSurface() + { + cairo_surface_destroy(surface_); + } + + void GetAccessor(Orthanc::ImageAccessor& target) + { + target.AssignWritable(Orthanc::PixelFormat_Grayscale8, + cairo_image_surface_get_width(surface_), + cairo_image_surface_get_height(surface_), + cairo_image_surface_get_stride(surface_), + cairo_image_surface_get_data(surface_)); + } + + void Blit(cairo_t* cr, + double x, + double y) + { + cairo_surface_mark_dirty(surface_); + cairo_mask_surface(cr, surface_, x, y); + cairo_fill(cr); + } + }; + + + void CairoContext::DrawText(const Orthanc::Font& font, + const std::string& text, + double x, + double y, + BitmapAnchor anchor) + { + // Render a bitmap containing the text + unsigned int width, height; + font.ComputeTextExtent(width, height, text); + + AlphaSurface surface(width, height); + + Orthanc::ImageAccessor accessor; + surface.GetAccessor(accessor); + font.Draw(accessor, text, 0, 0, 255); + + // Correct the text location given the anchor location + double deltaX, deltaY; + ComputeAnchorTranslation(deltaX, deltaY, anchor, width, height); + + // Cancel zoom/rotation before blitting the text onto the surface + double pixelX = x; + double pixelY = y; + cairo_user_to_device(context_, &pixelX, &pixelY); + + cairo_save(context_); + cairo_identity_matrix(context_); + + // Blit the text bitmap + surface.Blit(context_, pixelX + deltaX, pixelY + deltaY); + cairo_restore(context_); + } } diff -r fe4befe03935 -r d6136a7e914d Framework/Viewport/CairoContext.h --- a/Framework/Viewport/CairoContext.h Mon Nov 05 10:04:56 2018 +0100 +++ b/Framework/Viewport/CairoContext.h Mon Nov 05 10:06:18 2018 +0100 @@ -22,6 +22,9 @@ #pragma once #include "CairoSurface.h" +#include "../StoneEnumerations.h" + +#include namespace OrthancStone { @@ -29,7 +32,11 @@ class CairoContext : public boost::noncopyable { private: - cairo_t* context_; + class AlphaSurface; + + cairo_t* context_; + unsigned int width_; + unsigned int height_; public: CairoContext(CairoSurface& surface); @@ -41,6 +48,16 @@ return context_; } + unsigned int GetWidth() const + { + return width_; + } + + unsigned int GetHeight() const + { + return height_; + } + void SetSourceColor(uint8_t red, uint8_t green, uint8_t blue); @@ -49,5 +66,11 @@ { SetSourceColor(color[0], color[1], color[2]); } + + void DrawText(const Orthanc::Font& font, + const std::string& text, + double x, + double y, + BitmapAnchor anchor); }; } diff -r fe4befe03935 -r d6136a7e914d Framework/Viewport/CairoFont.h --- a/Framework/Viewport/CairoFont.h Mon Nov 05 10:04:56 2018 +0100 +++ b/Framework/Viewport/CairoFont.h Mon Nov 05 10:06:18 2018 +0100 @@ -21,6 +21,14 @@ #pragma once +#if !defined(ORTHANC_SANDBOXED) +# error The macro ORTHANC_SANDBOXED must be defined +#endif + +#if ORTHANC_SANDBOXED == 1 +# error The class CairoFont cannot be used in sandboxed environments +#endif + #include "CairoContext.h" namespace OrthancStone diff -r fe4befe03935 -r d6136a7e914d Framework/Viewport/CairoSurface.cpp --- a/Framework/Viewport/CairoSurface.cpp Mon Nov 05 10:04:56 2018 +0100 +++ b/Framework/Viewport/CairoSurface.cpp Mon Nov 05 10:06:18 2018 +0100 @@ -106,6 +106,7 @@ void CairoSurface::Copy(const CairoSurface& other) { Orthanc::ImageAccessor source, target; + other.GetReadOnlyAccessor(source); GetWriteableAccessor(target); diff -r fe4befe03935 -r d6136a7e914d Framework/Viewport/CairoSurface.h diff -r fe4befe03935 -r d6136a7e914d Framework/Viewport/IMouseTracker.h --- a/Framework/Viewport/IMouseTracker.h Mon Nov 05 10:04:56 2018 +0100 +++ b/Framework/Viewport/IMouseTracker.h Mon Nov 05 10:06:18 2018 +0100 @@ -25,6 +25,9 @@ namespace OrthancStone { + // this is tracking a mouse in screen coordinates/pixels unlike + // the IWorldSceneMouseTracker that is tracking a mouse + // in scene coordinates/mm. class IMouseTracker : public boost::noncopyable { public: diff -r fe4befe03935 -r d6136a7e914d Framework/Viewport/IViewport.h --- a/Framework/Viewport/IViewport.h Mon Nov 05 10:04:56 2018 +0100 +++ b/Framework/Viewport/IViewport.h Mon Nov 05 10:06:18 2018 +0100 @@ -40,14 +40,14 @@ { } - virtual void NotifyChange(const IViewport& scene) = 0; + virtual void OnViewportContentChanged(const IViewport& scene) = 0; }; virtual ~IViewport() { } - virtual void SetDefaultView() = 0; + virtual void FitContent() = 0; virtual void Register(IObserver& observer) = 0; @@ -78,7 +78,8 @@ int y, KeyboardModifiers modifiers) = 0; - virtual void KeyPressed(char key, + virtual void KeyPressed(KeyboardKeys key, + char keyChar, KeyboardModifiers modifiers) = 0; virtual bool HasUpdateContent() = 0; @@ -86,6 +87,6 @@ virtual void UpdateContent() = 0; // Should only be called from IWidget - virtual void NotifyChange(const IWidget& widget) = 0; + virtual void NotifyContentChanged(const IWidget& widget) = 0; }; } diff -r fe4befe03935 -r d6136a7e914d Framework/Viewport/WidgetViewport.cpp --- a/Framework/Viewport/WidgetViewport.cpp Mon Nov 05 10:04:56 2018 +0100 +++ b/Framework/Viewport/WidgetViewport.cpp Mon Nov 05 10:06:18 2018 +0100 @@ -36,11 +36,11 @@ } - void WidgetViewport::SetDefaultView() + void WidgetViewport::FitContent() { if (centralWidget_.get() != NULL) { - centralWidget_->SetDefaultView(); + centralWidget_->FitContent(); } } @@ -64,7 +64,7 @@ } mouseTracker_.reset(NULL); - + centralWidget_.reset(widget); centralWidget_->SetViewport(*this); @@ -74,16 +74,16 @@ } backgroundChanged_ = true; - observers_.Apply(*this, &IObserver::NotifyChange); + observers_.Apply(*this, &IObserver::OnViewportContentChanged); return *widget; } - void WidgetViewport::NotifyChange(const IWidget& widget) + void WidgetViewport::NotifyContentChanged(const IWidget& widget) { backgroundChanged_ = true; - observers_.Apply(*this, &IObserver::NotifyChange); + observers_.Apply(*this, &IObserver::OnViewportContentChanged); } @@ -97,7 +97,7 @@ centralWidget_->SetSize(width, height); } - observers_.Apply(*this, &IObserver::NotifyChange); + observers_.Apply(*this, &IObserver::OnViewportContentChanged); } @@ -155,7 +155,7 @@ mouseTracker_.reset(NULL); } - observers_.Apply(*this, &IObserver::NotifyChange); + observers_.Apply(*this, &IObserver::OnViewportContentChanged); } @@ -165,7 +165,7 @@ { mouseTracker_->MouseUp(); mouseTracker_.reset(NULL); - observers_.Apply(*this, &IObserver::NotifyChange); + observers_.Apply(*this, &IObserver::OnViewportContentChanged); } } @@ -196,7 +196,7 @@ if (repaint) { // The scene must be repainted, notify the observers - observers_.Apply(*this, &IObserver::NotifyChange); + observers_.Apply(*this, &IObserver::OnViewportContentChanged); } } @@ -204,7 +204,7 @@ void WidgetViewport::MouseEnter() { isMouseOver_ = true; - observers_.Apply(*this, &IObserver::NotifyChange); + observers_.Apply(*this, &IObserver::OnViewportContentChanged); } @@ -218,7 +218,7 @@ mouseTracker_.reset(NULL); } - observers_.Apply(*this, &IObserver::NotifyChange); + observers_.Apply(*this, &IObserver::OnViewportContentChanged); } @@ -235,13 +235,14 @@ } - void WidgetViewport::KeyPressed(char key, + void WidgetViewport::KeyPressed(KeyboardKeys key, + char keyChar, KeyboardModifiers modifiers) { if (centralWidget_.get() != NULL && mouseTracker_.get() == NULL) { - centralWidget_->KeyPressed(key, modifiers); + centralWidget_->KeyPressed(key, keyChar, modifiers); } } diff -r fe4befe03935 -r d6136a7e914d Framework/Viewport/WidgetViewport.h --- a/Framework/Viewport/WidgetViewport.h Mon Nov 05 10:04:56 2018 +0100 +++ b/Framework/Viewport/WidgetViewport.h Mon Nov 05 10:06:18 2018 +0100 @@ -45,13 +45,13 @@ public: WidgetViewport(); - virtual void SetDefaultView(); + virtual void FitContent(); virtual void SetStatusBar(IStatusBar& statusBar); IWidget& SetCentralWidget(IWidget* widget); // Takes ownership - virtual void NotifyChange(const IWidget& widget); + virtual void NotifyContentChanged(const IWidget& widget); virtual void Register(IObserver& observer) { @@ -82,7 +82,8 @@ int y, KeyboardModifiers modifiers); - virtual void KeyPressed(char key, + virtual void KeyPressed(KeyboardKeys key, + char keyChar, KeyboardModifiers modifiers); virtual bool HasUpdateContent(); diff -r fe4befe03935 -r d6136a7e914d Framework/Volumes/ImageBuffer3D.cpp --- a/Framework/Volumes/ImageBuffer3D.cpp Mon Nov 05 10:04:56 2018 +0100 +++ b/Framework/Volumes/ImageBuffer3D.cpp Mon Nov 05 10:06:18 2018 +0100 @@ -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 . **/ @@ -91,7 +91,7 @@ for (unsigned int y = 0; y < height_; y++) { - const void* source = (reinterpret_cast(image_.GetConstRow(y + z * height_)) + + const void* source = (reinterpret_cast(image_.GetConstRow(y + z * height_)) + bytesPerPixel * slice); memcpy(target, source, bytesPerPixel); @@ -157,20 +157,20 @@ Vector result; switch (projection) { - case VolumeProjection_Axial: - result = voxelDimensions_; - break; + case VolumeProjection_Axial: + result = voxelDimensions_; + break; - case VolumeProjection_Coronal: - LinearAlgebra::AssignVector(result, voxelDimensions_[0], voxelDimensions_[2], voxelDimensions_[1]); - break; + case VolumeProjection_Coronal: + LinearAlgebra::AssignVector(result, voxelDimensions_[0], voxelDimensions_[2], voxelDimensions_[1]); + break; - case VolumeProjection_Sagittal: - LinearAlgebra::AssignVector(result, voxelDimensions_[1], voxelDimensions_[2], voxelDimensions_[0]); - break; + case VolumeProjection_Sagittal: + LinearAlgebra::AssignVector(result, voxelDimensions_[1], voxelDimensions_[2], voxelDimensions_[0]); + break; - default: - throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); } return result; @@ -183,23 +183,23 @@ { switch (projection) { - case VolumeProjection_Axial: - width = width_; - height = height_; - break; + case VolumeProjection_Axial: + width = width_; + height = height_; + break; - case VolumeProjection_Coronal: - width = width_; - height = depth_; - break; + case VolumeProjection_Coronal: + width = width_; + height = depth_; + break; - case VolumeProjection_Sagittal: - width = height_; - height = depth_; - break; + case VolumeProjection_Sagittal: + width = height_; + height = depth_; + break; - default: - throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); } } @@ -210,51 +210,51 @@ switch (projection) { - case VolumeProjection_Axial: - for (unsigned int z = 0; z < depth_; z++) - { - Vector origin = axialGeometry_.GetOrigin(); - origin += static_cast(z) * voxelDimensions_[2] * axialGeometry_.GetNormal(); + case VolumeProjection_Axial: + for (unsigned int z = 0; z < depth_; z++) + { + Vector origin = axialGeometry_.GetOrigin(); + origin += static_cast(z) * voxelDimensions_[2] * axialGeometry_.GetNormal(); - result->AddSlice(origin, - axialGeometry_.GetAxisX(), - axialGeometry_.GetAxisY()); - } - break; + result->AddSlice(origin, + axialGeometry_.GetAxisX(), + axialGeometry_.GetAxisY()); + } + break; - case VolumeProjection_Coronal: - for (unsigned int y = 0; y < height_; y++) - { - Vector origin = axialGeometry_.GetOrigin(); - origin += static_cast(y) * voxelDimensions_[1] * axialGeometry_.GetAxisY(); - origin += static_cast(depth_ - 1) * voxelDimensions_[2] * axialGeometry_.GetNormal(); + case VolumeProjection_Coronal: + for (unsigned int y = 0; y < height_; y++) + { + Vector origin = axialGeometry_.GetOrigin(); + origin += static_cast(y) * voxelDimensions_[1] * axialGeometry_.GetAxisY(); + origin += static_cast(depth_ - 1) * voxelDimensions_[2] * axialGeometry_.GetNormal(); - result->AddSlice(origin, - axialGeometry_.GetAxisX(), - -axialGeometry_.GetNormal()); - } - break; + result->AddSlice(origin, + axialGeometry_.GetAxisX(), + -axialGeometry_.GetNormal()); + } + break; - case VolumeProjection_Sagittal: - for (unsigned int x = 0; x < width_; x++) - { - Vector origin = axialGeometry_.GetOrigin(); - origin += static_cast(x) * voxelDimensions_[0] * axialGeometry_.GetAxisX(); - origin += static_cast(depth_ - 1) * voxelDimensions_[2] * axialGeometry_.GetNormal(); + case VolumeProjection_Sagittal: + for (unsigned int x = 0; x < width_; x++) + { + Vector origin = axialGeometry_.GetOrigin(); + origin += static_cast(x) * voxelDimensions_[0] * axialGeometry_.GetAxisX(); + origin += static_cast(depth_ - 1) * voxelDimensions_[2] * axialGeometry_.GetNormal(); - result->AddSlice(origin, - axialGeometry_.GetAxisY(), - -axialGeometry_.GetNormal()); - } - break; + result->AddSlice(origin, + axialGeometry_.GetAxisY(), + -axialGeometry_.GetNormal()); + } + break; - default: - throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); } return result.release(); } - + uint64_t ImageBuffer3D::GetEstimatedMemorySize() const { @@ -272,27 +272,27 @@ } float sliceMin, sliceMax; - + switch (slice.GetFormat()) { - case Orthanc::PixelFormat_Grayscale8: - case Orthanc::PixelFormat_Grayscale16: - case Orthanc::PixelFormat_Grayscale32: - case Orthanc::PixelFormat_SignedGrayscale16: - { - int64_t a, b; - Orthanc::ImageProcessing::GetMinMaxIntegerValue(a, b, slice); - sliceMin = static_cast(a); - sliceMax = static_cast(b); - break; - } + case Orthanc::PixelFormat_Grayscale8: + case Orthanc::PixelFormat_Grayscale16: + case Orthanc::PixelFormat_Grayscale32: + case Orthanc::PixelFormat_SignedGrayscale16: + { + int64_t a, b; + Orthanc::ImageProcessing::GetMinMaxIntegerValue(a, b, slice); + sliceMin = static_cast(a); + sliceMax = static_cast(b); + break; + } - case Orthanc::PixelFormat_Float32: - Orthanc::ImageProcessing::GetMinMaxFloatValue(sliceMin, sliceMax, slice); - break; + case Orthanc::PixelFormat_Float32: + Orthanc::ImageProcessing::GetMinMaxFloatValue(sliceMin, sliceMax, slice); + break; - default: - return; + default: + return; } if (hasRange_) @@ -366,8 +366,8 @@ sagittal_->GetReadOnlyAccessor(accessor_); break; - default: - throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); } } @@ -379,7 +379,7 @@ if (sagittal_.get() != NULL) { // TODO - throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); } // Update the dynamic range of the underlying image, if @@ -410,8 +410,8 @@ sagittal_->GetWriteableAccessor(accessor_); break; - default: - throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); } } @@ -467,11 +467,11 @@ const CoordinateSystem3D& axial = GetAxialGeometry(); Vector origin = (axial.MapSliceToWorldCoordinates(-0.5 * ps[0], -0.5 * ps[1]) - - 0.5 * ps[2] * axial.GetNormal()); + 0.5 * ps[2] * axial.GetNormal()); return (origin + axial.GetAxisX() * ps[0] * x * static_cast(GetWidth()) + - axial.GetAxisY() * ps[1] * y * static_cast(GetHeight()) + - axial.GetNormal() * ps[2] * z * static_cast(GetDepth())); + axial.GetAxisY() * ps[1] * y * static_cast(GetHeight()) + + axial.GetNormal() * ps[2] * z * static_cast(GetDepth())); } } diff -r fe4befe03935 -r d6136a7e914d Framework/Volumes/ImageBuffer3D.h --- a/Framework/Volumes/ImageBuffer3D.h Mon Nov 05 10:04:56 2018 +0100 +++ b/Framework/Volumes/ImageBuffer3D.h Mon Nov 05 10:06:18 2018 +0100 @@ -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 . **/ diff -r fe4befe03935 -r d6136a7e914d Framework/Volumes/StructureSetLoader.cpp --- a/Framework/Volumes/StructureSetLoader.cpp Mon Nov 05 10:04:56 2018 +0100 +++ b/Framework/Volumes/StructureSetLoader.cpp Mon Nov 05 10:06:18 2018 +0100 @@ -27,130 +27,63 @@ namespace OrthancStone { - class StructureSetLoader::Operation : public Orthanc::IDynamicObject - { - public: - enum Type - { - Type_LoadStructureSet, - Type_LookupSopInstanceUid, - Type_LoadReferencedSlice - }; - - private: - Type type_; - std::string value_; - - public: - Operation(Type type, - const std::string& value) : - type_(type), - value_(value) - { - } - - Type GetType() const - { - return type_; - } - - const std::string& GetIdentifier() const - { - return value_; - } - }; - - - void StructureSetLoader::NotifyError(const std::string& uri, - Orthanc::IDynamicObject* payload) - { - // TODO - } - - void StructureSetLoader::NotifySuccess(const std::string& uri, - const void* answer, - size_t answerSize, - Orthanc::IDynamicObject* payload) - { - std::auto_ptr op(dynamic_cast(payload)); - - switch (op->GetType()) - { - case Operation::Type_LoadStructureSet: - { - OrthancPlugins::FullOrthancDataset dataset(answer, answerSize); - structureSet_.reset(new DicomStructureSet(dataset)); - - std::set instances; - structureSet_->GetReferencedInstances(instances); - - for (std::set::const_iterator it = instances.begin(); - it != instances.end(); ++it) - { - orthanc_.SchedulePostRequest(*this, "/tools/lookup", *it, - new Operation(Operation::Type_LookupSopInstanceUid, *it)); - } - - VolumeLoaderBase::NotifyGeometryReady(); - - break; - } - - case Operation::Type_LookupSopInstanceUid: - { - Json::Value lookup; - - if (MessagingToolbox::ParseJson(lookup, answer, answerSize)) - { - 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") - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol); - } - - const std::string& instance = lookup[0]["ID"].asString(); - orthanc_.ScheduleGetRequest(*this, "/instances/" + instance + "/tags", - new Operation(Operation::Type_LoadReferencedSlice, instance)); - } - else - { - // TODO - } - - break; - } - - case Operation::Type_LoadReferencedSlice: - { - OrthancPlugins::FullOrthancDataset dataset(answer, answerSize); - - Orthanc::DicomMap slice; - MessagingToolbox::ConvertDataset(slice, dataset); - structureSet_->AddReferencedSlice(slice); - - VolumeLoaderBase::NotifyContentChange(); - - break; - } - - default: - throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); - } - } - - - StructureSetLoader::StructureSetLoader(IWebService& orthanc) : + StructureSetLoader::StructureSetLoader(MessageBroker& broker, OrthancApiClient& orthanc) : + OrthancStone::IObserver(broker), orthanc_(orthanc) { } + void StructureSetLoader::OnReferencedSliceLoaded(const OrthancApiClient::JsonResponseReadyMessage& message) + { + OrthancPlugins::FullOrthancDataset dataset(message.Response); + + Orthanc::DicomMap slice; + MessagingToolbox::ConvertDataset(slice, dataset); + structureSet_->AddReferencedSlice(slice); + + VolumeLoaderBase::NotifyContentChange(); + } + + void StructureSetLoader::OnStructureSetLoaded(const OrthancApiClient::JsonResponseReadyMessage& message) + { + OrthancPlugins::FullOrthancDataset dataset(message.Response); + structureSet_.reset(new DicomStructureSet(dataset)); + + std::set instances; + structureSet_->GetReferencedInstances(instances); + + for (std::set::const_iterator it = instances.begin(); + it != instances.end(); ++it) + { + orthanc_.PostBinaryAsyncExpectJson("/tools/lookup", *it, + new Callable(*this, &StructureSetLoader::OnLookupCompleted)); + } + + VolumeLoaderBase::NotifyGeometryReady(); + } + + void StructureSetLoader::OnLookupCompleted(const OrthancApiClient::JsonResponseReadyMessage& message) + { + Json::Value lookup = message.Response; + + 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") + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol); + } + + const std::string& instance = lookup[0]["ID"].asString(); + orthanc_.GetJsonAsync("/instances/" + instance + "/tags", + new Callable(*this, &StructureSetLoader::OnReferencedSliceLoaded)); + } + void StructureSetLoader::ScheduleLoadInstance(const std::string& instance) { if (structureSet_.get() != NULL) @@ -159,8 +92,8 @@ } else { - const std::string uri = "/instances/" + instance + "/tags?ignore-length=3006-0050"; - orthanc_.ScheduleGetRequest(*this, uri, new Operation(Operation::Type_LoadStructureSet, instance)); + orthanc_.GetJsonAsync("/instances/" + instance + "/tags?ignore-length=3006-0050", + new Callable(*this, &StructureSetLoader::OnStructureSetLoaded)); } } diff -r fe4befe03935 -r d6136a7e914d Framework/Volumes/StructureSetLoader.h --- a/Framework/Volumes/StructureSetLoader.h Mon Nov 05 10:04:56 2018 +0100 +++ b/Framework/Volumes/StructureSetLoader.h Mon Nov 05 10:06:18 2018 +0100 @@ -22,31 +22,22 @@ #pragma once #include "../Toolbox/DicomStructureSet.h" -#include "../Toolbox/IWebService.h" +#include "../Toolbox/OrthancApiClient.h" #include "VolumeLoaderBase.h" namespace OrthancStone { class StructureSetLoader : public VolumeLoaderBase, - private IWebService::ICallback + public OrthancStone::IObserver { private: - class Operation; - - virtual void NotifyError(const std::string& uri, - Orthanc::IDynamicObject* payload); - virtual void NotifySuccess(const std::string& uri, - const void* answer, - size_t answerSize, - Orthanc::IDynamicObject* payload); - - IWebService& orthanc_; + OrthancApiClient& orthanc_; std::auto_ptr structureSet_; public: - StructureSetLoader(IWebService& orthanc); + StructureSetLoader(MessageBroker& broker, OrthancApiClient& orthanc); void ScheduleLoadInstance(const std::string& instance); @@ -56,5 +47,12 @@ } DicomStructureSet& GetStructureSet(); + + protected: + void OnReferencedSliceLoaded(const OrthancApiClient::JsonResponseReadyMessage& message); + + void OnStructureSetLoaded(const OrthancApiClient::JsonResponseReadyMessage& message); + + void OnLookupCompleted(const OrthancApiClient::JsonResponseReadyMessage& message); }; } diff -r fe4befe03935 -r d6136a7e914d Framework/Widgets/CairoWidget.cpp --- a/Framework/Widgets/CairoWidget.cpp Mon Nov 05 10:04:56 2018 +0100 +++ b/Framework/Widgets/CairoWidget.cpp Mon Nov 05 10:06:18 2018 +0100 @@ -32,6 +32,10 @@ return true; } + CairoWidget::CairoWidget(const std::string& name) : + WidgetBase(name) + { + } void CairoWidget::SetSize(unsigned int width, unsigned int height) diff -r fe4befe03935 -r d6136a7e914d Framework/Widgets/CairoWidget.h --- a/Framework/Widgets/CairoWidget.h Mon Nov 05 10:04:56 2018 +0100 +++ b/Framework/Widgets/CairoWidget.h Mon Nov 05 10:06:18 2018 +0100 @@ -38,6 +38,8 @@ int y) = 0; public: + CairoWidget(const std::string& name); + virtual void SetSize(unsigned int width, unsigned int height); diff -r fe4befe03935 -r d6136a7e914d Framework/Widgets/EmptyWidget.cpp --- a/Framework/Widgets/EmptyWidget.cpp Mon Nov 05 10:04:56 2018 +0100 +++ b/Framework/Widgets/EmptyWidget.cpp Mon Nov 05 10:06:18 2018 +0100 @@ -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 . **/ @@ -26,19 +26,16 @@ namespace OrthancStone { - namespace Samples + bool EmptyWidget::Render(Orthanc::ImageAccessor& surface) { - bool EmptyWidget::Render(Orthanc::ImageAccessor& surface) - { - // Note: This call is slow - Orthanc::ImageProcessing::Set(surface, red_, green_, blue_, 255); - return true; - } + // Note: This call is slow + Orthanc::ImageProcessing::Set(surface, red_, green_, blue_, 255); + return true; + } - - void EmptyWidget::UpdateContent() - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); - } + + void EmptyWidget::UpdateContent() + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); } } diff -r fe4befe03935 -r d6136a7e914d Framework/Widgets/EmptyWidget.h --- a/Framework/Widgets/EmptyWidget.h Mon Nov 05 10:04:56 2018 +0100 +++ b/Framework/Widgets/EmptyWidget.h Mon Nov 05 10:06:18 2018 +0100 @@ -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 . **/ @@ -25,93 +25,91 @@ namespace OrthancStone { - namespace Samples - { - /** + /** * This is a test widget that simply fills its surface with an * uniform color. **/ - class EmptyWidget : public IWidget - { - private: - uint8_t red_; - uint8_t green_; - uint8_t blue_; + class EmptyWidget : public IWidget + { + private: + uint8_t red_; + uint8_t green_; + uint8_t blue_; - public: - EmptyWidget(uint8_t red, - uint8_t green, - uint8_t blue) : - red_(red), - green_(green), - blue_(blue) - { - } + public: + EmptyWidget(uint8_t red, + uint8_t green, + uint8_t blue) : + red_(red), + green_(green), + blue_(blue) + { + } + + virtual void FitContent() + { + } - virtual void SetDefaultView() - { - } - - virtual void SetParent(OrthancStone::IWidget& widget) - { - } - - virtual void SetViewport(IViewport& viewport) - { - } + virtual void SetParent(IWidget& widget) + { + } + + virtual void SetViewport(IViewport& viewport) + { + } - virtual void NotifyChange() - { - } + virtual void NotifyContentChanged() + { + } - virtual void SetStatusBar(IStatusBar& statusBar) - { - } + virtual void SetStatusBar(IStatusBar& statusBar) + { + } + + virtual void SetSize(unsigned int width, + unsigned int height) + { + } - virtual void SetSize(unsigned int width, - unsigned int height) - { - } - - virtual bool Render(Orthanc::ImageAccessor& surface); + virtual bool Render(Orthanc::ImageAccessor& surface); - virtual IMouseTracker* CreateMouseTracker(MouseButton button, - int x, - int y, - KeyboardModifiers modifiers) - { - return NULL; - } + virtual IMouseTracker* CreateMouseTracker(MouseButton button, + int x, + int y, + KeyboardModifiers modifiers) + { + return NULL; + } - virtual void RenderMouseOver(Orthanc::ImageAccessor& target, - int x, - int y) - { - } + virtual void RenderMouseOver(Orthanc::ImageAccessor& target, + int x, + int y) + { + } - virtual void MouseWheel(MouseWheelDirection direction, - int x, - int y, - KeyboardModifiers modifiers) - { - } + virtual void MouseWheel(MouseWheelDirection direction, + int x, + int y, + KeyboardModifiers modifiers) + { + } - virtual void KeyPressed(char key, - KeyboardModifiers modifiers) - { - } + virtual void KeyPressed(KeyboardKeys key, + char keyChar, + KeyboardModifiers modifiers) + { + } - virtual bool HasUpdateContent() const - { - return false; - } + virtual bool HasUpdateContent() const + { + return false; + } - virtual void UpdateContent(); + virtual void UpdateContent(); - virtual bool HasRenderMouseOver() - { - return false; - } - }; - } + virtual bool HasRenderMouseOver() + { + return false; + } + }; } diff -r fe4befe03935 -r d6136a7e914d Framework/Widgets/IWidget.h --- a/Framework/Widgets/IWidget.h Mon Nov 05 10:04:56 2018 +0100 +++ b/Framework/Widgets/IWidget.h Mon Nov 05 10:06:18 2018 +0100 @@ -35,7 +35,7 @@ { } - virtual void SetDefaultView() = 0; + virtual void FitContent() = 0; virtual void SetParent(IWidget& parent) = 0; @@ -64,7 +64,8 @@ int y, KeyboardModifiers modifiers) = 0; - virtual void KeyPressed(char key, + virtual void KeyPressed(KeyboardKeys key, + char keyChar, KeyboardModifiers modifiers) = 0; virtual bool HasUpdateContent() const = 0; @@ -73,6 +74,6 @@ // Subclasses can call this method to signal the display of the // widget must be refreshed - virtual void NotifyChange() = 0; + virtual void NotifyContentChanged() = 0; }; } diff -r fe4befe03935 -r d6136a7e914d Framework/Widgets/IWorldSceneInteractor.h --- a/Framework/Widgets/IWorldSceneInteractor.h Mon Nov 05 10:04:56 2018 +0100 +++ b/Framework/Widgets/IWorldSceneInteractor.h Mon Nov 05 10:06:18 2018 +0100 @@ -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 . **/ @@ -29,37 +29,41 @@ namespace OrthancStone { - class WorldSceneWidget; + class WorldSceneWidget; - class IWorldSceneInteractor : public boost::noncopyable - { - public: - virtual ~IWorldSceneInteractor() + class IWorldSceneInteractor : public boost::noncopyable { - } - - virtual IWorldSceneMouseTracker* CreateMouseTracker(WorldSceneWidget& widget, - const ViewportGeometry& view, - MouseButton button, - double x, - double y, - IStatusBar* statusBar) = 0; + public: + virtual ~IWorldSceneInteractor() + { + } + + virtual IWorldSceneMouseTracker* CreateMouseTracker(WorldSceneWidget& widget, + const ViewportGeometry& view, + MouseButton button, + KeyboardModifiers modifiers, + int viewportX, + int viewportY, + double x, + double y, + IStatusBar* statusBar) = 0; - virtual void MouseOver(CairoContext& context, - WorldSceneWidget& widget, - const ViewportGeometry& view, - double x, - double y, - IStatusBar* statusBar) = 0; + virtual void MouseOver(CairoContext& context, + WorldSceneWidget& widget, + const ViewportGeometry& view, + double x, + double y, + IStatusBar* statusBar) = 0; - virtual void MouseWheel(WorldSceneWidget& widget, - MouseWheelDirection direction, - KeyboardModifiers modifiers, - IStatusBar* statusBar) = 0; + virtual void MouseWheel(WorldSceneWidget& widget, + MouseWheelDirection direction, + KeyboardModifiers modifiers, + IStatusBar* statusBar) = 0; - virtual void KeyPressed(WorldSceneWidget& widget, - char key, - KeyboardModifiers modifiers, - IStatusBar* statusBar) = 0; - }; + virtual void KeyPressed(WorldSceneWidget& widget, + KeyboardKeys key, + char keyChar, + KeyboardModifiers modifiers, + IStatusBar* statusBar) = 0; + }; } diff -r fe4befe03935 -r d6136a7e914d Framework/Widgets/IWorldSceneMouseTracker.h --- a/Framework/Widgets/IWorldSceneMouseTracker.h Mon Nov 05 10:04:56 2018 +0100 +++ b/Framework/Widgets/IWorldSceneMouseTracker.h Mon Nov 05 10:06:18 2018 +0100 @@ -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 . **/ @@ -25,19 +25,27 @@ namespace OrthancStone { + + // this is tracking a mouse in scene coordinates/mm unlike + // the IMouseTracker that is tracking a mouse + // in screen coordinates/pixels. class IWorldSceneMouseTracker : public boost::noncopyable { public: virtual ~IWorldSceneMouseTracker() { } - + + virtual bool HasRender() const = 0; + virtual void Render(CairoContext& context, double zoom) = 0; - + virtual void MouseUp() = 0; - virtual void MouseMove(double x, - double y) = 0; + virtual void MouseMove(int displayX, + int displayY, + double sceneX, + double sceneY) = 0; }; } diff -r fe4befe03935 -r d6136a7e914d Framework/Widgets/LayerWidget.cpp --- a/Framework/Widgets/LayerWidget.cpp Mon Nov 05 10:04:56 2018 +0100 +++ b/Framework/Widgets/LayerWidget.cpp Mon Nov 05 10:06:18 2018 +0100 @@ -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 . **/ @@ -23,6 +23,7 @@ #include "../Layers/SliceOutlineRenderer.h" #include "../Toolbox/GeometryToolbox.h" +#include "Framework/Layers/FrameRenderer.h" #include @@ -37,7 +38,7 @@ double thickness_; size_t countMissing_; std::vector renderers_; - +public: void DeleteLayer(size_t index) { if (index >= renderers_.size()) @@ -55,8 +56,7 @@ countMissing_++; } } - - public: + Scene(const CoordinateSystem3D& slice, double thickness, size_t countLayers) : @@ -184,7 +184,7 @@ #endif cairo_set_line_width(cr, 2.0 / view.GetZoom()); - cairo_set_source_rgb(cr, 1, 1, 1); + cairo_set_source_rgb(cr, 1, 1, 1); cairo_stroke_preserve(cr); cairo_set_source_rgb(cr, 1, 0, 0); cairo_fill(cr); @@ -215,7 +215,7 @@ { double z = (slice_.ProjectAlongNormal(slice.GetOrigin()) - slice_.ProjectAlongNormal(slice_.GetOrigin())); - + if (z < 0) { z = -z; @@ -249,13 +249,13 @@ return true; } } - + void LayerWidget::GetLayerExtent(Extent2D& extent, ILayerSource& source) const { extent.Reset(); - + std::vector points; if (source.GetExtent(points, slice_)) { @@ -268,7 +268,7 @@ } } - + Extent2D LayerWidget::GetSceneExtent() { Extent2D sceneExtent; @@ -341,7 +341,7 @@ currentScene_->ContainsPlane(slice)) { currentScene_->SetLayer(index, tmp.release()); - NotifyChange(); + NotifyContentChanged(); } else if (pendingScene_.get() != NULL && pendingScene_->ContainsPlane(slice)) @@ -353,13 +353,16 @@ pendingScene_->IsComplete()) { currentScene_ = pendingScene_; - NotifyChange(); + NotifyContentChanged(); } } } - LayerWidget::LayerWidget() : + LayerWidget::LayerWidget(MessageBroker& broker, const std::string& name) : + WorldSceneWidget(name), + IObserver(broker), + IObservable(broker), started_(false) { SetBackgroundCleared(true); @@ -374,6 +377,15 @@ } } + void LayerWidget::ObserveLayer(ILayerSource& layer) + { + layer.RegisterObserverCallback(new Callable(*this, &LayerWidget::OnGeometryReady)); + // currently ignore errors layer->RegisterObserverCallback(new Callable(*this, &LayerWidget::...)); + layer.RegisterObserverCallback(new Callable(*this, &LayerWidget::OnSliceChanged)); + layer.RegisterObserverCallback(new Callable(*this, &LayerWidget::OnContentChanged)); + layer.RegisterObserverCallback(new Callable(*this, &LayerWidget::OnLayerReady)); + } + size_t LayerWidget::AddLayer(ILayerSource* layer) // Takes ownership { @@ -388,14 +400,58 @@ layersIndex_[layer] = index; ResetPendingScene(); - layer->Register(*this); + + ObserveLayer(*layer); ResetChangedLayers(); return index; } - + void LayerWidget::ReplaceLayer(size_t index, ILayerSource* layer) // Takes ownership + { + if (layer == NULL) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); + } + + if (index >= layers_.size()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + + delete layers_[index]; + layers_[index] = layer; + layersIndex_[layer] = index; + + ResetPendingScene(); + + ObserveLayer(*layer); + + InvalidateLayer(index); + } + + void LayerWidget::RemoveLayer(size_t index) + { + if (index >= layers_.size()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + + ILayerSource* previousLayer = layers_[index]; + layersIndex_.erase(layersIndex_.find(previousLayer)); + layers_.erase(layers_.begin() + index); + changedLayers_.erase(changedLayers_.begin() + index); + styles_.erase(styles_.begin() + index); + + delete layers_[index]; + + currentScene_->DeleteLayer(index); + ResetPendingScene(); + + NotifyContentChanged(); + } + const RenderStyle& LayerWidget::GetLayerStyle(size_t layer) const { if (layer >= layers_.size()) @@ -429,7 +485,7 @@ pendingScene_->SetLayerStyle(layer, style); } - NotifyChange(); + NotifyContentChanged(); } @@ -457,26 +513,19 @@ } } - - void LayerWidget::NotifyGeometryReady(const ILayerSource& source) + void LayerWidget::OnGeometryReady(const ILayerSource::GeometryReadyMessage& message) { size_t i; - if (LookupLayer(i, source)) + if (LookupLayer(i, message.origin_)) { - LOG(INFO) << "Geometry ready for layer " << i; + LOG(INFO) << ": Geometry ready for layer " << i << " in " << GetName(); changedLayers_[i] = true; //layers_[i]->ScheduleLayerCreation(slice_); } + EmitMessage(GeometryChangedMessage(*this)); } - - void LayerWidget::NotifyGeometryError(const ILayerSource& source) - { - LOG(ERROR) << "Cannot get geometry"; - } - - void LayerWidget::InvalidateAllLayers() { for (size_t i = 0; i < layers_.size(); i++) @@ -503,39 +552,37 @@ } - void LayerWidget::NotifyContentChange(const ILayerSource& source) + void LayerWidget::OnContentChanged(const ILayerSource::ContentChangedMessage& message) { size_t index; - if (LookupLayer(index, source)) + if (LookupLayer(index, message.origin_)) { InvalidateLayer(index); } + EmitMessage(LayerWidget::ContentChangedMessage(*this)); } - void LayerWidget::NotifySliceChange(const ILayerSource& source, - const Slice& slice) + void LayerWidget::OnSliceChanged(const ILayerSource::SliceChangedMessage& message) { - if (slice.ContainsPlane(slice_)) + if (message.slice_.ContainsPlane(slice_)) { size_t index; - if (LookupLayer(index, source)) + if (LookupLayer(index, message.origin_)) { InvalidateLayer(index); } } + EmitMessage(LayerWidget::ContentChangedMessage(*this)); } - void LayerWidget::NotifyLayerReady(std::auto_ptr& renderer, - const ILayerSource& source, - const CoordinateSystem3D& slice, - bool isError) + void LayerWidget::OnLayerReady(const ILayerSource::LayerReadyMessage& message) { size_t index; - if (LookupLayer(index, source)) + if (LookupLayer(index, message.origin_)) { - if (isError) + if (message.isError_) { LOG(ERROR) << "Using error renderer on layer " << index; } @@ -543,17 +590,18 @@ { LOG(INFO) << "Renderer ready for layer " << index; } - - if (renderer.get() != NULL) + + if (message.renderer_.get() != NULL) { - UpdateLayer(index, renderer.release(), slice); + UpdateLayer(index, message.renderer_.release(), message.slice_); } - else if (isError) + else if (message.isError_) { // TODO //UpdateLayer(index, new SliceOutlineRenderer(slice), slice); } } + EmitMessage(LayerWidget::ContentChangedMessage(*this)); } diff -r fe4befe03935 -r d6136a7e914d Framework/Widgets/LayerWidget.h --- a/Framework/Widgets/LayerWidget.h Mon Nov 05 10:04:56 2018 +0100 +++ b/Framework/Widgets/LayerWidget.h Mon Nov 05 10:06:18 2018 +0100 @@ -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 . **/ @@ -24,15 +24,21 @@ #include "WorldSceneWidget.h" #include "../Layers/ILayerSource.h" #include "../Toolbox/Extent2D.h" +#include "../../Framework/Messages/IObserver.h" #include namespace OrthancStone { class LayerWidget : - public WorldSceneWidget, - private ILayerSource::IObserver + public WorldSceneWidget, + public IObserver, + public IObservable { + public: + typedef OriginMessage GeometryChangedMessage; + typedef OriginMessage ContentChangedMessage; + private: class Scene; @@ -53,25 +59,23 @@ void GetLayerExtent(Extent2D& extent, ILayerSource& source) const; - virtual void NotifyGeometryReady(const ILayerSource& source); + void OnGeometryReady(const ILayerSource::GeometryReadyMessage& message); - virtual void NotifyGeometryError(const ILayerSource& source); - - virtual void NotifyContentChange(const ILayerSource& source); + virtual void OnContentChanged(const ILayerSource::ContentChangedMessage& message); - virtual void NotifySliceChange(const ILayerSource& source, - const Slice& slice); + virtual void OnSliceChanged(const ILayerSource::SliceChangedMessage& message); - virtual void NotifyLayerReady(std::auto_ptr& renderer, - const ILayerSource& source, - const CoordinateSystem3D& slice, - bool isError); + virtual void OnLayerReady(const ILayerSource::LayerReadyMessage& message); + + void ObserveLayer(ILayerSource& source); void ResetChangedLayers(); public: + LayerWidget(MessageBroker& broker, const std::string& name); + virtual Extent2D GetSceneExtent(); - + protected: virtual bool RenderScene(CairoContext& context, const ViewportGeometry& view); @@ -87,12 +91,14 @@ void InvalidateLayer(size_t layer); public: - LayerWidget(); - virtual ~LayerWidget(); size_t AddLayer(ILayerSource* layer); // Takes ownership + void ReplaceLayer(size_t layerIndex, ILayerSource* layer); // Takes ownership + + void RemoveLayer(size_t layerIndex); + size_t GetLayerCount() const { return layers_.size(); diff -r fe4befe03935 -r d6136a7e914d Framework/Widgets/LayoutWidget.cpp --- a/Framework/Widgets/LayoutWidget.cpp Mon Nov 05 10:04:56 2018 +0100 +++ b/Framework/Widgets/LayoutWidget.cpp Mon Nov 05 10:06:18 2018 +0100 @@ -83,12 +83,10 @@ int top_; unsigned int width_; unsigned int height_; - bool hasUpdate_; public: ChildWidget(IWidget* widget) : - widget_(widget), - hasUpdate_(widget->HasUpdateContent()) + widget_(widget) { assert(widget != NULL); SetEmpty(); @@ -96,7 +94,7 @@ void UpdateContent() { - if (hasUpdate_) + if (widget_->HasUpdateContent()) { widget_->UpdateContent(); } @@ -266,11 +264,12 @@ } } - NotifyChange(*this); + NotifyContentChanged(*this); } - LayoutWidget::LayoutWidget() : + LayoutWidget::LayoutWidget(const std::string& name) : + WidgetBase(name), isHorizontal_(true), width_(0), height_(0), @@ -292,19 +291,19 @@ } - void LayoutWidget::SetDefaultView() + void LayoutWidget::FitContent() { for (size_t i = 0; i < children_.size(); i++) { - children_[i]->GetWidget().SetDefaultView(); + children_[i]->GetWidget().FitContent(); } } - void LayoutWidget::NotifyChange(const IWidget& widget) + void LayoutWidget::NotifyContentChanged(const IWidget& widget) { // One of the children has changed - WidgetBase::NotifyChange(); + WidgetBase::NotifyContentChanged(); } @@ -452,12 +451,13 @@ } - void LayoutWidget::KeyPressed(char key, + void LayoutWidget::KeyPressed(KeyboardKeys key, + char keyChar, KeyboardModifiers modifiers) { for (size_t i = 0; i < children_.size(); i++) { - children_[i]->GetWidget().KeyPressed(key, modifiers); + children_[i]->GetWidget().KeyPressed(key, keyChar, modifiers); } } diff -r fe4befe03935 -r d6136a7e914d Framework/Widgets/LayoutWidget.h --- a/Framework/Widgets/LayoutWidget.h Mon Nov 05 10:04:56 2018 +0100 +++ b/Framework/Widgets/LayoutWidget.h Mon Nov 05 10:06:18 2018 +0100 @@ -49,13 +49,13 @@ void ComputeChildrenExtents(); public: - LayoutWidget(); + LayoutWidget(const std::string& name); virtual ~LayoutWidget(); - virtual void SetDefaultView(); + virtual void FitContent(); - virtual void NotifyChange(const IWidget& widget); + virtual void NotifyContentChanged(const IWidget& widget); void SetHorizontal(); @@ -117,7 +117,8 @@ int y, KeyboardModifiers modifiers); - virtual void KeyPressed(char key, + virtual void KeyPressed(KeyboardKeys key, + char keyChar, KeyboardModifiers modifiers); virtual bool HasUpdateContent() const diff -r fe4befe03935 -r d6136a7e914d Framework/Widgets/PanMouseTracker.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Widgets/PanMouseTracker.cpp Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,55 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#include "PanMouseTracker.h" + +#include + +namespace OrthancStone +{ + PanMouseTracker::PanMouseTracker(WorldSceneWidget& that, + int x, + int y) : + that_(that) + { + that.GetView().GetPan(originalPanX_, originalPanY_); + that.GetView().MapPixelCenterToScene(downX_, downY_, x, y); + } + + + void PanMouseTracker::Render(CairoContext& context, + double zoom) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + + + void PanMouseTracker::MouseMove(int displayX, + int displayY, + double x, + double y) + { + ViewportGeometry view = that_.GetView(); + view.SetPan(originalPanX_ + (x - downX_) * view.GetZoom(), + originalPanY_ + (y - downY_) * view.GetZoom()); + that_.SetView(view); + } +} diff -r fe4befe03935 -r d6136a7e914d Framework/Widgets/PanMouseTracker.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Widgets/PanMouseTracker.h Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,59 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "WorldSceneWidget.h" + +namespace OrthancStone +{ + class PanMouseTracker : public IWorldSceneMouseTracker + { + private: + WorldSceneWidget& that_; + double originalPanX_; + double originalPanY_; + double downX_; + double downY_; + + public: + PanMouseTracker(WorldSceneWidget& that, + int x, + int y); + + virtual bool HasRender() const + { + return false; + } + + virtual void MouseUp() + { + } + + virtual void Render(CairoContext& context, + double zoom); + + virtual void MouseMove(int displayX, + int displayY, + double x, + double y); + }; +} diff -r fe4befe03935 -r d6136a7e914d Framework/Widgets/TestCairoWidget.cpp --- a/Framework/Widgets/TestCairoWidget.cpp Mon Nov 05 10:04:56 2018 +0100 +++ b/Framework/Widgets/TestCairoWidget.cpp Mon Nov 05 10:06:18 2018 +0100 @@ -36,7 +36,7 @@ value_ = 1; } - NotifyChange(); + NotifyContentChanged(); } @@ -77,7 +77,8 @@ } - TestCairoWidget::TestCairoWidget(bool animate) : + TestCairoWidget::TestCairoWidget(const std::string& name, bool animate) : + CairoWidget(name), width_(0), height_(0), value_(1), @@ -114,10 +115,11 @@ } - void TestCairoWidget::KeyPressed(char key, + void TestCairoWidget::KeyPressed(KeyboardKeys key, + char keyChar, KeyboardModifiers modifiers) { - UpdateStatusBar("Key pressed: \"" + std::string(1, key) + "\""); + UpdateStatusBar("Key pressed: \"" + std::string(1, keyChar) + "\""); } } } diff -r fe4befe03935 -r d6136a7e914d Framework/Widgets/TestCairoWidget.h --- a/Framework/Widgets/TestCairoWidget.h Mon Nov 05 10:04:56 2018 +0100 +++ b/Framework/Widgets/TestCairoWidget.h Mon Nov 05 10:06:18 2018 +0100 @@ -43,7 +43,7 @@ int y); public: - TestCairoWidget(bool animate); + TestCairoWidget(const std::string& name, bool animate); virtual void SetSize(unsigned int width, unsigned int height); @@ -58,7 +58,8 @@ int y, KeyboardModifiers modifiers); - virtual void KeyPressed(char key, + virtual void KeyPressed(KeyboardKeys key, + char keyChar, KeyboardModifiers modifiers); virtual bool HasUpdateContent() const diff -r fe4befe03935 -r d6136a7e914d Framework/Widgets/TestWorldSceneWidget.cpp --- a/Framework/Widgets/TestWorldSceneWidget.cpp Mon Nov 05 10:04:56 2018 +0100 +++ b/Framework/Widgets/TestWorldSceneWidget.cpp Mon Nov 05 10:06:18 2018 +0100 @@ -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 . **/ @@ -34,6 +34,9 @@ virtual IWorldSceneMouseTracker* CreateMouseTracker(WorldSceneWidget& widget, const ViewportGeometry& view, MouseButton button, + KeyboardModifiers modifiers, + int viewportX, + int viewportY, double x, double y, IStatusBar* statusBar) @@ -80,20 +83,21 @@ } virtual void KeyPressed(WorldSceneWidget& widget, - char key, + KeyboardKeys key, + char keyChar, KeyboardModifiers modifiers, IStatusBar* statusBar) { if (statusBar) { - statusBar->SetMessage("Key pressed: \"" + std::string(1, key) + "\""); + statusBar->SetMessage("Key pressed: \"" + std::string(1, keyChar) + "\""); } } }; bool TestWorldSceneWidget::RenderScene(CairoContext& context, - const ViewportGeometry& view) + const ViewportGeometry& view) { cairo_t* cr = context.GetObject(); @@ -110,7 +114,8 @@ } - TestWorldSceneWidget::TestWorldSceneWidget(bool animate) : + TestWorldSceneWidget::TestWorldSceneWidget(const std::string& name, bool animate) : + WorldSceneWidget(name), interactor_(new Interactor), animate_(animate), count_(0) @@ -130,7 +135,7 @@ if (animate_) { count_++; - NotifyChange(); + NotifyContentChanged(); } else { diff -r fe4befe03935 -r d6136a7e914d Framework/Widgets/TestWorldSceneWidget.h --- a/Framework/Widgets/TestWorldSceneWidget.h Mon Nov 05 10:04:56 2018 +0100 +++ b/Framework/Widgets/TestWorldSceneWidget.h Mon Nov 05 10:06:18 2018 +0100 @@ -43,7 +43,7 @@ const ViewportGeometry& view); public: - TestWorldSceneWidget(bool animate); + TestWorldSceneWidget(const std::string& name, bool animate); virtual Extent2D GetSceneExtent(); diff -r fe4befe03935 -r d6136a7e914d Framework/Widgets/WidgetBase.cpp --- a/Framework/Widgets/WidgetBase.cpp Mon Nov 05 10:04:56 2018 +0100 +++ b/Framework/Widgets/WidgetBase.cpp Mon Nov 05 10:06:18 2018 +0100 @@ -27,16 +27,16 @@ namespace OrthancStone { - void WidgetBase::NotifyChange() + void WidgetBase::NotifyContentChanged() { if (parent_ != NULL) { - parent_->NotifyChange(); + parent_->NotifyContentChanged(); } if (viewport_ != NULL) { - viewport_->NotifyChange(*this); + viewport_->NotifyContentChanged(*this); } } @@ -101,12 +101,13 @@ } - WidgetBase::WidgetBase() : + WidgetBase::WidgetBase(const std::string& name) : parent_(NULL), viewport_(NULL), statusBar_(NULL), backgroundCleared_(false), - transmitMouseOver_(false) + transmitMouseOver_(false), + name_(name) { backgroundColor_[0] = 0; backgroundColor_[1] = 0; diff -r fe4befe03935 -r d6136a7e914d Framework/Widgets/WidgetBase.h --- a/Framework/Widgets/WidgetBase.h Mon Nov 05 10:04:56 2018 +0100 +++ b/Framework/Widgets/WidgetBase.h Mon Nov 05 10:06:18 2018 +0100 @@ -36,6 +36,7 @@ bool backgroundCleared_; uint8_t backgroundColor_[3]; bool transmitMouseOver_; + std::string name_; protected: void ClearBackgroundOrthanc(Orthanc::ImageAccessor& target) const; @@ -52,9 +53,9 @@ } public: - WidgetBase(); + WidgetBase(const std::string& name); - virtual void SetDefaultView() + virtual void FitContent() { } @@ -104,6 +105,12 @@ return transmitMouseOver_; } - virtual void NotifyChange(); + virtual void NotifyContentChanged(); + + const std::string& GetName() const + { + return name_; + } + }; } diff -r fe4befe03935 -r d6136a7e914d Framework/Widgets/WorldSceneWidget.cpp --- a/Framework/Widgets/WorldSceneWidget.cpp Mon Nov 05 10:04:56 2018 +0100 +++ b/Framework/Widgets/WorldSceneWidget.cpp Mon Nov 05 10:06:18 2018 +0100 @@ -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 . **/ @@ -21,195 +21,61 @@ #include "WorldSceneWidget.h" +#include "PanMouseTracker.h" +#include "ZoomMouseTracker.h" + +#include + #include #include #include namespace OrthancStone { - static void MapMouseToScene(double& sceneX, - double& sceneY, - const ViewportGeometry& view, - int mouseX, - int mouseY) - { - // Take the center of the pixel - double x, y; - x = static_cast(mouseX) + 0.5; - y = static_cast(mouseY) + 0.5; - - view.MapDisplayToScene(sceneX, sceneY, x, y); - } - - - struct WorldSceneWidget::SizeChangeFunctor - { - ViewportGeometry& view_; - - SizeChangeFunctor(ViewportGeometry& view) : - view_(view) - { - } - - void operator() (IWorldObserver& observer, - const WorldSceneWidget& source) - { - observer.NotifySizeChange(source, view_); - } - }; - - + // this is an adapter between a IWorldSceneMouseTracker + // that is tracking a mouse in scene coordinates/mm and + // an IMouseTracker that is tracking a mouse + // in screen coordinates/pixels. class WorldSceneWidget::SceneMouseTracker : public IMouseTracker { private: - ViewportGeometry view_; + ViewportGeometry view_; std::auto_ptr tracker_; - + public: SceneMouseTracker(const ViewportGeometry& view, IWorldSceneMouseTracker* tracker) : view_(view), tracker_(tracker) { - assert(tracker != NULL); + if (tracker == NULL) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); + } } virtual void Render(Orthanc::ImageAccessor& target) { - CairoSurface surface(target); - CairoContext context(surface); - view_.ApplyTransform(context); - tracker_->Render(context, view_.GetZoom()); + if (tracker_->HasRender()) + { + CairoSurface surface(target); + CairoContext context(surface); + view_.ApplyTransform(context); + tracker_->Render(context, view_.GetZoom()); + } } - virtual void MouseUp() + virtual void MouseUp() { tracker_->MouseUp(); } - virtual void MouseMove(int x, + virtual void MouseMove(int x, int y) { double sceneX, sceneY; - MapMouseToScene(sceneX, sceneY, view_, x, y); - tracker_->MouseMove(sceneX, sceneY); - } - }; - - - class WorldSceneWidget::PanMouseTracker : public IMouseTracker - { - private: - WorldSceneWidget& that_; - double previousPanX_; - double previousPanY_; - double downX_; - double downY_; - - public: - PanMouseTracker(WorldSceneWidget& that, - int x, - int y) : - that_(that), - downX_(x), - downY_(y) - { - that_.view_.GetPan(previousPanX_, previousPanY_); - } - - virtual void Render(Orthanc::ImageAccessor& surface) - { - } - - virtual void MouseUp() - { - } - - virtual void MouseMove(int x, - int y) - { - that_.view_.SetPan(previousPanX_ + x - downX_, - previousPanY_ + y - downY_); - - that_.observers_.Apply(that_, &IWorldObserver::NotifyViewChange, that_.view_); - } - }; - - - class WorldSceneWidget::ZoomMouseTracker : public IMouseTracker - { - private: - WorldSceneWidget& that_; - int downX_; - int downY_; - double centerX_; - double centerY_; - double oldZoom_; - - public: - ZoomMouseTracker(WorldSceneWidget& that, - int x, - int y) : - that_(that), - downX_(x), - downY_(y) - { - oldZoom_ = that_.view_.GetZoom(); - MapMouseToScene(centerX_, centerY_, that_.view_, downX_, downY_); - } - - virtual void Render(Orthanc::ImageAccessor& surface) - { - } - - virtual void MouseUp() - { - } - - virtual void MouseMove(int x, - int y) - { - static const double MIN_ZOOM = -4; - static const double MAX_ZOOM = 4; - - if (that_.view_.GetDisplayHeight() <= 3) - { - return; // Cannot zoom on such a small image - } - - double dy = (static_cast(y - downY_) / - static_cast(that_.view_.GetDisplayHeight() - 1)); // In the range [-1,1] - double z; - - // Linear interpolation from [-1, 1] to [MIN_ZOOM, MAX_ZOOM] - if (dy < -1.0) - { - z = MIN_ZOOM; - } - else if (dy > 1.0) - { - z = MAX_ZOOM; - } - else - { - z = MIN_ZOOM + (MAX_ZOOM - MIN_ZOOM) * (dy + 1.0) / 2.0; - } - - z = pow(2.0, z); - - that_.view_.SetZoom(oldZoom_ * z); - - // Correct the pan so that the original click point is kept at - // the same location on the display - double panX, panY; - that_.view_.GetPan(panX, panY); - - int tx, ty; - that_.view_.MapSceneToDisplay(tx, ty, centerX_, centerY_); - that_.view_.SetPan(panX + static_cast(downX_ - tx), - panY + static_cast(downY_ - ty)); - - that_.observers_.Apply(that_, &IWorldObserver::NotifyViewChange, that_.view_); + view_.MapPixelCenterToScene(sceneX, sceneY, x, y); + tracker_->MouseMove(x, y, sceneX, sceneY); } }; @@ -229,8 +95,12 @@ view.ApplyTransform(context); double sceneX, sceneY; - MapMouseToScene(sceneX, sceneY, view, x, y); - RenderSceneMouseOver(context, view, sceneX, sceneY); + view.MapPixelCenterToScene(sceneX, sceneY, x, y); + + if (interactor_) + { + interactor_->MouseOver(context, *this, view, sceneX, sceneY, GetStatusBar()); + } } @@ -244,20 +114,7 @@ unsigned int height) { CairoWidget::SetSize(width, height); - view_.SetDisplaySize(width, height); - - if (observers_.IsEmpty()) - { - // Without a size observer, reset to the default view - // view_.SetDefaultView(); - } - else - { - // With a size observer, let it decide which view to use - SizeChangeFunctor functor(view_); - observers_.Notify(*this, functor); - } } @@ -267,14 +124,12 @@ } - void WorldSceneWidget::SetDefaultView() + void WorldSceneWidget::FitContent() { SetSceneExtent(view_); - view_.SetDefaultView(); + view_.FitContent(); - NotifyChange(); - - observers_.Apply(*this, &IWorldObserver::NotifyViewChange, view_); + NotifyContentChanged(); } @@ -282,15 +137,7 @@ { view_ = view; - NotifyChange(); - - observers_.Apply(*this, &IWorldObserver::NotifyViewChange, view_); - } - - - ViewportGeometry WorldSceneWidget::GetView() - { - return view_; + NotifyContentChanged(); } @@ -300,50 +147,33 @@ KeyboardModifiers modifiers) { double sceneX, sceneY; - MapMouseToScene(sceneX, sceneY, view_, x, y); + view_.MapPixelCenterToScene(sceneX, sceneY, x, y); + + // asks the Widget Interactor to provide a mouse tracker + std::auto_ptr tracker; - std::auto_ptr tracker - (CreateMouseSceneTracker(view_, button, sceneX, sceneY, modifiers)); - + if (interactor_) + { + tracker.reset(interactor_->CreateMouseTracker(*this, view_, button, modifiers, x, y, sceneX, sceneY, GetStatusBar())); + } + if (tracker.get() != NULL) { return new SceneMouseTracker(view_, tracker.release()); } - - switch (button) + else if (hasDefaultMouseEvents_) { - case MouseButton_Middle: - return new PanMouseTracker(*this, x, y); - - case MouseButton_Right: - return new ZoomMouseTracker(*this, x, y); - - default: - return NULL; - } - } - + switch (button) + { + case MouseButton_Middle: + return new SceneMouseTracker(view_, new PanMouseTracker(*this, x, y)); - void WorldSceneWidget::RenderSceneMouseOver(CairoContext& context, - const ViewportGeometry& view, - double x, - double y) - { - if (interactor_) - { - interactor_->MouseOver(context, *this, view, x, y, GetStatusBar()); - } - } + case MouseButton_Right: + return new SceneMouseTracker(view_, new ZoomMouseTracker(*this, x, y)); - IWorldSceneMouseTracker* WorldSceneWidget::CreateMouseSceneTracker(const ViewportGeometry& view, - MouseButton button, - double x, - double y, - KeyboardModifiers modifiers) - { - if (interactor_) - { - return interactor_->CreateMouseTracker(*this, view, button, x, y, GetStatusBar()); + default: + return NULL; + } } else { @@ -355,7 +185,7 @@ void WorldSceneWidget::MouseWheel(MouseWheelDirection direction, int x, int y, - KeyboardModifiers modifiers) + KeyboardModifiers modifiers) { if (interactor_) { @@ -364,12 +194,13 @@ } - void WorldSceneWidget::KeyPressed(char key, + void WorldSceneWidget::KeyPressed(KeyboardKeys key, + char keyChar, KeyboardModifiers modifiers) { if (interactor_) { - interactor_->KeyPressed(*this, key, modifiers, GetStatusBar()); + interactor_->KeyPressed(*this, key, keyChar, modifiers, GetStatusBar()); } } } diff -r fe4befe03935 -r d6136a7e914d Framework/Widgets/WorldSceneWidget.h --- a/Framework/Widgets/WorldSceneWidget.h Mon Nov 05 10:04:56 2018 +0100 +++ b/Framework/Widgets/WorldSceneWidget.h Mon Nov 05 10:06:18 2018 +0100 @@ -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 . **/ @@ -31,43 +31,23 @@ { class WorldSceneWidget : public CairoWidget { - public: - class IWorldObserver : public boost::noncopyable - { - public: - virtual ~IWorldObserver() - { - } - - virtual void NotifySizeChange(const WorldSceneWidget& source, - ViewportGeometry& view) = 0; // Can be tuned by the observer - - virtual void NotifyViewChange(const WorldSceneWidget& source, - const ViewportGeometry& view) = 0; - }; - private: - struct SizeChangeFunctor; - class SceneMouseTracker; - class PanMouseTracker; - class ZoomMouseTracker; - - typedef ObserversRegistry Observers; ViewportGeometry view_; - Observers observers_; IWorldSceneInteractor* interactor_; + bool hasDefaultMouseEvents_; - public: + protected: virtual Extent2D GetSceneExtent() = 0; - protected: virtual bool RenderScene(CairoContext& context, const ViewportGeometry& view) = 0; + // From CairoWidget virtual bool RenderCairo(CairoContext& context); + // From CairoWidget virtual void RenderMouseOverCairo(CairoContext& context, int x, int y); @@ -75,54 +55,49 @@ void SetSceneExtent(ViewportGeometry& geometry); public: - WorldSceneWidget() : - interactor_(NULL) + WorldSceneWidget(const std::string& name) : + CairoWidget(name), + interactor_(NULL), + hasDefaultMouseEvents_(true) { } - void Register(IWorldObserver& observer) + void SetDefaultMouseEvents(bool value) { - observers_.Register(observer); + hasDefaultMouseEvents_ = value; } - void Unregister(IWorldObserver& observer) + bool HasDefaultMouseEvents() const { - observers_.Unregister(observer); + return hasDefaultMouseEvents_; + } + + void SetInteractor(IWorldSceneInteractor& interactor); + + void SetView(const ViewportGeometry& view); + + const ViewportGeometry& GetView() const + { + return view_; } virtual void SetSize(unsigned int width, unsigned int height); - void SetInteractor(IWorldSceneInteractor& interactor); - - virtual void SetDefaultView(); - - void SetView(const ViewportGeometry& view); - - ViewportGeometry GetView(); + virtual void FitContent(); virtual IMouseTracker* CreateMouseTracker(MouseButton button, int x, int y, KeyboardModifiers modifiers); - virtual void RenderSceneMouseOver(CairoContext& context, - const ViewportGeometry& view, - double x, - double y); - - virtual IWorldSceneMouseTracker* CreateMouseSceneTracker(const ViewportGeometry& view, - MouseButton button, - double x, - double y, - KeyboardModifiers modifiers); - virtual void MouseWheel(MouseWheelDirection direction, int x, int y, KeyboardModifiers modifiers); - virtual void KeyPressed(char key, + virtual void KeyPressed(KeyboardKeys key, + char keyChar, KeyboardModifiers modifiers); }; } diff -r fe4befe03935 -r d6136a7e914d Framework/Widgets/ZoomMouseTracker.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Widgets/ZoomMouseTracker.cpp Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,107 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#include "ZoomMouseTracker.h" + +#include + +namespace OrthancStone +{ + ZoomMouseTracker::ZoomMouseTracker(WorldSceneWidget& that, + int x, + int y) : + that_(that), + originalZoom_(that.GetView().GetZoom()), + downX_(x), + downY_(y) + { + that.GetView().MapPixelCenterToScene(centerX_, centerY_, x, y); + + unsigned int height = that.GetView().GetDisplayHeight(); + + if (height <= 3) + { + idle_ = true; + LOG(WARNING) << "image is too small to zoom (current height = " << height << ")"; + } + else + { + idle_ = false; + normalization_ = 1.0 / static_cast(height - 1); + } + } + + + void ZoomMouseTracker::Render(CairoContext& context, + double zoom) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + + + void ZoomMouseTracker::MouseMove(int displayX, + int displayY, + double x, + double y) + { + static const double MIN_ZOOM = -4; + static const double MAX_ZOOM = 4; + + + if (!idle_) + { + double dy = static_cast(displayY - downY_) * normalization_; // In the range [-1,1] + double z; + + // Linear interpolation from [-1, 1] to [MIN_ZOOM, MAX_ZOOM] + if (dy < -1.0) + { + z = MIN_ZOOM; + } + else if (dy > 1.0) + { + z = MAX_ZOOM; + } + else + { + z = MIN_ZOOM + (MAX_ZOOM - MIN_ZOOM) * (dy + 1.0) / 2.0; + } + + z = pow(2.0, z); + + ViewportGeometry view = that_.GetView(); + + view.SetZoom(z * originalZoom_); + + // Correct the pan so that the original click point is kept at + // the same location on the display + double panX, panY; + view.GetPan(panX, panY); + + int tx, ty; + view.MapSceneToDisplay(tx, ty, centerX_, centerY_); + view.SetPan(panX + static_cast(downX_ - tx), + panY + static_cast(downY_ - ty)); + + that_.SetView(view); + } + } +} diff -r fe4befe03935 -r d6136a7e914d Framework/Widgets/ZoomMouseTracker.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Widgets/ZoomMouseTracker.h Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,62 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "WorldSceneWidget.h" + +namespace OrthancStone +{ + class ZoomMouseTracker : public IWorldSceneMouseTracker + { + private: + WorldSceneWidget& that_; + double originalZoom_; + int downX_; + int downY_; + double centerX_; + double centerY_; + bool idle_; + double normalization_; + + public: + ZoomMouseTracker(WorldSceneWidget& that, + int x, + int y); + + virtual bool HasRender() const + { + return false; + } + + virtual void MouseUp() + { + } + + virtual void Render(CairoContext& context, + double zoom); + + virtual void MouseMove(int displayX, + int displayY, + double x, + double y); + }; +} diff -r fe4befe03935 -r d6136a7e914d Framework/dev.h --- a/Framework/dev.h Mon Nov 05 10:04:56 2018 +0100 +++ b/Framework/dev.h Mon Nov 05 10:06:18 2018 +0100 @@ -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 . **/ @@ -41,10 +41,10 @@ namespace OrthancStone { // TODO: Handle errors while loading - class OrthancVolumeImage : - public SlicedVolumeBase, - private OrthancSlicesLoader::ICallback - { + class OrthancVolumeImage : + public SlicedVolumeBase, + public OrthancStone::IObserver + { private: OrthancSlicesLoader loader_; std::auto_ptr image_; @@ -64,7 +64,7 @@ } - static bool IsCompatible(const Slice& a, + static bool IsCompatible(const Slice& a, const Slice& b) { if (!GeometryToolbox::IsParallel(a.GetGeometry().GetNormal(), @@ -98,15 +98,15 @@ } - static double GetDistance(const Slice& a, + static double GetDistance(const Slice& a, const Slice& b) { - return fabs(a.GetGeometry().ProjectAlongNormal(a.GetGeometry().GetOrigin()) - + return fabs(a.GetGeometry().ProjectAlongNormal(a.GetGeometry().GetOrigin()) - a.GetGeometry().ProjectAlongNormal(b.GetGeometry().GetOrigin())); } - virtual void NotifyGeometryReady(const OrthancSlicesLoader& loader) + void OnSliceGeometryReady(const OrthancSlicesLoader& loader) { if (loader.GetSliceCount() == 0) { @@ -151,15 +151,15 @@ unsigned int width = loader.GetSlice(0).GetWidth(); unsigned int height = loader.GetSlice(0).GetHeight(); Orthanc::PixelFormat format = loader.GetSlice(0).GetConverter().GetExpectedPixelFormat(); - LOG(INFO) << "Creating a volume image of size " << width << "x" << height + LOG(INFO) << "Creating a volume image of size " << width << "x" << height << "x" << loader.GetSliceCount() << " in " << Orthanc::EnumerationToString(format); image_.reset(new ImageBuffer3D(format, width, height, loader.GetSliceCount(), computeRange_)); image_->SetAxialGeometry(loader.GetSlice(0).GetGeometry()); - image_->SetVoxelDimensions(loader.GetSlice(0).GetPixelSpacingX(), + image_->SetVoxelDimensions(loader.GetSlice(0).GetPixelSpacingX(), loader.GetSlice(0).GetPixelSpacingY(), spacingZ); image_->Clear(); - + downloadStack_.reset(new DownloadStack(loader.GetSliceCount())); pendingSlices_ = loader.GetSliceCount(); @@ -173,24 +173,18 @@ SlicedVolumeBase::NotifyGeometryReady(); } - virtual void NotifyGeometryError(const OrthancSlicesLoader& loader) - { - LOG(ERROR) << "Unable to download a volume image"; - SlicedVolumeBase::NotifyGeometryError(); - } - - virtual void NotifySliceImageReady(const OrthancSlicesLoader& loader, - unsigned int sliceIndex, - const Slice& slice, - std::auto_ptr& image, - SliceImageQuality quality) + virtual void OnSliceImageReady(const OrthancSlicesLoader& loader, + unsigned int sliceIndex, + const Slice& slice, + const boost::shared_ptr& image, + SliceImageQuality quality) { { ImageBuffer3D::SliceWriter writer(*image_, VolumeProjection_Axial, sliceIndex); Orthanc::ImageProcessing::Copy(writer.GetAccessor(), *image); } - SlicedVolumeBase::NotifySliceChange(sliceIndex, slice); + SlicedVolumeBase::NotifySliceChange(sliceIndex, slice); if (pendingSlices_ == 1) { @@ -205,22 +199,47 @@ ScheduleSliceDownload(); } - virtual void NotifySliceImageError(const OrthancSlicesLoader& loader, - unsigned int sliceIndex, - const Slice& slice, - SliceImageQuality quality) + virtual void HandleMessage(const IObservable& from, const IMessage& message) { - LOG(ERROR) << "Cannot download slice " << sliceIndex << " in a volume image"; - ScheduleSliceDownload(); + switch (message.GetType()) + { + case MessageType_SliceLoader_GeometryReady: + OnSliceGeometryReady(dynamic_cast(from)); + case MessageType_SliceLoader_GeometryError: + { + LOG(ERROR) << "Unable to download a volume image"; + SlicedVolumeBase::NotifyGeometryError(); + }; break; + case MessageType_SliceLoader_ImageReady: + { + const OrthancSlicesLoader::SliceImageReadyMessage& msg = dynamic_cast(message); + OnSliceImageReady(dynamic_cast(from), + msg.sliceIndex_, + msg.slice_, + msg.image_, + msg.effectiveQuality_); + }; break; + case MessageType_SliceLoader_ImageError: + { + const OrthancSlicesLoader::SliceImageErrorMessage& msg = dynamic_cast(message); + LOG(ERROR) << "Cannot download slice " << msg.sliceIndex_ << " in a volume image"; + ScheduleSliceDownload(); + }; break; + default: + VLOG("unhandled message type" << message.GetType()); + } } public: - OrthancVolumeImage(IWebService& orthanc, - bool computeRange) : - loader_(*this, orthanc), + OrthancVolumeImage(MessageBroker& broker, + OrthancApiClient& orthanc, + bool computeRange) : + OrthancStone::IObserver(broker), + loader_(broker, orthanc), computeRange_(computeRange), pendingSlices_(0) { + // TODO: replace with new callables loader_.RegisterObserver(*this); } void ScheduleLoadSeries(const std::string& seriesId) @@ -352,7 +371,7 @@ axialThickness * axial.GetGeometry().GetNormal()); reference_ = CoordinateSystem3D(origin, - axial.GetGeometry().GetAxisX(), + axial.GetGeometry().GetAxisX(), -axial.GetGeometry().GetNormal()); } @@ -374,7 +393,7 @@ axialThickness * axial.GetGeometry().GetNormal()); reference_ = CoordinateSystem3D(origin, - axial.GetGeometry().GetAxisY(), + axial.GetGeometry().GetAxisY(), axial.GetGeometry().GetNormal()); } @@ -391,20 +410,20 @@ switch (projection) { - case VolumeProjection_Axial: - SetupAxial(volume); - break; + case VolumeProjection_Axial: + SetupAxial(volume); + break; - case VolumeProjection_Coronal: - SetupCoronal(volume); - break; + case VolumeProjection_Coronal: + SetupCoronal(volume); + break; - case VolumeProjection_Sagittal: - SetupSagittal(volume); - break; + case VolumeProjection_Sagittal: + SetupSagittal(volume); + break; - default: - throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); } } @@ -468,8 +487,8 @@ class VolumeImageSource : - public LayerSourceBase, - private ISlicedVolume::IObserver + public LayerSourceBase, + private ISlicedVolume::IObserver { private: OrthancVolumeImage& volume_; @@ -493,12 +512,12 @@ LayerSourceBase::NotifyGeometryReady(); } - + virtual void NotifyGeometryError(const ISlicedVolume& volume) { LayerSourceBase::NotifyGeometryError(); } - + virtual void NotifyContentChange(const ISlicedVolume& volume) { LayerSourceBase::NotifyContentChange(); @@ -527,17 +546,17 @@ switch (projection) { - case VolumeProjection_Axial: - return *axialGeometry_; + case VolumeProjection_Axial: + return *axialGeometry_; - case VolumeProjection_Sagittal: - return *sagittalGeometry_; + case VolumeProjection_Sagittal: + return *sagittalGeometry_; - case VolumeProjection_Coronal: - return *coronalGeometry_; + case VolumeProjection_Coronal: + return *coronalGeometry_; - default: - throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); } } @@ -576,7 +595,8 @@ public: - VolumeImageSource(OrthancVolumeImage& volume) : + VolumeImageSource(MessageBroker& broker, OrthancVolumeImage& volume) : + LayerSourceBase(broker), volume_(volume) { volume_.Register(*this); @@ -593,7 +613,7 @@ return false; } else - { + { // As the slices of the volumic image are arranged in a box, // we only consider one single reference slice (the one with index 0). std::auto_ptr slice(GetProjectionGeometry(projection).GetSlice(0)); @@ -630,9 +650,9 @@ std::auto_ptr slice(geometry.GetSlice(closest)); LayerSourceBase::NotifyLayerReady( - FrameRenderer::CreateRenderer(frame.release(), *slice, isFullQuality), - //new SliceOutlineRenderer(slice), - slice->GetGeometry(), false); + FrameRenderer::CreateRenderer(frame.release(), *slice, isFullQuality), + //new SliceOutlineRenderer(slice), + slice->GetGeometry(), false); return; } } @@ -645,8 +665,8 @@ class VolumeImageInteractor : - public IWorldSceneInteractor, - protected ISlicedVolume::IObserver + public IWorldSceneInteractor, + protected ISlicedVolume::IObserver { private: LayerWidget& widget_; @@ -664,14 +684,14 @@ slices_.reset(new VolumeImageGeometry(image, projection_)); SetSlice(slices_->GetSliceCount() / 2); - widget_.SetDefaultView(); + widget_.FitContent(); } } - + virtual void NotifyGeometryError(const ISlicedVolume& volume) { } - + virtual void NotifyContentChange(const ISlicedVolume& volume) { } @@ -711,38 +731,39 @@ IStatusBar* statusBar) { int scale = (modifiers & KeyboardModifiers_Control ? 10 : 1); - + switch (direction) { - case MouseWheelDirection_Up: - OffsetSlice(-scale); - break; + case MouseWheelDirection_Up: + OffsetSlice(-scale); + break; - case MouseWheelDirection_Down: - OffsetSlice(scale); - break; + case MouseWheelDirection_Down: + OffsetSlice(scale); + break; - default: - break; + default: + break; } } virtual void KeyPressed(WorldSceneWidget& widget, - char key, + KeyboardKeys key, + char keyChar, KeyboardModifiers modifiers, IStatusBar* statusBar) { - switch (key) + switch (keyChar) { - case 's': - widget.SetDefaultView(); - break; + case 's': + widget.FitContent(); + break; - default: - break; + default: + break; } } - + public: VolumeImageInteractor(OrthancVolumeImage& volume, LayerWidget& widget, @@ -787,13 +808,13 @@ slice = slices_->GetSliceCount() - 1; } - if (slice != static_cast(slice_)) + if (slice != static_cast(slice_)) { SetSlice(slice); - } + } } } - + void SetSlice(size_t slice) { if (slices_.get() != NULL) @@ -814,7 +835,8 @@ LayerWidget& otherPlane_; public: - SliceLocationSource(LayerWidget& otherPlane) : + SliceLocationSource(MessageBroker& broker, LayerWidget& otherPlane) : + LayerSourceBase(broker), otherPlane_(otherPlane) { NotifyGeometryReady(); @@ -835,7 +857,7 @@ const CoordinateSystem3D& slice = otherPlane_.GetSlice(); // Compute the line of intersection between the two slices - if (!GeometryToolbox::IntersectTwoPlanes(p, d, + if (!GeometryToolbox::IntersectTwoPlanes(p, d, slice.GetOrigin(), slice.GetNormal(), viewportSlice.GetOrigin(), viewportSlice.GetNormal())) { @@ -850,7 +872,7 @@ const Extent2D extent = otherPlane_.GetSceneExtent(); - if (GeometryToolbox::ClipLineToRectangle(x1, y1, x2, y2, + if (GeometryToolbox::ClipLineToRectangle(x1, y1, x2, y2, x1, y1, x2, y2, extent.GetX1(), extent.GetY1(), extent.GetX2(), extent.GetY2())) @@ -863,6 +885,6 @@ NotifyLayerReady(NULL, reference.GetGeometry(), false); } } - } + } }; } diff -r fe4befe03935 -r d6136a7e914d Platforms/Generic/CMakeLists.txt --- a/Platforms/Generic/CMakeLists.txt Mon Nov 05 10:04:56 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,98 +0,0 @@ -cmake_minimum_required(VERSION 2.8) -project(OrthancStone) - - -##################################################################### -## Configuration for Orthanc -##################################################################### - -include(../../Resources/CMake/Version.cmake) - -if (ORTHANC_STONE_VERSION STREQUAL "mainline") - set(ORTHANC_FRAMEWORK_VERSION "mainline") - set(ORTHANC_FRAMEWORK_DEFAULT_SOURCE "hg") -else() - set(ORTHANC_FRAMEWORK_VERSION "1.3.2") - set(ORTHANC_FRAMEWORK_DEFAULT_SOURCE "web") -endif() - -set(ORTHANC_FRAMEWORK_SOURCE "${ORTHANC_FRAMEWORK_DEFAULT_SOURCE}" CACHE STRING "Source of the Orthanc source code (can be \"hg\", \"archive\", \"web\" or \"path\")") -set(ORTHANC_FRAMEWORK_ARCHIVE "" CACHE STRING "Path to the Orthanc archive, if ORTHANC_FRAMEWORK_SOURCE is \"archive\"") -set(ORTHANC_FRAMEWORK_ROOT "" CACHE STRING "Path to the Orthanc source directory, if ORTHANC_FRAMEWORK_SOURCE is \"path\"") - - -##################################################################### -## Build a static library containing the Orthanc Stone framework -##################################################################### - -include(../../Resources/CMake/OrthancStoneParameters.cmake) - -LIST(APPEND ORTHANC_BOOST_COMPONENTS program_options) - -SET(ORTHANC_SANDBOXED OFF) -SET(ENABLE_CRYPTO_OPTIONS ON) -SET(ENABLE_GOOGLE_TEST ON) -SET(ENABLE_WEB_CLIENT ON) - -include(../../Resources/CMake/OrthancStoneConfiguration.cmake) - -add_library(OrthancStone STATIC - ${ORTHANC_STONE_SOURCES} - ) - - -##################################################################### -## Build all the sample applications -##################################################################### - -macro(BuildSample Target Sample) - add_executable(${Target} - ${ORTHANC_STONE_ROOT}/Applications/Samples/SampleMainSdl.cpp - ${APPLICATIONS_SOURCES} - ) - set_target_properties(${Target} PROPERTIES COMPILE_DEFINITIONS ORTHANC_STONE_SAMPLE=${Sample}) - target_link_libraries(${Target} OrthancStone) -endmacro() - - -# TODO - Re-enable all these samples! - -BuildSample(OrthancStoneEmpty 1) -BuildSample(OrthancStoneTestPattern 2) -BuildSample(OrthancStoneSingleFrame 3) -BuildSample(OrthancStoneSingleVolume 4) -#BuildSample(OrthancStoneBasicPetCtFusion 5) -#BuildSample(OrthancStoneSynchronizedSeries 6) -#BuildSample(OrthancStoneLayoutPetCtFusion 7) - - -##################################################################### -## Build the unit tests -##################################################################### - -add_executable(UnitTests - ${GOOGLE_TEST_SOURCES} - ${ORTHANC_STONE_ROOT}/UnitTestsSources/UnitTestsMain.cpp - ) - -target_link_libraries(UnitTests OrthancStone) - - -##################################################################### -## Generate the documentation if Doxygen is present -##################################################################### - -find_package(Doxygen) -if (DOXYGEN_FOUND) - configure_file( - ${ORTHANC_STONE_ROOT}/Resources/OrthancStone.doxygen - ${CMAKE_CURRENT_BINARY_DIR}/OrthancStone.doxygen - @ONLY) - - add_custom_target(doc - ${DOXYGEN_EXECUTABLE} ${CMAKE_CURRENT_BINARY_DIR}/OrthancStone.doxygen - COMMENT "Generating documentation with Doxygen" VERBATIM - ) -else() - message("Doxygen not found. The documentation will not be built.") -endif() diff -r fe4befe03935 -r d6136a7e914d Platforms/Generic/Oracle.cpp --- a/Platforms/Generic/Oracle.cpp Mon Nov 05 10:04:56 2018 +0100 +++ b/Platforms/Generic/Oracle.cpp Mon Nov 05 10:06:18 2018 +0100 @@ -27,6 +27,7 @@ #include #include +#include namespace OrthancStone { @@ -40,7 +41,6 @@ State_Stopped }; - boost::mutex* globalMutex_; boost::mutex oracleMutex_; State state_; std::vector threads_; @@ -66,28 +66,26 @@ if (item.get() != NULL) { IOracleCommand& command = dynamic_cast(*item); - command.Execute(); + try + { + command.Execute(); + } + catch (Orthanc::OrthancException& ex) + { + // this is probably a curl error that has been triggered. We may just ignore it. + // The command.success_ will stay at false and this will be handled in the command.Commit + } // Random sleeping to test //boost::this_thread::sleep(boost::posix_time::milliseconds(50 * (1 + rand() % 10))); - if (that->globalMutex_ != NULL) - { - boost::mutex::scoped_lock lock(*that->globalMutex_); - command.Commit(); - } - else - { - command.Commit(); - } + command.Commit(); } } } public: - PImpl(boost::mutex* globalMutex, - unsigned int threadCount) : - globalMutex_(globalMutex), + PImpl(unsigned int threadCount) : state_(State_Init), threads_(threadCount) { @@ -182,19 +180,11 @@ }; - Oracle::Oracle(boost::mutex& globalMutex, - unsigned int threadCount) : - pimpl_(new PImpl(&globalMutex, threadCount)) + Oracle::Oracle(unsigned int threadCount) : + pimpl_(new PImpl(threadCount)) { } - - Oracle::Oracle(unsigned int threadCount) : - pimpl_(new PImpl(NULL, threadCount)) - { - } - - void Oracle::Start() { pimpl_->Start(); diff -r fe4befe03935 -r d6136a7e914d Platforms/Generic/Oracle.h --- a/Platforms/Generic/Oracle.h Mon Nov 05 10:04:56 2018 +0100 +++ b/Platforms/Generic/Oracle.h Mon Nov 05 10:06:18 2018 +0100 @@ -24,7 +24,6 @@ #include "IOracleCommand.h" #include -#include namespace OrthancStone { @@ -36,9 +35,6 @@ boost::shared_ptr pimpl_; public: - Oracle(boost::mutex& globalMutex, - unsigned int threadCount); - Oracle(unsigned int threadCount); void Start(); diff -r fe4befe03935 -r d6136a7e914d Platforms/Generic/OracleWebService.h --- a/Platforms/Generic/OracleWebService.h Mon Nov 05 10:04:56 2018 +0100 +++ b/Platforms/Generic/OracleWebService.h Mon Nov 05 10:06:18 2018 +0100 @@ -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 . **/ @@ -25,36 +25,73 @@ #include "Oracle.h" #include "WebServiceGetCommand.h" #include "WebServicePostCommand.h" +#include "WebServiceDeleteCommand.h" +#include "../../Applications/Generic/NativeStoneApplicationContext.h" namespace OrthancStone { + // The OracleWebService performs HTTP requests in a native environment. + // It uses a thread pool to handle multiple HTTP requests in a same time. + // It works asynchronously to mimick the behaviour of the WebService running in a WASM environment. class OracleWebService : public IWebService { private: Oracle& oracle_; + NativeStoneApplicationContext& context_; Orthanc::WebServiceParameters parameters_; public: - OracleWebService(Oracle& oracle, - const Orthanc::WebServiceParameters& parameters) : + OracleWebService(MessageBroker& broker, + Oracle& oracle, + const Orthanc::WebServiceParameters& parameters, + NativeStoneApplicationContext& context) : + IWebService(broker), oracle_(oracle), + context_(context), parameters_(parameters) { } - virtual void ScheduleGetRequest(ICallback& callback, - const std::string& uri, - Orthanc::IDynamicObject* payload) + virtual void GetAsync(const std::string& uri, + const Headers& headers, + Orthanc::IDynamicObject* payload, // takes ownership + MessageHandler* successCallback, // takes ownership + MessageHandler* failureCallback = NULL,// takes ownership + unsigned int timeoutInSeconds = 60) { - oracle_.Submit(new WebServiceGetCommand(callback, parameters_, uri, payload)); + oracle_.Submit(new WebServiceGetCommand(broker_, successCallback, failureCallback, parameters_, uri, headers, timeoutInSeconds, payload, context_)); } - virtual void SchedulePostRequest(ICallback& callback, - const std::string& uri, - const std::string& body, - Orthanc::IDynamicObject* payload) + virtual void PostAsync(const std::string& uri, + const Headers& headers, + const std::string& body, + Orthanc::IDynamicObject* payload, // takes ownership + MessageHandler* successCallback, // takes ownership + MessageHandler* failureCallback = NULL, // takes ownership + unsigned int timeoutInSeconds = 60) { - oracle_.Submit(new WebServicePostCommand(callback, parameters_, uri, body, payload)); + oracle_.Submit(new WebServicePostCommand(broker_, successCallback, failureCallback, parameters_, uri, headers, timeoutInSeconds, body, payload, context_)); + } + + virtual void DeleteAsync(const std::string& uri, + const Headers& headers, + Orthanc::IDynamicObject* payload, + MessageHandler* successCallback, + MessageHandler* failureCallback = NULL, + unsigned int timeoutInSeconds = 60) + { + oracle_.Submit(new WebServiceDeleteCommand(broker_, successCallback, failureCallback, parameters_, uri, headers, timeoutInSeconds, payload, context_)); + } + + + void Start() + { + oracle_.Start(); + } + + void Stop() + { + oracle_.Stop(); } }; } diff -r fe4befe03935 -r d6136a7e914d Platforms/Generic/WebServiceCommandBase.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Platforms/Generic/WebServiceCommandBase.cpp Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,65 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#include "WebServiceCommandBase.h" + +#include + +namespace OrthancStone +{ + WebServiceCommandBase::WebServiceCommandBase(MessageBroker& broker, + MessageHandler* successCallback, + MessageHandler* failureCallback, + const Orthanc::WebServiceParameters& parameters, + const std::string& uri, + const IWebService::Headers& headers, + unsigned int timeoutInSeconds, + Orthanc::IDynamicObject* payload /* takes ownership */, + NativeStoneApplicationContext& context) : + IObservable(broker), + successCallback_(successCallback), + failureCallback_(failureCallback), + parameters_(parameters), + uri_(uri), + headers_(headers), + payload_(payload), + context_(context), + timeoutInSeconds_(timeoutInSeconds) + { + } + + + void WebServiceCommandBase::Commit() + { + NativeStoneApplicationContext::GlobalMutexLocker lock(context_); // we want to make sure that, i.e, the UpdateThread is not triggered while we are updating the "model" with the result of a WebServiceCommand + + if (success_ && successCallback_.get() != NULL) + { + successCallback_->Apply(IWebService::HttpRequestSuccessMessage(uri_, answer_.c_str(), answer_.size(), payload_.release())); + } + else if (!success_ && failureCallback_.get() != NULL) + { + failureCallback_->Apply(IWebService::HttpRequestErrorMessage(uri_, payload_.release())); + } + + } + +} diff -r fe4befe03935 -r d6136a7e914d Platforms/Generic/WebServiceCommandBase.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Platforms/Generic/WebServiceCommandBase.h Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,68 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "IOracleCommand.h" + +#include "../../Framework/Toolbox/IWebService.h" +#include "../../Framework/Messages/IObservable.h" +#include "../../Framework/Messages/ICallable.h" +#include "../../Applications/Generic/NativeStoneApplicationContext.h" + +#include + +#include + +namespace OrthancStone +{ + class WebServiceCommandBase : public IOracleCommand, IObservable + { + protected: + std::auto_ptr > successCallback_; + std::auto_ptr > failureCallback_; + Orthanc::WebServiceParameters parameters_; + std::string uri_; + std::map headers_; + std::auto_ptr payload_; + bool success_; + std::string answer_; + NativeStoneApplicationContext& context_; + unsigned int timeoutInSeconds_; + + public: + WebServiceCommandBase(MessageBroker& broker, + MessageHandler* successCallback, // takes ownership + MessageHandler* failureCallback, // takes ownership + const Orthanc::WebServiceParameters& parameters, + const std::string& uri, + const std::map& headers, + unsigned int timeoutInSeconds, + Orthanc::IDynamicObject* payload /* takes ownership */, + NativeStoneApplicationContext& context + ); + + virtual void Execute() = 0; + + virtual void Commit(); + }; + +} diff -r fe4befe03935 -r d6136a7e914d Platforms/Generic/WebServiceDeleteCommand.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Platforms/Generic/WebServiceDeleteCommand.cpp Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,55 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#include "WebServiceDeleteCommand.h" + +#include + +namespace OrthancStone +{ + WebServiceDeleteCommand::WebServiceDeleteCommand(MessageBroker& broker, + MessageHandler* successCallback, // takes ownership + MessageHandler* failureCallback, // takes ownership + const Orthanc::WebServiceParameters& parameters, + const std::string& uri, + const IWebService::Headers& headers, + unsigned int timeoutInSeconds, + Orthanc::IDynamicObject* payload /* takes ownership */, + NativeStoneApplicationContext& context) : + WebServiceCommandBase(broker, successCallback, failureCallback, parameters, uri, headers, timeoutInSeconds, payload, context) + { + } + + void WebServiceDeleteCommand::Execute() + { + Orthanc::HttpClient client(parameters_, uri_); + client.SetTimeout(timeoutInSeconds_); + client.SetMethod(Orthanc::HttpMethod_Delete); + + for (IWebService::Headers::const_iterator it = headers_.begin(); it != headers_.end(); it++ ) + { + client.AddHeader(it->first, it->second); + } + + success_ = client.Apply(answer_); + } + +} diff -r fe4befe03935 -r d6136a7e914d Platforms/Generic/WebServiceDeleteCommand.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Platforms/Generic/WebServiceDeleteCommand.h Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,43 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "WebServiceCommandBase.h" + +namespace OrthancStone +{ + class WebServiceDeleteCommand : public WebServiceCommandBase + { + public: + WebServiceDeleteCommand(MessageBroker& broker, + MessageHandler* successCallback, // takes ownership + MessageHandler* failureCallback, // takes ownership + const Orthanc::WebServiceParameters& parameters, + const std::string& uri, + const IWebService::Headers& headers, + unsigned int timeoutInSeconds, + Orthanc::IDynamicObject* payload /* takes ownership */, + NativeStoneApplicationContext& context); + + virtual void Execute(); + }; +} diff -r fe4befe03935 -r d6136a7e914d Platforms/Generic/WebServiceGetCommand.cpp --- a/Platforms/Generic/WebServiceGetCommand.cpp Mon Nov 05 10:04:56 2018 +0100 +++ b/Platforms/Generic/WebServiceGetCommand.cpp Mon Nov 05 10:06:18 2018 +0100 @@ -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 . **/ @@ -25,14 +25,17 @@ namespace OrthancStone { - WebServiceGetCommand::WebServiceGetCommand(IWebService::ICallback& callback, + + WebServiceGetCommand::WebServiceGetCommand(MessageBroker& broker, + MessageHandler* successCallback, // takes ownership + MessageHandler* failureCallback, // takes ownership const Orthanc::WebServiceParameters& parameters, const std::string& uri, - Orthanc::IDynamicObject* payload /* takes ownership */) : - callback_(callback), - parameters_(parameters), - uri_(uri), - payload_(payload) + const IWebService::Headers& headers, + unsigned int timeoutInSeconds, + Orthanc::IDynamicObject* payload /* takes ownership */, + NativeStoneApplicationContext& context) : + WebServiceCommandBase(broker, successCallback, failureCallback, parameters, uri, headers, timeoutInSeconds, payload, context) { } @@ -40,21 +43,15 @@ void WebServiceGetCommand::Execute() { Orthanc::HttpClient client(parameters_, uri_); - client.SetTimeout(60); + client.SetTimeout(timeoutInSeconds_); client.SetMethod(Orthanc::HttpMethod_Get); + + for (IWebService::Headers::const_iterator it = headers_.begin(); it != headers_.end(); it++ ) + { + client.AddHeader(it->first, it->second); + } + success_ = client.Apply(answer_); } - - void WebServiceGetCommand::Commit() - { - if (success_) - { - callback_.NotifySuccess(uri_, answer_.c_str(), answer_.size(), payload_.release()); - } - else - { - callback_.NotifyError(uri_, payload_.release()); - } - } } diff -r fe4befe03935 -r d6136a7e914d Platforms/Generic/WebServiceGetCommand.h --- a/Platforms/Generic/WebServiceGetCommand.h Mon Nov 05 10:04:56 2018 +0100 +++ b/Platforms/Generic/WebServiceGetCommand.h Mon Nov 05 10:06:18 2018 +0100 @@ -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 . **/ @@ -21,34 +21,24 @@ #pragma once -#include "IOracleCommand.h" - -#include "../../Framework/Toolbox/IWebService.h" - -#include - -#include +#include "WebServiceCommandBase.h" namespace OrthancStone { - class WebServiceGetCommand : public IOracleCommand + class WebServiceGetCommand : public WebServiceCommandBase { - private: - IWebService::ICallback& callback_; - Orthanc::WebServiceParameters parameters_; - std::string uri_; - std::auto_ptr payload_; - bool success_; - std::string answer_; - public: - WebServiceGetCommand(IWebService::ICallback& callback, + WebServiceGetCommand(MessageBroker& broker, + MessageHandler* successCallback, // takes ownership + MessageHandler* failureCallback, // takes ownership const Orthanc::WebServiceParameters& parameters, const std::string& uri, - Orthanc::IDynamicObject* payload /* takes ownership */); + const IWebService::Headers& headers, + unsigned int timeoutInSeconds, + Orthanc::IDynamicObject* payload /* takes ownership */, + NativeStoneApplicationContext& context); virtual void Execute(); + }; - virtual void Commit(); - }; } diff -r fe4befe03935 -r d6136a7e914d Platforms/Generic/WebServicePostCommand.cpp --- a/Platforms/Generic/WebServicePostCommand.cpp Mon Nov 05 10:04:56 2018 +0100 +++ b/Platforms/Generic/WebServicePostCommand.cpp Mon Nov 05 10:06:18 2018 +0100 @@ -25,37 +25,34 @@ namespace OrthancStone { - WebServicePostCommand::WebServicePostCommand(IWebService::ICallback& callback, + WebServicePostCommand::WebServicePostCommand(MessageBroker& broker, + MessageHandler* successCallback, // takes ownership + MessageHandler* failureCallback, // takes ownership const Orthanc::WebServiceParameters& parameters, const std::string& uri, + const IWebService::Headers& headers, + unsigned int timeoutInSeconds, const std::string& body, - Orthanc::IDynamicObject* payload /* takes ownership */) : - callback_(callback), - parameters_(parameters), - uri_(uri), - body_(body), - payload_(payload) + Orthanc::IDynamicObject* payload /* takes ownership */, + NativeStoneApplicationContext& context) : + WebServiceCommandBase(broker, successCallback, failureCallback, parameters, uri, headers, timeoutInSeconds, payload, context), + body_(body) { } void WebServicePostCommand::Execute() { Orthanc::HttpClient client(parameters_, uri_); - client.SetTimeout(60); + client.SetTimeout(timeoutInSeconds_); client.SetMethod(Orthanc::HttpMethod_Post); client.GetBody().swap(body_); + + for (IWebService::Headers::const_iterator it = headers_.begin(); it != headers_.end(); it++ ) + { + client.AddHeader(it->first, it->second); + } + success_ = client.Apply(answer_); } - void WebServicePostCommand::Commit() - { - if (success_) - { - callback_.NotifySuccess(uri_, answer_.c_str(), answer_.size(), payload_.release()); - } - else - { - callback_.NotifyError(uri_, payload_.release()); - } - } } diff -r fe4befe03935 -r d6136a7e914d Platforms/Generic/WebServicePostCommand.h --- a/Platforms/Generic/WebServicePostCommand.h Mon Nov 05 10:04:56 2018 +0100 +++ b/Platforms/Generic/WebServicePostCommand.h Mon Nov 05 10:06:18 2018 +0100 @@ -21,36 +21,27 @@ #pragma once -#include "IOracleCommand.h" - -#include "../../Framework/Toolbox/IWebService.h" - -#include - -#include +#include "WebServiceCommandBase.h" namespace OrthancStone { - class WebServicePostCommand : public IOracleCommand + class WebServicePostCommand : public WebServiceCommandBase { - private: - IWebService::ICallback& callback_; - Orthanc::WebServiceParameters parameters_; - std::string uri_; + protected: std::string body_; - std::auto_ptr payload_; - bool success_; - std::string answer_; public: - WebServicePostCommand(IWebService::ICallback& callback, + WebServicePostCommand(MessageBroker& broker, + MessageHandler* successCallback, // takes ownership + MessageHandler* failureCallback, // takes ownership const Orthanc::WebServiceParameters& parameters, const std::string& uri, + const IWebService::Headers& headers, + unsigned int timeoutInSeconds, const std::string& body, - Orthanc::IDynamicObject* payload /* takes ownership */); + Orthanc::IDynamicObject* payload /* takes ownership */, + NativeStoneApplicationContext& context); virtual void Execute(); - - virtual void Commit(); }; } diff -r fe4befe03935 -r d6136a7e914d Platforms/Wasm/Defaults.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Platforms/Wasm/Defaults.cpp Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,284 @@ +#include "Defaults.h" + +#include "WasmWebService.h" +#include +#include "Framework/Widgets/TestCairoWidget.h" +#include +#include +#include +#include "Applications/Wasm/StartupParametersBuilder.h" +#include "Platforms/Wasm/WasmPlatformApplicationAdapter.h" + +static unsigned int width_ = 0; +static unsigned int height_ = 0; + +/**********************************/ + +static std::unique_ptr application; +static std::unique_ptr applicationWasmAdapter = NULL; +static std::unique_ptr context; +static OrthancStone::StartupParametersBuilder startupParametersBuilder; +static OrthancStone::MessageBroker broker; + +static OrthancStone::ViewportContentChangedObserver viewportContentChangedObserver_; +static OrthancStone::StatusBar statusBar_; + +static std::list> viewports_; + +std::shared_ptr FindViewportSharedPtr(ViewportHandle viewport) { + for (const auto& v : viewports_) { + if (v.get() == viewport) { + return v; + } + } + assert(false); + return std::shared_ptr(); +} + +#ifdef __cplusplus +extern "C" { +#endif + + using namespace OrthancStone; + + // when WASM needs a C++ viewport + ViewportHandle EMSCRIPTEN_KEEPALIVE CreateCppViewport() { + + std::shared_ptr viewport(new OrthancStone::WidgetViewport); + printf("viewport %x\n", (int)viewport.get()); + + viewports_.push_back(viewport); + + printf("There are now %d viewports in C++\n", viewports_.size()); + + viewport->SetStatusBar(statusBar_); + viewport->Register(viewportContentChangedObserver_); + + return viewport.get(); + } + + // when WASM does not need a viewport anymore, it should release it + void EMSCRIPTEN_KEEPALIVE ReleaseCppViewport(ViewportHandle viewport) { + viewports_.remove_if([viewport](const std::shared_ptr& v) { return v.get() == viewport;}); + + printf("There are now %d viewports in C++\n", viewports_.size()); + } + + void EMSCRIPTEN_KEEPALIVE CreateWasmApplication(ViewportHandle viewport) { + + printf("CreateWasmApplication\n"); + + application.reset(CreateUserApplication(broker)); + applicationWasmAdapter.reset(CreateWasmApplicationAdapter(broker, application.get())); + WasmWebService::SetBroker(broker); + + startupParametersBuilder.Clear(); + } + + void EMSCRIPTEN_KEEPALIVE SetStartupParameter(const char* keyc, + const char* value) { + startupParametersBuilder.SetStartupParameter(keyc, value); + } + + void EMSCRIPTEN_KEEPALIVE StartWasmApplication() { + + printf("StartWasmApplication\n"); + + // recreate a command line from uri arguments and parse it + boost::program_options::variables_map parameters; + boost::program_options::options_description options; + application->DeclareStartupOptions(options); + startupParametersBuilder.GetStartupParameters(parameters, options); + + context.reset(new OrthancStone::StoneApplicationContext()); + context->SetWebService(OrthancStone::WasmWebService::GetInstance()); + application->Initialize(context.get(), statusBar_, parameters); + application->InitializeWasm(); + +// viewport->SetSize(width_, height_); + printf("StartWasmApplication - completed\n"); + } + + void EMSCRIPTEN_KEEPALIVE NotifyUpdateContent() + { + for (auto viewport : viewports_) { + // TODO Only launch the JavaScript timer if "HasUpdateContent()" + if (viewport->HasUpdateContent()) + { + viewport->UpdateContent(); + } + + } + + } + + + void EMSCRIPTEN_KEEPALIVE ViewportSetSize(ViewportHandle viewport, unsigned int width, unsigned int height) + { + width_ = width; + height_ = height; + + viewport->SetSize(width, height); + } + + int EMSCRIPTEN_KEEPALIVE ViewportRender(ViewportHandle viewport, + unsigned int width, + unsigned int height, + uint8_t* data) + { + viewportContentChangedObserver_.Reset(); + + //printf("ViewportRender called %dx%d\n", width, height); + if (width == 0 || + height == 0) + { + return 1; + } + + Orthanc::ImageAccessor surface; + surface.AssignWritable(Orthanc::PixelFormat_BGRA32, width, height, 4 * width, data); + + viewport->Render(surface); + + // 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. + uint8_t* p = data; + for (unsigned int y = 0; y < height; y++) { + for (unsigned int x = 0; x < width; x++) { + uint8_t tmp = p[0]; + p[0] = p[2]; + p[2] = tmp; + + p += 4; + } + } + + return 1; + } + + + void EMSCRIPTEN_KEEPALIVE ViewportMouseDown(ViewportHandle viewport, + unsigned int rawButton, + int x, + int y, + unsigned int rawModifiers) + { + OrthancStone::MouseButton button; + switch (rawButton) + { + case 0: + button = OrthancStone::MouseButton_Left; + break; + + case 1: + button = OrthancStone::MouseButton_Middle; + break; + + case 2: + button = OrthancStone::MouseButton_Right; + break; + + default: + return; // Unknown button + } + + viewport->MouseDown(button, x, y, OrthancStone::KeyboardModifiers_None /* TODO */); + } + + + void EMSCRIPTEN_KEEPALIVE ViewportMouseWheel(ViewportHandle viewport, + int deltaY, + int x, + int y, + int isControl) + { + if (deltaY != 0) + { + OrthancStone::MouseWheelDirection direction = (deltaY < 0 ? + OrthancStone::MouseWheelDirection_Up : + OrthancStone::MouseWheelDirection_Down); + OrthancStone::KeyboardModifiers modifiers = OrthancStone::KeyboardModifiers_None; + + if (isControl != 0) + { + modifiers = OrthancStone::KeyboardModifiers_Control; + } + + viewport->MouseWheel(direction, x, y, modifiers); + } + } + + + void EMSCRIPTEN_KEEPALIVE ViewportMouseMove(ViewportHandle viewport, + int x, + int y) + { + viewport->MouseMove(x, y); + } + + void EMSCRIPTEN_KEEPALIVE ViewportKeyPressed(ViewportHandle viewport, + int key, + const char* keyChar, + bool isShiftPressed, + bool isControlPressed, + bool isAltPressed) + + { + OrthancStone::KeyboardModifiers modifiers = OrthancStone::KeyboardModifiers_None; + if (isShiftPressed) { + modifiers = static_cast(modifiers + OrthancStone::KeyboardModifiers_Shift); + } + if (isControlPressed) { + modifiers = static_cast(modifiers + OrthancStone::KeyboardModifiers_Control); + } + if (isAltPressed) { + modifiers = static_cast(modifiers + OrthancStone::KeyboardModifiers_Alt); + } + + char c = 0; + if (keyChar != NULL && key == OrthancStone::KeyboardKeys_Generic) { + c = keyChar[0]; + } + viewport->KeyPressed(static_cast(key), c, modifiers); + } + + + void EMSCRIPTEN_KEEPALIVE ViewportMouseUp(ViewportHandle viewport) + { + viewport->MouseUp(); + } + + + void EMSCRIPTEN_KEEPALIVE ViewportMouseEnter(ViewportHandle viewport) + { + viewport->MouseEnter(); + } + + + void EMSCRIPTEN_KEEPALIVE ViewportMouseLeave(ViewportHandle viewport) + { + viewport->MouseLeave(); + } + + const char* EMSCRIPTEN_KEEPALIVE SendMessageToStoneApplication(const char* message) + { + static std::string output; // we don't want the string to be deallocated when we return to JS code so we always use the same string (this is fine since JS is single-thread) + + printf("SendMessageToStoneApplication\n"); + printf("%s", message); + + if (applicationWasmAdapter.get() != NULL) { + printf("sending message to C++\n"); + applicationWasmAdapter->HandleMessageFromWeb(output, std::string(message)); + return output.c_str(); + } + printf("This stone application does not have a Web Adapter"); + return NULL; + } + + +#ifdef __cplusplus +} +#endif diff -r fe4befe03935 -r d6136a7e914d Platforms/Wasm/Defaults.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Platforms/Wasm/Defaults.h Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,79 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include +#include + +typedef OrthancStone::WidgetViewport* ViewportHandle; // the objects exchanged between JS and C++ + +#ifdef __cplusplus +extern "C" { +#endif + + // JS methods accessible from C++ + extern void ScheduleWebViewportRedrawFromCpp(ViewportHandle cppViewportHandle); + extern void UpdateStoneApplicationStatusFromCpp(const char* statusUpdateMessage); + + // C++ methods accessible from JS + extern void EMSCRIPTEN_KEEPALIVE CreateWasmApplication(ViewportHandle cppViewportHandle); + extern void EMSCRIPTEN_KEEPALIVE SetStartupParameter(const char* keyc, const char* value); + + +#ifdef __cplusplus +} +#endif + +// these methods must be implemented in the custom app "mainWasm.cpp" +extern OrthancStone::IStoneApplication* CreateUserApplication(OrthancStone::MessageBroker& broker); +extern OrthancStone::WasmPlatformApplicationAdapter* CreateWasmApplicationAdapter(OrthancStone::MessageBroker& broker, OrthancStone::IStoneApplication* application); + +namespace OrthancStone { + + // default Observer to trigger Viewport redraw when something changes in the Viewport + class ViewportContentChangedObserver : + public OrthancStone::IViewport::IObserver + { + private: + // Flag to avoid flooding JavaScript with redundant Redraw requests + bool isScheduled_; + + public: + ViewportContentChangedObserver() : + isScheduled_(false) + { + } + + void Reset() + { + isScheduled_ = false; + } + + virtual void OnViewportContentChanged(const OrthancStone::IViewport &viewport) + { + if (!isScheduled_) + { + ScheduleWebViewportRedrawFromCpp((ViewportHandle)&viewport); // loosing constness when transmitted to Web + isScheduled_ = true; + } + } + }; + + // default status bar to log messages on the console/stdout + class StatusBar : public OrthancStone::IStatusBar + { + public: + virtual void ClearMessage() + { + } + + virtual void SetMessage(const std::string& message) + { + printf("%s\n", message.c_str()); + } + }; +} \ No newline at end of file diff -r fe4befe03935 -r d6136a7e914d Platforms/Wasm/WasmPlatformApplicationAdapter.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Platforms/Wasm/WasmPlatformApplicationAdapter.cpp Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,51 @@ +#include "WasmPlatformApplicationAdapter.h" + +#include "Framework/Toolbox/MessagingToolbox.h" +#include "Framework/StoneException.h" +#include +#include +#include "Platforms/Wasm/Defaults.h" + +namespace OrthancStone +{ + WasmPlatformApplicationAdapter::WasmPlatformApplicationAdapter(MessageBroker& broker, IStoneApplication& application) + : IObserver(broker), + application_(application) + { + } + + void WasmPlatformApplicationAdapter::HandleMessageFromWeb(std::string& output, const std::string& input) + { + try + { + Json::Value inputJson; + // if the message is a command, build it and execute it + if (MessagingToolbox::ParseJson(inputJson, input.c_str(), input.size())) + { + std::unique_ptr command(application_.GetCommandBuilder().CreateFromJson(inputJson)); + if (command.get() == NULL) + printf("Could not parse command: '%s'\n", input.c_str()); + else + application_.ExecuteCommand(*command); + } + } + catch (StoneException& exc) + { + printf("Error while handling message from web (error code = %d):\n", exc.GetErrorCode()); + printf("While interpreting input: '%s'\n", input.c_str()); + } + } + + void WasmPlatformApplicationAdapter::NotifyStatusUpdateFromCppToWeb(const std::string& statusUpdateMessage) + { + try + { + UpdateStoneApplicationStatusFromCpp(statusUpdateMessage.c_str()); + } + catch (...) + { + printf("Error while handling message to web\n"); + } + } + +} \ No newline at end of file diff -r fe4befe03935 -r d6136a7e914d Platforms/Wasm/WasmPlatformApplicationAdapter.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Platforms/Wasm/WasmPlatformApplicationAdapter.h Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,18 @@ +#pragma once + +#include +#include +#include + +namespace OrthancStone +{ + class WasmPlatformApplicationAdapter : public IObserver + { + IStoneApplication& application_; + public: + WasmPlatformApplicationAdapter(MessageBroker& broker, IStoneApplication& application); + + virtual void HandleMessageFromWeb(std::string& output, const std::string& input); + virtual void NotifyStatusUpdateFromCppToWeb(const std::string& statusUpdateMessage); + }; +} \ No newline at end of file diff -r fe4befe03935 -r d6136a7e914d Platforms/Wasm/WasmViewport.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Platforms/Wasm/WasmViewport.cpp Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,13 @@ +#include "WasmViewport.h" + +#include +#include + +std::vector> wasmViewports; + +void AttachWidgetToWasmViewport(const char* htmlCanvasId, OrthancStone::IWidget* centralWidget) { + std::shared_ptr viewport(CreateWasmViewportFromCpp(htmlCanvasId)); + viewport->SetCentralWidget(centralWidget); + + wasmViewports.push_back(viewport); +} \ No newline at end of file diff -r fe4befe03935 -r d6136a7e914d Platforms/Wasm/WasmViewport.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Platforms/Wasm/WasmViewport.h Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,18 @@ +#pragma once + +#include + +#include + +#ifdef __cplusplus +extern "C" { +#endif + + // JS methods accessible from C++ + extern OrthancStone::WidgetViewport* CreateWasmViewportFromCpp(const char* htmlCanvasId); + +#ifdef __cplusplus +} +#endif + +extern void AttachWidgetToWasmViewport(const char* htmlCanvasId, OrthancStone::IWidget* centralWidget); diff -r fe4befe03935 -r d6136a7e914d Platforms/Wasm/WasmWebService.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Platforms/Wasm/WasmWebService.cpp Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,152 @@ +#include "WasmWebService.h" +#include "json/value.h" +#include "json/writer.h" +#include + +#ifdef __cplusplus +extern "C" { +#endif + + extern void WasmWebService_GetAsync(void* callableSuccess, + void* callableFailure, + const char* uri, + const char* headersInJsonString, + void* payload, + unsigned int timeoutInSeconds); + + extern void WasmWebService_PostAsync(void* callableSuccess, + void* callableFailure, + const char* uri, + const char* headersInJsonString, + const void* body, + size_t bodySize, + void* payload, + unsigned int timeoutInSeconds); + + extern void WasmWebService_DeleteAsync(void* callableSuccess, + void* callableFailure, + const char* uri, + const char* headersInJsonString, + void* payload, + unsigned int timeoutInSeconds); + + void EMSCRIPTEN_KEEPALIVE WasmWebService_NotifyError(void* failureCallable, + const char* uri, + void* payload) + { + if (failureCallable == NULL) + { + throw; + } + else + { + reinterpret_cast*>(failureCallable)-> + Apply(OrthancStone::IWebService::HttpRequestErrorMessage(uri, reinterpret_cast(payload))); + } + } + + void EMSCRIPTEN_KEEPALIVE WasmWebService_NotifySuccess(void* successCallable, + const char* uri, + const void* body, + size_t bodySize, + void* payload) + { + if (successCallable == NULL) + { + throw; + } + else + { + reinterpret_cast*>(successCallable)-> + Apply(OrthancStone::IWebService::HttpRequestSuccessMessage(uri, body, bodySize, reinterpret_cast(payload))); + } + } + + void EMSCRIPTEN_KEEPALIVE WasmWebService_SetBaseUri(const char* baseUri) + { + OrthancStone::WasmWebService::GetInstance().SetBaseUri(baseUri); + } + +#ifdef __cplusplus +} +#endif + + + +namespace OrthancStone +{ + MessageBroker* WasmWebService::broker_ = NULL; + + void WasmWebService::SetBaseUri(const std::string baseUri) + { + // Make sure the base url ends with "/" + if (baseUri.empty() || + baseUri[baseUri.size() - 1] != '/') + { + baseUri_ = baseUri + "/"; + } + else + { + baseUri_ = baseUri; + } + } + + void ToJsonString(std::string& output, const IWebService::Headers& headers) + { + Json::Value jsonHeaders; + for (IWebService::Headers::const_iterator it = headers.begin(); it != headers.end(); it++ ) + { + jsonHeaders[it->first] = it->second; + } + + Json::StreamWriterBuilder builder; + std::unique_ptr writer(builder.newStreamWriter()); + std::ostringstream outputStr; + + writer->write(jsonHeaders, &outputStr); + output = outputStr.str(); + } + + void WasmWebService::PostAsync(const std::string& relativeUri, + const Headers& headers, + const std::string& body, + Orthanc::IDynamicObject* payload, + MessageHandler* successCallable, + MessageHandler* failureCallable, + unsigned int timeoutInSeconds) + { + std::string uri = baseUri_ + relativeUri; + std::string headersInJsonString; + ToJsonString(headersInJsonString, headers); + WasmWebService_PostAsync(successCallable, failureCallable, uri.c_str(), headersInJsonString.c_str(), + body.c_str(), body.size(), payload, timeoutInSeconds); + } + + void WasmWebService::DeleteAsync(const std::string& relativeUri, + const Headers& headers, + Orthanc::IDynamicObject* payload, + MessageHandler* successCallable, + MessageHandler* failureCallable, + unsigned int timeoutInSeconds) + { + std::string uri = baseUri_ + relativeUri; + std::string headersInJsonString; + ToJsonString(headersInJsonString, headers); + WasmWebService_DeleteAsync(successCallable, failureCallable, uri.c_str(), headersInJsonString.c_str(), + payload, timeoutInSeconds); + } + + void WasmWebService::GetAsync(const std::string& relativeUri, + const Headers& headers, + Orthanc::IDynamicObject* payload, + MessageHandler* successCallable, + MessageHandler* failureCallable, + unsigned int timeoutInSeconds) + { + std::string uri = baseUri_ + relativeUri; + std::string headersInJsonString; + ToJsonString(headersInJsonString, headers); + WasmWebService_GetAsync(successCallable, failureCallable, uri.c_str(), headersInJsonString.c_str(), payload, timeoutInSeconds); + } + +} diff -r fe4befe03935 -r d6136a7e914d Platforms/Wasm/WasmWebService.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Platforms/Wasm/WasmWebService.h Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,70 @@ +#pragma once + +#include +#include + +namespace OrthancStone +{ + class WasmWebService : public IWebService + { + private: + std::string baseUri_; + static MessageBroker* broker_; + + // Private constructor => Singleton design pattern + WasmWebService(MessageBroker& broker) : + IWebService(broker), + baseUri_("../../") // note: this is configurable from the JS code by calling WasmWebService_SetBaseUri + { + } + + public: + static WasmWebService& GetInstance() + { + if (broker_ == NULL) + { + printf("WasmWebService::GetInstance(): broker not initialized\n"); + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + static WasmWebService instance(*broker_); + return instance; + } + + static void SetBroker(MessageBroker& broker) + { + broker_ = &broker; + } + + void SetBaseUri(const std::string baseUri); + + virtual void GetAsync(const std::string& uri, + const Headers& headers, + Orthanc::IDynamicObject* payload, + MessageHandler* successCallable, + MessageHandler* failureCallable = NULL, + unsigned int timeoutInSeconds = 60); + + virtual void PostAsync(const std::string& uri, + const Headers& headers, + const std::string& body, + Orthanc::IDynamicObject* payload, + MessageHandler* successCallable, + MessageHandler* failureCallable = NULL, + unsigned int timeoutInSeconds = 60); + + virtual void DeleteAsync(const std::string& uri, + const Headers& headers, + Orthanc::IDynamicObject* payload, + MessageHandler* successCallable, + MessageHandler* failureCallable = NULL, + unsigned int timeoutInSeconds = 60); + + virtual void Start() + { + } + + virtual void Stop() + { + } + }; +} diff -r fe4befe03935 -r d6136a7e914d Platforms/Wasm/WasmWebService.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Platforms/Wasm/WasmWebService.js Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,90 @@ +mergeInto(LibraryManager.library, { + WasmWebService_GetAsync: function(callableSuccess, callableFailure, url, headersInJsonString, payload, timeoutInSeconds) { + // Directly use XMLHttpRequest (no jQuery) to retrieve the raw binary data + // http://www.henryalgus.com/reading-binary-files-using-jquery-ajax/ + var xhr = new XMLHttpRequest(); + var url_ = UTF8ToString(url); + var headersInJsonString_ = UTF8ToString(headersInJsonString); + + xhr.open('GET', url_, true); + xhr.responseType = 'arraybuffer'; + xhr.timeout = timeoutInSeconds * 1000; + var headers = JSON.parse(headersInJsonString_); + for (var key in headers) { + xhr.setRequestHeader(key, headers[key]); + } + //console.log(xhr); + xhr.onreadystatechange = function() { + if (this.readyState == XMLHttpRequest.DONE) { + if (xhr.status === 200) { + // TODO - Is "new Uint8Array()" necessary? This copies the + // answer to the WebAssembly stack, hence necessitating + // increasing the TOTAL_STACK parameter of Emscripten + WasmWebService_NotifySuccess(callableSuccess, url_, new Uint8Array(this.response), + this.response.byteLength, payload); + } else { + WasmWebService_NotifyError(callableFailure, url_, payload); + } + } + } + + xhr.send(); + }, + + WasmWebService_PostAsync: function(callableSuccess, callableFailure, url, headersInJsonString, body, bodySize, payload, timeoutInSeconds) { + var xhr = new XMLHttpRequest(); + var url_ = UTF8ToString(url); + var headersInJsonString_ = UTF8ToString(headersInJsonString); + xhr.open('POST', url_, true); + xhr.timeout = timeoutInSeconds * 1000; + xhr.responseType = 'arraybuffer'; + xhr.setRequestHeader('Content-type', 'application/octet-stream'); + + var headers = JSON.parse(headersInJsonString_); + for (var key in headers) { + xhr.setRequestHeader(key, headers[key]); + } + + xhr.onreadystatechange = function() { + if (this.readyState == XMLHttpRequest.DONE) { + if (xhr.status === 200) { + WasmWebService_NotifySuccess(callableSuccess, url_, new Uint8Array(this.response), + this.response.byteLength, payload); + } else { + WasmWebService_NotifyError(callableFailure, url_, payload); + } + } + } + + xhr.send(new Uint8ClampedArray(HEAPU8.buffer, body, bodySize)); + }, + + WasmWebService_DeleteAsync: function(callableSuccess, callableFailure, url, headersInJsonString, payload, timeoutInSeconds) { + var xhr = new XMLHttpRequest(); + var url_ = UTF8ToString(url); + var headersInJsonString_ = UTF8ToString(headersInJsonString); + xhr.open('DELETE', url_, true); + xhr.timeout = timeoutInSeconds * 1000; + xhr.responseType = 'arraybuffer'; + xhr.setRequestHeader('Content-type', 'application/octet-stream'); + + var headers = JSON.parse(headersInJsonString_); + for (var key in headers) { + xhr.setRequestHeader(key, headers[key]); + } + + xhr.onreadystatechange = function() { + if (this.readyState == XMLHttpRequest.DONE) { + if (xhr.status === 200) { + WasmWebService_NotifySuccess(callableSuccess, url_, new Uint8Array(this.response), + this.response.byteLength, payload); + } else { + WasmWebService_NotifyError(callableFailure, url_, payload); + } + } + } + + xhr.send(); + } + +}); diff -r fe4befe03935 -r d6136a7e914d Platforms/Wasm/default-library.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Platforms/Wasm/default-library.js Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,16 @@ +// this file contains the JS method you want to expose to C++ code + +mergeInto(LibraryManager.library, { + ScheduleWebViewportRedrawFromCpp: function(cppViewportHandle) { + ScheduleWebViewportRedraw(cppViewportHandle); + }, + CreateWasmViewportFromCpp: function(htmlCanvasId) { + return CreateWasmViewport(htmlCanvasId); + }, + // each time the StoneApplication updates its status, it may signal it through this method. i.e, to change the status of a button in the web interface + UpdateStoneApplicationStatusFromCpp: function(statusUpdateMessage) { + var statusUpdateMessage_ = UTF8ToString(statusUpdateMessage); + UpdateWebApplication(statusUpdateMessage_); + } +}); + \ No newline at end of file diff -r fe4befe03935 -r d6136a7e914d Platforms/Wasm/stone-framework-loader.ts --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Platforms/Wasm/stone-framework-loader.ts Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,96 @@ +module Stone { + /** + * This file contains primitives to interface with WebAssembly and + * with the Stone framework. + **/ + + export declare type InitializationCallback = () => void; + + export declare var StoneFrameworkModule : any; + + //const ASSETS_FOLDER : string = "assets/lib"; + //const WASM_FILENAME : string = "orthanc-framework"; + + + export class Framework + { + private static singleton_ : Framework = null; + private static wasmModuleName_ : string = null; + + public static Configure(wasmModuleName: string) { + this.wasmModuleName_ = wasmModuleName; + } + + private constructor(verbose : boolean) + { + //this.ccall('Initialize', null, [ 'number' ], [ verbose ]); + } + + + public ccall(name: string, + returnType: string, + argTypes: Array, + argValues: Array) : any + { + return StoneFrameworkModule.ccall(name, returnType, argTypes, argValues); + } + + + public cwrap(name: string, + returnType: string, + argTypes: Array) : any + { + return StoneFrameworkModule.cwrap(name, returnType, argTypes); + } + + + public static GetInstance() : Framework + { + if (Framework.singleton_ == null) { + throw new Error('The WebAssembly module is not loaded yet'); + } else { + return Framework.singleton_; + } + } + + + public static Initialize(verbose: boolean, + callback: InitializationCallback) + { + console.log('Initializing WebAssembly Module'); + + ( window).StoneFrameworkModule = { + preRun: [ + function() { + console.log('Loading the Stone Framework using WebAssembly'); + } + ], + postRun: [ + function() { + // This function is called by ".js" wrapper once the ".wasm" + // WebAssembly module has been loaded and compiled by the + // browser + console.log('WebAssembly is ready'); + Framework.singleton_ = new Framework(verbose); + callback(); + } + ], + print: function(text : string) { + console.log(text); + }, + printErr: function(text : string) { + console.error(text); + }, + totalDependencies: 0 + }; + + // Dynamic loading of the JavaScript wrapper around WebAssembly + var script = document.createElement('script'); + script.type = 'application/javascript'; + //script.src = "orthanc-stone.js"; // ASSETS_FOLDER + '/' + WASM_FILENAME + '.js'; + script.src = this.wasmModuleName_ + ".js";// "OrthancStoneSimpleViewer.js"; // ASSETS_FOLDER + '/' + WASM_FILENAME + '.js'; + script.async = true; + document.head.appendChild(script); + } + } +} \ No newline at end of file diff -r fe4befe03935 -r d6136a7e914d Platforms/Wasm/tsconfig-stone.json --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Platforms/Wasm/tsconfig-stone.json Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,7 @@ +{ + "include" : [ + "stone-framework-loader.ts", + "wasm-application-runner.ts", + "wasm-viewport.ts" + ] +} diff -r fe4befe03935 -r d6136a7e914d Platforms/Wasm/wasm-application-runner.ts --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Platforms/Wasm/wasm-application-runner.ts Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,113 @@ +/// +/// + +if (!('WebAssembly' in window)) { + alert('Sorry, your browser does not support WebAssembly :('); +} + +declare var StoneFrameworkModule : Stone.Framework; + +// global functions +var WasmWebService_NotifyError: Function = null; +var WasmWebService_NotifySuccess: Function = null; +var WasmWebService_SetBaseUri: Function = null; +var NotifyUpdateContent: Function = null; +var SetStartupParameter: Function = null; +var CreateWasmApplication: Function = null; +var CreateCppViewport: Function = null; +var ReleaseCppViewport: Function = null; +var StartWasmApplication: Function = null; +var SendMessageToStoneApplication: Function = null; + + +function UpdateContentThread() { + if (NotifyUpdateContent != null) { + NotifyUpdateContent(); + } + + setTimeout(UpdateContentThread, 100); // Update the viewport content every 100ms if need be +} + + +function GetUriParameters(): Map { + var parameters = window.location.search.substr(1); + + if (parameters != null && + parameters != '') { + var result = new Map(); + var tokens = parameters.split('&'); + + for (var i = 0; i < tokens.length; i++) { + var tmp = tokens[i].split('='); + if (tmp.length == 2) { + result[tmp[0]] = decodeURIComponent(tmp[1]); + } + } + + return result; + } + else { + return new Map(); + } +} + +// function UpdateWebApplication(statusUpdateMessage: string) { +// console.log(statusUpdateMessage); +// } + +function _InitializeWasmApplication(orthancBaseUrl: string): void { + + CreateWasmApplication(); + WasmWebService_SetBaseUri(orthancBaseUrl); + + + // parse uri and transmit the parameters to the app before initializing it + let parameters = GetUriParameters(); + + for (let key in parameters) { + if (parameters.hasOwnProperty(key)) { + SetStartupParameter(key, parameters[key]); + } + } + + StartWasmApplication(); + + // trigger a first resize of the canvas that have just been initialized + Stone.WasmViewport.ResizeAll(); + + UpdateContentThread(); +} + +function InitializeWasmApplication(wasmModuleName: string, orthancBaseUrl: string) { + + Stone.Framework.Configure(wasmModuleName); + + // Wait for the Orthanc Framework to be initialized (this initializes + // the WebAssembly environment) and then, create and initialize the Wasm application + Stone.Framework.Initialize(true, function () { + + console.log("Connecting C++ methods to JS methods"); + + SetStartupParameter = StoneFrameworkModule.cwrap('SetStartupParameter', null, ['string', 'string']); + CreateWasmApplication = StoneFrameworkModule.cwrap('CreateWasmApplication', null, ['number']); + CreateCppViewport = StoneFrameworkModule.cwrap('CreateCppViewport', 'number', []); + ReleaseCppViewport = StoneFrameworkModule.cwrap('ReleaseCppViewport', null, ['number']); + StartWasmApplication = StoneFrameworkModule.cwrap('StartWasmApplication', null, ['number']); + + WasmWebService_NotifySuccess = StoneFrameworkModule.cwrap('WasmWebService_NotifySuccess', null, ['number', 'string', 'array', 'number', 'number']); + WasmWebService_NotifyError = StoneFrameworkModule.cwrap('WasmWebService_NotifyError', null, ['number', 'string', 'number']); + WasmWebService_SetBaseUri = StoneFrameworkModule.cwrap('WasmWebService_SetBaseUri', null, ['string']); + NotifyUpdateContent = StoneFrameworkModule.cwrap('NotifyUpdateContent', null, []); + + SendMessageToStoneApplication = StoneFrameworkModule.cwrap('SendMessageToStoneApplication', 'string', ['string']); + + console.log("Connecting C++ methods to JS methods - done"); + + // Prevent scrolling + document.body.addEventListener('touchmove', function (event) { + event.preventDefault(); + }, false); + + _InitializeWasmApplication(orthancBaseUrl); + }); +} \ No newline at end of file diff -r fe4befe03935 -r d6136a7e914d Platforms/Wasm/wasm-viewport.ts --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Platforms/Wasm/wasm-viewport.ts Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,298 @@ +var isPendingRedraw = false; + +function ScheduleWebViewportRedraw(cppViewportHandle: any) : void +{ + if (!isPendingRedraw) { + isPendingRedraw = true; + console.log('Scheduling a refresh of the viewport, as its content changed'); + window.requestAnimationFrame(function() { + isPendingRedraw = false; + Stone.WasmViewport.GetFromCppViewport(cppViewportHandle).Redraw(); + }); + } +} + +declare function UTF8ToString(any): string; + +function CreateWasmViewport(htmlCanvasId: string) : any { + var cppViewportHandle = CreateCppViewport(); + var canvasId = UTF8ToString(htmlCanvasId); + var webViewport = new Stone.WasmViewport(StoneFrameworkModule, canvasId, cppViewportHandle); // viewports are stored in a static map in WasmViewport -> won't be deleted + webViewport.Initialize(); + + return cppViewportHandle; +} + +module Stone { + +// export declare type InitializationCallback = () => void; + +// export declare var StoneFrameworkModule : any; + + //const ASSETS_FOLDER : string = "assets/lib"; + //const WASM_FILENAME : string = "orthanc-framework"; + + export class WasmViewport { + + private static viewportsMapByCppHandle_ : Map = new Map(); // key = the C++ handle + private static viewportsMapByCanvasId_ : Map = new Map(); // key = the canvasId + + private module_ : any; + private canvasId_ : string; + private htmlCanvas_ : HTMLCanvasElement; + private context_ : CanvasRenderingContext2D; + private imageData_ : any = null; + private renderingBuffer_ : any = null; + private touchZoom_ : any = false; + private touchTranslation_ : any = false; + + private ViewportSetSize : Function; + private ViewportRender : Function; + private ViewportMouseDown : Function; + private ViewportMouseMove : Function; + private ViewportMouseUp : Function; + private ViewportMouseEnter : Function; + private ViewportMouseLeave : Function; + private ViewportMouseWheel : Function; + private ViewportKeyPressed : Function; + + private pimpl_ : any; // Private pointer to the underlying WebAssembly C++ object + + public constructor(module: any, canvasId: string, cppViewport: any) { + + this.pimpl_ = cppViewport; + WasmViewport.viewportsMapByCppHandle_[this.pimpl_] = this; + WasmViewport.viewportsMapByCanvasId_[canvasId] = this; + + this.module_ = module; + this.canvasId_ = canvasId; + this.htmlCanvas_ = document.getElementById(this.canvasId_) as HTMLCanvasElement; + if (this.htmlCanvas_ == null) { + console.log("Can not create WasmViewport, did not find the canvas whose id is '", this.canvasId_, "'"); + } + this.context_ = this.htmlCanvas_.getContext('2d'); + + this.ViewportSetSize = this.module_.cwrap('ViewportSetSize', null, [ 'number', 'number', 'number' ]); + this.ViewportRender = this.module_.cwrap('ViewportRender', null, [ 'number', 'number', 'number', 'number' ]); + this.ViewportMouseDown = this.module_.cwrap('ViewportMouseDown', null, [ 'number', 'number', 'number', 'number', 'number' ]); + this.ViewportMouseMove = this.module_.cwrap('ViewportMouseMove', null, [ 'number', 'number', 'number' ]); + this.ViewportMouseUp = this.module_.cwrap('ViewportMouseUp', null, [ 'number' ]); + this.ViewportMouseEnter = this.module_.cwrap('ViewportMouseEnter', null, [ 'number' ]); + this.ViewportMouseLeave = this.module_.cwrap('ViewportMouseLeave', null, [ 'number' ]); + this.ViewportMouseWheel = this.module_.cwrap('ViewportMouseWheel', null, [ 'number', 'number', 'number', 'number', 'number' ]); + this.ViewportKeyPressed = this.module_.cwrap('ViewportKeyPressed', null, [ 'number', 'number', 'string', 'number', 'number' ]); + } + + public GetCppViewport() : number { + return this.pimpl_; + } + + public static GetFromCppViewport(cppViewportHandle: number) : WasmViewport { + if (WasmViewport.viewportsMapByCppHandle_[cppViewportHandle] !== undefined) { + return WasmViewport.viewportsMapByCppHandle_[cppViewportHandle]; + } + console.log("WasmViewport not found !"); + return undefined; + } + + public static GetFromCanvasId(canvasId: string) : WasmViewport { + if (WasmViewport.viewportsMapByCanvasId_[canvasId] !== undefined) { + return WasmViewport.viewportsMapByCanvasId_[canvasId]; + } + console.log("WasmViewport not found !"); + return undefined; + } + + public static ResizeAll() { + for (let canvasId in WasmViewport.viewportsMapByCanvasId_) { + WasmViewport.viewportsMapByCanvasId_[canvasId].Resize(); + } + } + + public Redraw() { + if (this.imageData_ === null || + this.renderingBuffer_ === null || + this.ViewportRender(this.pimpl_, + this.imageData_.width, + this.imageData_.height, + this.renderingBuffer_) == 0) { + console.log('The rendering has failed'); + } else { + // Create an accessor to the rendering buffer (i.e. create a + // "window" above the heap of the WASM module), then copy it to + // the ImageData object + this.imageData_.data.set(new Uint8ClampedArray( + this.module_.buffer, + this.renderingBuffer_, + this.imageData_.width * this.imageData_.height * 4)); + + this.context_.putImageData(this.imageData_, 0, 0); + } + } + + public Resize() { + if (this.imageData_ != null && + (this.imageData_.width != window.innerWidth || + this.imageData_.height != window.innerHeight)) { + this.imageData_ = null; + } + + // width/height can be defined in percent of window width/height through html attributes like data-width-ratio="50" and data-height-ratio="20" + var widthRatio = Number(this.htmlCanvas_.dataset["widthRatio"]) || 100; + var heightRatio = Number(this.htmlCanvas_.dataset["heightRatio"]) || 100; + + this.htmlCanvas_.width = window.innerWidth * (widthRatio / 100); + this.htmlCanvas_.height = window.innerHeight * (heightRatio / 100); + + console.log("resizing WasmViewport: ", this.htmlCanvas_.width, "x", this.htmlCanvas_.height); + + if (this.imageData_ === null) { + this.imageData_ = this.context_.getImageData(0, 0, this.htmlCanvas_.width, this.htmlCanvas_.height); + this.ViewportSetSize(this.pimpl_, this.htmlCanvas_.width, this.htmlCanvas_.height); + + if (this.renderingBuffer_ != null) { + this.module_._free(this.renderingBuffer_); + } + + this.renderingBuffer_ = this.module_._malloc(this.imageData_.width * this.imageData_.height * 4); + } else { + this.ViewportSetSize(this.pimpl_, this.htmlCanvas_.width, this.htmlCanvas_.height); + } + + this.Redraw(); + } + + public Initialize() { + + // Force the rendering of the viewport for the first time + this.Resize(); + + var that : WasmViewport = this; + // Register an event listener to call the Resize() function + // each time the window is resized. + window.addEventListener('resize', function(event) { + that.Resize(); + }, false); + + this.htmlCanvas_.addEventListener('contextmenu', function(event) { + // Prevent right click on the canvas + event.preventDefault(); + }, false); + + this.htmlCanvas_.addEventListener('mouseleave', function(event) { + that.ViewportMouseLeave(that.pimpl_); + }); + + this.htmlCanvas_.addEventListener('mouseenter', function(event) { + that.ViewportMouseEnter(that.pimpl_); + }); + + this.htmlCanvas_.addEventListener('mousedown', function(event) { + var x = event.pageX - this.offsetLeft; + var y = event.pageY - this.offsetTop; + that.ViewportMouseDown(that.pimpl_, event.button, x, y, 0 /* TODO */); + }); + + this.htmlCanvas_.addEventListener('mousemove', function(event) { + var x = event.pageX - this.offsetLeft; + var y = event.pageY - this.offsetTop; + that.ViewportMouseMove(that.pimpl_, x, y); + }); + + this.htmlCanvas_.addEventListener('mouseup', function(event) { + that.ViewportMouseUp(that.pimpl_); + }); + + window.addEventListener('keydown', function(event) { + var keyChar = event.key; + var keyCode = event.keyCode + if (keyChar.length == 1) { + keyCode = 0; // maps to OrthancStone::KeyboardKeys_Generic + } else { + keyChar = null; + } +// console.log("key: ", keyCode, keyChar); + that.ViewportKeyPressed(that.pimpl_, keyCode, keyChar, event.shiftKey, event.ctrlKey, event.altKey); + }); + + this.htmlCanvas_.addEventListener('wheel', function(event) { + var x = event.pageX - this.offsetLeft; + var y = event.pageY - this.offsetTop; + that.ViewportMouseWheel(that.pimpl_, event.deltaY, x, y, event.ctrlKey); + event.preventDefault(); + }); + + this.htmlCanvas_.addEventListener('touchstart', function(event) { + that.ResetTouch(); + }); + + this.htmlCanvas_.addEventListener('touchend', function(event) { + that.ResetTouch(); + }); + + this.htmlCanvas_.addEventListener('touchmove', function(event) { + if (that.touchTranslation_.length == 2) { + var t = that.GetTouchTranslation(event); + that.ViewportMouseMove(that.pimpl_, t[0], t[1]); + } + else if (that.touchZoom_.length == 3) { + var z0 = that.touchZoom_; + var z1 = that.GetTouchZoom(event); + that.ViewportMouseMove(that.pimpl_, z0[0], z0[1] - z0[2] + z1[2]); + } + else { + // Realize the gesture event + if (event.targetTouches.length == 1) { + // Exactly one finger inside the canvas => Setup a translation + that.touchTranslation_ = that.GetTouchTranslation(event); + that.ViewportMouseDown(that.pimpl_, + 1 /* middle button */, + that.touchTranslation_[0], + that.touchTranslation_[1], 0); + } else if (event.targetTouches.length == 2) { + // Exactly 2 fingers inside the canvas => Setup a pinch/zoom + that.touchZoom_ = that.GetTouchZoom(event); + var z0 = that.touchZoom_; + that.ViewportMouseDown(that.pimpl_, + 2 /* right button */, + z0[0], + z0[1], 0); + } + } + }); + } + + public ResetTouch() { + if (this.touchTranslation_ || + this.touchZoom_) { + this.ViewportMouseUp(this.pimpl_); + } + + this.touchTranslation_ = false; + this.touchZoom_ = false; + } + + public GetTouchTranslation(event) { + var touch = event.targetTouches[0]; + return [ + touch.pageX, + touch.pageY + ]; + } + + public GetTouchZoom(event) { + var touch1 = event.targetTouches[0]; + var touch2 = event.targetTouches[1]; + var dx = (touch1.pageX - touch2.pageX); + var dy = (touch1.pageY - touch2.pageY); + var d = Math.sqrt(dx * dx + dy * dy); + return [ + (touch1.pageX + touch2.pageX) / 2.0, + (touch1.pageY + touch2.pageY) / 2.0, + d + ]; + } + +} +} + \ No newline at end of file diff -r fe4befe03935 -r d6136a7e914d Platforms/WebAssembly/CMakeLists.txt --- a/Platforms/WebAssembly/CMakeLists.txt Mon Nov 05 10:04:56 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,49 +0,0 @@ -# Usage (Linux): -# source ~/Downloads/emsdk/emsdk_env.sh && cmake -DCMAKE_TOOLCHAIN_FILE=${EMSCRIPTEN}/cmake/Modules/Platform/Emscripten.cmake .. - -cmake_minimum_required(VERSION 2.8.3) - - -##################################################################### -## Configuration of the Emscripten compiler for WebAssembly target -##################################################################### - -set(WASM_FLAGS "-s WASM=1") -set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${WASM_FLAGS}") -set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${WASM_FLAGS}") -set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} --js-library ${CMAKE_SOURCE_DIR}/library.js") - -# Handling of memory -#set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s ALLOW_MEMORY_GROWTH=1") # Resize -#set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s TOTAL_MEMORY=536870912") # 512MB -set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s ALLOW_MEMORY_GROWTH=1 -s TOTAL_MEMORY=536870912") # 512MB + resize -#set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s ALLOW_MEMORY_GROWTH=1 -s TOTAL_MEMORY=1073741824") # 1GB + resize - -# To debug exceptions -#set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s DEMANGLE_SUPPORT=1 -s ASSERTIONS=2") - - -##################################################################### -## Build a static library containing the Orthanc Stone framework -##################################################################### - -include(../../Resources/CMake/OrthancStoneParameters.cmake) - -SET(ORTHANC_SANDBOXED ON) -SET(ENABLE_SDL OFF) - -include(../../Resources/CMake/OrthancStoneConfiguration.cmake) - -add_library(OrthancStone STATIC ${ORTHANC_STONE_SOURCES}) - - - - - - -# Regenerate a dummy "library.c" file each time the "library.js" file -# is modified, so as to force a new execution of the linking -add_custom_command( - OUTPUT "${AUTOGENERATED_DIR}/library.c" - COMMAND ${CMAKE_COMMAND} -E touch "${AUTOGENERATED_DIR}/library.c" "" - DEPENDS "${CMAKE_SOURCE_DIR}/library.js") diff -r fe4befe03935 -r d6136a7e914d Platforms/WebAssembly/library.js --- a/Platforms/WebAssembly/library.js Mon Nov 05 10:04:56 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,2 +0,0 @@ -mergeInto(LibraryManager.library, { -}); diff -r fe4befe03935 -r d6136a7e914d README --- a/README Mon Nov 05 10:04:56 2018 +0100 +++ b/README Mon Nov 05 10:06:18 2018 +0100 @@ -72,6 +72,10 @@ * Optionally, SDL, a cross-platform multimedia library: https://www.libsdl.org/ +Prerequisites to compile on Ubuntu: +``` +sudo apt-get install -y libcairo-dev libpixman-1-dev libsdl2-dev +``` Installation and usage ---------------------- @@ -83,7 +87,43 @@ http://book.orthanc-server.com/developers/stone.html Stone of Orthanc comes with several sample applications in the -"Samples" folder. These samples use SDL. +"Samples" folder. These samples can be compiled into Web Assembly +or into native SDL applications. + +to build the WASM samples: +------------------------- +``` +cd ~/orthanc-stone/Applications/Samples +./build-wasm.sh +``` + +to serve the WASM samples: +``` +# launch an Orthanc listening on 8042 port: +Orthanc + +# launch an nginx that will serve the WASM static files and reverse proxy Orthanc +sudo nginx -p $(pwd) -c nginx.local.conf +``` +Now, you can open the samples in http://localhost:9977 + +to build the SDL native samples (SimpleViewer only): +------------------------------- +``` +mkdir -p ~/builds/orthanc-stone-build +cd ~/builds/orthanc-stone-build +cmake -DALLOW_DOWNLOADS=ON -DENABLE_SDL=ON ~/orthanc-stone/Applications/Samples/ +cmake --build . --target OrthancStoneSimpleViewer -- -j 5 +``` + +to execute the native samples: +``` +# launch an Orthanc listening on 8042 port: +Orthanc + +# launch the sample +./OrthancStoneSimpleViewer --studyId=XX +``` Licensing diff -r fe4befe03935 -r d6136a7e914d Resources/CMake/OrthancStoneConfiguration.cmake --- a/Resources/CMake/OrthancStoneConfiguration.cmake Mon Nov 05 10:04:56 2018 +0100 +++ b/Resources/CMake/OrthancStoneConfiguration.cmake Mon Nov 05 10:06:18 2018 +0100 @@ -22,6 +22,12 @@ ## Configure the Orthanc Framework ##################################################################### +if (ENABLE_DCMTK) + set(ENABLE_LOCALE ON) +else() + set(ENABLE_LOCALE OFF) # Disable support for locales (notably in Boost) +endif() + include(${ORTHANC_ROOT}/Resources/CMake/OrthancFrameworkConfiguration.cmake) include_directories(${ORTHANC_ROOT}) @@ -39,6 +45,10 @@ message(FATAL_ERROR "Cannot enable SDL in sandboxed environments") endif() + if (ENABLE_QT) + message(FATAL_ERROR "Cannot enable QT in sandboxed environments") + endif() + if (ENABLE_SSL) message(FATAL_ERROR "Cannot enable SSL in sandboxed environments") endif() @@ -69,12 +79,26 @@ endif() -if (ENABLE_SDL) - include(${CMAKE_CURRENT_LIST_DIR}/SdlConfiguration.cmake) +if (ENABLE_SDL AND ENABLE_QT) + message("SDL and QT may not be defined together") +elseif(ENABLE_SDL) + message("SDL is enabled") + include(${CMAKE_CURRENT_LIST_DIR}/SdlConfiguration.cmake) + add_definitions(-DORTHANC_ENABLE_NATIVE=1) + add_definitions(-DORTHANC_ENABLE_QT=0) add_definitions(-DORTHANC_ENABLE_SDL=1) +elseif(ENABLE_QT) + message("QT is enabled") + include(${CMAKE_CURRENT_LIST_DIR}/QtConfiguration.cmake) + add_definitions(-DORTHANC_ENABLE_NATIVE=1) + add_definitions(-DORTHANC_ENABLE_QT=1) + add_definitions(-DORTHANC_ENABLE_SDL=0) else() + message("SDL and QT are both disabled") unset(USE_SYSTEM_SDL CACHE) add_definitions(-DORTHANC_ENABLE_SDL=0) + add_definitions(-DORTHANC_ENABLE_QT=0) + add_definitions(-DORTHANC_ENABLE_NATIVE=0) endif() @@ -93,7 +117,9 @@ -DORTHANC_ENABLE_LOGGING_PLUGIN=0 ) - +if (CMAKE_BUILD_TYPE STREQUAL "Debug") + add_definitions(-DCHECK_OBSERVERS_MESSAGES) +endif() ##################################################################### ## Embed the colormaps into the binaries @@ -105,6 +131,9 @@ # "OrthancStoneParameters.cmake" ${DCMTK_DICTIONARIES} + FONT_UBUNTU_MONO_BOLD_16 ${ORTHANC_ROOT}/Resources/Fonts/UbuntuMonoBold-16.json + #FONT_UBUNTU_MONO_BOLD_64 ${ORTHANC_ROOT}/Resources/Fonts/UbuntuMonoBold-64.json + # Resources specific to the Stone of Orthanc COLORMAP_HOT ${ORTHANC_STONE_ROOT}/Resources/Colormaps/hot.lut COLORMAP_JET ${ORTHANC_STONE_ROOT}/Resources/Colormaps/jet.lut @@ -141,39 +170,87 @@ ## All the source files required to build Stone of Orthanc ##################################################################### +set(APPLICATIONS_SOURCES + ${ORTHANC_STONE_ROOT}/Applications/IStoneApplication.h + ${ORTHANC_STONE_ROOT}/Applications/StoneApplicationContext.cpp + ${ORTHANC_STONE_ROOT}/Applications/Commands/BaseCommandBuilder.cpp + ${ORTHANC_STONE_ROOT}/Applications/Commands/ICommand.h + ${ORTHANC_STONE_ROOT}/Applications/Commands/ICommandExecutor.h + ${ORTHANC_STONE_ROOT}/Applications/Commands/ICommandBuilder.h + ) + if (NOT ORTHANC_SANDBOXED) set(PLATFORM_SOURCES + ${ORTHANC_STONE_ROOT}/Framework/Viewport/CairoFont.cpp + ${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/WebServiceDeleteCommand.cpp ${ORTHANC_STONE_ROOT}/Platforms/Generic/Oracle.cpp + ${ORTHANC_STONE_ROOT}/Platforms/Generic/OracleWebService.h ) - set(APPLICATIONS_SOURCES - ${ORTHANC_STONE_ROOT}/Applications/BasicApplicationContext.cpp - ${ORTHANC_STONE_ROOT}/Applications/IBasicApplication.cpp - ${ORTHANC_STONE_ROOT}/Applications/Sdl/SdlEngine.cpp - ${ORTHANC_STONE_ROOT}/Applications/Sdl/SdlCairoSurface.cpp - ${ORTHANC_STONE_ROOT}/Applications/Sdl/SdlOrthancSurface.cpp - ${ORTHANC_STONE_ROOT}/Applications/Sdl/SdlWindow.cpp + if (ENABLE_SDL OR ENABLE_QT) + list(APPEND APPLICATIONS_SOURCES + ${ORTHANC_STONE_ROOT}/Applications/Generic/NativeStoneApplicationRunner.cpp + ${ORTHANC_STONE_ROOT}/Applications/Generic/NativeStoneApplicationContext.cpp + ) + if (ENABLE_SDL) + list(APPEND APPLICATIONS_SOURCES + ${ORTHANC_STONE_ROOT}/Applications/Sdl/SdlStoneApplicationRunner.cpp + ${ORTHANC_STONE_ROOT}/Applications/Sdl/SdlEngine.cpp + ${ORTHANC_STONE_ROOT}/Applications/Sdl/SdlCairoSurface.cpp + ${ORTHANC_STONE_ROOT}/Applications/Sdl/SdlOrthancSurface.cpp + ${ORTHANC_STONE_ROOT}/Applications/Sdl/SdlWindow.cpp + ) + 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/WasmWebService.cpp + ${ORTHANC_STONE_ROOT}/Platforms/Wasm/WasmViewport.cpp + ${ORTHANC_STONE_ROOT}/Platforms/Wasm/WasmPlatformApplicationAdapter.cpp + ${AUTOGENERATED_DIR}/WasmWebService.c + ${AUTOGENERATED_DIR}/default-library.c + ) + + # Regenerate a dummy "WasmWebService.c" file each time the "WasmWebService.js" file + # is modified, so as to force a new execution of the linking + add_custom_command( + OUTPUT "${AUTOGENERATED_DIR}/WasmWebService.c" + COMMAND ${CMAKE_COMMAND} -E touch "${AUTOGENERATED_DIR}/WasmWebService.c" "" + DEPENDS "${ORTHANC_STONE_ROOT}/Platforms/Wasm/WasmWebService.js") + add_custom_command( + OUTPUT "${AUTOGENERATED_DIR}/default-library.c" + COMMAND ${CMAKE_COMMAND} -E touch "${AUTOGENERATED_DIR}/default-library.c" "" + DEPENDS "${ORTHANC_STONE_ROOT}/Platforms/Wasm/default-library.js") endif() list(APPEND ORTHANC_STONE_SOURCES #${ORTHANC_STONE_ROOT}/Framework/Layers/SeriesFrameRendererFactory.cpp #${ORTHANC_STONE_ROOT}/Framework/Layers/SiblingSliceLocationFactory.cpp #${ORTHANC_STONE_ROOT}/Framework/Layers/SingleFrameRendererFactory.cpp - ${ORTHANC_STONE_ROOT}/Framework/StoneEnumerations.cpp + ${ORTHANC_STONE_ROOT}/Framework/Layers/CircleMeasureTracker.cpp ${ORTHANC_STONE_ROOT}/Framework/Layers/ColorFrameRenderer.cpp ${ORTHANC_STONE_ROOT}/Framework/Layers/DicomStructureSetRendererFactory.cpp ${ORTHANC_STONE_ROOT}/Framework/Layers/FrameRenderer.cpp ${ORTHANC_STONE_ROOT}/Framework/Layers/GrayscaleFrameRenderer.cpp + ${ORTHANC_STONE_ROOT}/Framework/Layers/ILayerSource.h ${ORTHANC_STONE_ROOT}/Framework/Layers/LayerSourceBase.cpp ${ORTHANC_STONE_ROOT}/Framework/Layers/LineLayerRenderer.cpp ${ORTHANC_STONE_ROOT}/Framework/Layers/LineMeasureTracker.cpp ${ORTHANC_STONE_ROOT}/Framework/Layers/OrthancFrameLayerSource.cpp ${ORTHANC_STONE_ROOT}/Framework/Layers/RenderStyle.cpp ${ORTHANC_STONE_ROOT}/Framework/Layers/SliceOutlineRenderer.cpp + ${ORTHANC_STONE_ROOT}/Framework/SmartLoader.cpp + ${ORTHANC_STONE_ROOT}/Framework/StoneEnumerations.cpp + ${ORTHANC_STONE_ROOT}/Framework/StoneException.h ${ORTHANC_STONE_ROOT}/Framework/Toolbox/CoordinateSystem3D.cpp ${ORTHANC_STONE_ROOT}/Framework/Toolbox/DicomFrameConverter.cpp ${ORTHANC_STONE_ROOT}/Framework/Toolbox/DicomStructureSet.cpp @@ -181,10 +258,12 @@ ${ORTHANC_STONE_ROOT}/Framework/Toolbox/Extent2D.cpp ${ORTHANC_STONE_ROOT}/Framework/Toolbox/FiniteProjectiveCamera.cpp ${ORTHANC_STONE_ROOT}/Framework/Toolbox/GeometryToolbox.cpp + ${ORTHANC_STONE_ROOT}/Framework/Toolbox/IWebService.h ${ORTHANC_STONE_ROOT}/Framework/Toolbox/ImageGeometry.cpp ${ORTHANC_STONE_ROOT}/Framework/Toolbox/LinearAlgebra.cpp ${ORTHANC_STONE_ROOT}/Framework/Toolbox/MessagingToolbox.cpp ${ORTHANC_STONE_ROOT}/Framework/Toolbox/OrientedBoundingBox.cpp + ${ORTHANC_STONE_ROOT}/Framework/Toolbox/OrthancApiClient.cpp ${ORTHANC_STONE_ROOT}/Framework/Toolbox/OrthancSlicesLoader.cpp ${ORTHANC_STONE_ROOT}/Framework/Toolbox/ParallelSlices.cpp ${ORTHANC_STONE_ROOT}/Framework/Toolbox/ParallelSlicesCursor.cpp @@ -193,8 +272,9 @@ ${ORTHANC_STONE_ROOT}/Framework/Toolbox/SlicesSorter.cpp ${ORTHANC_STONE_ROOT}/Framework/Toolbox/ViewportGeometry.cpp ${ORTHANC_STONE_ROOT}/Framework/Viewport/CairoContext.cpp - ${ORTHANC_STONE_ROOT}/Framework/Viewport/CairoFont.cpp ${ORTHANC_STONE_ROOT}/Framework/Viewport/CairoSurface.cpp + ${ORTHANC_STONE_ROOT}/Framework/Viewport/IStatusBar.h + ${ORTHANC_STONE_ROOT}/Framework/Viewport/IViewport.h ${ORTHANC_STONE_ROOT}/Framework/Viewport/WidgetViewport.cpp ${ORTHANC_STONE_ROOT}/Framework/Volumes/ImageBuffer3D.cpp ${ORTHANC_STONE_ROOT}/Framework/Volumes/SlicedVolumeBase.cpp @@ -203,17 +283,32 @@ ${ORTHANC_STONE_ROOT}/Framework/Volumes/VolumeReslicer.cpp ${ORTHANC_STONE_ROOT}/Framework/Widgets/CairoWidget.cpp ${ORTHANC_STONE_ROOT}/Framework/Widgets/EmptyWidget.cpp + ${ORTHANC_STONE_ROOT}/Framework/Widgets/IWidget.h + ${ORTHANC_STONE_ROOT}/Framework/Widgets/IWorldSceneInteractor.h + ${ORTHANC_STONE_ROOT}/Framework/Widgets/IWorldSceneMouseTracker.h ${ORTHANC_STONE_ROOT}/Framework/Widgets/LayerWidget.cpp ${ORTHANC_STONE_ROOT}/Framework/Widgets/LayoutWidget.cpp + ${ORTHANC_STONE_ROOT}/Framework/Widgets/PanMouseTracker.cpp ${ORTHANC_STONE_ROOT}/Framework/Widgets/TestCairoWidget.cpp ${ORTHANC_STONE_ROOT}/Framework/Widgets/TestWorldSceneWidget.cpp ${ORTHANC_STONE_ROOT}/Framework/Widgets/WidgetBase.cpp ${ORTHANC_STONE_ROOT}/Framework/Widgets/WorldSceneWidget.cpp + ${ORTHANC_STONE_ROOT}/Framework/Widgets/ZoomMouseTracker.cpp + ${ORTHANC_STONE_ROOT}/Framework/dev.h + ${ORTHANC_STONE_ROOT}/Framework/Messages/ICallable.h + ${ORTHANC_STONE_ROOT}/Framework/Messages/IMessage.h + ${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/MessageType.h + ${ORTHANC_STONE_ROOT}/Framework/Messages/Promise.h + + ${ORTHANC_ROOT}/Plugins/Samples/Common/DicomDatasetReader.cpp ${ORTHANC_ROOT}/Plugins/Samples/Common/DicomPath.cpp + ${ORTHANC_ROOT}/Plugins/Samples/Common/FullOrthancDataset.cpp ${ORTHANC_ROOT}/Plugins/Samples/Common/IOrthancConnection.cpp - ${ORTHANC_ROOT}/Plugins/Samples/Common/DicomDatasetReader.cpp - ${ORTHANC_ROOT}/Plugins/Samples/Common/FullOrthancDataset.cpp ${PLATFORM_SOURCES} ${APPLICATIONS_SOURCES} @@ -227,5 +322,7 @@ # Optional components ${SDL_SOURCES} + ${QT_SOURCES} ${BOOST_EXTENDED_SOURCES} ) + diff -r fe4befe03935 -r d6136a7e914d Resources/CMake/OrthancStoneParameters.cmake --- a/Resources/CMake/OrthancStoneParameters.cmake Mon Nov 05 10:04:56 2018 +0100 +++ b/Resources/CMake/OrthancStoneParameters.cmake Mon Nov 05 10:06:18 2018 +0100 @@ -25,7 +25,7 @@ include(${CMAKE_CURRENT_LIST_DIR}/../../Resources/Orthanc/DownloadOrthancFramework.cmake) include(${ORTHANC_ROOT}/Resources/CMake/OrthancFrameworkParameters.cmake) -set(ENABLE_LOCALE OFF) # Disable support for locales (notably in Boost) +set(ENABLE_DCMTK OFF) set(ENABLE_GOOGLE_TEST ON) set(ENABLE_SQLITE OFF) set(ENABLE_JPEG ON) @@ -49,4 +49,3 @@ ## the Stone of Orthanc ##################################################################### -set(ENABLE_SDL ON CACHE INTERNAL "Include support for SDL") diff -r fe4befe03935 -r d6136a7e914d Resources/CMake/QtConfiguration.cmake --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/CMake/QtConfiguration.cmake Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,77 @@ +# Stone of Orthanc +# Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics +# Department, University Hospital of Liege, Belgium +# Copyright (C) 2017-2018 Osimis S.A., Belgium +# +# This program is free software: you can redistribute it and/or +# modify it under the terms of the GNU Affero General Public License +# as published by the Free Software Foundation, either version 3 of +# the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + + +set(CMAKE_AUTOMOC OFF) +set(CMAKE_AUTOUIC OFF) + +# Find the QtWidgets library +find_package(Qt5Widgets QUIET) + +if (Qt5Widgets_FOUND) + message("Qt5 has been detected") + find_package(Qt5Core REQUIRED) + link_libraries( + Qt5::Widgets + Qt5::Core + ) + + # Create aliases for the CMake commands + macro(ORTHANC_QT_WRAP_UI) + QT5_WRAP_UI(${ARGN}) + endmacro() + + macro(ORTHANC_QT_WRAP_CPP) + QT5_WRAP_CPP(${ARGN}) + endmacro() + +else() + message("Qt5 has not been found, trying with Qt4") + find_package(Qt4 REQUIRED QtGui) + link_libraries( + Qt4::QtGui + ) + + # Create aliases for the CMake commands + macro(ORTHANC_QT_WRAP_UI) + QT4_WRAP_UI(${ARGN}) + endmacro() + + macro(ORTHANC_QT_WRAP_CPP) + QT4_WRAP_CPP(${ARGN}) + endmacro() + +endif() + +list(APPEND QT_SOURCES + ${ORTHANC_STONE_ROOT}/Applications/Qt/QCairoWidget.cpp + ${ORTHANC_STONE_ROOT}/Applications/Qt/QtStoneApplicationRunner.cpp + ${ORTHANC_STONE_ROOT}/Applications/Qt/QStoneMainWindow.cpp + ) + + +# NB: Including CMAKE_CURRENT_BINARY_DIR is mandatory, as the CMake +# macros for Qt will put their result in that directory, which cannot +# be changed. +# https://stackoverflow.com/a/4016784/881731 + +include_directories( + ${ORTHANC_STONE_ROOT}/Applications/Qt/ + ${CMAKE_CURRENT_BINARY_DIR} + ) + diff -r fe4befe03935 -r d6136a7e914d Resources/Orthanc/DownloadOrthancFramework.cmake --- a/Resources/Orthanc/DownloadOrthancFramework.cmake Mon Nov 05 10:04:56 2018 +0100 +++ b/Resources/Orthanc/DownloadOrthancFramework.cmake Mon Nov 05 10:06:18 2018 +0100 @@ -87,6 +87,8 @@ set(ORTHANC_FRAMEWORK_MD5 "d0ccdf68e855d8224331f13774992750") elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.4.0") set(ORTHANC_FRAMEWORK_MD5 "81e15f34d97ac32bbd7d26e85698835a") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.4.1") + set(ORTHANC_FRAMEWORK_MD5 "9b6f6114264b17ed421b574cd6476127") elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.4.2") set(ORTHANC_FRAMEWORK_MD5 "d1ee84927dcf668e60eb5868d24b9394") endif() diff -r fe4befe03935 -r d6136a7e914d TODO --- a/TODO Mon Nov 05 10:04:56 2018 +0100 +++ b/TODO Mon Nov 05 10:06:18 2018 +0100 @@ -26,7 +26,6 @@ ------------- * Tune number of loading threads in LayeredSceneWidget -* Add cache over IOrthancServices (for SDL/Qt/...) * LayoutWidget: Do not update full background if only 1 widget has changed * LayoutWidget: Threads to refresh each child * Implement binary search to speed up search for closest slice @@ -38,7 +37,6 @@ Platform-specific ----------------- -* Qt widget example * Add precompiled headers for Microsoft Visual Studio * Investigate crash in CurlOrthancConnection if using MinGW32 in Release mode diff -r fe4befe03935 -r d6136a7e914d UnitTestsSources/TestCommands.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/UnitTestsSources/TestCommands.cpp Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,108 @@ +///** +// * Stone of Orthanc +// * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics +// * Department, University Hospital of Liege, Belgium +// * Copyright (C) 2017-2018 Osimis S.A., Belgium +// * +// * This program is free software: you can redistribute it and/or +// * modify it under the terms of the GNU Affero General Public License +// * as published by the Free Software Foundation, either version 3 of +// * the License, or (at your option) any later version. +// * +// * This program is distributed in the hope that it will be useful, but +// * WITHOUT ANY WARRANTY; without even the implied warranty of +// * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// * Affero General Public License for more details. +// * +// * You should have received a copy of the GNU Affero General Public License +// * along with this program. If not, see . +// **/ + + +//#include "gtest/gtest.h" + +//#include "../Applications/Commands/BaseCommandFactory.h" +//#include "Core/OrthancException.h" + +//class CommandIncrement: public OrthancStone::BaseCommand +//{ +//public: +// static int counter; +// int increment_; +//public: +// CommandIncrement() +// : OrthancStone::BaseCommand("increment"), +// increment_(0) +// {} + +// virtual void Execute() +// { +// counter += increment_; +// } +// virtual void Configure(const Json::Value& arguments) +// { +// increment_ = arguments["increment"].asInt(); +// } +//}; + +//// COMMAND("name", "arg1", "int", "arg2", "string") +//// COMMAND(name, arg1, arg2) + + +//int CommandIncrement::counter = 0; + +//TEST(Commands, CreateNoop) +//{ +// OrthancStone::BaseCommandFactory factory; + +// factory.RegisterCommandClass(); + +// Json::Value cmdJson; +// cmdJson["command"] = "noop"; + +// std::auto_ptr command(factory.CreateFromJson(cmdJson)); + +// ASSERT_TRUE(command.get() != NULL); +// ASSERT_EQ("noop", command->GetName()); +//} + +//TEST(Commands, Execute) +//{ +// OrthancStone::BaseCommandFactory factory; + +// factory.RegisterCommandClass(); +// factory.RegisterCommandClass(); + +// Json::Value cmdJson; +// cmdJson["command"] = "increment"; +// cmdJson["args"]["increment"] = 2; + +// std::auto_ptr command(factory.CreateFromJson(cmdJson)); + +// ASSERT_TRUE(command.get() != NULL); +// CommandIncrement::counter = 0; +// command->Execute(); +// ASSERT_EQ(2, CommandIncrement::counter); +//} + +//TEST(Commands, TryCreateUnknowCommand) +//{ +// OrthancStone::BaseCommandFactory factory; +// factory.RegisterCommandClass(); + +// Json::Value cmdJson; +// cmdJson["command"] = "unknown"; + +// ASSERT_THROW(std::auto_ptr command(factory.CreateFromJson(cmdJson)), Orthanc::OrthancException); +//} + +//TEST(Commands, TryCreateCommandFromInvalidJson) +//{ +// OrthancStone::BaseCommandFactory factory; +// factory.RegisterCommandClass(); + +// Json::Value cmdJson; +// cmdJson["command-name"] = "noop"; + +// ASSERT_THROW(std::auto_ptr command(factory.CreateFromJson(cmdJson)), Orthanc::OrthancException); +//} diff -r fe4befe03935 -r d6136a7e914d UnitTestsSources/TestExceptions.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/UnitTestsSources/TestExceptions.cpp Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,43 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#include "gtest/gtest.h" + +#include "../Framework/StoneException.h" + + + +TEST(StoneExceptions, OrthancToStoneConversion) +{ + bool hasBeenCatched = false; + try { + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + catch (Orthanc::OrthancException& orthancException) + { + hasBeenCatched = true; + OrthancStone::StoneOrthancException stoneException(orthancException); + ASSERT_EQ(OrthancStone::ErrorCode_OrthancError, stoneException.GetErrorCode()); + ASSERT_EQ(Orthanc::ErrorCode_InternalError, stoneException.GetOrthancErrorCode()); + } + + ASSERT_TRUE(hasBeenCatched); +} diff -r fe4befe03935 -r d6136a7e914d UnitTestsSources/TestMessageBroker.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/UnitTestsSources/TestMessageBroker.cpp Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,158 @@ +///** +// * Stone of Orthanc +// * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics +// * Department, University Hospital of Liege, Belgium +// * Copyright (C) 2017-2018 Osimis S.A., Belgium +// * +// * This program is free software: you can redistribute it and/or +// * modify it under the terms of the GNU Affero General Public License +// * as published by the Free Software Foundation, either version 3 of +// * the License, or (at your option) any later version. +// * +// * This program is distributed in the hope that it will be useful, but +// * WITHOUT ANY WARRANTY; without even the implied warranty of +// * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// * Affero General Public License for more details. +// * +// * You should have received a copy of the GNU Affero General Public License +// * along with this program. If not, see . +// **/ + + +//#include "gtest/gtest.h" + +//#include "../Framework/Messages/MessageBroker.h" +//#include "../Framework/Messages/IMessage.h" +//#include "../Framework/Messages/IObservable.h" +//#include "../Framework/Messages/IObserver.h" +//#include "../Framework/StoneEnumerations.h" + + +//static int test1Counter = 0; +//static int test2Counter = 0; +//class MyFullObserver : public OrthancStone::IObserver +//{ + +//public: +// MyFullObserver(OrthancStone::MessageBroker& broker) +// : OrthancStone::IObserver(broker) +// { +//// DeclareHandledMessage(OrthancStone::MessageType_Test1); +//// DeclareIgnoredMessage(OrthancStone::MessageType_Test2); +// } + + +// void HandleMessage(OrthancStone::IObservable& from, const OrthancStone::IMessage& message) { +// switch (message.GetType()) +// { +// case OrthancStone::MessageType_Test1: +// test1Counter++; +// break; +// case OrthancStone::MessageType_Test2: +// test2Counter++; +// break; +// default: +// throw OrthancStone::MessageNotDeclaredException(message.GetType()); +// } +// } + +//}; + +//class MyPartialObserver : public OrthancStone::IObserver +//{ + +//public: +// MyPartialObserver(OrthancStone::MessageBroker& broker) +// : OrthancStone::IObserver(broker) +// { +//// DeclareHandledMessage(OrthancStone::MessageType_Test1); +// // don't declare Test2 on purpose +// } + + +// void HandleMessage(OrthancStone::IObservable& from, const OrthancStone::IMessage& message) { +// switch (message.GetType()) +// { +// case OrthancStone::MessageType_Test1: +// test1Counter++; +// break; +// case OrthancStone::MessageType_Test2: +// test2Counter++; +// break; +// default: +// throw OrthancStone::MessageNotDeclaredException(message.GetType()); +// } +// } + +//}; + + +//class MyObservable : public OrthancStone::IObservable +//{ + +//public: +// MyObservable(OrthancStone::MessageBroker& broker) +// : OrthancStone::IObservable(broker) +// { +// DeclareEmittableMessage(OrthancStone::MessageType_Test1); +// DeclareEmittableMessage(OrthancStone::MessageType_Test2); +// } + +//}; + + +//TEST(MessageBroker, NormalUsage) +//{ +// OrthancStone::MessageBroker broker; +// MyObservable observable(broker); + +// test1Counter = 0; + +// // no observers have been registered -> nothing shall happen +// observable.EmitMessage(OrthancStone::IMessage(OrthancStone::MessageType_Test1)); + +// ASSERT_EQ(0, test1Counter); + +// // register an observer, check it is called +// MyFullObserver fullObserver(broker); +// ASSERT_NO_THROW(observable.RegisterObserver(fullObserver)); + +// observable.EmitMessage(OrthancStone::IMessage(OrthancStone::MessageType_Test1)); + +// ASSERT_EQ(1, test1Counter); + +// // register an invalid observer, check it raises an exception +// MyPartialObserver partialObserver(broker); +// ASSERT_THROW(observable.RegisterObserver(partialObserver), OrthancStone::MessageNotDeclaredException); + +// // check an exception is thrown when the observable emits an undeclared message +// ASSERT_THROW(observable.EmitMessage(OrthancStone::IMessage(OrthancStone::MessageType_LayerSource_GeometryReady)), OrthancStone::MessageNotDeclaredException); + +// // unregister the observer, make sure nothing happens afterwards +// observable.UnregisterObserver(fullObserver); +// observable.EmitMessage(OrthancStone::IMessage(OrthancStone::MessageType_Test1)); +// ASSERT_EQ(1, test1Counter); +//} + +//TEST(MessageBroker, DeleteObserverWhileRegistered) +//{ +// OrthancStone::MessageBroker broker; +// MyObservable observable(broker); + +// test1Counter = 0; + +// { +// // register an observer, check it is called +// MyFullObserver observer(broker); +// observable.RegisterObserver(observer); + +// observable.EmitMessage(OrthancStone::IMessage(OrthancStone::MessageType_Test1)); + +// ASSERT_EQ(1, test1Counter); +// } + +// // at this point, the observer has been deleted, the handle shall not be called again (and it shall not crash !) +// observable.EmitMessage(OrthancStone::IMessage(OrthancStone::MessageType_Test1)); + +// ASSERT_EQ(1, test1Counter); +//} diff -r fe4befe03935 -r d6136a7e914d UnitTestsSources/TestMessageBroker2.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/UnitTestsSources/TestMessageBroker2.cpp Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,410 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#include "gtest/gtest.h" + +#include "Framework/Messages/MessageBroker.h" +#include "Framework/Messages/Promise.h" +#include "Framework/Messages/IObservable.h" +#include "Framework/Messages/IObserver.h" +#include "Framework/Messages/MessageForwarder.h" + + +int testCounter = 0; +namespace { + + using namespace OrthancStone; + + + enum CustomMessageType + { + CustomMessageType_First = MessageType_CustomMessage + 1, + + CustomMessageType_Completed, + CustomMessageType_Increment + }; + + + class MyObservable : public IObservable + { + public: + struct MyCustomMessage: public BaseMessage + { + int payload_; + + MyCustomMessage(int payload) + : BaseMessage(), + payload_(payload) + {} + }; + + MyObservable(MessageBroker& broker) + : IObservable(broker) + {} + + }; + + class MyObserver : public IObserver + { + public: + MyObserver(MessageBroker& broker) + : IObserver(broker) + {} + + void HandleCompletedMessage(const MyObservable::MyCustomMessage& message) + { + testCounter += message.payload_; + } + + }; + + + class MyIntermediate : public IObserver, public IObservable + { + IObservable& observedObject_; + public: + MyIntermediate(MessageBroker& broker, IObservable& observedObject) + : IObserver(broker), + IObservable(broker), + observedObject_(observedObject) + { + observedObject_.RegisterObserverCallback(new MessageForwarder(broker, *this)); + } + }; + + + class MyPromiseSource : public IObservable + { + Promise* currentPromise_; + public: + struct MyPromiseMessage: public BaseMessage + { + int increment; + + MyPromiseMessage(int increment) + : BaseMessage(), + increment(increment) + {} + }; + + MyPromiseSource(MessageBroker& broker) + : IObservable(broker), + currentPromise_(NULL) + {} + + Promise& StartSomethingAsync() + { + currentPromise_ = new Promise(broker_); + return *currentPromise_; + } + + void CompleteSomethingAsyncWithSuccess(int payload) + { + currentPromise_->Success(MyPromiseMessage(payload)); + delete currentPromise_; + } + + void CompleteSomethingAsyncWithFailure(int payload) + { + currentPromise_->Failure(MyPromiseMessage(payload)); + delete currentPromise_; + } + }; + + + class MyPromiseTarget : public IObserver + { + public: + MyPromiseTarget(MessageBroker& broker) + : IObserver(broker) + {} + + void IncrementCounter(const MyPromiseSource::MyPromiseMessage& args) + { + testCounter += args.increment; + } + + void DecrementCounter(const MyPromiseSource::MyPromiseMessage& args) + { + testCounter -= args.increment; + } + }; +} + + +TEST(MessageBroker2, TestPermanentConnectionSimpleUseCase) +{ + MessageBroker broker; + MyObservable observable(broker); + MyObserver observer(broker); + + // create a permanent connection between an observable and an observer + observable.RegisterObserverCallback(new Callable(observer, &MyObserver::HandleCompletedMessage)); + + testCounter = 0; + observable.EmitMessage(MyObservable::MyCustomMessage(12)); + ASSERT_EQ(12, testCounter); + + // the connection is permanent; if we emit the same message again, the observer will be notified again + testCounter = 0; + observable.EmitMessage(MyObservable::MyCustomMessage(20)); + ASSERT_EQ(20, testCounter); +} + +TEST(MessageBroker2, TestMessageForwarderSimpleUseCase) +{ + MessageBroker broker; + MyObservable observable(broker); + MyIntermediate intermediate(broker, observable); + MyObserver observer(broker); + + // let the observer observers the intermediate that is actually forwarding the messages from the observable + intermediate.RegisterObserverCallback(new Callable(observer, &MyObserver::HandleCompletedMessage)); + + testCounter = 0; + observable.EmitMessage(MyObservable::MyCustomMessage(12)); + ASSERT_EQ(12, testCounter); + + // the connection is permanent; if we emit the same message again, the observer will be notified again + testCounter = 0; + observable.EmitMessage(MyObservable::MyCustomMessage(20)); + ASSERT_EQ(20, testCounter); +} + +TEST(MessageBroker2, TestPermanentConnectionDeleteObserver) +{ + MessageBroker broker; + MyObservable observable(broker); + MyObserver* observer = new MyObserver(broker); + + // create a permanent connection between an observable and an observer + observable.RegisterObserverCallback(new Callable(*observer, &MyObserver::HandleCompletedMessage)); + + testCounter = 0; + observable.EmitMessage(MyObservable::MyCustomMessage(12)); + ASSERT_EQ(12, testCounter); + + // delete the observer and check that the callback is not called anymore + delete observer; + + // the connection is permanent; if we emit the same message again, the observer will be notified again + testCounter = 0; + observable.EmitMessage(MyObservable::MyCustomMessage(20)); + ASSERT_EQ(0, testCounter); +} + +TEST(MessageBroker2, TestMessageForwarderDeleteIntermediate) +{ + MessageBroker broker; + MyObservable observable(broker); + MyIntermediate* intermediate = new MyIntermediate(broker, observable); + MyObserver observer(broker); + + // let the observer observers the intermediate that is actually forwarding the messages from the observable + intermediate->RegisterObserverCallback(new Callable(observer, &MyObserver::HandleCompletedMessage)); + + testCounter = 0; + observable.EmitMessage(MyObservable::MyCustomMessage(12)); + ASSERT_EQ(12, testCounter); + + delete intermediate; + + observable.EmitMessage(MyObservable::MyCustomMessage(20)); + ASSERT_EQ(12, testCounter); +} + +TEST(MessageBroker2, TestCustomMessage) +{ + MessageBroker broker; + MyObservable observable(broker); + MyIntermediate intermediate(broker, observable); + MyObserver observer(broker); + + // let the observer observers the intermediate that is actually forwarding the messages from the observable + intermediate.RegisterObserverCallback(new Callable(observer, &MyObserver::HandleCompletedMessage)); + + testCounter = 0; + observable.EmitMessage(MyObservable::MyCustomMessage(12)); + ASSERT_EQ(12, testCounter); + + // the connection is permanent; if we emit the same message again, the observer will be notified again + testCounter = 0; + observable.EmitMessage(MyObservable::MyCustomMessage(20)); + ASSERT_EQ(20, testCounter); +} + + +TEST(MessageBroker2, TestPromiseSuccessFailure) +{ + MessageBroker broker; + MyPromiseSource source(broker); + MyPromiseTarget target(broker); + + // test a successful promise + source.StartSomethingAsync() + .Then(new Callable(target, &MyPromiseTarget::IncrementCounter)) + .Else(new Callable(target, &MyPromiseTarget::DecrementCounter)); + + testCounter = 0; + source.CompleteSomethingAsyncWithSuccess(10); + ASSERT_EQ(10, testCounter); + + // test a failing promise + source.StartSomethingAsync() + .Then(new Callable(target, &MyPromiseTarget::IncrementCounter)) + .Else(new Callable(target, &MyPromiseTarget::DecrementCounter)); + + testCounter = 0; + source.CompleteSomethingAsyncWithFailure(15); + ASSERT_EQ(-15, testCounter); +} + +TEST(MessageBroker2, TestPromiseDeleteTarget) +{ + MessageBroker broker; + MyPromiseSource source(broker); + MyPromiseTarget* target = new MyPromiseTarget(broker); + + // create the promise + source.StartSomethingAsync() + .Then(new Callable(*target, &MyPromiseTarget::IncrementCounter)) + .Else(new Callable(*target, &MyPromiseTarget::DecrementCounter)); + + // delete the promise target + delete target; + + // trigger the promise, make sure it does not throw and does not call the callback + testCounter = 0; + source.CompleteSomethingAsyncWithSuccess(10); + ASSERT_EQ(0, testCounter); + + // test a failing promise + source.StartSomethingAsync() + .Then(new Callable(*target, &MyPromiseTarget::IncrementCounter)) + .Else(new Callable(*target, &MyPromiseTarget::DecrementCounter)); + + testCounter = 0; + source.CompleteSomethingAsyncWithFailure(15); + ASSERT_EQ(0, testCounter); +} + +#if __cplusplus >= 201103L + +#include + +namespace OrthancStone { + + template + class LambdaCallable : public MessageHandler + { + private: + + IObserver& observer_; + std::function lambda_; + + public: + LambdaCallable(IObserver& observer, + std::function lambdaFunction) : + observer_(observer), + lambda_(lambdaFunction) + { + } + + virtual void Apply(const IMessage& message) + { + lambda_(dynamic_cast(message)); + } + + virtual MessageType GetMessageType() const + { + return static_cast(TMessage::Type); + } + + virtual IObserver* GetObserver() const + { + return &observer_; + } + }; + + +} + +TEST(MessageBroker2, TestLambdaSimpleUseCase) +{ + MessageBroker broker; + MyObservable observable(broker); + MyObserver* observer = new MyObserver(broker); + + // create a permanent connection between an observable and an observer + observable.RegisterObserverCallback(new LambdaCallable(*observer, [&](const MyObservable::MyCustomMessage& message) {testCounter += 2 * message.payload_;})); + + testCounter = 0; + observable.EmitMessage(MyObservable::MyCustomMessage(12)); + ASSERT_EQ(24, testCounter); + + // delete the observer and check that the callback is not called anymore + delete observer; + + // the connection is permanent; if we emit the same message again, the observer will be notified again + testCounter = 0; + observable.EmitMessage(MyObservable::MyCustomMessage(20)); + ASSERT_EQ(0, testCounter); +} + +namespace { + class MyObserverWithLambda : public IObserver { + private: + int multiplier_; // this is a private variable we want to access in a lambda + + public: + MyObserverWithLambda(MessageBroker& broker, int multiplier, MyObservable& observable) + : IObserver(broker), + multiplier_(multiplier) + { + // register a callable to a lambda that access private members + observable.RegisterObserverCallback(new LambdaCallable(*this, [this](const MyObservable::MyCustomMessage& message) { + testCounter += multiplier_ * message.payload_; + })); + + } + }; +} + +TEST(MessageBroker2, TestLambdaCaptureThisAndAccessPrivateMembers) +{ + MessageBroker broker; + MyObservable observable(broker); + MyObserverWithLambda* observer = new MyObserverWithLambda(broker, 3, observable); + + testCounter = 0; + observable.EmitMessage(MyObservable::MyCustomMessage(12)); + ASSERT_EQ(36, testCounter); + + // delete the observer and check that the callback is not called anymore + delete observer; + + // the connection is permanent; if we emit the same message again, the observer will be notified again + testCounter = 0; + observable.EmitMessage(MyObservable::MyCustomMessage(20)); + ASSERT_EQ(0, testCounter); +} + +#endif // C++ 11 diff -r fe4befe03935 -r d6136a7e914d UnitTestsSources/TestMessageBroker2_connect_ok.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/UnitTestsSources/TestMessageBroker2_connect_ok.cpp Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,226 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#include "gtest/gtest.h" + +#include +#include +#include + +#include +#include +#include + +int testCounter = 0; +namespace { + + enum MessageType + { + // used in unit tests only + MessageType_Test1, + MessageType_Test2, + + MessageType_LastGenericStoneMessage + }; + + struct IMessage : public boost::noncopyable + { + MessageType messageType_; + public: + IMessage(const MessageType& messageType) + : messageType_(messageType) + {} + virtual ~IMessage() {} + + MessageType GetType() const {return messageType_;} + }; + + + class IObserver; + class IObservable; + + /* + * This is a central message broker. It keeps track of all observers and knows + * when an observer is deleted. + * This way, it can prevent an observable to send a message to a dead observer. + */ + class MessageBroker : public boost::noncopyable + { + + std::set activeObservers_; // the list of observers that are currently alive (that have not been deleted) + + public: + + void Register(IObserver& observer) + { + activeObservers_.insert(&observer); + } + + void Unregister(IObserver& observer) + { + activeObservers_.erase(&observer); + } + + void EmitMessage(IObservable& from, std::set observers, const IMessage& message); + }; + + + class IObserver : public boost::noncopyable + { + protected: + MessageBroker& broker_; + + public: + IObserver(MessageBroker& broker) + : broker_(broker) + { + broker_.Register(*this); + } + + virtual ~IObserver() + { + broker_.Unregister(*this); + } + + void HandleMessage_(IObservable &from, const IMessage &message) + { + + HandleMessage(from, message); + } + + virtual void HandleMessage(IObservable& from, const IMessage& message) = 0; + + + protected: + + + }; + +// struct ICallableObserver +// { +// IObserver* observer; +// }; + +// typedef void (IObserver::*ObserverSingleMesssageHandler)(IObservable& from, const IMessage& message); + +// template +// struct CallableObserver : public ICallableObserver +// { +// void (TObserver::*ptrToMemberHandler)(IObservable& from, const IMessage& message); +// }; + + struct CallableObserver + { + IObserver* observer; + boost::function f; + }; + + class IObservable : public boost::noncopyable + { + protected: + MessageBroker& broker_; + + std::set observers_; + + std::map > callables_; + public: + + IObservable(MessageBroker& broker) + : broker_(broker) + { + } + virtual ~IObservable() + { + } + + void EmitMessage(const IMessage& message) + { + //broker_.EmitMessage(*this, observers_, message); + + // TODO check if observer is still alive and call ! + CallableObserver* callable = *(callables_[message.GetType()].begin()); + callable->f(*this, message); + } + + void RegisterObserver(IObserver& observer) + { + observers_.insert(&observer); + } + + void UnregisterObserver(IObserver& observer) + { + observers_.erase(&observer); + } + + + //template void Connect(MessageType messageType, IObserver& observer, void (TObserver::*ptrToMemberHandler)(IObservable& from, const IMessage& message)) + void Connect(MessageType messageType, IObserver& observer, boost::function f) + { + callables_[messageType] = std::set(); + CallableObserver* callable = new CallableObserver(); + callable->observer = &observer; + callable->f = f; + callables_[messageType].insert(callable); + } + }; + + + class MyObservable : public IObservable + { + public: + MyObservable(MessageBroker& broker) + : IObservable(broker) + {} + }; + + class MyObserver : public IObserver + { + public: + MyObserver(MessageBroker& broker) + : IObserver(broker) + {} + virtual void HandleMessage(IObservable& from, const IMessage& message) {} + void HandleSpecificMessage(IObservable& from, const IMessage& message) + { + testCounter++; + } + + }; + +} + +//#define STONE_CONNECT(observabe, messageType, observerPtr, observerMemberFnPtr) + +TEST(MessageBroker2, Test1) +{ + MessageBroker broker; + MyObservable observable(broker); + MyObserver observer(broker); + + + observable.Connect(MessageType_Test1, observer, boost::bind(&MyObserver::HandleSpecificMessage, &observer, _1, _2)); + //STONE_CONNECT(observable, MessageType_Test1, observer, &MyObserver::HandleSpecificMessage) + observable.EmitMessage(IMessage(MessageType_Test1)); + + ASSERT_EQ(1, testCounter); +} + + diff -r fe4befe03935 -r d6136a7e914d UnitTestsSources/TestMessageBroker2_promise_and_connect_ok.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/UnitTestsSources/TestMessageBroker2_promise_and_connect_ok.cpp Mon Nov 05 10:06:18 2018 +0100 @@ -0,0 +1,520 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#include "gtest/gtest.h" + +#include +#include +#include + +#include +#include +#include + +int testCounter = 0; +namespace { + + enum MessageType + { + MessageType_Test1, + MessageType_Test2, + + MessageType_CustomMessage, + MessageType_LastGenericStoneMessage + }; + + struct IMessage : public boost::noncopyable + { + MessageType messageType_; + public: + IMessage(const MessageType& messageType) + : messageType_(messageType) + {} + virtual ~IMessage() {} + + virtual int GetType() const {return messageType_;} + }; + + + struct ICustomMessage : public IMessage + { + int customMessageType_; + public: + ICustomMessage(int customMessageType) + : IMessage(MessageType_CustomMessage), + customMessageType_(customMessageType) + {} + virtual ~ICustomMessage() {} + + virtual int GetType() const {return customMessageType_;} + }; + + + class IObserver; + class IObservable; + class IPromiseTarget; + class IPromiseSource; + class Promise; + + /* + * This is a central message broker. It keeps track of all observers and knows + * when an observer is deleted. + * This way, it can prevent an observable to send a message to a delete observer. + * It does the same book-keeping for the IPromiseTarget and IPromiseSource + */ + class MessageBroker : public boost::noncopyable + { + + std::set activeObservers_; // the list of observers that are currently alive (that have not been deleted) + std::set activePromiseTargets_; + std::set activePromiseSources_; + + public: + + void Register(IObserver& observer) + { + activeObservers_.insert(&observer); + } + + void Unregister(IObserver& observer) + { + activeObservers_.erase(&observer); + } + + void Register(IPromiseTarget& target) + { + activePromiseTargets_.insert(&target); + } + + void Unregister(IPromiseTarget& target) + { + activePromiseTargets_.erase(&target); + } + + void Register(IPromiseSource& source) + { + activePromiseSources_.insert(&source); + } + + void Unregister(IPromiseSource& source) + { + activePromiseSources_.erase(&source); + } + + void EmitMessage(IObservable& from, std::set observers, const IMessage& message); + + bool IsActive(IPromiseTarget* target) + { + return activePromiseTargets_.find(target) != activePromiseTargets_.end(); + } + + bool IsActive(IPromiseSource* source) + { + return activePromiseSources_.find(source) != activePromiseSources_.end(); + } + + bool IsActive(IObserver* observer) + { + return activeObservers_.find(observer) != activeObservers_.end(); + } + }; + + struct IPromiseArgs + { +public: + virtual ~IPromiseArgs() {} + }; + + class EmptyPromiseArguments : public IPromiseArgs + { + + }; + + class Promise : public boost::noncopyable + { + protected: + MessageBroker& broker_; + + IPromiseTarget* successTarget_; + boost::function successCallable_; + + IPromiseTarget* failureTarget_; + boost::function failureCallable_; + + public: + Promise(MessageBroker& broker) + : broker_(broker), + successTarget_(NULL), + failureTarget_(NULL) + { + } + + void Success(const IPromiseArgs& message) + { + // check the target is still alive in the broker + if (broker_.IsActive(successTarget_)) + { + successCallable_(message); + } + } + + void Failure(const IPromiseArgs& message) + { + // check the target is still alive in the broker + if (broker_.IsActive(failureTarget_)) + { + failureCallable_(message); + } + } + + Promise& Then(IPromiseTarget* target, boost::function f) + { + if (successTarget_ != NULL) + { + // TODO: throw throw new "Promise may only have a single success target" + } + successTarget_ = target; + successCallable_ = f; + return *this; + } + + Promise& Else(IPromiseTarget* target, boost::function f) + { + if (failureTarget_ != NULL) + { + // TODO: throw throw new "Promise may only have a single failure target" + } + failureTarget_ = target; + failureCallable_ = f; + return *this; + } + + }; + + class IObserver : public boost::noncopyable + { + protected: + MessageBroker& broker_; + + public: + IObserver(MessageBroker& broker) + : broker_(broker) + { + broker_.Register(*this); + } + + virtual ~IObserver() + { + broker_.Unregister(*this); + } + + }; + + class IPromiseTarget : public boost::noncopyable + { + protected: + MessageBroker& broker_; + + public: + IPromiseTarget(MessageBroker& broker) + : broker_(broker) + { + broker_.Register(*this); + } + + virtual ~IPromiseTarget() + { + broker_.Unregister(*this); + } + }; + + class IPromiseSource : public boost::noncopyable + { + protected: + MessageBroker& broker_; + + public: + IPromiseSource(MessageBroker& broker) + : broker_(broker) + { + broker_.Register(*this); + } + + virtual ~IPromiseSource() + { + broker_.Unregister(*this); + } + }; + + + struct CallableObserver + { + IObserver* observer; + boost::function f; + }; + + class IObservable : public boost::noncopyable + { + protected: + MessageBroker& broker_; + + std::set observers_; + + std::map > callables_; + public: + + IObservable(MessageBroker& broker) + : broker_(broker) + { + } + virtual ~IObservable() + { + } + + void EmitMessage(const IMessage& message) + { + //broker_.EmitMessage(*this, observers_, message); + int messageType = message.GetType(); + if (callables_.find(messageType) != callables_.end()) + { + for (std::set::iterator observer = callables_[messageType].begin(); observer != callables_[messageType].end(); observer++) + { + CallableObserver* callable = *observer; + if (broker_.IsActive(callable->observer)) + { + callable->f(*this, message); + } + } + } + + } + + void RegisterObserver(IObserver& observer) + { + observers_.insert(&observer); + } + + void UnregisterObserver(IObserver& observer) + { + observers_.erase(&observer); + } + + //template void Connect(MessageType messageType, IObserver& observer, void (TObserver::*ptrToMemberHandler)(IObservable& from, const IMessage& message)) + void Connect(int messageType, IObserver& observer, boost::function f) + { + callables_[messageType] = std::set(); + CallableObserver* callable = new CallableObserver(); + callable->observer = &observer; + callable->f = f; + callables_[messageType].insert(callable); + } + }; + + + enum CustomMessageType + { + CustomMessageType_First = MessageType_LastGenericStoneMessage + 1, + + CustomMessageType_Completed + }; + + class MyObservable : public IObservable + { + public: + struct MyCustomMessage: public ICustomMessage + { + int payload_; + MyCustomMessage(int payload) + : ICustomMessage(CustomMessageType_Completed), + payload_(payload) + {} + }; + + MyObservable(MessageBroker& broker) + : IObservable(broker) + {} + + }; + + class MyObserver : public IObserver + { + public: + MyObserver(MessageBroker& broker) + : IObserver(broker) + {} + void HandleCompletedMessage(IObservable& from, const IMessage& message) + { + const MyObservable::MyCustomMessage& msg = dynamic_cast(message); + testCounter += msg.payload_; + } + + }; + + + class MyPromiseSource : public IPromiseSource + { + Promise* currentPromise_; + public: + struct MyPromiseArgs : public IPromiseArgs + { + int increment; + }; + + MyPromiseSource(MessageBroker& broker) + : IPromiseSource(broker), + currentPromise_(NULL) + {} + + Promise& StartSomethingAsync() + { + currentPromise_ = new Promise(broker_); + return *currentPromise_; + } + + void CompleteSomethingAsyncWithSuccess() + { + currentPromise_->Success(EmptyPromiseArguments()); + delete currentPromise_; + } + + void CompleteSomethingAsyncWithFailure() + { + currentPromise_->Failure(EmptyPromiseArguments()); + delete currentPromise_; + } + }; + + + class MyPromiseTarget : public IPromiseTarget + { + public: + MyPromiseTarget(MessageBroker& broker) + : IPromiseTarget(broker) + {} + + void IncrementCounter(const IPromiseArgs& args) + { + testCounter++; + } + + void DecrementCounter(const IPromiseArgs& args) + { + testCounter--; + } + }; +} + +#define CONNECT_MESSAGES(observablePtr, messageType, observerPtr, observerFnPtr) (observablePtr)->Connect(messageType, *(observerPtr), boost::bind(observerFnPtr, observerPtr, _1, _2)) +#define PTHEN(targetPtr, targetFnPtr) Then(targetPtr, boost::bind(targetFnPtr, targetPtr, _1)) +#define PELSE(targetPtr, targetFnPtr) Else(targetPtr, boost::bind(targetFnPtr, targetPtr, _1)) + + +TEST(MessageBroker2, TestPermanentConnectionSimpleUseCase) +{ + MessageBroker broker; + MyObservable observable(broker); + MyObserver observer(broker); + + // create a permanent connection between an observable and an observer + CONNECT_MESSAGES(&observable, CustomMessageType_Completed, &observer, &MyObserver::HandleCompletedMessage); + + testCounter = 0; + observable.EmitMessage(MyObservable::MyCustomMessage(12)); + ASSERT_EQ(12, testCounter); + + // the connection is permanent; if we emit the same message again, the observer will be notified again + testCounter = 0; + observable.EmitMessage(MyObservable::MyCustomMessage(20)); + ASSERT_EQ(20, testCounter); +} + +TEST(MessageBroker2, TestPermanentConnectionDeleteObserver) +{ + MessageBroker broker; + MyObservable observable(broker); + MyObserver* observer = new MyObserver(broker); + + // create a permanent connection between an observable and an observer + CONNECT_MESSAGES(&observable, CustomMessageType_Completed, observer, &MyObserver::HandleCompletedMessage); + + testCounter = 0; + observable.EmitMessage(MyObservable::MyCustomMessage(12)); + ASSERT_EQ(12, testCounter); + + // delete the observer and check that the callback is not called anymore + delete observer; + + // the connection is permanent; if we emit the same message again, the observer will be notified again + testCounter = 0; + observable.EmitMessage(MyObservable::MyCustomMessage(20)); + ASSERT_EQ(0, testCounter); +} + + +TEST(MessageBroker2, TestPromiseSuccessFailure) +{ + MessageBroker broker; + MyPromiseSource source(broker); + MyPromiseTarget target(broker); + + // test a successful promise + source.StartSomethingAsync() + .PTHEN(&target, &MyPromiseTarget::IncrementCounter) + .PELSE(&target, &MyPromiseTarget::DecrementCounter); + + testCounter = 0; + source.CompleteSomethingAsyncWithSuccess(); + ASSERT_EQ(1, testCounter); + + // test a failing promise + source.StartSomethingAsync() + .PTHEN(&target, &MyPromiseTarget::IncrementCounter) + .PELSE(&target, &MyPromiseTarget::DecrementCounter); + + testCounter = 0; + source.CompleteSomethingAsyncWithFailure(); + ASSERT_EQ(-1, testCounter); +} + +//TEST(MessageBroker2, TestPromiseDeleteTarget) +//{ +// MessageBroker broker; +// MyPromiseSource source(broker); +// MyPromiseTarget target(broker); + +// // test a successful promise +// source.StartSomethingAsync() +// .PTHEN(&target, &MyPromiseTarget::IncrementCounter) +// .PELSE(&target, &MyPromiseTarget::DecrementCounter); + +// testCounter = 0; +// source.CompleteSomethingAsyncWithSuccess(); +// ASSERT_EQ(1, testCounter); + +// // test a failing promise +// source.StartSomethingAsync() +// .PTHEN(&target, &MyPromiseTarget::IncrementCounter) +// .PELSE(&target, &MyPromiseTarget::DecrementCounter); + +// testCounter = 0; +// source.CompleteSomethingAsyncWithFailure(); +// ASSERT_EQ(-1, testCounter); +//} diff -r fe4befe03935 -r d6136a7e914d UnitTestsSources/UnitTestsMain.cpp --- a/UnitTestsSources/UnitTestsMain.cpp Mon Nov 05 10:04:56 2018 +0100 +++ b/UnitTestsSources/UnitTestsMain.cpp Mon Nov 05 10:06:18 2018 +0100 @@ -26,6 +26,7 @@ #include "../Framework/Layers/LayerSourceBase.h" #include "../Framework/Toolbox/DownloadStack.h" #include "../Framework/Toolbox/FiniteProjectiveCamera.h" +#include "../Framework/Toolbox/MessagingToolbox.h" #include "../Framework/Toolbox/OrthancSlicesLoader.h" #include "../Framework/Volumes/ImageBuffer3D.h" #include "../Framework/Volumes/SlicedVolumeBase.h" @@ -55,7 +56,7 @@ for (size_t i = 0; i < loader.GetSliceCount(); i++) { - const_cast(loader).ScheduleLoadSliceImage(i, SliceImageQuality_Full); + const_cast(loader).ScheduleLoadSliceImage(i, SliceImageQuality_FullPng); } } @@ -724,6 +725,12 @@ */ } +TEST(MessagingToolbox, ParseJson) +{ + Json::Value response; + std::string source = "{\"command\":\"panel:takeDarkImage\",\"commandType\":\"simple\",\"args\":{}}"; + ASSERT_TRUE(OrthancStone::MessagingToolbox::ParseJson(response, source.c_str(), source.size())); +} int main(int argc, char **argv) {