view Samples/Sdl/TrackerSample.cpp @ 632:500c3f70b6c2

- Added a ClearAllChains method to PolylineSceneLayer --> revision must change when calling it ==> BumpRevision has been added to base class - Added some docs = Added GetMinDepth + GetMaxDepth to Scene2D (to alleviate the need for app- specific "Z depth registry" : clients may simply add a new layer on top or at the bottom of the existing layer set. - Added the line tracker measurement tools, commands and trackers. Generic base classes + Line measure - started work on the line measure handles
author Benjamin Golinvaux <bgo@osimis.io>
date Thu, 09 May 2019 10:41:31 +0200
parents
children 104c379f3f1b f939f449482c
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-2019 Osimis S.A., Belgium
 *
 * This program is free software: you can redistribute it and/or
 * modify it under the terms of the GNU Affero General Public License
 * as published by the Free Software Foundation, either version 3 of
 * the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 **/


 // From Stone
#include "../../Applications/Sdl/SdlOpenGLWindow.h"
#include "../../Framework/Scene2D/CairoCompositor.h"
#include "../../Framework/Scene2D/ColorTextureSceneLayer.h"
#include "../../Framework/Scene2D/OpenGLCompositor.h"
#include "../../Framework/Scene2D/PanSceneTracker.h"
#include "../../Framework/Scene2D/RotateSceneTracker.h"
#include "../../Framework/Scene2D/Scene2D.h"
#include "../../Framework/Scene2D/ZoomSceneTracker.h"
#include "../../Framework/StoneInitialization.h"

// From Orthanc framework
#include <Core/Logging.h>
#include <Core/OrthancException.h>
#include <Core/Images/Image.h>
#include <Core/Images/ImageProcessing.h>
#include <Core/Images/PngWriter.h>

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

#include <SDL.h>
#include <stdio.h>


// to be moved into Stone
#include "../Common/MeasureTrackers.h"
#include "../Common/MeasureCommands.h"

/*
TODO:

- to decouple the trackers from the sample, we need to supply them with
  the scene rather than the app

- in order to do that, we need a GetNextFreeZIndex function (or something 
  along those lines) in the scene object

*/


using namespace Orthanc;
using namespace OrthancStone;

namespace OrthancStone
{
  enum GuiTool
  {
    GuiTool_Rotate = 0,
    GuiTool_Pan,
    GuiTool_Zoom,
    GuiTool_LineMeasure,
    GuiTool_CircleMeasure,
    GuiTool_AngleMeasure,
    GuiTool_EllipseMeasure,
    GuiTool_LAST
  };

  const char* MeasureToolToString(size_t i)
  {
    static const char* descs[] = {
      "GuiTool_Rotate",
      "GuiTool_Pan",
      "GuiTool_Zoom",
      "GuiTool_LineMeasure",
      "GuiTool_CircleMeasure",
      "GuiTool_AngleMeasure",
      "GuiTool_EllipseMeasure",
      "GuiTool_LAST"
    };
    if (i >= GuiTool_LAST)
    {
      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError, "Wrong tool index");
    }
    return descs[i];
  }
}

class TrackerSampleApp
{
public:
  // 12 because.
  TrackerSampleApp() : currentTool_(GuiTool_Rotate)
  {
    TEXTURE_2x2_1_ZINDEX  = 1;
    TEXTURE_1x1_ZINDEX    = 2;
    TEXTURE_2x2_2_ZINDEX  = 3;
    LINESET_1_ZINDEX      = 4;
    LINESET_2_ZINDEX      = 5;
    INFOTEXT_LAYER_ZINDEX = 6;
  }
  void PrepareScene();
  void Run();

private:
  Scene2D& GetScene()
  {
    return scene_;
  }

  void SelectNextTool()
  {
    currentTool_ = static_cast<GuiTool>(currentTool_ + 1);
    if (currentTool_ == GuiTool_LAST)
      currentTool_ = static_cast<GuiTool>(0);;
    printf("Current tool is now: %s\n", MeasureToolToString(currentTool_));
  }

  void HandleApplicationEvent(
    const OpenGLCompositor& compositor,
    const SDL_Event& event,
    std::auto_ptr<IPointerTracker>& activeTracker);

  IPointerTracker* TrackerSampleApp::TrackerHitTest(const PointerEvent& e);

  IPointerTracker* CreateSuitableTracker(
    const SDL_Event& event,
    const PointerEvent& e,
    const OpenGLCompositor& compositor);
  
  void TakeScreenshot(
    const std::string& target,
    unsigned int canvasWidth,
    unsigned int canvasHeight);

  /**
    This adds the command at the top of the undo stack  
  */
  void Commit(TrackerCommandPtr cmd);
  void Undo();
  void Redo();
  
private:
  static const unsigned int FONT_SIZE = 32;

  std::vector<TrackerCommandPtr> undoStack_;
  
  // we store the measure tools here so that they don't get deleted
  std::vector<MeasureToolPtr> measureTools_;

  //static const int LAYER_POSITION = 150;
#if 0
  int TEXTURE_2x2_1_ZINDEX = 12;
  int TEXTURE_1x1_ZINDEX = 13;
  int TEXTURE_2x2_2_ZINDEX = 14;
  int LINESET_1_ZINDEX = 50;
  int LINESET_2_ZINDEX = 100;
  int INFOTEXT_LAYER_ZINDEX = 150;
#else
  int TEXTURE_2x2_1_ZINDEX;
  int TEXTURE_1x1_ZINDEX;
  int TEXTURE_2x2_2_ZINDEX;
  int LINESET_1_ZINDEX;
  int LINESET_2_ZINDEX;
  int INFOTEXT_LAYER_ZINDEX;
#endif
  Scene2D scene_;
  GuiTool currentTool_;
};


void TrackerSampleApp::PrepareScene()
{
  // Texture of 2x2 size
  {
    Orthanc::Image i(Orthanc::PixelFormat_RGB24, 2, 2, false);

    uint8_t *p = reinterpret_cast<uint8_t*>(i.GetRow(0));
    p[0] = 255;
    p[1] = 0;
    p[2] = 0;

    p[3] = 0;
    p[4] = 255;
    p[5] = 0;

    p = reinterpret_cast<uint8_t*>(i.GetRow(1));
    p[0] = 0;
    p[1] = 0;
    p[2] = 255;

    p[3] = 255;
    p[4] = 0;
    p[5] = 0;

    scene_.SetLayer(TEXTURE_2x2_1_ZINDEX, new ColorTextureSceneLayer(i));

    std::auto_ptr<ColorTextureSceneLayer> l(new ColorTextureSceneLayer(i));
    l->SetOrigin(-3, 2);
    l->SetPixelSpacing(1.5, 1);
    l->SetAngle(20.0 / 180.0 * M_PI);
    scene_.SetLayer(TEXTURE_2x2_2_ZINDEX, l.release());
  }

  // Texture of 1x1 size
  {
    Orthanc::Image i(Orthanc::PixelFormat_RGB24, 1, 1, false);

    uint8_t *p = reinterpret_cast<uint8_t*>(i.GetRow(0));
    p[0] = 255;
    p[1] = 0;
    p[2] = 0;

    std::auto_ptr<ColorTextureSceneLayer> l(new ColorTextureSceneLayer(i));
    l->SetOrigin(-2, 1);
    l->SetAngle(20.0 / 180.0 * M_PI);
    scene_.SetLayer(TEXTURE_1x1_ZINDEX, l.release());
  }

  // Some lines
  {
    std::auto_ptr<PolylineSceneLayer> layer(new PolylineSceneLayer);

    layer->SetThickness(1);

    PolylineSceneLayer::Chain chain;
    chain.push_back(ScenePoint2D(0 - 0.5, 0 - 0.5));
    chain.push_back(ScenePoint2D(0 - 0.5, 2 - 0.5));
    chain.push_back(ScenePoint2D(2 - 0.5, 2 - 0.5));
    chain.push_back(ScenePoint2D(2 - 0.5, 0 - 0.5));
    layer->AddChain(chain, true);

    chain.clear();
    chain.push_back(ScenePoint2D(-5, -5));
    chain.push_back(ScenePoint2D(5, -5));
    chain.push_back(ScenePoint2D(5, 5));
    chain.push_back(ScenePoint2D(-5, 5));
    layer->AddChain(chain, true);

    double dy = 1.01;
    chain.clear();
    chain.push_back(ScenePoint2D(-4, -4));
    chain.push_back(ScenePoint2D(4, -4 + dy));
    chain.push_back(ScenePoint2D(-4, -4 + 2.0 * dy));
    chain.push_back(ScenePoint2D(4, 2));
    layer->AddChain(chain, false);

    layer->SetColor(0, 255, 255);
    scene_.SetLayer(LINESET_1_ZINDEX, layer.release());
  }

  // Some text
  {
    std::auto_ptr<TextSceneLayer> layer(new TextSceneLayer);
    layer->SetText("Hello");
    scene_.SetLayer(LINESET_2_ZINDEX, layer.release());
  }
}


void TrackerSampleApp::TakeScreenshot(const std::string& target,
  unsigned int canvasWidth,
  unsigned int canvasHeight)
{
  // Take a screenshot, then save it as PNG file
  CairoCompositor compositor(scene_, canvasWidth, canvasHeight);
  compositor.SetFont(0, Orthanc::EmbeddedResources::UBUNTU_FONT, FONT_SIZE, Orthanc::Encoding_Latin1);
  compositor.Refresh();

  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;
  writer.WriteToFile(target, png);
}


IPointerTracker* TrackerSampleApp::TrackerHitTest(const PointerEvent& e)
{
  // std::vector<MeasureToolPtr> measureTools_;
  return nullptr;
}

IPointerTracker* TrackerSampleApp::CreateSuitableTracker(
  const SDL_Event& event, 
  const PointerEvent& e, 
  const OpenGLCompositor& compositor)
{
  switch (event.button.button)
  {
  case SDL_BUTTON_MIDDLE:
    return new PanSceneTracker(scene_, e);

  case SDL_BUTTON_RIGHT:
    return new ZoomSceneTracker(
      scene_, e, compositor.GetCanvasHeight());

  case SDL_BUTTON_LEFT:
  {
    // TODO: we need to iterate on the set of measuring tool and perform
    // a hit test to check if a tracker needs to be created for edition.
    // Otherwise, depending upon the active tool, we might want to create
    // a "measuring tool creation" tracker

    // TODO: if there are conflicts, we should prefer a tracker that 
    // pertains to the type of measuring tool currently selected (TBD?)
    IPointerTracker* hitTestTracker = TrackerHitTest(e);
    
    if (hitTestTracker != NULL)
    {
      return hitTestTracker;
    }
    else
    { 
      switch (currentTool_)
      {
      case GuiTool_Rotate:
        return new RotateSceneTracker(scene_, e);
      case GuiTool_LineMeasure:
        return new CreateLineMeasureTracker(
          scene_, undoStack_, measureTools_, e);
        //case GuiTool_AngleMeasure:
        //  return new AngleMeasureTracker(scene_, measureTools_, undoStack_, e);
        //case GuiTool_CircleMeasure:
        //  return new CircleMeasureTracker(scene_, measureTools_, undoStack_, e);
        //case GuiTool_EllipseMeasure:
        //  return new EllipseMeasureTracker(scene_, measureTools_, undoStack_, e);
      default:
        throw OrthancException(ErrorCode_InternalError, "Wrong tool!");
      }
    }
  }
  default:
    return NULL;
  }
}

void TrackerSampleApp::HandleApplicationEvent(
  const OpenGLCompositor& compositor,
  const SDL_Event& event,
  std::auto_ptr<IPointerTracker>& activeTracker)
{
  if (event.type == SDL_MOUSEMOTION)
  {
    int scancodeCount = 0;
    const uint8_t* keyboardState = SDL_GetKeyboardState(&scancodeCount);

    if (activeTracker.get() == NULL &&
      SDL_SCANCODE_LCTRL < scancodeCount &&
      keyboardState[SDL_SCANCODE_LCTRL])
    {
      // The "left-ctrl" key is down, while no tracker is present

      PointerEvent e;
      e.AddPosition(compositor.GetPixelCenterCoordinates(event.button.x, event.button.y));

      ScenePoint2D p = e.GetMainPosition().Apply(scene_.GetCanvasToSceneTransform());

      char buf[64];
      sprintf(buf, "(%0.02f,%0.02f)", p.GetX(), p.GetY());

      if (scene_.HasLayer(INFOTEXT_LAYER_ZINDEX))
      {
        TextSceneLayer& layer =
          dynamic_cast<TextSceneLayer&>(scene_.GetLayer(INFOTEXT_LAYER_ZINDEX));
        layer.SetText(buf);
        layer.SetPosition(p.GetX(), p.GetY());
      }
      else
      {
        std::auto_ptr<TextSceneLayer> layer(new TextSceneLayer);
        layer->SetColor(0, 255, 0);
        layer->SetText(buf);
        layer->SetBorder(20);
        layer->SetAnchor(BitmapAnchor_BottomCenter);
        layer->SetPosition(p.GetX(), p.GetY());
        scene_.SetLayer(INFOTEXT_LAYER_ZINDEX, layer.release());
      }
    }
    else
    {
      scene_.DeleteLayer(INFOTEXT_LAYER_ZINDEX);
    }
  }
  else if (event.type == SDL_MOUSEBUTTONDOWN)
  {
    PointerEvent e;
    e.AddPosition(compositor.GetPixelCenterCoordinates(event.button.x, event.button.y));

    activeTracker.reset(CreateSuitableTracker(event, e, compositor));
  }
  else if (event.type == SDL_KEYDOWN &&
    event.key.repeat == 0 /* Ignore key bounce */)
  {
    switch (event.key.keysym.sym)
    {
    case SDLK_s:
      scene_.FitContent(compositor.GetCanvasWidth(),
        compositor.GetCanvasHeight());
      break;

    case SDLK_c:
      TakeScreenshot(
        "screenshot.png",
        compositor.GetCanvasWidth(),
        compositor.GetCanvasHeight());
      break;

    default:
      break;
    }
  }
}


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);
  }
}

bool g_stopApplication = false;

void TrackerSampleApp::Run()
{
  SdlOpenGLWindow window("Hello", 1024, 768);

  scene_.FitContent(window.GetCanvasWidth(), window.GetCanvasHeight());

  glEnable(GL_DEBUG_OUTPUT);
  glDebugMessageCallback(OpenGLMessageCallback, 0);

  OpenGLCompositor compositor(window, scene_);
  compositor.SetFont(0, Orthanc::EmbeddedResources::UBUNTU_FONT,
    FONT_SIZE, Orthanc::Encoding_Latin1);

  // this will either be empty or contain the current tracker, if any
  std::auto_ptr<IPointerTracker>  tracker;

 
  while (!g_stopApplication)
  {
    compositor.Refresh();

    SDL_Event event;
    while (!g_stopApplication && SDL_PollEvent(&event))
    {
      if (event.type == SDL_QUIT)
      {
        g_stopApplication = true;
        break;
      }
      else if (event.type == SDL_MOUSEMOTION)
      {
        if (tracker.get() != NULL)
        {
          PointerEvent e;
          e.AddPosition(compositor.GetPixelCenterCoordinates(event.button.x, event.button.y));
          LOG(TRACE) << "event.button.x = " << event.button.x << "     " <<
            "event.button.y = " << event.button.y;
          tracker->Update(e);
        }
      }
      else if (event.type == SDL_MOUSEBUTTONUP)
      {
        if (tracker.get() != NULL)
        {
          tracker->Release();
          tracker.reset(NULL);
        }
      }
      else if (event.type == SDL_WINDOWEVENT &&
        event.window.event == SDL_WINDOWEVENT_SIZE_CHANGED)
      {
        tracker.reset(NULL);
        compositor.UpdateSize();
      }
      else if (event.type == SDL_KEYDOWN &&
        event.key.repeat == 0 /* Ignore key bounce */)
      {
        switch (event.key.keysym.sym)
        {
        case SDLK_f:
          window.GetWindow().ToggleMaximize();
          break;

        case SDLK_q:
          g_stopApplication = true;
          break;

        case SDLK_t:
          SelectNextTool();
          break;

        default:
          break;
        }
      }
      HandleApplicationEvent(compositor, event, tracker);
    }
    SDL_Delay(1);
  }
}


/**
 * 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[])
{
  StoneInitialize();
  Orthanc::Logging::EnableInfoLevel(true);

  try
  {
    TrackerSampleApp app;
    app.PrepareScene();
    app.Run();
  }
  catch (Orthanc::OrthancException& e)
  {
    LOG(ERROR) << "EXCEPTION: " << e.What();
  }

  StoneFinalize();

  return 0;
}