Mercurial > hg > orthanc-stone
changeset 319:daa04d15192c am-2
new SimpleViewer sample that has been split in multiple files to be able to scale it
line wrap: on
line diff
--- a/.hgignore Mon Oct 08 17:10:08 2018 +0200 +++ b/.hgignore Thu Oct 11 13:16:54 2018 +0200 @@ -4,3 +4,4 @@ Applications/Samples/ThirdPartyDownloads/ Applications/Samples/build-wasm/ Applications/Samples/build-web/ +.vscode/
--- a/Applications/Commands/BaseCommandBuilder.cpp Mon Oct 08 17:10:08 2018 +0200 +++ b/Applications/Commands/BaseCommandBuilder.cpp Thu Oct 11 13:16:54 2018 +0200 @@ -32,32 +32,18 @@ throw StoneException(ErrorCode_CommandJsonInvalidFormat); } - if (commandJson["commandType"].isString() && commandJson["commandType"].asString() == "simple") + if (commandJson["commandType"].isString() && commandJson["commandType"].asString() == "generic-no-arg-command") { printf("creating a simple command\n"); - return new SimpleCommand(commandJson["command"].asString().c_str()); + 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; - // std::string commandName = commandJson["command"].asString(); - - - - - // CommandCreationFunctions::const_iterator it = commands_.find(commandName); - // if (it == commands_.end()) - // { - // throw Orthanc::OrthancException(Orthanc::ErrorCode_UnknownResource); // TODO: use StoneException ? - // } - - // // call the CreateCommandFn to build the command - // ICommand* command = it->second(); - // if (commandJson["args"].isObject()) - // { - // command->Configure(commandJson["args"]); - // } - - // return command; } }
--- a/Applications/Commands/ICommand.h Mon Oct 08 17:10:08 2018 +0200 +++ b/Applications/Commands/ICommand.h Thu Oct 11 13:16:54 2018 +0200 @@ -70,13 +70,25 @@ virtual void Execute() {} }; - class SimpleCommand : public BaseCommand<SimpleCommand> + class GenericNoArgCommand : public BaseCommand<GenericNoArgCommand> { public: - SimpleCommand(const std::string& name) + 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<GenericOneStringArgCommand> + { + 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 ! + }; + }
--- a/Applications/IStoneApplication.h Mon Oct 08 17:10:08 2018 +0200 +++ b/Applications/IStoneApplication.h Thu Oct 11 13:16:54 2018 +0200 @@ -28,8 +28,13 @@ #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. @@ -50,6 +55,9 @@ #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;
--- a/Applications/Qt/QtStoneApplicationRunner.cpp Mon Oct 08 17:10:08 2018 +0200 +++ b/Applications/Qt/QtStoneApplicationRunner.cpp Thu Oct 11 13:16:54 2018 +0200 @@ -50,11 +50,11 @@ { context.Start(); - QApplication app(argc, argv); - InitializeMainWindow(context); + QApplication qtApplication(argc, argv); + window_.reset(application_.CreateQtMainWindow()); window_->show(); - app.exec(); + qtApplication.exec(); context.Stop(); }
--- a/Applications/Qt/QtStoneApplicationRunner.h Mon Oct 08 17:10:08 2018 +0200 +++ b/Applications/Qt/QtStoneApplicationRunner.h Thu Oct 11 13:16:54 2018 +0200 @@ -35,7 +35,6 @@ protected: std::auto_ptr<QStoneMainWindow> window_; - virtual void InitializeMainWindow(NativeStoneApplicationContext& context) = 0; public: QtStoneApplicationRunner(MessageBroker& broker, IStoneApplication& application)
--- a/Applications/Samples/CMakeLists.txt Mon Oct 08 17:10:08 2018 +0200 +++ b/Applications/Samples/CMakeLists.txt Thu Oct 11 13:16:54 2018 +0200 @@ -85,13 +85,13 @@ include_directories(${ORTHANC_STONE_ROOT}) # files common to all samples -list(APPEND APPLICATIONS_SOURCES +list(APPEND SAMPLE_APPLICATIONS_SOURCES ${ORTHANC_STONE_ROOT}/Applications/Samples/SampleInteractor.h ${ORTHANC_STONE_ROOT}/Applications/Samples/SampleApplicationBase.h ) if (ENABLE_QT) - list(APPEND APPLICATIONS_SOURCES + 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/SampleMainWindow.ui @@ -99,13 +99,13 @@ endif() if (ENABLE_NATIVE) - list(APPEND APPLICATIONS_SOURCES + list(APPEND SAMPLE_APPLICATIONS_SOURCES ${ORTHANC_STONE_ROOT}/Applications/Samples/SampleMainNative.cpp ) elseif (ENABLE_WASM) - list(APPEND APPLICATIONS_SOURCES + list(APPEND SAMPLE_APPLICATIONS_SOURCES ${ORTHANC_STONE_ROOT}/Applications/Samples/SampleMainWasm.cpp ${STONE_WASM_SOURCES} ) @@ -115,7 +115,7 @@ macro(BuildSingleFileSample Target Header Sample) add_executable(${Target} ${ORTHANC_STONE_ROOT}/Applications/Samples/${Header} - ${APPLICATIONS_SOURCES} + ${SAMPLE_APPLICATIONS_SOURCES} ) set_target_properties(${Target} PROPERTIES COMPILE_DEFINITIONS ORTHANC_STONE_SAMPLE=${Sample}) target_link_libraries(${Target} OrthancStone) @@ -128,7 +128,34 @@ #BuildSingleFileSample(OrthancStoneBasicPetCtFusion 5) #BuildSingleFileSample(OrthancStoneSynchronizedSeries 6) #BuildSingleFileSample(OrthancStoneLayoutPetCtFusion 7) -BuildSingleFileSample(OrthancStoneSimpleViewer SimpleViewerApplication.h 8) +BuildSingleFileSample(OrthancStoneSimpleViewerSingleFile SimpleViewerApplicationSingleFile.h 8) # we keep that one just as a sample before we convert another sample to this pattern + +##### SimpleViewer sample ####### + +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 + ) +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) + ##################################################################### ## Build the unit tests
--- a/Applications/Samples/SampleList.h Mon Oct 08 17:10:08 2018 +0200 +++ b/Applications/Samples/SampleList.h Thu Oct 11 13:16:54 2018 +0200 @@ -29,7 +29,7 @@ typedef OrthancStone::Samples::LayoutPetCtFusionApplication SampleApplication; #elif ORTHANC_STONE_SAMPLE == 8 -#include "SimpleViewerApplication.h" +#include "SimpleViewerApplicationSingleFile.h" typedef OrthancStone::Samples::SimpleViewerApplication SampleApplication; #else
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Applications/Samples/SimpleViewer/AppStatus.h Thu Oct 11 13:16:54 2018 +0200 @@ -0,0 +1,27 @@ +#pragma once + +#include <string> + + +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; + } + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Applications/Samples/SimpleViewer/MainWidgetInteractor.cpp Thu Oct 11 13:16:54 2018 +0200 @@ -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 <http://www.gnu.org/licenses/>. + **/ + +#include "MainWidgetInteractor.h" + +#include "SimpleViewerApplication.h" + +namespace SimpleViewer { + + IWorldSceneMouseTracker* MainWidgetInteractor::CreateMouseTracker(WorldSceneWidget& widget, + const ViewportGeometry& view, + MouseButton button, + KeyboardModifiers modifiers, + double x, + double y, + IStatusBar* statusBar) + { + if (button == MouseButton_Left) + { + if (application_.GetCurrentTool() == SimpleViewerApplication::Tools_LineMeasure) + { + return new LineMeasureTracker(statusBar, dynamic_cast<LayerWidget&>(widget).GetSlice(), x, y, 255, 0, 0, 10); + } + else if (application_.GetCurrentTool() == SimpleViewerApplication::Tools_CircleMeasure) + { + return new CircleMeasureTracker(statusBar, dynamic_cast<LayerWidget&>(widget).GetSlice(), x, y, 255, 0, 0, 10); + } + 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<LayerWidget&>(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, + char key, + KeyboardModifiers modifiers, + IStatusBar* statusBar) + { + switch (key) + { + case 's': + widget.SetDefaultView(); + break; + + default: + break; + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Applications/Samples/SimpleViewer/MainWidgetInteractor.h Thu Oct 11 13:16:54 2018 +0200 @@ -0,0 +1,70 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + **/ + + +#pragma once + +#include "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, + 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, + char key, + KeyboardModifiers modifiers, + IStatusBar* statusBar); + }; + + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Applications/Samples/SimpleViewer/Messages.h Thu Oct 11 13:16:54 2018 +0200 @@ -0,0 +1,13 @@ +#pragma once + +#include "Framework/Messages/MessageType.h" + +namespace SimpleViewer +{ + enum SimpleViewerMessageType + { + SimpleViewerMessageType_First = OrthancStone::MessageType_CustomMessage, + SimpleViewerMessageType_AppStatusUpdated + + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Applications/Samples/SimpleViewer/Qt/SimpleViewerMainWindow.cpp Thu Oct 11 13:16:54 2018 +0200 @@ -0,0 +1,89 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + **/ + +#include "SimpleViewerMainWindow.h" + +/** + * Don't use "ui_MainWindow.h" instead of <ui_MainWindow.h> below, as + * this makes CMake unable to detect when the UI file changes. + **/ +#include <ui_SimpleViewerMainWindow.h> +#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); + + connect(ui_->toolButtonCrop, &QToolButton::clicked, this, &SimpleViewerMainWindow::cropClicked); + 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); + } + + SimpleViewerMainWindow::~SimpleViewerMainWindow() + { + delete ui_; + } + + void SimpleViewerMainWindow::cropClicked() + { + GenericNoArgCommand command("selectTool: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); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Applications/Samples/SimpleViewer/Qt/SimpleViewerMainWindow.h Thu Oct 11 13:16:54 2018 +0200 @@ -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 <http://www.gnu.org/licenses/>. + **/ +#pragma once + +#include <Applications/Qt/QCairoWidget.h> +#include <Applications/Qt/QStoneMainWindow.h> + +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 rotateClicked(); + void windowingClicked(); + void lineClicked(); + void circleClicked(); + void invertClicked(); + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Applications/Samples/SimpleViewer/Qt/SimpleViewerMainWindow.ui Thu Oct 11 13:16:54 2018 +0200 @@ -0,0 +1,144 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>SimpleViewerMainWindow</class> + <widget class="QMainWindow" name="SimpleViewerMainWindow"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>903</width> + <height>634</height> + </rect> + </property> + <property name="minimumSize"> + <size> + <width>500</width> + <height>300</height> + </size> + </property> + <property name="baseSize"> + <size> + <width>500</width> + <height>300</height> + </size> + </property> + <property name="windowTitle"> + <string>Stone of Orthanc</string> + </property> + <property name="layoutDirection"> + <enum>Qt::LeftToRight</enum> + </property> + <widget class="QWidget" name="centralwidget"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="layoutDirection"> + <enum>Qt::LeftToRight</enum> + </property> + <layout class="QVBoxLayout" name="verticalLayout_2" stretch="0,0"> + <property name="sizeConstraint"> + <enum>QLayout::SetDefaultConstraint</enum> + </property> + <item> + <widget class="QCairoWidget" name="cairoCentralWidget"> + <property name="minimumSize"> + <size> + <width>0</width> + <height>500</height> + </size> + </property> + </widget> + </item> + <item> + <widget class="QGroupBox" name="horizontalGroupBox"> + <property name="minimumSize"> + <size> + <width>0</width> + <height>100</height> + </size> + </property> + <property name="maximumSize"> + <size> + <width>16777215</width> + <height>100</height> + </size> + </property> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <widget class="QToolButton" name="toolButtonWindowing"> + <property name="text"> + <string>windowing</string> + </property> + </widget> + </item> + <item> + <widget class="QToolButton" name="toolButtonCrop"> + <property name="text"> + <string>crop</string> + </property> + </widget> + </item> + <item> + <widget class="QToolButton" name="toolButtonLine"> + <property name="text"> + <string>line</string> + </property> + </widget> + </item> + <item> + <widget class="QToolButton" name="toolButtonCircle"> + <property name="text"> + <string>circle</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="pushButtonRotate"> + <property name="text"> + <string>rotate</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="pushButtonInvert"> + <property name="text"> + <string>invert</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + </layout> + </widget> + <widget class="QMenuBar" name="menubar"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>903</width> + <height>22</height> + </rect> + </property> + <widget class="QMenu" name="menuTest"> + <property name="title"> + <string>Test</string> + </property> + </widget> + <addaction name="menuTest"/> + </widget> + <widget class="QStatusBar" name="statusbar"/> + </widget> + <customwidgets> + <customwidget> + <class>QCairoWidget</class> + <extends>QGraphicsView</extends> + <header location="global">QCairoWidget.h</header> + </customwidget> + </customwidgets> + <resources/> + <connections/> +</ui>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Applications/Samples/SimpleViewer/Qt/mainQt.cpp Thu Oct 11 13:16:54 2018 +0200 @@ -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); +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Applications/Samples/SimpleViewer/SimpleViewerApplication.cpp Thu Oct 11 13:16:54 2018 +0200 @@ -0,0 +1,238 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + **/ + + +#include "SimpleViewerApplication.h" + +#if ORTHANC_ENABLE_QT==1 +#include "Qt/SimpleViewerMainWindow.h" +#endif + +#if ORTHANC_ENABLE_WASM==1 +#include <Platforms/Wasm/WasmViewport.h> +#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<SimpleViewerApplication, OrthancApiClient::JsonResponseReadyMessage>(*this, &SimpleViewerApplication::OnStudyListReceived)); + } + else + { + SelectStudy(parameters["studyId"].as<std::string>()); + } + } + + + 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<std::string>(), + "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<SimpleViewerApplication, OrthancApiClient::JsonResponseReadyMessage>(*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<std::string>(); + for (size_t i = 0; i < response["Instances"].size(); i++) + { + const std::string& instanceId = response["Instances"][static_cast<int>(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<SimpleViewerApplication, LayerWidget::GeometryChangedMessage>(*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<SimpleViewerApplication, OrthancApiClient::JsonResponseReadyMessage>(*this, &SimpleViewerApplication::OnStudyReceived)); + } + + void SimpleViewerApplication::OnWidgetGeometryChanged(const LayerWidget::GeometryChangedMessage& message) + { + message.origin_.SetDefaultView(); + } + + 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: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<OrthancStone::NativeStoneApplicationContext&>(*context_), *this); + } +#endif + +#if ORTHANC_ENABLE_WASM==1 + void SimpleViewerApplication::InitializeWasm() { + + AttachWidgetToWasmViewport("canvasThumbnails", thumbnailsLayout_); + AttachWidgetToWasmViewport("canvasMain", mainWidget_); + } +#endif + + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Applications/Samples/SimpleViewer/SimpleViewerApplication.h Thu Oct 11 13:16:54 2018 +0200 @@ -0,0 +1,162 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + **/ + + +#pragma once + +#include "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 <Core/Logging.h> + +#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<SimpleViewerMessageType_AppStatusUpdated> + { + 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 + }; + + private: + Tools currentTool_; + std::unique_ptr<MainWidgetInteractor> mainWidgetInteractor_; + std::unique_ptr<ThumbnailInteractor> thumbnailInteractor_; + LayoutWidget* mainLayout_; + LayoutWidget* thumbnailsLayout_; + LayerWidget* mainWidget_; + std::vector<LayerWidget*> thumbnails_; + std::map<std::string, std::vector<std::string>> instancesIdsPerSeriesId_; + std::map<std::string, Json::Value> seriesTags_; + BaseCommandBuilder commandBuilder_; + + unsigned int currentInstanceIndex_; + OrthancStone::WidgetViewport* wasmViewport1_; + OrthancStone::WidgetViewport* wasmViewport2_; + + IStatusBar* statusBar_; + std::unique_ptr<SmartLoader> smartLoader_; + std::unique_ptr<OrthancApiClient> orthancApiClient_; + + public: + SimpleViewerApplication(MessageBroker& broker) : + IObserver(broker), + IObservable(broker), + currentTool_(Tools_LineMeasure), + mainLayout_(NULL), + currentInstanceIndex_(0), + wasmViewport1_(NULL), + wasmViewport2_(NULL) + { + } + + 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_;} + + 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 + }; + + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Applications/Samples/SimpleViewer/ThumbnailInteractor.cpp Thu Oct 11 13:16:54 2018 +0200 @@ -0,0 +1,43 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + **/ + +#include "ThumbnailInteractor.h" + +#include "SimpleViewerApplication.h" + +namespace SimpleViewer { + + IWorldSceneMouseTracker* ThumbnailInteractor::CreateMouseTracker(WorldSceneWidget& widget, + const ViewportGeometry& view, + MouseButton button, + KeyboardModifiers modifiers, + 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; + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Applications/Samples/SimpleViewer/ThumbnailInteractor.h Thu Oct 11 13:16:54 2018 +0200 @@ -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 <http://www.gnu.org/licenses/>. + **/ + + +#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, + 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, + char key, + KeyboardModifiers modifiers, + IStatusBar* statusBar) + {} + + }; + + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Applications/Samples/SimpleViewer/Wasm/SimpleViewerWasmApplicationAdapter.cpp Thu Oct 11 13:16:54 2018 +0200 @@ -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 <http://www.gnu.org/licenses/>. + **/ + +#include "SimpleViewerWasmApplicationAdapter.h" + +namespace SimpleViewer +{ + + SimpleViewerWasmApplicationAdapter::SimpleViewerWasmApplicationAdapter(MessageBroker &broker, SimpleViewerApplication &application) + : WasmPlatformApplicationAdapter(broker, application), + viewerApplication_(application) + { + application.RegisterObserverCallback(new Callable<SimpleViewerWasmApplicationAdapter, SimpleViewerApplication::StatusUpdatedMessage>(*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<Json::StreamWriter> writer(builder.newStreamWriter()); + std::ostringstream outputStr; + + writer->write(event, &outputStr); + + NotifyStatusUpdateFromCppToWeb(outputStr.str()); + } + +} // namespace SimpleViewer \ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Applications/Samples/SimpleViewer/Wasm/SimpleViewerWasmApplicationAdapter.h Thu Oct 11 13:16:54 2018 +0200 @@ -0,0 +1,43 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + **/ + +#pragma once + +#include <string> +#include <Framework/Messages/IObserver.h> +#include <Platforms/Wasm/WasmPlatformApplicationAdapter.h> + +#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
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Applications/Samples/SimpleViewer/Wasm/mainWasm.cpp Thu Oct 11 13:16:54 2018 +0200 @@ -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 <http://www.gnu.org/licenses/>. + **/ + +#include "Platforms/Wasm/WasmWebService.h" +#include "Platforms/Wasm/WasmViewport.h" + +#include <emscripten/emscripten.h> + +#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<SimpleViewer::SimpleViewerApplication*>(application))); +} \ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Applications/Samples/SimpleViewer/Wasm/simple-viewer.html Thu Oct 11 13:16:54 2018 +0200 @@ -0,0 +1,39 @@ +<!doctype html> + +<html lang="us"> + <head> + <meta charset="utf-8" /> + <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> + + <!-- Disable pinch zoom on mobile devices --> + <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" /> + <meta name="HandheldFriendly" content="true" /> + + <title>Simple Viewer</title> + <link href="styles.css" rel="stylesheet" /> + +<body> + <div id="breadcrumb"> + <span id="label-patient-id"></span> + <span id="label-study-description"></span> + <span id="label-series-description"></span> + </div> + <div> + <canvas id="canvasThumbnails" data-width-ratio="20" data-height-ratio="50"></canvas> + <canvas id="canvasMain" data-width-ratio="70" data-height-ratio="50"></canvas> + </div> + <div id="toolbox"> + <button tool-selector="line-measure" class="tool-selector">line</button> + <button tool-selector="circle-measure" class="tool-selector">circle</button> + <button tool-selector="crop" class="tool-selector">crop</button> + <button tool-selector="windowing" class="tool-selector">windowing</button> + <button tool-selector="zoom" class="tool-selector">zoom</button> + <button tool-selector="pan" class="tool-selector">pan</button> + <button action-trigger="rotate-left" class="action-trigger">rotate left</button> + <button action-trigger="rotate-right" class="action-trigger">rotate right</button> + <button action-trigger="invert" class="action-trigger">invert</button> + </div> + <script type="text/javascript" src="app-simple-viewer.js"></script> +</body> + +</html> \ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Applications/Samples/SimpleViewer/Wasm/simple-viewer.ts Thu Oct 11 13:16:54 2018 +0200 @@ -0,0 +1,75 @@ +///<reference path='../../../../Platforms/Wasm/wasm-application-runner.ts'/> + +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"]); + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Applications/Samples/SimpleViewer/Wasm/styles.css Thu Oct 11 13:16:54 2018 +0200 @@ -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
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Applications/Samples/SimpleViewer/Wasm/tsconfig-simple-viewer.json Thu Oct 11 13:16:54 2018 +0200 @@ -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
--- a/Applications/Samples/SimpleViewerApplication.h Mon Oct 08 17:10:08 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,414 +0,0 @@ -/** - * Stone of Orthanc - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2018 Osimis S.A., Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU Affero General Public License - * as published by the Free Software Foundation, either version 3 of - * the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - **/ - - -#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 <Core/Logging.h> - -namespace OrthancStone -{ - namespace Samples - { - class SimpleViewerApplication : - public SampleApplicationBase, - 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, - 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, - char key, - 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, - double x, - double y, - IStatusBar* statusBar) - { - if (button == MouseButton_Left) - { - if (application_.currentTool_ == Tools_LineMeasure) - { - return new LineMeasureTracker(statusBar, dynamic_cast<LayerWidget&>(widget).GetSlice(), x, y, 255, 0, 0, 10); - } - else if (application_.currentTool_ == Tools_CircleMeasure) - { - return new CircleMeasureTracker(statusBar, dynamic_cast<LayerWidget&>(widget).GetSlice(), x, y, 255, 0, 0, 10); - } - } - 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<LayerWidget&>(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, - char key, - KeyboardModifiers modifiers, - IStatusBar* statusBar) - { - switch (key) - { - case 's': - widget.SetDefaultView(); - 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::unique_ptr<MainWidgetInteractor> mainWidgetInteractor_; - std::unique_ptr<ThumbnailInteractor> thumbnailInteractor_; - LayoutWidget* mainLayout_; - LayoutWidget* thumbnailsLayout_; - LayerWidget* mainWidget_; - std::vector<LayerWidget*> thumbnails_; - std::map<std::string, std::vector<std::string>> instancesIdsPerSeriesId_; - std::map<std::string, Json::Value> seriesTags_; - - unsigned int currentInstanceIndex_; - OrthancStone::WidgetViewport* wasmViewport1_; - OrthancStone::WidgetViewport* wasmViewport2_; - - IStatusBar* statusBar_; - std::unique_ptr<SmartLoader> smartLoader_; - std::unique_ptr<OrthancApiClient> orthancApiClient_; - - public: - SimpleViewerApplication(MessageBroker& broker) : - IObserver(broker), - currentTool_(Tools_LineMeasure), - mainLayout_(NULL), - currentInstanceIndex_(0), - wasmViewport1_(NULL), - wasmViewport2_(NULL) - { -// DeclareIgnoredMessage(MessageType_Widget_ContentChanged); - } - - virtual void Finalize() {} - virtual IWidget* GetCentralWidget() {return mainLayout_;} - - 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<std::string>(), - "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<SimpleViewerApplication, OrthancApiClient::JsonResponseReadyMessage>(*this, &SimpleViewerApplication::OnStudyListReceived)); - } - else - { - SelectStudy(parameters["studyId"].as<std::string>()); - } - } - - 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<SimpleViewerApplication, OrthancApiClient::JsonResponseReadyMessage>(*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<std::string>(); - for (size_t i = 0; i < response["Instances"].size(); i++) - { - const std::string& instanceId = response["Instances"][static_cast<int>(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 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<SimpleViewerApplication, LayerWidget::GeometryChangedMessage>(*this, &SimpleViewerApplication::OnWidgetGeometryChanged)); - smartLoader_->SetFrameInWidget(*thumbnailWidget, 0, instanceId, 0); - thumbnailWidget->SetInteractor(*thumbnailInteractor_); - } - - void SelectStudy(const std::string& studyId) - { - orthancApiClient_->GetJsonAsync("/studies/" + studyId, new Callable<SimpleViewerApplication, OrthancApiClient::JsonResponseReadyMessage>(*this, &SimpleViewerApplication::OnStudyReceived)); - } - - void OnWidgetGeometryChanged(const LayerWidget::GeometryChangedMessage& message) - { - message.origin_.SetDefaultView(); - } - - void SelectSeriesInMainViewport(const std::string& seriesId) - { - smartLoader_->SetFrameInWidget(*mainWidget_, 0, instancesIdsPerSeriesId_[seriesId][0], 0); - } - - 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 - }; - - - } -}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Applications/Samples/SimpleViewerApplicationSingleFile.h Thu Oct 11 13:16:54 2018 +0200 @@ -0,0 +1,424 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + **/ + + +#pragma once + +#include "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 + +#if ORTHANC_ENABLE_QT==1 +#include "Qt/SampleMainWindow.h" +#endif +#include <Core/Logging.h> + +namespace OrthancStone +{ + namespace Samples + { + class SimpleViewerApplication : + public SampleApplicationBase, + 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, + 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, + char key, + 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, + double x, + double y, + IStatusBar* statusBar) + { + if (button == MouseButton_Left) + { + if (application_.currentTool_ == Tools_LineMeasure) + { + return new LineMeasureTracker(statusBar, dynamic_cast<LayerWidget&>(widget).GetSlice(), x, y, 255, 0, 0, 10); + } + else if (application_.currentTool_ == Tools_CircleMeasure) + { + return new CircleMeasureTracker(statusBar, dynamic_cast<LayerWidget&>(widget).GetSlice(), x, y, 255, 0, 0, 10); + } + } + 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<LayerWidget&>(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, + char key, + KeyboardModifiers modifiers, + IStatusBar* statusBar) + { + switch (key) + { + case 's': + widget.SetDefaultView(); + 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::unique_ptr<MainWidgetInteractor> mainWidgetInteractor_; + std::unique_ptr<ThumbnailInteractor> thumbnailInteractor_; + LayoutWidget* mainLayout_; + LayoutWidget* thumbnailsLayout_; + LayerWidget* mainWidget_; + std::vector<LayerWidget*> thumbnails_; + std::map<std::string, std::vector<std::string>> instancesIdsPerSeriesId_; + std::map<std::string, Json::Value> seriesTags_; + + unsigned int currentInstanceIndex_; + OrthancStone::WidgetViewport* wasmViewport1_; + OrthancStone::WidgetViewport* wasmViewport2_; + + IStatusBar* statusBar_; + std::unique_ptr<SmartLoader> smartLoader_; + std::unique_ptr<OrthancApiClient> orthancApiClient_; + + public: + SimpleViewerApplication(MessageBroker& broker) : + IObserver(broker), + currentTool_(Tools_LineMeasure), + mainLayout_(NULL), + currentInstanceIndex_(0), + wasmViewport1_(NULL), + wasmViewport2_(NULL) + { +// DeclareIgnoredMessage(MessageType_Widget_ContentChanged); + } + + virtual void Finalize() {} + virtual IWidget* GetCentralWidget() {return mainLayout_;} + + 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<std::string>(), + "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<SimpleViewerApplication, OrthancApiClient::JsonResponseReadyMessage>(*this, &SimpleViewerApplication::OnStudyListReceived)); + } + else + { + SelectStudy(parameters["studyId"].as<std::string>()); + } + } + + 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<SimpleViewerApplication, OrthancApiClient::JsonResponseReadyMessage>(*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<std::string>(); + for (size_t i = 0; i < response["Instances"].size(); i++) + { + const std::string& instanceId = response["Instances"][static_cast<int>(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 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<SimpleViewerApplication, LayerWidget::GeometryChangedMessage>(*this, &SimpleViewerApplication::OnWidgetGeometryChanged)); + smartLoader_->SetFrameInWidget(*thumbnailWidget, 0, instanceId, 0); + thumbnailWidget->SetInteractor(*thumbnailInteractor_); + } + + void SelectStudy(const std::string& studyId) + { + orthancApiClient_->GetJsonAsync("/studies/" + studyId, new Callable<SimpleViewerApplication, OrthancApiClient::JsonResponseReadyMessage>(*this, &SimpleViewerApplication::OnStudyReceived)); + } + + void OnWidgetGeometryChanged(const LayerWidget::GeometryChangedMessage& message) + { + message.origin_.SetDefaultView(); + } + + void SelectSeriesInMainViewport(const std::string& seriesId) + { + smartLoader_->SetFrameInWidget(*mainWidget_, 0, instancesIdsPerSeriesId_[seriesId][0], 0); + } + + 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 + +#if ORTHANC_ENABLE_QT==1 + virtual QStoneMainWindow* CreateQtMainWindow() { + return new SampleMainWindow(dynamic_cast<OrthancStone::NativeStoneApplicationContext&>(*context_), *this); + } +#endif + }; + + + } +}
--- a/Applications/Samples/Web/index.html Mon Oct 08 17:10:08 2018 +0200 +++ b/Applications/Samples/Web/index.html Thu Oct 11 13:16:54 2018 +0200 @@ -14,7 +14,8 @@ <body> <ul> - <li><a href="simple-viewer.html">Simple Viewer</a></li> + <li><a href="simple-viewer/simple-viewer.html">Simple Viewer Project</a></li> + <li><a href="simple-viewer-single-file.html">Simple Viewer Single file (to be replaced by other samples)</a></li> </ul> </body>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Applications/Samples/Web/simple-viewer-single-file.html Thu Oct 11 13:16:54 2018 +0200 @@ -0,0 +1,34 @@ +<!doctype html> + +<html lang="us"> + <head> + <meta charset="utf-8" /> + <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> + + <!-- Disable pinch zoom on mobile devices --> + <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" /> + <meta name="HandheldFriendly" content="true" /> + + <title>Simple Viewer</title> + <link href="samples-styles.css" rel="stylesheet" /> + +<body> + <div id="breadcrumb"> + <span id="patient-id"></span> + <span id="study-description"></span> + <span id="series-description"></span> + </div> + <div> + <canvas id="canvas" data-width-ratio="20" data-height-ratio="50"></canvas> + <canvas id="canvas2" data-width-ratio="70" data-height-ratio="50"></canvas> + </div> + <div id="toolbox"> + <input tool-selector="line-measure" type="radio" name="radio-tool-selector" class="tool-selector">line + <input tool-selector="circle-measure" type="radio" name="radio-tool-selector" class="tool-selector">circle + <button action-trigger="action1" class="action-trigger">action1</button> + <button action-trigger="action2" class="action-trigger">action2</button> + </div> + <script type="text/javascript" src="app-simple-viewer-single-file.js"></script> +</body> + +</html> \ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Applications/Samples/Web/simple-viewer-single-file.ts Thu Oct 11 13:16:54 2018 +0200 @@ -0,0 +1,51 @@ +///<reference path='../../../Platforms/Wasm/wasm-application-runner.ts'/> + +InitializeWasmApplication("OrthancStoneSimpleViewer", "/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]; + } +}
--- a/Applications/Samples/Web/simple-viewer.html Mon Oct 08 17:10:08 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,34 +0,0 @@ -<!doctype html> - -<html lang="us"> - <head> - <meta charset="utf-8" /> - <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> - - <!-- Disable pinch zoom on mobile devices --> - <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" /> - <meta name="HandheldFriendly" content="true" /> - - <title>Simple Viewer</title> - <link href="samples-styles.css" rel="stylesheet" /> - -<body> - <div id="breadcrumb"> - <span id="patient-id"></span> - <span id="study-description"></span> - <span id="series-description"></span> - </div> - <div> - <canvas id="canvas" data-width-ratio="20" data-height-ratio="50"></canvas> - <canvas id="canvas2" data-width-ratio="70" data-height-ratio="50"></canvas> - </div> - <div id="toolbox"> - <input tool-selector="line-measure" type="radio" name="radio-tool-selector" class="tool-selector">line - <input tool-selector="circle-measure" type="radio" name="radio-tool-selector" class="tool-selector">circle - <button action-trigger="action1" class="action-trigger">action1</button> - <button action-trigger="action2" class="action-trigger">action2</button> - </div> - <script type="text/javascript" src="app-simple-viewer.js"></script> -</body> - -</html> \ No newline at end of file
--- a/Applications/Samples/Web/simple-viewer.ts Mon Oct 08 17:10:08 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,51 +0,0 @@ -///<reference path='../../../Platforms/Wasm/wasm-application-runner.ts'/> - -InitializeWasmApplication("OrthancStoneSimpleViewer", "/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]; - } -}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Applications/Samples/Web/tsconfig-simple-viewer-single-file.json Thu Oct 11 13:16:54 2018 +0200 @@ -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
--- a/Applications/Samples/Web/tsconfig-simple-viewer.json Mon Oct 08 17:10:08 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,10 +0,0 @@ -{ - "extends" : "./tsconfig-samples", - "compilerOptions": { - "outFile": "../build-web/app-simple-viewer.js" - }, - "include" : [ - "simple-viewer.ts", - "common-samples.ts" - ] -} \ No newline at end of file
--- a/Applications/Samples/build-web.sh Mon Oct 08 17:10:08 2018 +0200 +++ b/Applications/Samples/build-web.sh Thu Oct 11 13:16:54 2018 +0200 @@ -2,7 +2,7 @@ set -e -# this script currently assumes that the wasm code has been built on its side and is availabie in Wasm/build/ +# this script currently assumes that the wasm code has been built on its side and is availabie in build-wasm/ currentDir=$(pwd) samplesRootDir=$(pwd) @@ -10,12 +10,22 @@ 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 -cp $samplesRootDir/Web/simple-viewer.html $outputDir -tsc --allowJs --project $samplesRootDir/Web/tsconfig-simple-viewer.json -cp $currentDir/build-wasm/OrthancStoneSimpleViewer.js $outputDir -cp $currentDir/build-wasm/OrthancStoneSimpleViewer.wasm $outputDir +# build simple-viewer-single-file (obsolete project) +cp $samplesRootDir/Web/simple-viewer-single-file.html $outputDir +tsc --allowJs --project $samplesRootDir/Web/tsconfig-simple-viewer-single-file.json +cp $currentDir/build-wasm/OrthancStoneSimpleViewerSingleFile.js $outputDir +cp $currentDir/build-wasm/OrthancStoneSimpleViewerSingleFile.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
--- a/Platforms/Wasm/WasmPlatformApplicationAdapter.cpp Mon Oct 08 17:10:08 2018 +0200 +++ b/Platforms/Wasm/WasmPlatformApplicationAdapter.cpp Thu Oct 11 13:16:54 2018 +0200 @@ -19,10 +19,14 @@ 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<ICommand> command(application_.GetCommandBuilder().CreateFromJson(inputJson)); - application_.ExecuteCommand(*command); + if (command.get() == NULL) + printf("Could not parse command: '%s'\n", input.c_str()); + else + application_.ExecuteCommand(*command); } } catch (StoneException& exc)