view Applications/Samples/Sdl/RtViewer/RtViewerSdl.cpp @ 1985:bb307007f8e2

improved interaction with rectangle probes
author Sebastien Jodogne <s.jodogne@gmail.com>
date Mon, 31 Oct 2022 15:01:09 +0100
parents f81cdf283859
children 07964689cb0b
line wrap: on
line source

/**
 * Stone of Orthanc
 * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 * Department, University Hospital of Liege, Belgium
 * Copyright (C) 2017-2022 Osimis S.A., Belgium
 * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, 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 "../../Common/RtViewerApp.h"
#include "../../Common/RtViewerView.h"
#include "../SdlHelpers.h"

#include <EmbeddedResources.h>

// Stone of Orthanc includes
#include "../../../../OrthancStone/Sources/Loaders/GenericLoadersContext.h"
#include "../../../../OrthancStone/Sources/OpenGL/OpenGLIncludes.h"
#include "../../../../OrthancStone/Sources/Platforms/Sdl/SdlOpenGLContext.h"
#include "../../../../OrthancStone/Sources/StoneException.h"
#include "../../../../OrthancStone/Sources/StoneInitialization.h"

// Orthanc (a.o. for screenshot capture)
#include <Compatibility.h>  // For std::unique_ptr<>
#include <Images/Image.h>
#include <Images/ImageProcessing.h>
#include <Images/PngWriter.h>


#include <boost/program_options.hpp>
#include <boost/shared_ptr.hpp>

// #include <boost/pointer_cast.hpp> this include might be necessary in more recent boost versions

#include <SDL.h>

#include <string>


#if !defined(__APPLE__)
/**
 * OpenGL: "OS X does not seem to support debug output functionality
 * (as gathered online)."
 * https://learnopengl.com/In-Practice/Debugging
 **/
static void GLAPIENTRY
OpenGLMessageCallback(GLenum source,
                      GLenum type,
                      GLuint id,
                      GLenum severity,
                      GLsizei length,
                      const GLchar* message,
                      const void* userParam)
{
  if (severity != GL_DEBUG_SEVERITY_NOTIFICATION)
  {
    fprintf(stderr, "GL CALLBACK: %s type = 0x%x, severity = 0x%x, message = %s\n",
            (type == GL_DEBUG_TYPE_ERROR ? "** GL ERROR **" : ""),
            type, severity, message);
  }
}
#endif

namespace OrthancStone
{
  void RtViewerView::EnableGLDebugOutput()
  {
#if !defined(__APPLE__)
    glEnable(GL_DEBUG_OUTPUT);
    glDebugMessageCallback(OpenGLMessageCallback, 0);
#endif
  }

  boost::shared_ptr<IViewport> RtViewerView::CreateViewport(const std::string& canvasId)
  {
    // False means we do NOT let Windows treat this as a legacy application that needs to be scaled
    return SdlOpenGLViewport::Create(canvasId, 1024, 1024, false);
  }

  void RtViewerView::CleanupViewportEvents()
  {
    // only used in WASM at the moment
  }

  void RtViewerApp::ProcessOptions(int argc, char* argv[])
  {
    namespace po = boost::program_options;
    po::options_description desc("Usage");

    desc.add_options()
      ("loglevel", po::value<std::string>()->default_value("WARNING"),
       "You can choose WARNING, INFO or TRACE for the logging level: Errors and warnings will always be displayed. (default: WARNING)")

      ("orthanc", po::value<std::string>()->default_value("http://localhost:8042"),
       "Base URL of the Orthanc instance")

      ("ctseries", po::value<std::string>()->default_value("a04ecf01-79b2fc33-58239f7e-ad9db983-28e81afa"),
       "Orthanc ID of the CT series to load. This must be supplied.")

      ("rtdose", po::value<std::string>()->default_value("830a69ff-8e4b5ee3-b7f966c8-bccc20fb-d322dceb"),
       "Orthanc ID of the RTDOSE instance to load. This may be an empty string.")

      ("rtstruct", po::value<std::string>()->default_value("54460695-ba3885ee-ddf61ac0-f028e31d-a6e474d9"),
       "Orthanc ID of the RTSTRUCT instance to load. This may be an empty string.")
      ;

    std::cout << desc << std::endl;

    po::variables_map vm;
    try
    {
      po::store(po::parse_command_line(argc, argv, desc), vm);
      po::notify(vm);
    }
    catch (std::exception& e)
    {
      std::cerr << "Please check your command line options! (\"" << e.what() << "\")" << std::endl;
    }

    for (po::variables_map::iterator it = vm.begin(); it != vm.end(); ++it)
    {
      std::string key = it->first;
      const po::variable_value& value = it->second;
      const std::string& strValue = value.as<std::string>();
      SetArgument(key, strValue);
    }
  }

  void RtViewerApp::RunSdl(int argc, char* argv[])
  {
    ProcessOptions(argc, argv);

    /**
    Create the shared loaders context
    */
    loadersContext_.reset(new GenericLoadersContext(1, 4, 1));

    // we are in SDL --> downcast to concrete type
    boost::shared_ptr<GenericLoadersContext> loadersContext = boost::dynamic_pointer_cast<GenericLoadersContext>(loadersContext_);

    /**
      Url of the Orthanc instance
      Typically, in a native application (Qt, SDL), it will be an absolute URL like "http://localhost:8042". In 
      wasm on the browser, it could be an absolute URL, provided you do not have cross-origin problems, or a relative
      URL. In our wasm samples, it is set to "..", because we set up either a reverse proxy or an Orthanc ServeFolders
      plugin that serves the main web application from an URL like "http://localhost:8042/stone-rtviewer" (with ".." 
      leading to the main Orthanc root URL)
    */
    std::string orthancUrl = arguments_["orthanc"];

    {
      Orthanc::WebServiceParameters p;
      if (HasArgument("orthanc"))
      {
        p.SetUrl(orthancUrl);
      }
      if (HasArgument("user"))
      {
        ORTHANC_ASSERT(HasArgument("password"));
        p.SetCredentials(GetArgument("user"), GetArgument("password"));
      } 
      else
      {
        ORTHANC_ASSERT(!HasArgument("password"));
      }
      loadersContext->SetOrthancParameters(p);
    }

    loadersContext->StartOracle();

    CreateLoaders();

    /**
    Create viewports
    */
    CreateView("RtViewer Axial", VolumeProjection_Axial);
    CreateView("RtViewer Coronal", VolumeProjection_Coronal);
    CreateView("RtViewer Sagittal", VolumeProjection_Sagittal);

    for (size_t i = 0; i < views_.size(); ++i)
    {
      views_[i]->PrepareViewport();
      views_[i]->EnableGLDebugOutput();
    }

    DefaultViewportInteractor interactor;

    /**
    It is very important that the Oracle (responsible for network I/O) be started before creating and firing the 
    loaders, for any command scheduled by the loader before the oracle is started will be lost.
    */
    StartLoaders();


    SdlRunLoop(views_, interactor);
    loadersContext->StopOracle();
  }

  void RtViewerView::TakeScreenshot(const std::string& target,
                                   unsigned int canvasWidth,
                                   unsigned int canvasHeight)
  {
    std::unique_ptr<IViewport::ILock> lock(viewport_->Lock());
    ViewportController& controller = lock->GetController();
    const Scene2D& scene = controller.GetScene();

    std::string ttf;
    Orthanc::EmbeddedResources::GetFileResource(ttf, Orthanc::EmbeddedResources::UBUNTU_FONT);
    
    CairoCompositor compositor(canvasWidth, canvasHeight);
    compositor.SetFont(0, ttf, FONT_SIZE_0, Orthanc::Encoding_Latin1);
    compositor.Refresh(scene);

    Orthanc::ImageAccessor canvas;
    compositor.GetCanvas().GetReadOnlyAccessor(canvas);

    Orthanc::Image png(Orthanc::PixelFormat_RGB24, canvas.GetWidth(), canvas.GetHeight(), false);
    Orthanc::ImageProcessing::Convert(png, canvas);

    Orthanc::PngWriter writer;
    Orthanc::IImageWriter::WriteToFile(writer, target, png);
  }

  static boost::shared_ptr<RtViewerView> GetViewFromWindowId(
    const std::vector<boost::shared_ptr<RtViewerView> >& views,
    Uint32 windowID)
  {
    using namespace OrthancStone;
    for (size_t i = 0; i < views.size(); ++i)
    {
      boost::shared_ptr<RtViewerView> view = views[i];
      boost::shared_ptr<IViewport> viewport = view->GetViewport();
      boost::shared_ptr<SdlViewport> sdlViewport = boost::dynamic_pointer_cast<SdlViewport>(viewport);
      Uint32 curWindowID = sdlViewport->GetSdlWindowId();
      if (windowID == curWindowID)
        return view;
    }
    return boost::shared_ptr<RtViewerView>();
  }

  void RtViewerApp::SdlRunLoop(const std::vector<boost::shared_ptr<RtViewerView> >& views,
                               DefaultViewportInteractor& interactor)
  {
    using namespace OrthancStone;

    // const std::vector<boost::shared_ptr<RtViewerView> >& views
    std::vector<boost::shared_ptr<SdlViewport> > viewports;
    for (size_t i = 0; i < views.size(); ++i)
    {
      boost::shared_ptr<RtViewerView> view = views[i];
      boost::shared_ptr<IViewport> viewport = view->GetViewport();
      boost::shared_ptr<SdlViewport> sdlViewport =
        boost::dynamic_pointer_cast<SdlViewport>(viewport);
      viewports.push_back(sdlViewport);
    }

    {
      int scancodeCount = 0;
      const uint8_t* keyboardState = SDL_GetKeyboardState(&scancodeCount);

      bool stop = false;
      while (!stop)
      {
        std::vector<SDL_Event> sdlEvents;
        std::map<Uint32,SDL_Event> userEventsMap;

        {
          SDL_Event sdlEvent;

          // FIRST: collect all pending events
          while (SDL_PollEvent(&sdlEvent) != 0)
          {
            if ( (sdlEvent.type >= SDL_USEREVENT) && 
                 (sdlEvent.type < SDL_LASTEVENT) )
            {
              // we don't want to have multiple refresh events ,
              // and since every refresh event is a user event with a special type,
              // we use a map
              userEventsMap[sdlEvent.type] = sdlEvent;
            }
            else
            {
              sdlEvents.push_back(sdlEvent);
            }
          }
        }

        // SECOND: add all user events to sdlEvents
        for (std::map<Uint32,SDL_Event>::const_iterator it = userEventsMap.begin(); it != userEventsMap.end(); ++it)
        {
          sdlEvents.push_back(it->second);
        }

        // now process the events
        for (std::vector<SDL_Event>::const_iterator it = sdlEvents.begin(); it != sdlEvents.end(); ++it)
        {
          const SDL_Event& sdlEvent = *it;

          if (sdlEvent.type == SDL_QUIT)
          {
            stop = true;
            break;
          }
          else if (sdlEvent.type == SDL_WINDOWEVENT &&
                   (sdlEvent.window.event == SDL_WINDOWEVENT_RESIZED ||
                    sdlEvent.window.event == SDL_WINDOWEVENT_SIZE_CHANGED))
          {
            boost::shared_ptr<RtViewerView> view = GetViewFromWindowId(
              views, sdlEvent.window.windowID);

            boost::shared_ptr<SdlViewport> sdlViewport =
              boost::dynamic_pointer_cast<SdlViewport>(view->GetViewport());

            sdlViewport->UpdateSize(sdlEvent.window.data1, sdlEvent.window.data2);
          }
          else if (sdlEvent.type == SDL_WINDOWEVENT &&
                   (sdlEvent.window.event == SDL_WINDOWEVENT_SHOWN ||
                    sdlEvent.window.event == SDL_WINDOWEVENT_EXPOSED))
          {
            boost::shared_ptr<RtViewerView> view = GetViewFromWindowId(
              views, sdlEvent.window.windowID);
            boost::shared_ptr<SdlViewport> sdlViewport =
              boost::dynamic_pointer_cast<SdlViewport>(view->GetViewport());

            {
              std::unique_ptr<IViewport::ILock> lock(sdlViewport->Lock());
              lock->RefreshCanvasSize();
            }
          }
          else if (sdlEvent.type == SDL_KEYDOWN &&
                   sdlEvent.key.repeat == 0 /* Ignore key bounce */)
          {
            boost::shared_ptr<RtViewerView> view = GetViewFromWindowId(
              views, sdlEvent.window.windowID);

            switch (sdlEvent.key.keysym.sym)
            {
            case SDLK_f:
            {
              boost::shared_ptr<SdlViewport> sdlViewport =
                boost::dynamic_pointer_cast<SdlViewport>(view->GetViewport());
              sdlViewport->ToggleMaximize();
            }
            break;

            case SDLK_s:
            {
              std::unique_ptr<IViewport::ILock> lock(view->GetViewport()->Lock());
              lock->GetCompositor().FitContent(lock->GetController().GetScene());
              lock->Invalidate();
            }
            break;

            case SDLK_q:
              stop = true;
              break;

            default:
              break;
            }
          }
          else if (sdlEvent.type == SDL_MOUSEBUTTONDOWN ||
                   sdlEvent.type == SDL_MOUSEMOTION ||
                   sdlEvent.type == SDL_MOUSEBUTTONUP)
          {
            boost::shared_ptr<RtViewerView> view = GetViewFromWindowId(
              views, sdlEvent.window.windowID);

            std::unique_ptr<IViewport::ILock> lock(view->GetViewport()->Lock());
            if (lock->HasCompositor())
            {
              PointerEvent p;
              OrthancStoneHelpers::GetPointerEvent(p, lock->GetCompositor(),
                                                   sdlEvent, keyboardState, scancodeCount);

              switch (sdlEvent.type)
              {
              case SDL_MOUSEBUTTONDOWN:
                interactor.SetWindowingLayer(view->GetCtLayerIndex());
                lock->GetController().HandleMousePress(interactor, p,
                                                       lock->GetCompositor().GetCanvasWidth(),
                                                       lock->GetCompositor().GetCanvasHeight());
                lock->Invalidate();
                break;

              case SDL_MOUSEMOTION:
                if (lock->GetController().HandleMouseMove(p))
                {
                  lock->Invalidate();
                }
                break;

              case SDL_MOUSEBUTTONUP:
                lock->GetController().HandleMouseRelease(p);
                lock->Invalidate();
                break;

              default:
                throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
              }
            }
          }
          else if (sdlEvent.type == SDL_MOUSEWHEEL)
          {
            boost::shared_ptr<RtViewerView> view = GetViewFromWindowId(
              views, sdlEvent.window.windowID);

            int delta = 0;
            if (sdlEvent.wheel.y < 0)
              delta = -1;
            if (sdlEvent.wheel.y > 0)
              delta = 1;

            if (SDL_SCANCODE_LCTRL < scancodeCount &&
                keyboardState[SDL_SCANCODE_LCTRL])  // Speed up scrolling if CTRL is down
            {
              delta *= 10;
            }

            view->Scroll(delta);
          }
          else
          {
            for (size_t i = 0; i < views.size(); ++i)
            {
              boost::shared_ptr<SdlViewport> sdlViewport =
                boost::dynamic_pointer_cast<SdlViewport>(views[i]->GetViewport());
              if (sdlViewport->IsRefreshEvent(sdlEvent))
              {
                sdlViewport->Paint();
              }
            }
          }
        }
        // Small delay to avoid using 100% of CPU
        SDL_Delay(1);
      }
    }
  }
}

boost::weak_ptr<OrthancStone::RtViewerApp> g_app;

/**
 * IMPORTANT: The full arguments to "main()" are needed for SDL on
 * Windows. Otherwise, one gets the linking error "undefined reference
 * to `SDL_main'". https://wiki.libsdl.org/FAQWindows
 **/
int main(int argc, char* argv[])
{
  using namespace OrthancStone;

  StoneInitialize();
  OrthancStone::SdlWindow::GlobalInitialize();

  try
  {
    boost::shared_ptr<RtViewerApp> app = RtViewerApp::Create();
    g_app = app;
    app->RunSdl(argc,argv);
  }
  catch (Orthanc::OrthancException& e)
  {
    LOG(ERROR) << "EXCEPTION: " << e.What();
  }
  
  OrthancStone::SdlWindow::GlobalFinalize();
  StoneFinalize();

  return 0;
}