changeset 2161:e65fe2e50fde dicom-sr

integration mainline->dicom-sr
author Sebastien Jodogne <s.jodogne@gmail.com>
date Fri, 27 Sep 2024 22:34:17 +0200
parents f68f9a8d0d63 (current diff) 917e40af6b45 (diff)
children 4596ad1b2aa4
files
diffstat 16 files changed, 785 insertions(+), 303 deletions(-) [+]
line wrap: on
line diff
--- a/OrthancStone/Resources/CMake/OrthancStoneConfiguration.cmake	Sat Aug 31 08:40:01 2024 +0200
+++ b/OrthancStone/Resources/CMake/OrthancStoneConfiguration.cmake	Fri Sep 27 22:34:17 2024 +0200
@@ -404,6 +404,8 @@
   ${ORTHANC_STONE_ROOT}/Toolbox/BucketAccumulator2D.h
   ${ORTHANC_STONE_ROOT}/Toolbox/CoordinateSystem3D.cpp
   ${ORTHANC_STONE_ROOT}/Toolbox/CoordinateSystem3D.h
+  ${ORTHANC_STONE_ROOT}/Toolbox/DebugDrawing2D.cpp
+  ${ORTHANC_STONE_ROOT}/Toolbox/DebugDrawing2D.h
   ${ORTHANC_STONE_ROOT}/Toolbox/DicomInstanceParameters.cpp
   ${ORTHANC_STONE_ROOT}/Toolbox/DicomInstanceParameters.h
   ${ORTHANC_STONE_ROOT}/Toolbox/DicomStructureSet.cpp
@@ -439,6 +441,8 @@
   ${ORTHANC_STONE_ROOT}/Toolbox/SlicesSorter.h
   ${ORTHANC_STONE_ROOT}/Toolbox/SortedFrames.cpp
   ${ORTHANC_STONE_ROOT}/Toolbox/SortedFrames.h
+  ${ORTHANC_STONE_ROOT}/Toolbox/StoneToolbox.cpp
+  ${ORTHANC_STONE_ROOT}/Toolbox/StoneToolbox.h
   ${ORTHANC_STONE_ROOT}/Toolbox/SubpixelReader.h
   ${ORTHANC_STONE_ROOT}/Toolbox/SubvoxelReader.h
   ${ORTHANC_STONE_ROOT}/Toolbox/TextRenderer.cpp
--- a/OrthancStone/Sources/Loaders/DicomSource.cpp	Sat Aug 31 08:40:01 2024 +0200
+++ b/OrthancStone/Sources/Loaders/DicomSource.cpp	Fri Sep 27 22:34:17 2024 +0200
@@ -25,6 +25,7 @@
 
 #include "../Oracle/HttpCommand.h"
 #include "../Oracle/OrthancRestApiCommand.h"
+#include "../Toolbox/StoneToolbox.h"
 
 #include <OrthancException.h>
 #include <Toolbox.h>
@@ -60,39 +61,6 @@
   }
 
 
-  static std::string AddUriSuffix(const std::string& base,
-                                  const std::string& suffix)
-  {
-    if (base.empty())
-    {
-      return suffix;
-    }
-    else if (suffix.empty())
-    {
-      return base;
-    }
-    else
-    {
-      char lastBase = base[base.size() - 1];
-      
-      if (lastBase == '/' &&
-          suffix[0] == '/')
-      {
-        return base + suffix.substr(1);
-      }
-      else if (lastBase == '/' ||
-               suffix[0] == '/')
-      {
-        return base + suffix;
-      }
-      else
-      {
-        return base + "/" + suffix;
-      }
-    }
-  }
-
-
   void DicomSource::SetOrthancSource(const Orthanc::WebServiceParameters& parameters)
   {
     type_ = DicomSourceType_Orthanc;
@@ -229,7 +197,7 @@
         std::unique_ptr<HttpCommand> command(new HttpCommand);
         
         command->SetMethod(Orthanc::HttpMethod_Get);
-        command->SetUrl(AddUriSuffix(webService_.GetUrl(), uri + EncodeGetArguments(arguments)));
+        command->SetUrl(StoneToolbox::JoinUrl(webService_.GetUrl(), uri + EncodeGetArguments(arguments)));
         command->SetHttpHeaders(webService_.GetHttpHeaders());
 
         for (std::map<std::string, std::string>::const_iterator
@@ -274,7 +242,7 @@
 
         std::unique_ptr<OrthancRestApiCommand> command(new OrthancRestApiCommand);
         command->SetMethod(Orthanc::HttpMethod_Post);
-        command->SetUri(AddUriSuffix(orthancDicomWebRoot_, "/servers/" + serverName_ + "/get"));
+        command->SetUri(StoneToolbox::JoinUrl(orthancDicomWebRoot_, "/servers/" + serverName_ + "/get"));
         command->SetBody(body);
 
         if (protection.get())
--- a/OrthancStone/Sources/Platforms/WebAssembly/WebAssemblyOracle.cpp	Sat Aug 31 08:40:01 2024 +0200
+++ b/OrthancStone/Sources/Platforms/WebAssembly/WebAssemblyOracle.cpp	Fri Sep 27 22:34:17 2024 +0200
@@ -29,6 +29,8 @@
 #  include <Oracle/WebAssemblyOracle_Includes.h>
 #endif
 
+#include "../../Toolbox/StoneToolbox.h"
+
 #include <OrthancException.h>
 #include <Toolbox.h>
 
@@ -542,11 +544,11 @@
   {
     if (isLocalOrthanc_)
     {
-      command.SetUrl(localOrthancRoot_ + uri);
+      command.SetUrl(StoneToolbox::JoinUrl(localOrthancRoot_, uri));
     }
     else
     {
-      command.SetUrl(remoteOrthanc_.GetUrl() + uri);
+      command.SetUrl(StoneToolbox::JoinUrl(remoteOrthanc_.GetUrl(), uri));
       command.AddHttpHeaders(remoteOrthanc_.GetHttpHeaders());
       
       if (!remoteOrthanc_.GetUsername().empty())
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancStone/Sources/Toolbox/DebugDrawing2D.cpp	Fri Sep 27 22:34:17 2024 +0200
@@ -0,0 +1,160 @@
+/**
+ * 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-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "DebugDrawing2D.h"
+
+
+namespace OrthancStone
+{
+  class DebugDrawing2D::Segment
+  {
+  private:
+    double       x1_;
+    double       y1_;
+    double       x2_;
+    double       y2_;
+    std::string  color_;
+    bool         arrow_;
+
+  public:
+    Segment(double x1,
+            double y1,
+            double x2,
+            double y2,
+            const std::string& color,
+            bool arrow) :
+      x1_(x1),
+      y1_(y1),
+      x2_(x2),
+      y2_(y2),
+      color_(color),
+      arrow_(arrow)
+    {
+    }
+
+    double GetX1() const
+    {
+      return x1_;
+    }
+
+    double GetY1() const
+    {
+      return y1_;
+    }
+
+    double GetX2() const
+    {
+      return x2_;
+    }
+
+    double GetY2() const
+    {
+      return y2_;
+    }
+
+    const std::string& GetColor() const
+    {
+      return color_;
+    }
+
+    bool IsArrow() const
+    {
+      return arrow_;
+    }
+  };
+
+
+  void DebugDrawing2D::AddSegment(double x1,
+                                  double y1,
+                                  double x2,
+                                  double y2,
+                                  const std::string& color,
+                                  bool arrow,
+                                  bool addToExtent)
+  {
+    if (addToExtent)
+    {
+      extent_.AddPoint(x1, y1);
+      extent_.AddPoint(x2, y2);
+    }
+
+    segments_.push_back(Segment(x1, y1, x2, y2, color, arrow));
+  }
+
+
+  void DebugDrawing2D::SaveSvg(const std::string& path)
+  {
+    // Size in pixels
+    float ww, hh;
+    if (extent_.IsEmpty())
+    {
+      ww = 2048.0f;
+      hh = 2048.0f;
+    }
+    else if (extent_.GetWidth() > extent_.GetHeight())
+    {
+      ww = 2048.0f;
+      hh = ww * extent_.GetHeight() / extent_.GetWidth();
+    }
+    else
+    {
+      hh = 2048.0f;
+      ww = hh * extent_.GetWidth() / extent_.GetHeight();
+    }
+
+    FILE* fp = fopen(path.c_str(), "w");
+    fprintf(fp, "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n");
+    fprintf(fp, "<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n");
+    fprintf(fp, "<svg width=\"%f\" height=\"%f\" viewBox=\"0 0 %f %f\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\">\n", ww, hh, extent_.GetWidth(), extent_.GetHeight());
+
+    // http://thenewcode.com/1068/Making-Arrows-in-SVG
+    fprintf(fp, "<defs>\n");
+    fprintf(fp, "<marker id=\"arrowhead\" markerWidth=\"2\" markerHeight=\"3\" \n");
+    fprintf(fp, "refX=\"2\" refY=\"1.5\" orient=\"auto\">\n");
+    fprintf(fp, "<polygon points=\"0 0, 2 1.5, 0 3\" />\n");
+    fprintf(fp, "</marker>\n");
+    fprintf(fp, "</defs>\n");
+
+    fprintf(fp, "<rect fill=\"#fff\" stroke=\"#000\" x=\"0\" y=\"0\" width=\"%f\" height=\"%f\"/>\n", extent_.GetWidth(), extent_.GetHeight());
+
+    for (std::list<Segment>::const_iterator it = segments_.begin(); it != segments_.end(); ++it)
+    {
+      float strokeWidth = 0.1;
+
+      std::string s;
+      if (it->IsArrow())
+      {
+        s = "marker-end=\"url(#arrowhead)\"";
+      }
+
+      fprintf(fp, "<line x1=\"%f\" y1=\"%f\" x2=\"%f\" y2=\"%f\" stroke=\"%s\" stroke-width=\"%f\" %s/>\n",
+              it->GetX1() - extent_.GetX1(), it->GetY1() - extent_.GetY1(),
+              it->GetX2() - extent_.GetX1(), it->GetY2() - extent_.GetY1(),
+              it->GetColor().c_str(), strokeWidth, s.c_str());
+    }
+
+    fprintf(fp, "</svg>\n");
+
+    fclose(fp);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancStone/Sources/Toolbox/DebugDrawing2D.h	Fri Sep 27 22:34:17 2024 +0200
@@ -0,0 +1,54 @@
+/**
+ * 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-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "Extent2D.h"
+
+#include <boost/noncopyable.hpp>
+#include <list>
+#include <string>
+
+
+namespace OrthancStone
+{
+  class DebugDrawing2D : public boost::noncopyable
+  {
+  private:
+    class Segment;
+
+    Extent2D            extent_;
+    std::list<Segment>  segments_;
+
+  public:
+    void AddSegment(double x1,
+                    double y1,
+                    double x2,
+                    double y2,
+                    const std::string& color,
+                    bool arrow,
+                    bool addToExtent);
+
+    void SaveSvg(const std::string& path);
+  };
+}
--- a/OrthancStone/Sources/Toolbox/DicomStructureSet.cpp	Sat Aug 31 08:40:01 2024 +0200
+++ b/OrthancStone/Sources/Toolbox/DicomStructureSet.cpp	Fri Sep 27 22:34:17 2024 +0200
@@ -121,15 +121,17 @@
 
 namespace OrthancStone
 {
+  static const Orthanc::DicomTag DICOM_TAG_CONTOUR_DATA(0x3006, 0x0050);
   static const Orthanc::DicomTag DICOM_TAG_CONTOUR_GEOMETRIC_TYPE(0x3006, 0x0042);
   static const Orthanc::DicomTag DICOM_TAG_CONTOUR_IMAGE_SEQUENCE(0x3006, 0x0016);
   static const Orthanc::DicomTag DICOM_TAG_CONTOUR_SEQUENCE(0x3006, 0x0040);
-  static const Orthanc::DicomTag DICOM_TAG_CONTOUR_DATA(0x3006, 0x0050);
   static const Orthanc::DicomTag DICOM_TAG_NUMBER_OF_CONTOUR_POINTS(0x3006, 0x0046);
+  static const Orthanc::DicomTag DICOM_TAG_REFERENCED_ROI_NUMBER(0x3006, 0x0084);
   static const Orthanc::DicomTag DICOM_TAG_REFERENCED_SOP_INSTANCE_UID(0x0008, 0x1155);
   static const Orthanc::DicomTag DICOM_TAG_ROI_CONTOUR_SEQUENCE(0x3006, 0x0039);
   static const Orthanc::DicomTag DICOM_TAG_ROI_DISPLAY_COLOR(0x3006, 0x002a);
   static const Orthanc::DicomTag DICOM_TAG_ROI_NAME(0x3006, 0x0026);
+  static const Orthanc::DicomTag DICOM_TAG_ROI_NUMBER(0x3006, 0x0022);
   static const Orthanc::DicomTag DICOM_TAG_RT_ROI_INTERPRETED_TYPE(0x3006, 0x00a4);
   static const Orthanc::DicomTag DICOM_TAG_RT_ROI_OBSERVATIONS_SEQUENCE(0x3006, 0x0080);
   static const Orthanc::DicomTag DICOM_TAG_STRUCTURE_SET_ROI_SEQUENCE(0x3006, 0x0020);
@@ -281,82 +283,151 @@
   }
 
 
-  static void AddSegmentIfIntersection(Extent2D& extent,
-                                       const CoordinateSystem3D& cuttingPlane,
-                                       const Vector& p1,
-                                       const Vector& p2,
-                                       double originDistance)
-  {
-    // Does this segment intersects the cutting plane?
-    double d1 = cuttingPlane.ProjectAlongNormal(p1);
-    double d2 = cuttingPlane.ProjectAlongNormal(p2);
-      
-    if ((d1 < originDistance && d2 > originDistance) ||
-        (d1 > originDistance && d2 < originDistance))
-    {
-      // This is an intersection: Add the segment
-      double x, y;
-      cuttingPlane.ProjectPoint2(x, y, p1);
-      extent.AddPoint(x, y);
-      cuttingPlane.ProjectPoint2(x, y, p2);
-      extent.AddPoint(x, y);
-    }
-  }
-  
-  bool DicomStructureSet::Polygon::Project(double& x1,
-                                           double& y1,
-                                           double& x2,
-                                           double& y2,
+  void DicomStructureSet::Polygon::Project(std::list<Extent2D>& target,
                                            const CoordinateSystem3D& cuttingPlane,
                                            const Vector& estimatedNormal,
                                            double estimatedSliceThickness) const
   {
-    if (points_.size() <= 1)
-    {
-      return false;
-    }
+    CoordinateSystem3D geometry;
+    double thickness = estimatedSliceThickness;
 
-    Vector normal = estimatedNormal;
-    double thickness = estimatedSliceThickness;
+    /**
+     * 1. Estimate the 3D plane associated with this polygon.
+     **/
+
     if (hasSlice_)
     {
-      normal = geometry_.GetNormal();
+      // The exact geometry is known for this slice
+      geometry = geometry_;
       thickness = sliceThickness_;
     }
+    else if (points_.size() < 2)
+    {
+      return;
+    }
+    else
+    {
+      // Estimate the geometry
+      Vector origin = points_[0];
 
-    bool isOpposite;
-    if (!GeometryToolbox::IsParallelOrOpposite(isOpposite, normal, cuttingPlane.GetAxisX()) &&
-        !GeometryToolbox::IsParallelOrOpposite(isOpposite, normal, cuttingPlane.GetAxisY()))
-    {
-      return false;
+      Vector axisX;
+      bool found = false;
+      for (size_t i = 1; i < points_.size(); i++)
+      {
+        axisX = points_[1] - origin;
+        if (boost::numeric::ublas::norm_2(axisX) > 10.0 * std::numeric_limits<double>::epsilon())
+        {
+          found = true;
+          break;
+        }
+      }
+
+      if (!found)
+      {
+        return;  // The polygon is too small to extract a reliable geometry out of it
+      }
+
+      LinearAlgebra::NormalizeVector(axisX);
+
+      Vector axisY;
+      LinearAlgebra::CrossProduct(axisY, axisX, estimatedNormal);
+      geometry = CoordinateSystem3D(origin, axisX, axisY);
     }
 
-    const double d = cuttingPlane.ProjectAlongNormal(cuttingPlane.GetOrigin());
     
-    Extent2D extent;
-    AddSegmentIfIntersection(extent, cuttingPlane, points_[points_.size() - 1], points_[0], d);
-    for (size_t i = 1; i < points_.size(); i++)
-    {
-      AddSegmentIfIntersection(extent, cuttingPlane, points_[i - 1], points_[i], d);
-    }
+    /**
+     * 2. Project the 3D cutting plane as a 2D line onto the polygon plane.
+     **/
+
+    double cuttingX1, cuttingY1, cuttingX2, cuttingY2;
+    geometry.ProjectPoint(cuttingX1, cuttingY1, cuttingPlane.GetOrigin());
 
-    if (extent.GetWidth() > 0 ||
-        extent.GetHeight() > 0)
+    bool isOpposite;
+    if (GeometryToolbox::IsParallelOrOpposite(isOpposite, geometry.GetNormal(), cuttingPlane.GetAxisX()))
     {
-      // Let's convert them to 3D world geometry to add the slice thickness
-      Vector p1 = (cuttingPlane.MapSliceToWorldCoordinates(extent.GetX1(), extent.GetY1()) +
-                   thickness / 2.0 * normal);
-      Vector p2 = (cuttingPlane.MapSliceToWorldCoordinates(extent.GetX2(), extent.GetY2()) -
-                   thickness / 2.0 * normal);
-
-      // Then back to the cutting plane geometry
-      cuttingPlane.ProjectPoint2(x1, y1, p1);
-      cuttingPlane.ProjectPoint2(x2, y2, p2);
-      return true;
+      geometry.ProjectPoint(cuttingX2, cuttingY2, cuttingPlane.GetOrigin() + cuttingPlane.GetAxisY());
+    }
+    else if (GeometryToolbox::IsParallelOrOpposite(isOpposite, geometry.GetNormal(), cuttingPlane.GetAxisY()))
+    {
+      geometry.ProjectPoint(cuttingX2, cuttingY2, cuttingPlane.GetOrigin() + cuttingPlane.GetAxisX());
     }
     else
     {
-      return false;
+      return;
+    }
+
+
+    /**
+     * 3. Compute the intersection of the 2D cutting line with the polygon.
+     **/
+
+    // Initialize the projection of a point onto a line:
+    // https://stackoverflow.com/a/64330724
+    const double abx = cuttingX2 - cuttingX1;
+    const double aby = cuttingY2 - cuttingY1;
+    const double denominator = abx * abx + aby * aby;
+    if (LinearAlgebra::IsCloseToZero(denominator))
+    {
+      return;  // Should never happen
+    }
+
+    std::vector<double> intersections;
+    intersections.reserve(points_.size());
+
+    for (size_t i = 0; i < points_.size(); i++)
+    {
+      double segmentX1, segmentY1, segmentX2, segmentY2;
+      geometry.ProjectPoint(segmentX1, segmentY1, points_[i]);
+      geometry.ProjectPoint(segmentX2, segmentY2, points_[(i + 1) % points_.size()]);
+
+      double x, y;
+      if (GeometryToolbox::IntersectLineAndSegment(x, y, cuttingX1, cuttingY1, cuttingX2, cuttingY2,
+                                                   segmentX1, segmentY1, segmentX2, segmentY2))
+      {
+        // For each polygon segment that intersect the cutting line,
+        // register its offset over the cutting line
+        const double acx = x - cuttingX1;
+        const double acy = y - cuttingY1;
+        intersections.push_back((abx * acx + aby * acy) / denominator);
+      }
+    }
+
+
+    /**
+     * 4. Sort the intersection offsets, then generates one 2D rectangle on the
+     * cutting plane from each pair of successive intersections.
+     **/
+
+    std::sort(intersections.begin(), intersections.end());
+
+    if (intersections.size() % 2 == 1)
+    {
+      return;  // Should never happen
+    }
+
+    for (size_t i = 0; i < intersections.size(); i += 2)
+    {
+      Vector p1, p2;
+
+      {
+        // Let's convert them to 3D world geometry to add the slice thickness
+        const double x1 = cuttingX1 + intersections[i] * abx;
+        const double y1 = cuttingY1 + intersections[i] * aby;
+        const double x2 = cuttingX1 + intersections[i + 1] * abx;
+        const double y2 = cuttingY1 + intersections[i + 1] * aby;
+
+        p1 = (geometry.MapSliceToWorldCoordinates(x1, y1) + thickness / 2.0 * geometry.GetNormal());
+        p2 = (geometry.MapSliceToWorldCoordinates(x2, y2) - thickness / 2.0 * geometry.GetNormal());
+      }
+
+      {
+        // Then back to the cutting plane geometry
+        double x1, y1, x2, y2;
+        cuttingPlane.ProjectPoint2(x1, y1, p1);
+        cuttingPlane.ProjectPoint2(x2, y2, p2);
+
+        target.push_back(Extent2D(x1, y1, x2, y2));
+      }
     }
   }
 
@@ -388,162 +459,246 @@
     boost::posix_time::ptime timerStart = boost::posix_time::microsec_clock::universal_time();
 #endif
 
+    std::map<int, size_t> roiNumbersIndex;
+
     DicomDatasetReader reader(tags);
-    
-    size_t count, tmp;
-    if (!tags.GetSequenceSize(count, Orthanc::DicomPath(DICOM_TAG_RT_ROI_OBSERVATIONS_SEQUENCE)) ||
-        !tags.GetSequenceSize(tmp, Orthanc::DicomPath(DICOM_TAG_ROI_CONTOUR_SEQUENCE)) ||
-        tmp != count ||
-        !tags.GetSequenceSize(tmp, Orthanc::DicomPath(DICOM_TAG_STRUCTURE_SET_ROI_SEQUENCE)) ||
-        tmp != count)
-    {
-      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
-    }
+
 
-    structures_.resize(count);
-    structureNamesIndex_.clear();
-    
-    for (size_t i = 0; i < count; i++)
+    /**
+     * 1. Read all the available ROIs.
+     **/
+
     {
-      structures_[i].interpretation_ = reader.GetStringValue
-        (Orthanc::DicomPath(DICOM_TAG_RT_ROI_OBSERVATIONS_SEQUENCE, i,
-                            DICOM_TAG_RT_ROI_INTERPRETED_TYPE),
-         "No interpretation");
-
-      structures_[i].name_ = reader.GetStringValue
-        (Orthanc::DicomPath(DICOM_TAG_STRUCTURE_SET_ROI_SEQUENCE, i,
-                            DICOM_TAG_ROI_NAME),
-         "No name");
-
-      if (structureNamesIndex_.find(structures_[i].name_) == structureNamesIndex_.end())
+      size_t count;
+      if (!tags.GetSequenceSize(count, Orthanc::DicomPath(DICOM_TAG_STRUCTURE_SET_ROI_SEQUENCE)))
       {
-        structureNamesIndex_[structures_[i].name_] = i;
-      }
-      else
-      {
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat,
-                                        "RT-STRUCT with twice the same name for a structure: " + structures_[i].name_);
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
       }
 
-      Vector color;
-      if (FastParseVector(color, tags, Orthanc::DicomPath(DICOM_TAG_ROI_CONTOUR_SEQUENCE, i,
-                                                          DICOM_TAG_ROI_DISPLAY_COLOR)) &&
-          color.size() == 3)
-      {
-        structures_[i].red_ = ConvertColor(color[0]);
-        structures_[i].green_ = ConvertColor(color[1]);
-        structures_[i].blue_ = ConvertColor(color[2]);
-      }
-      else
+      structures_.resize(count);
+      structureNamesIndex_.clear();
+
+      for (size_t i = 0; i < count; i++)
       {
-        structures_[i].red_ = 255;
-        structures_[i].green_ = 0;
-        structures_[i].blue_ = 0;
-      }
+        int roiNumber;
+        if (!reader.GetIntegerValue
+            (roiNumber, Orthanc::DicomPath(DICOM_TAG_STRUCTURE_SET_ROI_SEQUENCE, i, DICOM_TAG_ROI_NUMBER)))
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+        }
+
+        if (roiNumbersIndex.find(roiNumber) != roiNumbersIndex.end())
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat,
+                                          "Twice the same ROI number: " + boost::lexical_cast<std::string>(roiNumber));
+        }
+
+        roiNumbersIndex[roiNumber] = i;
+
+        structures_[i].name_ = reader.GetStringValue
+          (Orthanc::DicomPath(DICOM_TAG_STRUCTURE_SET_ROI_SEQUENCE, i, DICOM_TAG_ROI_NAME), "No name");
+        structures_[i].interpretation_ = "No interpretation";
 
-      size_t countSlices;
-      if (!tags.GetSequenceSize(countSlices, Orthanc::DicomPath(DICOM_TAG_ROI_CONTOUR_SEQUENCE, i,
-                                                                DICOM_TAG_CONTOUR_SEQUENCE)))
+        if (structureNamesIndex_.find(structures_[i].name_) == structureNamesIndex_.end())
+        {
+          structureNamesIndex_[structures_[i].name_] = i;
+        }
+        else
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat,
+                                          "RT-STRUCT with twice the same name for a structure: " + structures_[i].name_);
+        }
+      }
+    }
+
+
+    /**
+     * 2. Read the interpretation of the ROIs (if available).
+     **/
+
+    {
+      size_t count;
+      if (!tags.GetSequenceSize(count, Orthanc::DicomPath(DICOM_TAG_RT_ROI_OBSERVATIONS_SEQUENCE)))
       {
-        countSlices = 0;
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
       }
 
-      LOG(INFO) << "New RT structure: \"" << structures_[i].name_ 
-                << "\" with interpretation \"" << structures_[i].interpretation_
-                << "\" containing " << countSlices << " slices (color: " 
-                << static_cast<int>(structures_[i].red_) << "," 
-                << static_cast<int>(structures_[i].green_) << ","
-                << static_cast<int>(structures_[i].blue_) << ")";
+      for (size_t i = 0; i < count; i++)
+      {
+        std::string interpretation;
+        if (reader.GetDataset().GetStringValue(interpretation,
+                                               Orthanc::DicomPath(DICOM_TAG_RT_ROI_OBSERVATIONS_SEQUENCE, i,
+                                                                  DICOM_TAG_RT_ROI_INTERPRETED_TYPE)))
+        {
+          int roiNumber;
+          if (!reader.GetIntegerValue(roiNumber,
+                                      Orthanc::DicomPath(DICOM_TAG_RT_ROI_OBSERVATIONS_SEQUENCE, i,
+                                                         DICOM_TAG_REFERENCED_ROI_NUMBER)))
+          {
+            throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+          }
 
-      /**
-       * These temporary variables avoid allocating many vectors in
-       * the loop below (indeed, "Orthanc::DicomPath" handles a
-       * "std::vector<PrefixItem>")
-       **/
-      Orthanc::DicomPath countPointsPath(DICOM_TAG_ROI_CONTOUR_SEQUENCE, i,
-                                         DICOM_TAG_CONTOUR_SEQUENCE, 0,
-                                         DICOM_TAG_NUMBER_OF_CONTOUR_POINTS);
+          std::map<int, size_t>::const_iterator found = roiNumbersIndex.find(roiNumber);
+          if (found == roiNumbersIndex.end())
+          {
+            throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+          }
 
-      Orthanc::DicomPath geometricTypePath(DICOM_TAG_ROI_CONTOUR_SEQUENCE, i,
-                                           DICOM_TAG_CONTOUR_SEQUENCE, 0,
-                                           DICOM_TAG_CONTOUR_GEOMETRIC_TYPE);
-      
-      Orthanc::DicomPath imageSequencePath(DICOM_TAG_ROI_CONTOUR_SEQUENCE, i,
-                                           DICOM_TAG_CONTOUR_SEQUENCE, 0,
-                                           DICOM_TAG_CONTOUR_IMAGE_SEQUENCE);
+          structures_[found->second].interpretation_ = interpretation;
+        }
+      }
+    }
+
+
+    /**
+     * 3. Read the contours.
+     **/
 
-      // (3006,0039)[i] / (0x3006, 0x0040)[0] / (0x3006, 0x0016)[0] / (0x0008, 0x1155)
-      Orthanc::DicomPath referencedInstancePath(DICOM_TAG_ROI_CONTOUR_SEQUENCE, i,
-                                                DICOM_TAG_CONTOUR_SEQUENCE, 0,
-                                                DICOM_TAG_CONTOUR_IMAGE_SEQUENCE, 0,
-                                                DICOM_TAG_REFERENCED_SOP_INSTANCE_UID);
+    {
+      size_t count;
+      if (!tags.GetSequenceSize(count, Orthanc::DicomPath(DICOM_TAG_ROI_CONTOUR_SEQUENCE)))
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+      }
 
-      Orthanc::DicomPath contourDataPath(DICOM_TAG_ROI_CONTOUR_SEQUENCE, i,
-                                         DICOM_TAG_CONTOUR_SEQUENCE, 0,
-                                         DICOM_TAG_CONTOUR_DATA);
+      for (size_t i = 0; i < count; i++)
+      {
+        int roiNumber;
+        if (!reader.GetIntegerValue(roiNumber,
+                                    Orthanc::DicomPath(DICOM_TAG_ROI_CONTOUR_SEQUENCE, i,
+                                                       DICOM_TAG_REFERENCED_ROI_NUMBER)))
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+        }
 
-      for (size_t j = 0; j < countSlices; j++)
-      {
-        unsigned int countPoints;
-
-        countPointsPath.SetPrefixIndex(1, j);
-        if (!reader.GetUnsignedIntegerValue(countPoints, countPointsPath))
+        std::map<int, size_t>::const_iterator found = roiNumbersIndex.find(roiNumber);
+        if (found == roiNumbersIndex.end())
         {
           throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
         }
-            
-        //LOG(INFO) << "Parsing slice containing " << countPoints << " vertices";
+
+        Structure& target = structures_[found->second];
 
-        geometricTypePath.SetPrefixIndex(1, j);
-        std::string type = reader.GetMandatoryStringValue(geometricTypePath);
-        if (type != "CLOSED_PLANAR")
+        Vector color;
+        if (FastParseVector(color, tags, Orthanc::DicomPath(DICOM_TAG_ROI_CONTOUR_SEQUENCE, i,
+                                                            DICOM_TAG_ROI_DISPLAY_COLOR)) &&
+            color.size() == 3)
         {
-          LOG(WARNING) << "Ignoring contour with geometry type: " << type;
-          continue;
+          target.red_ = ConvertColor(color[0]);
+          target.green_ = ConvertColor(color[1]);
+          target.blue_ = ConvertColor(color[2]);
+        }
+        else
+        {
+          target.red_ = 255;
+          target.green_ = 0;
+          target.blue_ = 0;
         }
 
-        size_t size;
-
-        imageSequencePath.SetPrefixIndex(1, j);
-        if (!tags.GetSequenceSize(size, imageSequencePath) || size != 1)
+        size_t countSlices;
+        if (!tags.GetSequenceSize(countSlices, Orthanc::DicomPath(DICOM_TAG_ROI_CONTOUR_SEQUENCE, i,
+                                                                  DICOM_TAG_CONTOUR_SEQUENCE)))
         {
-          LOG(ERROR) << "The ContourImageSequence sequence (tag 3006,0016) must be present and contain one entry.";
-          throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);          
+          countSlices = 0;
         }
 
-        referencedInstancePath.SetPrefixIndex(1, j);
-        std::string sopInstanceUid = reader.GetMandatoryStringValue(referencedInstancePath);
+        LOG(INFO) << "New RT structure: \"" << target.name_
+                  << "\" with interpretation \"" << target.interpretation_
+                  << "\" containing " << countSlices << " slices (color: " 
+                  << static_cast<int>(target.red_) << ","
+                  << static_cast<int>(target.green_) << ","
+                  << static_cast<int>(target.blue_) << ")";
 
-        contourDataPath.SetPrefixIndex(1, j);        
-        std::string slicesData = reader.GetMandatoryStringValue(contourDataPath);
-
-        Vector points;
+        /**
+         * These temporary variables avoid allocating many vectors in
+         * the loop below (indeed, "Orthanc::DicomPath" handles a
+         * "std::vector<PrefixItem>")
+         **/
+        Orthanc::DicomPath countPointsPath(DICOM_TAG_ROI_CONTOUR_SEQUENCE, i,
+                                           DICOM_TAG_CONTOUR_SEQUENCE, 0,
+                                           DICOM_TAG_NUMBER_OF_CONTOUR_POINTS);
 
-        if (!GenericToolbox::FastParseVector(points, slicesData) ||
-            points.size() != 3 * countPoints)
-        {
-          throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);          
-        }
+        Orthanc::DicomPath geometricTypePath(DICOM_TAG_ROI_CONTOUR_SEQUENCE, i,
+                                             DICOM_TAG_CONTOUR_SEQUENCE, 0,
+                                             DICOM_TAG_CONTOUR_GEOMETRIC_TYPE);
+
+        Orthanc::DicomPath imageSequencePath(DICOM_TAG_ROI_CONTOUR_SEQUENCE, i,
+                                             DICOM_TAG_CONTOUR_SEQUENCE, 0,
+                                             DICOM_TAG_CONTOUR_IMAGE_SEQUENCE);
 
-        // seen in real world
-        if(Orthanc::Toolbox::StripSpaces(sopInstanceUid) == "") 
+        // (3006,0039)[i] / (0x3006, 0x0040)[0] / (0x3006, 0x0016)[0] / (0x0008, 0x1155)
+        Orthanc::DicomPath referencedInstancePath(DICOM_TAG_ROI_CONTOUR_SEQUENCE, i,
+                                                  DICOM_TAG_CONTOUR_SEQUENCE, 0,
+                                                  DICOM_TAG_CONTOUR_IMAGE_SEQUENCE, 0,
+                                                  DICOM_TAG_REFERENCED_SOP_INSTANCE_UID);
+
+        Orthanc::DicomPath contourDataPath(DICOM_TAG_ROI_CONTOUR_SEQUENCE, i,
+                                           DICOM_TAG_CONTOUR_SEQUENCE, 0,
+                                           DICOM_TAG_CONTOUR_DATA);
+
+        for (size_t j = 0; j < countSlices; j++)
         {
-          LOG(ERROR) << "WARNING. The following Dicom tag (Referenced SOP Instance UID) contains an empty value : // (3006,0039)[" << i << "] / (0x3006, 0x0040)[0] / (0x3006, 0x0016)[0] / (0x0008, 0x1155)";
-        }
+          unsigned int countPoints;
+
+          countPointsPath.SetPrefixIndex(1, j);
+          if (!reader.GetUnsignedIntegerValue(countPoints, countPointsPath))
+          {
+            throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+          }
+
+          //LOG(INFO) << "Parsing slice containing " << countPoints << " vertices";
 
-        Polygon polygon(sopInstanceUid);
-        polygon.Reserve(countPoints);
+          geometricTypePath.SetPrefixIndex(1, j);
+          std::string type = reader.GetMandatoryStringValue(geometricTypePath);
+          if (type != "CLOSED_PLANAR")
+          {
+            LOG(WARNING) << "Ignoring contour with geometry type: " << type;
+            continue;
+          }
+
+          size_t size;
+
+          imageSequencePath.SetPrefixIndex(1, j);
+          if (!tags.GetSequenceSize(size, imageSequencePath) || size != 1)
+          {
+            LOG(ERROR) << "The ContourImageSequence sequence (tag 3006,0016) must be present and contain one entry.";
+            throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+          }
+
+          referencedInstancePath.SetPrefixIndex(1, j);
+          std::string sopInstanceUid = reader.GetMandatoryStringValue(referencedInstancePath);
 
-        for (size_t k = 0; k < countPoints; k++)
-        {
-          Vector v(3);
-          v[0] = points[3 * k];
-          v[1] = points[3 * k + 1];
-          v[2] = points[3 * k + 2];
-          polygon.AddPoint(v);
+          contourDataPath.SetPrefixIndex(1, j);
+          std::string slicesData = reader.GetMandatoryStringValue(contourDataPath);
+
+          Vector points;
+
+          if (!GenericToolbox::FastParseVector(points, slicesData) ||
+              points.size() != 3 * countPoints)
+          {
+            throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+          }
+
+          // seen in real world
+          if(Orthanc::Toolbox::StripSpaces(sopInstanceUid) == "")
+          {
+            LOG(ERROR) << "WARNING. The following Dicom tag (Referenced SOP Instance UID) contains an empty value : // (3006,0039)[" << i << "] / (0x3006, 0x0040)[0] / (0x3006, 0x0016)[0] / (0x0008, 0x1155)";
+          }
+
+          Polygon polygon(sopInstanceUid);
+          polygon.Reserve(countPoints);
+
+          for (size_t k = 0; k < countPoints; k++)
+          {
+            Vector v(3);
+            v[0] = points[3 * k];
+            v[1] = points[3 * k + 1];
+            v[2] = points[3 * k + 2];
+            polygon.AddPoint(v);
+          }
+
+          target.polygons_.push_back(polygon);
         }
-
-        structures_[i].polygons_.push_back(polygon);
       }
     }
 
@@ -804,11 +959,12 @@
       for (Polygons::const_iterator polygon = structure.polygons_.begin();
            polygon != structure.polygons_.end(); ++polygon)
       {
-        double x1, y1, x2, y2;
+        std::list<Extent2D> rectangles;
+        polygon->Project(rectangles, cutting, GetEstimatedNormal(), GetEstimatedSliceThickness());
 
-        if (polygon->Project(x1, y1, x2, y2, cutting, GetEstimatedNormal(), GetEstimatedSliceThickness()))
+        for (std::list<Extent2D>::const_iterator it = rectangles.begin(); it != rectangles.end(); ++it)
         {
-          projected.push_back(CreateRectangle(x1, y1, x2, y2));
+          projected.push_back(CreateRectangle(it->GetX1(), it->GetY1(), it->GetX2(), it->GetY2()));
         }
       }
 
@@ -834,12 +990,7 @@
       for (Polygons::const_iterator polygon = structure.polygons_.begin();
            polygon != structure.polygons_.end(); ++polygon)
       {
-        double x1, y1, x2, y2;
-
-        if (polygon->Project(x1, y1, x2, y2, cutting, GetEstimatedNormal(), GetEstimatedSliceThickness()))
-        {
-          rectangles.push_back(Extent2D(x1, y1, x2, y2));
-        }
+        polygon->Project(rectangles, cutting, GetEstimatedNormal(), GetEstimatedSliceThickness());
       }
 
       typedef std::list< std::vector<ScenePoint2D> >  Contours;
--- a/OrthancStone/Sources/Toolbox/DicomStructureSet.h	Sat Aug 31 08:40:01 2024 +0200
+++ b/OrthancStone/Sources/Toolbox/DicomStructureSet.h	Fri Sep 27 22:34:17 2024 +0200
@@ -127,10 +127,7 @@
         return sliceThickness_;
       }
 
-      bool Project(double& x1,
-                   double& y1,
-                   double& x2,
-                   double& y2,
+      void Project(std::list<Extent2D>& target,
                    const CoordinateSystem3D& cuttingPlane,
                    const Vector& estimatedNormal,
                    double estimatedSliceThickness) const;
--- a/OrthancStone/Sources/Toolbox/GeometryToolbox.cpp	Sat Aug 31 08:40:01 2024 +0200
+++ b/OrthancStone/Sources/Toolbox/GeometryToolbox.cpp	Fri Sep 27 22:34:17 2024 +0200
@@ -565,5 +565,78 @@
         return false;
       }
     }
+
+
+    static bool SolveLineIntersectionSystem(double& x,
+                                            double& y,
+                                            double& s,
+                                            double& t,
+                                            double x1, double y1,
+                                            double x2, double y2,
+                                            double x3, double y3,
+                                            double x4, double y4)
+    {
+      // https://en.wikipedia.org/wiki/Intersection_(geometry)#Two_line_segments
+      const double a1 = x2 - x1;
+      const double b1 = -x4 + x3;
+      const double c1 = x3 - x1;
+      const double a2 = y2 - y1;
+      const double b2 = -y4 + y3;
+      const double c2 = y3 - y1;
+
+      const double denominator = a1 * b2 - a2 * b1;
+      if (LinearAlgebra::IsCloseToZero(denominator))
+      {
+        return false;
+      }
+      else
+      {
+        // This is Cramer's rule
+        s = (c1 * b2 - c2 * b1) / denominator;
+        t = (a1 * c2 - a2 * c1) / denominator;
+        x = x1 + s * (x2 - x1);
+        y = y1 + s * (y2 - y1);
+        return true;
+      }
+    }
+
+
+    bool IntersectTwoLines(double& x,
+                           double& y,
+                           double ax1,
+                           double ay1,
+                           double ax2,
+                           double ay2,
+                           double bx1,
+                           double by1,
+                           double bx2,
+                           double by2)
+    {
+      double s, t;
+      return SolveLineIntersectionSystem(x, y, s, t, ax1, ay1, ax2, ay2, bx1, by1, bx2, by2);
+    }
+
+
+    bool IntersectLineAndSegment(double& x,
+                                 double& y,
+                                 double lineX1,
+                                 double lineY1,
+                                 double lineX2,
+                                 double lineY2,
+                                 double segmentX1,
+                                 double segmentY1,
+                                 double segmentX2,
+                                 double segmentY2)
+    {
+      double s, t;
+      if (SolveLineIntersectionSystem(x, y, s, t, lineX1, lineY1, lineX2, lineY2, segmentX1, segmentY1, segmentX2, segmentY2))
+      {
+        return (t >= 0 && t <= 1);
+      }
+      else
+      {
+        return false;
+      }
+    }
   }
 }
--- a/OrthancStone/Sources/Toolbox/GeometryToolbox.h	Sat Aug 31 08:40:01 2024 +0200
+++ b/OrthancStone/Sources/Toolbox/GeometryToolbox.h	Fri Sep 27 22:34:17 2024 +0200
@@ -117,6 +117,28 @@
     bool ComputeNormal(Vector& normal,
                        const Orthanc::DicomMap& dicom);
 
+    bool IntersectTwoLines(double& x,
+                           double& y,
+                           double ax1,
+                           double ay1,
+                           double ax2,
+                           double ay2,
+                           double bx1,
+                           double by1,
+                           double bx2,
+                           double by2);
+
+    bool IntersectLineAndSegment(double& x,
+                                 double& y,
+                                 double lineX1,
+                                 double lineY1,
+                                 double lineX2,
+                                 double lineY2,
+                                 double segmentX1,
+                                 double segmentY1,
+                                 double segmentX2,
+                                 double segmentY2);
+
     inline float ComputeBilinearInterpolationUnitSquare(float x,
                                                         float y,
                                                         float f00,    // source(0, 0)
--- a/OrthancStone/Sources/Toolbox/OrthancDatasets/OrthancHttpConnection.cpp	Sat Aug 31 08:40:01 2024 +0200
+++ b/OrthancStone/Sources/Toolbox/OrthancDatasets/OrthancHttpConnection.cpp	Fri Sep 27 22:34:17 2024 +0200
@@ -23,6 +23,8 @@
 
 #include "OrthancHttpConnection.h"
 
+#include "../StoneToolbox.h"
+
 namespace OrthancStone
 {
   void OrthancHttpConnection::Setup()
@@ -54,7 +56,7 @@
     boost::mutex::scoped_lock lock(mutex_);
 
     client_.SetMethod(Orthanc::HttpMethod_Get);
-    client_.SetUrl(url_ + uri);
+    client_.SetUrl(StoneToolbox::JoinUrl(url_, uri));
     client_.ApplyAndThrowException(result);
   }
 
@@ -66,7 +68,7 @@
     boost::mutex::scoped_lock lock(mutex_);
 
     client_.SetMethod(Orthanc::HttpMethod_Post);
-    client_.SetUrl(url_ + uri);
+    client_.SetUrl(StoneToolbox::JoinUrl(url_, uri));
 
 #if defined(ORTHANC_FRAMEWORK_VERSION_IS_ABOVE) && ORTHANC_FRAMEWORK_VERSION_IS_ABOVE(1, 9, 3)
     client_.SetExternalBody(body);
@@ -86,7 +88,7 @@
     boost::mutex::scoped_lock lock(mutex_);
 
     client_.SetMethod(Orthanc::HttpMethod_Put);
-    client_.SetUrl(url_ + uri);
+    client_.SetUrl(StoneToolbox::JoinUrl(url_, uri));
 
 #if defined(ORTHANC_FRAMEWORK_VERSION_IS_ABOVE) && ORTHANC_FRAMEWORK_VERSION_IS_ABOVE(1, 9, 3)
     client_.SetExternalBody(body);
@@ -106,7 +108,7 @@
     std::string result;
 
     client_.SetMethod(Orthanc::HttpMethod_Delete);
-    client_.SetUrl(url_ + uri);
+    client_.SetUrl(StoneToolbox::JoinUrl(url_, uri));
     client_.ApplyAndThrowException(result);
   }
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancStone/Sources/Toolbox/StoneToolbox.cpp	Fri Sep 27 22:34:17 2024 +0200
@@ -0,0 +1,50 @@
+/**
+ * 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-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "StoneToolbox.h"
+
+namespace OrthancStone
+{
+  namespace StoneToolbox
+  {
+    std::string JoinUrl(const std::string& base,
+                        const std::string& path)
+    {
+      size_t end = base.size();
+      while (end > 0 &&
+             base[end - 1] == '/')
+      {
+        end--;
+      }
+
+      size_t start = 0;
+      while (start < path.size() &&
+             path[start] == '/')
+      {
+        start++;
+      }
+
+      return base.substr(0, end) + "/" + path.substr(start);
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancStone/Sources/Toolbox/StoneToolbox.h	Fri Sep 27 22:34:17 2024 +0200
@@ -0,0 +1,35 @@
+/**
+ * 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-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include <string>
+
+namespace OrthancStone
+{
+  namespace StoneToolbox
+  {
+    std::string JoinUrl(const std::string& base,
+                        const std::string& path);
+  }
+}
--- a/OrthancStone/UnitTestsSources/ComputationalGeometryTests.cpp	Sat Aug 31 08:40:01 2024 +0200
+++ b/OrthancStone/UnitTestsSources/ComputationalGeometryTests.cpp	Fri Sep 27 22:34:17 2024 +0200
@@ -540,71 +540,6 @@
 }
 
 
-#if 0
-static void SaveSvg(const std::list< std::vector<OrthancStone::ScenePoint2D> >& contours)
-{
-  float ww = 15;
-  float hh = 13;
-
-  FILE* fp = fopen("test.svg", "w");
-  fprintf(fp, "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n");
-  fprintf(fp, "<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n");
-  fprintf(fp, "<svg width=\"%f\" height=\"%f\" viewBox=\"0 0 %f %f\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\">\n", 100.0f*ww, 100.0f*hh, ww, hh);
-
-  // http://thenewcode.com/1068/Making-Arrows-in-SVG
-  fprintf(fp, "<defs>\n");
-  fprintf(fp, "<marker id=\"arrowhead\" markerWidth=\"2\" markerHeight=\"3\" \n");
-  fprintf(fp, "refX=\"2\" refY=\"1.5\" orient=\"auto\">\n");
-  fprintf(fp, "<polygon points=\"0 0, 2 1.5, 0 3\" />\n");
-  fprintf(fp, "</marker>\n");
-  fprintf(fp, "</defs>\n");
-
-  fprintf(fp, "<rect fill=\"#fff\" stroke=\"#000\" x=\"0\" y=\"0\" width=\"%f\" height=\"%f\"/>\n", ww, hh);
-
-  unsigned int count = 0;
-  
-  for (std::list< std::vector<OrthancStone::ScenePoint2D> >::const_iterator
-         it = contours.begin(); it != contours.end(); ++it, count++)
-  {
-    std::string color;
-    if (count == 0)
-    {
-      color = "blue";
-    }
-    else if (count == 1)
-    {
-      color = "red";
-    }
-    else if (count == 2)
-    {
-      color = "green";
-    }
-    else if (count == 3)
-    {
-      color = "orange";
-    }
-    else
-    {
-      throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
-    }
-    
-    for (size_t i = 0; i + 1 < it->size(); i++)
-    {
-      float x1 = (*it)[i].GetX();
-      float x2 = (*it)[i + 1].GetX();
-      float y1 = (*it)[i].GetY();
-      float y2 = (*it)[i + 1].GetY();
-      
-      fprintf(fp, "<line x1=\"%f\" y1=\"%f\" x2=\"%f\" y2=\"%f\" stroke=\"%s\" stroke-width=\"0.05\" marker-end=\"url(#arrowhead)\"/>\n", x1, y1, x2, y2, color.c_str());
-    }
-  }
-  fprintf(fp, "</svg>\n");
-  
-  fclose(fp);
-}
-#endif
-
-
 TEST(UnionOfRectangles, EdgeCases)
 {
   {
--- a/OrthancStone/UnitTestsSources/GenericToolboxTests.cpp	Sat Aug 31 08:40:01 2024 +0200
+++ b/OrthancStone/UnitTestsSources/GenericToolboxTests.cpp	Fri Sep 27 22:34:17 2024 +0200
@@ -4342,7 +4342,7 @@
   const char* s = "  \t   0.0/.123/3  \t/12.5e-3//-43.1   \t     ";
 
   int32_t size;
-  double r;
+  double r = -1.0;
   const char* p = s;
 
   while (*p == ' ' || *p == '\t')
@@ -4390,7 +4390,7 @@
   const char* s = "  \t   0.0/.123/3/12.5e-3//-43.1e-2   \t     ";
 
   int32_t size;
-  double r;
+  double r = -1.0;
   const char* p = s;
 
   while (*p == ' ' || *p == '\t')
--- a/OrthancStone/UnitTestsSources/GeometryToolboxTests.cpp	Sat Aug 31 08:40:01 2024 +0200
+++ b/OrthancStone/UnitTestsSources/GeometryToolboxTests.cpp	Fri Sep 27 22:34:17 2024 +0200
@@ -1174,3 +1174,15 @@
     ASSERT_EQ("LHP", right);
   }
 }
+
+
+TEST(GeometryToolbox, IntersectTwoLines)
+{
+  double x, y;
+  ASSERT_TRUE(OrthancStone::GeometryToolbox::IntersectTwoLines(x, y, 1, 1, 3, 2, 1, 4, 2, -1));
+  ASSERT_DOUBLE_EQ(x, 17.0 / 11.0);
+  ASSERT_DOUBLE_EQ(y, 14.0 / 11.0);
+  ASSERT_TRUE(OrthancStone::GeometryToolbox::IntersectLineAndSegment(x, y, 1, 1, 3, 2, 1, 4, 2, -1));
+  ASSERT_DOUBLE_EQ(x, 17.0 / 11.0);
+  ASSERT_DOUBLE_EQ(y, 14.0 / 11.0);
+}
--- a/OrthancStone/UnitTestsSources/UnitTestsMain.cpp	Sat Aug 31 08:40:01 2024 +0200
+++ b/OrthancStone/UnitTestsSources/UnitTestsMain.cpp	Fri Sep 27 22:34:17 2024 +0200
@@ -25,6 +25,7 @@
 
 #include "../Sources/StoneEnumerations.h"
 #include "../Sources/StoneInitialization.h"
+#include "../Sources/Toolbox/StoneToolbox.h"
 
 #include <Logging.h>
 
@@ -58,6 +59,22 @@
 }
 
 
+TEST(StoneToolbox, JoinUrl)
+{
+  ASSERT_EQ("/", OrthancStone::StoneToolbox::JoinUrl("", ""));
+  ASSERT_EQ("/", OrthancStone::StoneToolbox::JoinUrl("", "/"));
+  ASSERT_EQ("/", OrthancStone::StoneToolbox::JoinUrl("", "//"));
+  ASSERT_EQ("/", OrthancStone::StoneToolbox::JoinUrl("/", ""));
+  ASSERT_EQ("/", OrthancStone::StoneToolbox::JoinUrl("//", ""));
+  ASSERT_EQ("/", OrthancStone::StoneToolbox::JoinUrl("////", "/////"));
+  ASSERT_EQ("a/b/d/e/", OrthancStone::StoneToolbox::JoinUrl("a/b", "d/e/"));
+  ASSERT_EQ("a/b/d/e/", OrthancStone::StoneToolbox::JoinUrl("a/b", "/d/e/"));
+  ASSERT_EQ("a/b/d/e/", OrthancStone::StoneToolbox::JoinUrl("a/b/", "d/e/"));
+  ASSERT_EQ("a/b/d/e/", OrthancStone::StoneToolbox::JoinUrl("a/b/", "/d/e/"));
+  ASSERT_EQ("a/b/d/e/", OrthancStone::StoneToolbox::JoinUrl("a/b///", "///d/e/"));
+}
+
+
 int main(int argc, char **argv)
 {
 #if defined(__EMSCRIPTEN__)