view Applications/Samples/Common/SimpleViewerCore.cpp @ 2390:ba477e5a4981 refactoring tip

class PeriodicRunnableThread moved to orthanc
author Sebastien Jodogne <s.jodogne@gmail.com>
date Wed, 21 Oct 2026 14:53:38 +0200
parents 560be8bec5d7
children
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-2023 Osimis S.A., Belgium
 * Copyright (C) 2021-2026 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 "SimpleViewerCore.h"

// TODO Refactoring - Remove
#define TEST_MEASURE_TOOLS   0


#include "../../../OrthancStone/Sources/Scene2DViewport/AngleMeasureTool.h"
#include "../../../OrthancStone/Sources/Scene2DViewport/LineMeasureTool.h"
#include "../../../OrthancStone/Sources/Scene2DViewport/UndoStack.h"
#include "../../../OrthancStone/Sources/Viewport/DefaultViewportInteractor.h"

#include "../../../OrthancStone/Sources/Scene2DViewport/MeasureCommands.h"  // TODO Refactoring - Remove


#include <boost/make_shared.hpp>


namespace OrthancStone
{
  void SimpleViewerCore::Handle(const New::IObservable& observable,
                                const FrameDecodedMessage& message)
  {
    LOG(INFO) << "Frame decoded! "
              << message.GetImage().GetWidth() << "x" << message.GetImage().GetHeight()
              << " " << Orthanc::EnumerationToString(message.GetImage().GetFormat());

    std::unique_ptr<TextureBaseSceneLayer> layer(
      message.GetInstanceParameters().CreateTexture(message.GetImage()));
    //layer->SetLinearInterpolation(true);
    layer->SetLinearInterpolation(false);

    controller_->GetScene().SetLayer(0, layer.release());
    controller_->FitContent();
  }


  void SimpleViewerCore::Handle(const New::IObservable& observable,
                                const DicomResourcesLoadedMessage& message)
  {
    if (message.GetResources()->GetSize() != 1)
    {
      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
    }

    DicomInstanceParameters parameters(message.GetResources()->GetResource(0));
    if (parameters.HasPixelSpacing())
    {
      /**
       * TODO - Ultrasound (US) images store an equivalent to
       * "PixelSpacing" in the "SequenceOfUltrasoundRegions"
       * (0018,6011) sequence, cf. tags "PhysicalDeltaX" (0018,602c)
       * and "PhysicalDeltaY" (0018,602e) => This would require
       * parsing "message.GetResources()->GetSourceJson(0)"
       * => cf. "DicomInstanceParameters::EnrichUsingDicomWeb()"
       **/

      // std::cout << message.GetResources()->GetSourceJson(0).toStyledString();

      LOG(INFO) << "Using millimeters units, as the DICOM instance contains the PixelSpacing tag";
      annotations_->SetUnits(Units_Millimeters);
    }
    else
    {
      LOG(INFO) << "Using pixels units, as the DICOM instance does *not* contain the PixelSpacing tag";
      annotations_->SetUnits(Units_Pixels);
    }

    //message.GetResources()->GetResource(0).Print(stdout);

    {
      std::unique_ptr<IEnvironment::ILock> environmentLock(context_->GetEnvironment().AcquireLock());  // TODO Refactoring - Should already be locked

      framesLoader_ = SeriesFramesLoader::Create(*context_, *message.GetResources());

      //Register<FrameDecodedMessage>(*framesLoader_, &SimpleViewerCore::Handle);

      //framesLoader_->TypedObservable<FrameDecodedMessage>::Register(shared_from_this());
      New::IObservable::Bind<FrameDecodedMessage>(framesLoader_, shared_from_this());

      assert(message.HasUserPayload());

      const Orthanc::SingleValueObject<unsigned int>& payload =
        dynamic_cast<const Orthanc::SingleValueObject<unsigned int>&>(
          message.GetUserPayload());

      LOG(INFO) << "Loading pixel data of frame: " << payload.GetValue();
      framesLoader_->ScheduleLoadFrame(
        0, message.GetDicomSource(), payload.GetValue(),
        message.GetDicomSource().GetQualityCount() - 1 /* download best quality available */,
        NULL);
    }
  }


  void SimpleViewerCore::RenderAnnotations()
  {
    annotations_->Render(controller_->GetScene());
    controller_->InvalidateViewport();
  }


  void SimpleViewerCore::CreateComponents(const boost::shared_ptr<StoneApplication::Context>& context,
                                          const boost::shared_ptr<IViewport>& viewport)
  {
    if (!context ||
        !viewport)
    {
      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
    }

    if (!viewport->GetController())
    {
      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
    }

    if (context_ ||
        controller_ ||
        annotations_)
    {
      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
    }

    context_ = context;
    controller_ = viewport->GetController();

    dicomLoader_ = DicomResourcesLoader::Create(*context);

    //Register<DicomResourcesLoadedMessage>(*dicomLoader_, &SimpleViewerCore::Handle);

    //dicomLoader_->TypedObservable<DicomResourcesLoadedMessage>::Register(shared_from_this());
    New::IObservable::Bind<DicomResourcesLoadedMessage>(dicomLoader_, shared_from_this());

    annotations_.reset(new AnnotationsSceneLayer(10));
    annotations_->SetUnits(Units_Millimeters);
    annotations_->SetActiveTool(AnnotationsSceneLayer::Tool_Edit);
    annotations_->SetProbedLayer(0);

    Register<AnnotationsSceneLayer::AnnotationRemovedMessage>(*annotations_, &SimpleViewerCore::Handle);

    controller_->SetUndoStack(boost::make_shared<UndoStack>());

#if TEST_MEASURE_TOOLS == 1
    {
      boost::shared_ptr<LineMeasureTool> tool = OrthancStone::LineMeasureTool::Create(controller_);
      tool->Enable();
      tool->Set(OrthancStone::ScenePoint2D(200, 100),
                OrthancStone::ScenePoint2D(100, 200));

      controller_->AddMeasureTool(boost::dynamic_pointer_cast<MeasureTool>(tool));
    }

    {
      boost::shared_ptr<AngleMeasureTool> tool = OrthancStone::AngleMeasureTool::Create(controller_);
      tool->Enable();
      tool->SetCenter(OrthancStone::ScenePoint2D(300, 300));
      tool->SetSide1End(OrthancStone::ScenePoint2D(200, 400));
      tool->SetSide2End(OrthancStone::ScenePoint2D(400, 400));

      controller_->AddMeasureTool(boost::dynamic_pointer_cast<MeasureTool>(tool));
    }
#endif
  }


  class SimpleViewerCore::Interactor : public IViewportInteractor
  {
  private:
    class Observer : public IFlexiblePointerTracker::IObserver
    {
    private:
      boost::shared_ptr<SimpleViewerCore>  core_;

    public:
      Observer(const boost::shared_ptr<SimpleViewerCore>& core) :
        core_(core)
      {
        assert(core_);
      }

      virtual void HandleGrayscaleWindowingChange(const OrthancStone::Windowing& windowing) ORTHANC_OVERRIDE
      {
      }

      virtual void HandleSceneTransformChange(const OrthancStone::Scene2D& scene) ORTHANC_OVERRIDE
      {
        core_->RenderAnnotations();
      }
    };

    class Factory : public DefaultViewportInteractor::IObserverFactory
    {
    private:
      boost::shared_ptr<SimpleViewerCore>  core_;

    public:
      Factory(const boost::shared_ptr<SimpleViewerCore>& core) :
        core_(core)
      {
        assert(core_);
      }

      virtual IFlexiblePointerTracker::IObserver* CreateObserver()
      {
        return new Observer(core_);
      }
    };

    DefaultViewportInteractor            default_;
    boost::shared_ptr<SimpleViewerCore>  core_;

  public:
    Interactor(const boost::shared_ptr<SimpleViewerCore>& core) :
      core_(core)
    {
      if (!core)
      {
        throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
      }

      default_.SetWindowingLayer(0);
      default_.SetObserverFactory(new Factory(core));
    }

    virtual IFlexiblePointerTracker* CreateTracker(Scene2D& scene,
                                                   const PointerEvent& event,
                                                   unsigned int viewportWidth,
                                                   unsigned int viewportHeight) ORTHANC_OVERRIDE
    {
#if TEST_MEASURE_TOOLS == 1
      if (event.GetMouseButton() == MouseButton_Middle ||
          event.GetMouseButton() == MouseButton_Right)
      {
        return default_.CreateTracker(scene, event, viewportWidth, viewportHeight);
      }
      else
      {
        const ScenePoint2D p = event.GetMainPosition().Apply(scene.GetCanvasToSceneTransform());

        std::vector< boost::shared_ptr<MeasureTool> > tools;
        tools = core_->controller_->HitTestMeasureTools(p);

        if (!tools.empty())
        {
          if (core_->annotations_->GetActiveTool() == AnnotationsSceneLayer::Tool_Remove)
          {
            boost::shared_ptr<MeasureCommand> command(new DeleteMeasureCommand(tools[0]));
            core_->controller_->PushCommand(command);
            return NULL;
          }
          else
          {
            return tools[0]->CreateEditionTracker(false, event);
          }
        }
        else
        {
          switch (core_->annotations_->GetActiveTool())
          {
            case AnnotationsSceneLayer::Tool_Remove:
              LOG(WARNING) << "Nothing to delete there";
              break;

            case AnnotationsSceneLayer::Tool_Length:
              return core_->controller_->CreateLineMeasure(event);

            case AnnotationsSceneLayer::Tool_Angle:
              return core_->controller_->CreateAngleMeasure(event);

            default:
              LOG(ERROR) << "This tool is not supported if not using the annotations layer";
          }

          return NULL;
        }
      }
#else
      core_->annotations_->ClearHover();

      if (event.GetMouseButton() == MouseButton_Left)
      {
        return core_->annotations_->CreateTracker(event.GetMainPosition(), scene);
      }
      else
      {
        return default_.CreateTracker(scene, event, viewportWidth, viewportHeight);
      }
#endif
    }

    virtual bool HasMouseHover() const ORTHANC_OVERRIDE
    {
      return true;
    }

    virtual void HandleMouseHover(Scene2D& scene,
                                  const PointerEvent& event) ORTHANC_OVERRIDE
    {
      core_->annotations_->SetMouseHover(event.GetMainPosition(), scene);
    }
  };


  IViewportInteractor* SimpleViewerCore::CreateMouseInteractor()
  {
    if (annotations_.get() == NULL)
    {
      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
    }
    else
    {
      return new Interactor(shared_from_this());
    }
  }


  void SimpleViewerCore::HandleKeyDown(const IEnvironment::ILock& environmentLock,
                                       char key)
  {
    switch (key)
    {
      case 's':
        controller_->FitContent();
        break;

      case 'u':
      {
        if (controller_->CanUndo())
        {
          controller_->Undo();
        }
        break;
      }

      case 'U':
      {
        if (controller_->CanRedo())
        {
          controller_->Redo();
        }
        break;
      }

      case 'c':
        annotations_->SetActiveTool(AnnotationsSceneLayer::Tool_Circle);
        break;

      case 'm':
        annotations_->SetActiveTool(AnnotationsSceneLayer::Tool_Edit);
        break;

      case 'd':
        annotations_->SetActiveTool(AnnotationsSceneLayer::Tool_Remove);
        break;

      case 'l':
        annotations_->SetActiveTool(AnnotationsSceneLayer::Tool_Length);
        break;

      case 'a':
        annotations_->SetActiveTool(AnnotationsSceneLayer::Tool_Angle);
        break;

      case 'p':
        annotations_->SetActiveTool(AnnotationsSceneLayer::Tool_PixelProbe);
        break;

      case 'e':
        annotations_->SetActiveTool(AnnotationsSceneLayer::Tool_EllipseProbe);
        break;

      case 'r':
        annotations_->SetActiveTool(AnnotationsSceneLayer::Tool_RectangleProbe);
        break;

      default:
        break;
    }
  }


  void SimpleViewerCore::LoadFrameFromOrthanc(const std::string& instanceId,
                                              unsigned int frameIndex)
  {
    std::unique_ptr<IEnvironment::ILock> environmentLock(context_->GetEnvironment().AcquireLock());

    DicomSource source;
    dicomLoader_->ScheduleLoadOrthancResource(boost::make_shared<LoadedDicomResources>(Orthanc::DICOM_TAG_SOP_INSTANCE_UID),
                                              0, source, Orthanc::ResourceType_Instance, instanceId,
                                              new Orthanc::SingleValueObject<unsigned int>(frameIndex));
  }


  void SimpleViewerCore::LoadFrameFromDicomWeb(const std::string& server,
                                               const std::string& studyInstanceUid,
                                               const std::string& seriesInstanceUid,
                                               const std::string& sopInstanceUid,
                                               unsigned int frameIndex)
  {
    std::unique_ptr<IEnvironment::ILock> environmentLock(context_->GetEnvironment().AcquireLock());

    OrthancStone::DicomSource source;
    source.SetDicomWebThroughOrthancSource(server);

    throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
  }
}