# HG changeset patch # User Sebastien Jodogne # Date 1727469257 -7200 # Node ID e65fe2e50fde8829c4c9729fba6ac0e5d6329ce2 # Parent f68f9a8d0d633b5f07470416ec67d021dc49f673# Parent 917e40af6b45572476986fd47f5be559857e8f50 integration mainline->dicom-sr diff -r f68f9a8d0d63 -r e65fe2e50fde OrthancStone/Resources/CMake/OrthancStoneConfiguration.cmake --- 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 diff -r f68f9a8d0d63 -r e65fe2e50fde OrthancStone/Sources/Loaders/DicomSource.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 #include @@ -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 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::const_iterator @@ -274,7 +242,7 @@ std::unique_ptr 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()) diff -r f68f9a8d0d63 -r e65fe2e50fde OrthancStone/Sources/Platforms/WebAssembly/WebAssemblyOracle.cpp --- 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 #endif +#include "../../Toolbox/StoneToolbox.h" + #include #include @@ -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()) diff -r f68f9a8d0d63 -r e65fe2e50fde OrthancStone/Sources/Toolbox/DebugDrawing2D.cpp --- /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 + * . + **/ + + +#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, "\n"); + fprintf(fp, "\n"); + fprintf(fp, "\n", ww, hh, extent_.GetWidth(), extent_.GetHeight()); + + // http://thenewcode.com/1068/Making-Arrows-in-SVG + fprintf(fp, "\n"); + fprintf(fp, "\n"); + fprintf(fp, "\n"); + fprintf(fp, "\n"); + fprintf(fp, "\n"); + + fprintf(fp, "\n", extent_.GetWidth(), extent_.GetHeight()); + + for (std::list::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, "\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, "\n"); + + fclose(fp); + } +} diff -r f68f9a8d0d63 -r e65fe2e50fde OrthancStone/Sources/Toolbox/DebugDrawing2D.h --- /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 + * . + **/ + + +#pragma once + +#include "Extent2D.h" + +#include +#include +#include + + +namespace OrthancStone +{ + class DebugDrawing2D : public boost::noncopyable + { + private: + class Segment; + + Extent2D extent_; + std::list 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); + }; +} diff -r f68f9a8d0d63 -r e65fe2e50fde OrthancStone/Sources/Toolbox/DicomStructureSet.cpp --- 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& 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::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 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 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(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(structures_[i].red_) << "," - << static_cast(structures_[i].green_) << "," - << static_cast(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") - **/ - Orthanc::DicomPath countPointsPath(DICOM_TAG_ROI_CONTOUR_SEQUENCE, i, - DICOM_TAG_CONTOUR_SEQUENCE, 0, - DICOM_TAG_NUMBER_OF_CONTOUR_POINTS); + std::map::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::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(target.red_) << "," + << static_cast(target.green_) << "," + << static_cast(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") + **/ + 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 rectangles; + polygon->Project(rectangles, cutting, GetEstimatedNormal(), GetEstimatedSliceThickness()); - if (polygon->Project(x1, y1, x2, y2, cutting, GetEstimatedNormal(), GetEstimatedSliceThickness())) + for (std::list::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 > Contours; diff -r f68f9a8d0d63 -r e65fe2e50fde OrthancStone/Sources/Toolbox/DicomStructureSet.h --- 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& target, const CoordinateSystem3D& cuttingPlane, const Vector& estimatedNormal, double estimatedSliceThickness) const; diff -r f68f9a8d0d63 -r e65fe2e50fde OrthancStone/Sources/Toolbox/GeometryToolbox.cpp --- 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; + } + } } } diff -r f68f9a8d0d63 -r e65fe2e50fde OrthancStone/Sources/Toolbox/GeometryToolbox.h --- 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) diff -r f68f9a8d0d63 -r e65fe2e50fde OrthancStone/Sources/Toolbox/OrthancDatasets/OrthancHttpConnection.cpp --- 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); } } diff -r f68f9a8d0d63 -r e65fe2e50fde OrthancStone/Sources/Toolbox/StoneToolbox.cpp --- /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 + * . + **/ + + +#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); + } + } +} diff -r f68f9a8d0d63 -r e65fe2e50fde OrthancStone/Sources/Toolbox/StoneToolbox.h --- /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 + * . + **/ + + +#pragma once + +#include + +namespace OrthancStone +{ + namespace StoneToolbox + { + std::string JoinUrl(const std::string& base, + const std::string& path); + } +} diff -r f68f9a8d0d63 -r e65fe2e50fde OrthancStone/UnitTestsSources/ComputationalGeometryTests.cpp --- 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 >& contours) -{ - float ww = 15; - float hh = 13; - - FILE* fp = fopen("test.svg", "w"); - fprintf(fp, "\n"); - fprintf(fp, "\n"); - fprintf(fp, "\n", 100.0f*ww, 100.0f*hh, ww, hh); - - // http://thenewcode.com/1068/Making-Arrows-in-SVG - fprintf(fp, "\n"); - fprintf(fp, "\n"); - fprintf(fp, "\n"); - fprintf(fp, "\n"); - fprintf(fp, "\n"); - - fprintf(fp, "\n", ww, hh); - - unsigned int count = 0; - - for (std::list< std::vector >::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, "\n", x1, y1, x2, y2, color.c_str()); - } - } - fprintf(fp, "\n"); - - fclose(fp); -} -#endif - - TEST(UnionOfRectangles, EdgeCases) { { diff -r f68f9a8d0d63 -r e65fe2e50fde OrthancStone/UnitTestsSources/GenericToolboxTests.cpp --- 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') diff -r f68f9a8d0d63 -r e65fe2e50fde OrthancStone/UnitTestsSources/GeometryToolboxTests.cpp --- 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); +} diff -r f68f9a8d0d63 -r e65fe2e50fde OrthancStone/UnitTestsSources/UnitTestsMain.cpp --- 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 @@ -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__)