diff --git a/doc/concept/polyhedral_surface.qbk b/doc/concept/polyhedral_surface.qbk index 1ac5055b28..dc6b1cf582 100644 --- a/doc/concept/polyhedral_surface.qbk +++ b/doc/concept/polyhedral_surface.qbk @@ -25,6 +25,7 @@ The PolyhedralSurface Concept is defined as following: * It must behave like a Boost.Range Random Access Range * The type defined by the metafunction `range_value<...>::type` must fulfill the [link geometry.reference.concepts.concept_polygon Polygon Concept] +* It must be 3-dimensional and cartesian [heading Rules] diff --git a/include/boost/geometry/algorithms/detail/is_valid/implementation.hpp b/include/boost/geometry/algorithms/detail/is_valid/implementation.hpp index 3e2061ed68..0bd4514977 100644 --- a/include/boost/geometry/algorithms/detail/is_valid/implementation.hpp +++ b/include/boost/geometry/algorithms/detail/is_valid/implementation.hpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include diff --git a/include/boost/geometry/algorithms/detail/is_valid/interface.hpp b/include/boost/geometry/algorithms/detail/is_valid/interface.hpp index 49acc24d4b..620fcd854b 100644 --- a/include/boost/geometry/algorithms/detail/is_valid/interface.hpp +++ b/include/boost/geometry/algorithms/detail/is_valid/interface.hpp @@ -26,7 +26,8 @@ #include #include #include - +#include +#include namespace boost { namespace geometry { @@ -77,13 +78,15 @@ struct is_valid default_strategy) { // NOTE: Currently the strategy is only used for Areal geometries - typedef typename strategies::relate::services::default_strategy + // Select strategy_type based on Geometry's dimension + using strategy_type = typename std::conditional_t < - Geometry, Geometry - >::type strategy_type; + geometry::dimension::value == 3, + typename strategies::relate3::services::default_strategy, + typename strategies::relate::services::default_strategy + >::type; - return dispatch::is_valid::apply(geometry, visitor, - strategy_type()); + return dispatch::is_valid::apply(geometry, visitor, strategy_type()); } }; diff --git a/include/boost/geometry/algorithms/detail/is_valid/polyhedral_surface.hpp b/include/boost/geometry/algorithms/detail/is_valid/polyhedral_surface.hpp new file mode 100644 index 0000000000..7c46040dd0 --- /dev/null +++ b/include/boost/geometry/algorithms/detail/is_valid/polyhedral_surface.hpp @@ -0,0 +1,651 @@ +// Boost.Geometry + +// Copyright (c) 2025 Oracle and/or its affiliates. +// Contributed and/or modified by Vissarion Fysikopoulos, on behalf of Oracle + +// Use, modification and distribution is subject to the Boost Software License, +// Version 1.0. (See accompanying file LICENSE_1_0.txt or copy at +// http://www.boost.org/LICENSE_1_0.txt) + +#ifndef BOOST_GEOMETRY_ALGORITHMS_DETAIL_IS_VALID_POLYHEDRAL_SURFACE_HPP +#define BOOST_GEOMETRY_ALGORITHMS_DETAIL_IS_VALID_POLYHEDRAL_SURFACE_HPP + +#include +#include +#include +#include +#include +#include //needed for rtree +#include +#include +#include +#include + +#include +#include + +namespace boost { namespace geometry +{ + +#ifndef DOXYGEN_NO_DETAIL +namespace detail { namespace is_valid +{ + +struct polygon_frontier_type +{ + polygon_frontier_type(std::size_t size) + : m_stack(), m_visited(size, false) + {} + + void push(std::size_t index) + { + if (!m_visited[index]) + { + m_stack.push(index); + m_visited[index] = true; + } + } + bool empty() + { + return m_stack.empty(); + } + std::size_t top() + { + return m_stack.top(); + } + void pop() + { + m_stack.pop(); + } +private : + std::stack m_stack; + std::vector m_visited; +}; + +// Implementation of the polygon segment intersection algorithm +// This function checks if a segment (s1, s2) intersects with a 2D polygon in 3D space. +// proj1 and proj2 are the indices of the coordinate axes used to determine the sign of three points +// of the polygon after projection. +// It returns: +// - 0 if the segment does not intersect the polygon +// - 1 if the segment intersects the polygon in the interior +// - 2 if the segment intersects the polygon at a vertex +// - 3 if the segment intersects the polygon at an edge +// Reference: R.J.Segura, F.R.Feito - An algorithm for determining intersection segment-polygon in +// 3D, Computers & Graphics 1998 +template +int polygon_segment_intersection(Point const& s1, + Point const& s2, + Polygon const& polygon, + int proj1, + int proj2, + Strategy const& strategy) +{ + BOOST_GEOMETRY_ASSERT(geometry::dimension::value == 3); + BOOST_GEOMETRY_ASSERT(geometry::dimension::value == 3); + + auto const pit_a = points_begin(polygon); + auto const& p_a = *pit_a; + + int cut = 0; + + for (auto pit = boost::next(pit_a); pit != boost::prior(points_end(polygon)); ++pit) + { + auto const& p_b = *pit; + auto const& p_c = *boost::next(pit); + + int const sign = strategy.side().apply(p_a, p_b, p_c, proj1, proj2); + + int const sign1 = strategy.side3().apply(s1, p_a, s2, p_b); + int const sign2 = strategy.side3().apply(s1, p_c, p_b, s2); + int const sign3 = strategy.side3().apply(s1, p_a, p_c, s2); + + // Insects at p_b or p_a or p_c -> vertex + if ((sign1 == 0 && sign2 == 0) || (sign1 == 0 && sign3 == 0) || (sign2 == 0 && sign3 == 0)) + { + return 2; + } + + // Intersects at edge p_a, p_c -> internal edge + if (sign1 == 0 && sign2 == sign3) cut += sign; + + // Intersects at edge p_b, p_c -> external edge + if (sign2 == 0 && sign1 == sign3) return 3; + + // Intersects at edge p_a, p_b -> internal edge + if (sign3 == 0 && sign1 == sign2) cut += sign; + + // Intersects inside p_a, p_b, p_c -> inside + if (sign1 == sign2 && sign2 == sign3) cut += 2 * sign; + } + + return (cut == 2) ? 1 : 0; +} + +template +struct valid_intersection +{ + valid_intersection(std::size_t num_of_polygons) + : m_polygon_projections(num_of_polygons) + {} + + using coordinate_type = typename coordinate_type::type; + using polygon_3d = typename boost::range_value::type; + using ring_3d = typename boost::geometry::ring_type::type; + using point_3d = typename boost::geometry::point_type::type; + using segment_3d = typename boost::geometry::model::segment; + using box_3d = typename boost::geometry::model::box; + + using point_2d = boost::geometry::model::point + ; + using linestring_2d = typename boost::geometry::model::linestring; + using polygon_2d = typename std::conditional + < + boost::geometry::point_order::value == boost::geometry::counterclockwise, + typename boost::geometry::model::polygon, + typename boost::geometry::model::polygon + >::type; + using ring_2d = typename boost::geometry::ring_type::type; + +private: + + enum class intersection_type + { + none, + valid, + invalid + }; + + struct polygon_projection + { + polygon_2d projected_polygon; + int p1_idx; + int p2_idx; + int p3_idx; + int proj1; + int proj2; + int dir; + }; + + template + static std::tuple affinely_independent(point_3d const& p1, + point_3d const& p2, + point_3d const& p3, + Strategy const& strategy) + { + int side = strategy.side().apply(p1, p2, p3, 0, 1); + if (side != 0) return {0, 1, side}; + + side = strategy.side().apply(p1, p2, p3, 0, 2); + if (side != 0) return {0, 2, side}; + + side = strategy.side().apply(p1, p2, p3, 1, 2); + if (side != 0) return {1, 2, side}; + + return {-1, -1, 0}; // All points are collinear + } + + template + static std::tuple find_non_collinear_points(polygon_3d const& polygon, + int& p1_index, + int& p2_index, + int& p3_index, + Strategy const& strategy) + { + for (auto pit1 = points_begin(polygon); pit1 != points_end(polygon); ++pit1) + { + for (auto pit2 = boost::next(pit1); pit2 != points_end(polygon); ++pit2) + { + for (auto pit3 = boost::next(pit2); pit3 != points_end(polygon); ++pit3) + { + // A set of 3 independent points is sufficient to construct a plane. + auto res = affinely_independent(*pit1, *pit2, *pit3, strategy); + if (std::get<0>(res) != -1) + { + p1_index = std::distance(points_begin(polygon), pit1); + p2_index = std::distance(points_begin(polygon), pit2); + p3_index = std::distance(points_begin(polygon), pit3); + return res; + } + } + } + } + return {-1, -1, 0}; // All points are collinear + } + + template + static bool has_correct_intersection_orientation(polygon_2d const& polygon, + point_2d const& segment_point1, + point_2d const& segment_point2, + VisitPolicy& visitor) + { + if (boost::geometry::equals(segment_point1, segment_point2)) + { + visitor.template apply(); + return false; + } + + boost::geometry::closing_iterator it(boost::geometry::exterior_ring(polygon)); + auto begin = it; + boost::geometry::closing_iterator end(boost::geometry::exterior_ring(polygon), true); + + for (; it != end - 1; ++it) + { + if (boost::geometry::equals(*it, segment_point1)) + { + auto prev = (it == begin) ? end - 1 : boost::prior(it); + + if (boost::geometry::equals(segment_point2, *prev)) + { + // valid intersection and consistent orientation + return true; + } + + auto next = (it == end) ? begin : boost::next(it); + + if (boost::geometry::equals(segment_point2, *next)) + { + // valid intersection but inconsistent orientation + visitor.template apply(); + return false; + } + } + } + + visitor.template apply(); + + return false; + } + + point_2d project_point_to_coordinate_plane(point_3d const& point, int proj1, int proj2) + { + if (proj1 == 1 && proj2 == 2) return point_2d{geometry::get<1>(point), geometry::get<2>(point)}; + if (proj1 == 0 && proj2 == 2) return point_2d{geometry::get<0>(point), geometry::get<2>(point)}; + if (proj1 == 0 && proj2 == 1) return point_2d{geometry::get<0>(point), geometry::get<1>(point)}; + return point_2d{}; + } + + // Check if a segment intersects a polygon in 3D space + template + intersection_type check_polygon_segment_intersection(point_3d const& segment_point1, + point_3d const& segment_point2, + polygon_3d const& polygon, + std::size_t const polygon_index, + VisitPolicy& visitor, + Strategy const& strategy) + { + auto const& projection = m_polygon_projections[polygon_index]; + auto const& projected_polygon = projection.projected_polygon; + + auto const& p1 = polygon.outer()[projection.p1_idx]; + auto const& p2 = polygon.outer()[projection.p2_idx]; + auto const& p3 = polygon.outer()[projection.p3_idx]; + + int proj1 = projection.proj1; + int proj2 = projection.proj2; + int dir = projection.dir; + + // Check if the segment intersects the plane of the polygon + auto sign1 = strategy.side3().apply(p1, p2, p3, segment_point1); + auto sign2 = strategy.side3().apply(p1, p2, p3, segment_point2); + + if ((sign1 > 0 && sign2 > 0) || (sign1 < 0 && sign2 < 0)) + { + return intersection_type::none; // No intersection, this is valid + } + + // Check if the segment is on the same plane as the polygon + if (sign1 == 0 && sign2 == 0) + { + auto const& projected_p1 = project_point_to_coordinate_plane(segment_point1, proj1, proj2); + auto const& projected_p2 = project_point_to_coordinate_plane(segment_point2, proj1, proj2); + + linestring_2d projected_segment = {projected_p1, projected_p2}; + + // Check if the projected segment intersects the polygon + std::tuple + < + boost::geometry::model::multi_point, + boost::geometry::model::multi_linestring> intersections; + boost::geometry::intersection(projected_segment, projected_polygon, intersections); + + auto const& points = std::get<0>(intersections); + auto const& linestrings = std::get<1>(intersections); + + if (boost::geometry::is_empty(points) && boost::geometry::is_empty(linestrings)) + { + return intersection_type::none; // No intersection, this is valid + } + + if (!points.empty()) + { + if (points.size() == 1 && + (boost::geometry::equals(points.front(), projected_p1) || + boost::geometry::equals(points.front(), projected_p2))) + { + return intersection_type::none; // Valid empty intersection + } + visitor.template apply(); + return intersection_type::invalid; // Invalid intersection + } + + if (linestrings.size() == 1) + { + if (boost::geometry::equals(linestrings.front(), projected_segment)) + { + auto const& pp1 = dir < 0 ? projected_p2 : projected_p1; + auto const& pp2 = dir < 0 ? projected_p1 : projected_p2; + if (!has_correct_intersection_orientation(projected_polygon, pp1, pp2, visitor)) + { + return intersection_type::invalid; + } + return intersection_type::valid; // The only case of valid non empty intersection + } + } + visitor.template apply(); + return intersection_type::invalid; + } + + int inter = polygon_segment_intersection(segment_point1, segment_point2, polygon, proj1, + proj2, strategy); + + if ((inter == 1 || inter == 3) || (inter == 2 && sign1 !=0 && sign2 != 0)) + { + visitor.template apply(); + return intersection_type::invalid; + } + + return intersection_type::none; + } + + // Helper function to check if any segment of a ring intersects a polygon + template + intersection_type check_polygon_polygon_intersection(polygon_3d const& polygon1, + polygon_3d const& polygon2, + std::size_t const& index2, + VisitPolicy& visitor, + Strategy const& strategy) + { + bool valid_intersection = false; + auto const& ring1 = boost::geometry::exterior_ring(polygon1); + + for (auto it = boost::begin(ring1); it + 1 != boost::end(ring1); ++it) + { + auto result = check_polygon_segment_intersection(*it, *(it + 1), polygon2, index2, + visitor, strategy); + + if (result == intersection_type::invalid) + { + return intersection_type::invalid; // Found invalid intersection + } + + if (result == intersection_type::valid) + { + valid_intersection = true; // Found a valid intersection + } + } + // Either no intersection or valid intersection + return valid_intersection ? intersection_type::valid : intersection_type::none; + } + + public: + + template + intersection_type apply(polygon_3d const& polygon1, + polygon_3d const& polygon2, + std::size_t const& index1, + std::size_t const& index2, + VisitPolicy& visitor, + Strategy const& strategy) + { + auto intersection1 = check_polygon_polygon_intersection(polygon1, polygon2, index2, + visitor, strategy); + + if (intersection1 == intersection_type::invalid) + { + return intersection_type::invalid; // Found invalid intersection + } + + auto intersection2 = check_polygon_polygon_intersection(polygon2, polygon1, index1, + visitor, strategy); + + if (intersection2 == intersection_type::invalid) + { + return intersection_type::invalid; // Found invalid intersection + } + + if (intersection1 == intersection_type::none && intersection2 == intersection_type::none) + { + return intersection_type::none; // No intersection found + } + + return intersection_type::valid; + } + + template + intersection_type initialize(polygon_3d const& polygon, + std::size_t const polygon_index, + VisitPolicy& visitor, + Strategy const& strategy) + { + // TODO: Support interior rings + ring_3d const& ring = boost::geometry::exterior_ring(polygon); + + if (boost::size(ring) < detail::minimum_ring_size::value) + { + visitor.template apply(); + return intersection_type::invalid; + } + + // Get three non-collinear points from the polygon to define the plane + int p1_idx = 0; + int p2_idx = 0; + int p3_idx = 0; + + auto res = find_non_collinear_points(polygon, p1_idx, p2_idx, p3_idx, strategy); + + if (std::get<0>(res) == -1) + { + visitor.template apply(); + return intersection_type::invalid; + } + + int proj1 = std::get<0>(res); + int proj2 = std::get<1>(res); + int dir = std::get<2>(res); + + auto const& p1 = polygon.outer()[p1_idx]; + auto const& p2 = polygon.outer()[p2_idx]; + auto const& p3 = polygon.outer()[p3_idx]; + + // Check for non-coplanar points + for (auto pit = points_begin(polygon); pit != points_end(polygon); ++pit) + { + if (strategy.side3().apply(p1, p2, p3, *pit) != 0) + { + visitor.template apply(); + return intersection_type::invalid; + } + } + + // Project the polygon + polygon_2d projected_polygon; + + auto add_projected_points = [&](auto begin_it, auto end_it) + { + for (auto it = begin_it; it != end_it; ++it) + { + projected_polygon.outer().emplace_back( + project_point_to_coordinate_plane(*it, proj1, proj2)); + } + }; + + if (dir > 0) { + add_projected_points(points_begin(ring), points_end(ring)); + } else { + add_projected_points(points_rbegin(ring), points_rend(ring)); + } + + // Check if the projected polygon is valid + if (!resolve_dynamic::is_valid::apply(projected_polygon, visitor, strategy)) + { + return intersection_type::invalid; + } + + // Cache the projection for future use + m_polygon_projections[polygon_index] = polygon_projection{std::move(projected_polygon), + p1_idx, p2_idx, p3_idx, proj1, proj2, dir}; + + return intersection_type::valid; + } + +private: + std::vector m_polygon_projections; +}; + +template +class is_valid_polyhedral_surface +{ +public: + template + static inline bool apply(PolyhedralSurface const& surface, + VisitPolicy& visitor, + Strategy const& strategy) + { + boost::ignore_unused(strategy); + + valid_intersection valid_intersection(surface.size()); + + // Check for colinear/coplanar points and compute projections + for (std::size_t i = 0; i < surface.size(); ++i) + { + auto intersection = valid_intersection.initialize(surface[i], i, visitor, strategy); + + using intersection_type = decltype(intersection); + + if (intersection == intersection_type::invalid) + { + return false; + } + } + + // Create an rtree to use it for polygon pair intersections + using box_3d = typename boost::geometry::detail::is_valid::valid_intersection + ::box_3d; + using value = std::pair; + + boost::geometry::index::rtree> rtree; + std::vector> boxes(surface.size()); + + for (std::size_t i = 0; i < surface.size(); ++i) + { + auto b = boost::geometry::return_envelope(surface[i], strategy); + boxes[i] = std::make_unique(b); + rtree.insert(std::make_pair(b, i)); // use balancing algorithm + } + + // We check pairs of polygons for having a valid intersection. + // We iterate in a BFS manner to check connectivity of the surface. + + auto it1 = boost::begin(surface); + + polygon_frontier_type polygon_frontier(surface.size()); + polygon_frontier.push(0); + + std::vector remaining_polygons(surface.size()); + std::vector positions(surface.size()); + + for (std::size_t i = 0; i < surface.size(); ++i) + { + remaining_polygons[i] = i; + positions[i] = i; + } + + int remaining_count = surface.size(); + + while (!polygon_frontier.empty()) + { + auto index1 = polygon_frontier.top(); + polygon_frontier.pop(); + + it1 = boost::begin(surface) + index1; + + // Remove index from remaining polygons and update positions + auto moving_index = remaining_polygons[remaining_count - 1]; + remaining_polygons[positions[index1]] = moving_index; + positions[moving_index] = positions[index1]; + + // Query all polygons that intersect with the current polygon. + // We use a buffer to avoid precision issues with the rtree query. + std::vector intersecting_boxes; + auto const& b_query = *boxes[index1]; + auto const& b_query_buf = boost::geometry::return_buffer(b_query, 0.0001); + + rtree.query(boost::geometry::index::intersects(b_query_buf), + std::back_inserter(intersecting_boxes)); + + for (int i = 0; i < remaining_count - 1; ++i) + { + auto index2 = remaining_polygons[i]; + + // Skip if index2 is not in the intersecting boxes + if (std::find_if(intersecting_boxes.begin(), intersecting_boxes.end(), + [index2](const value& v) { return v.second == index2; } + ) == intersecting_boxes.end()) + { + continue; + } + + auto intersection = valid_intersection.apply(*it1, surface[index2], index1, index2, + visitor, strategy); + + using intersection_type = decltype(intersection); + + if (intersection == intersection_type::invalid) + { + return false; // Invalid intersection found (including inconsistent orientation) + } + else if (intersection == intersection_type::valid) + { + polygon_frontier.push(index2); + } + } + remaining_count--; + } + + if (remaining_count > 0) + { + visitor.template apply(); + return false; // Not all polygons were visited, hence disconnected + } + + return true; + } +}; + +}} // namespace detail::is_valid +#endif // DOXYGEN_NO_DETAIL + +#ifndef DOXYGEN_NO_DISPATCH +namespace dispatch +{ + + +// Reference (for validity of Polyhedral Surfaces): OGC +// OpenGISĀ® Implementation Standard for Geographic +// information - Simple feature access - Part 1: Common +// architecture: 6.1.12 +template +struct is_valid + < + PolyhedralSurface, polyhedral_surface_tag, AllowEmptyMultiGeometries + > : detail::is_valid::is_valid_polyhedral_surface +{}; + + +} // namespace dispatch +#endif // DOXYGEN_NO_DISPATCH + +}} // namespace boost::geometry + +#endif // BOOST_GEOMETRY_ALGORITHMS_DETAIL_IS_VALID_POLYHEDRAL_SURFACE_HPP diff --git a/include/boost/geometry/algorithms/detail/overlay/graph/assign_counts.hpp b/include/boost/geometry/algorithms/detail/overlay/graph/assign_counts.hpp index f35bd7a1ef..01d12fed07 100644 --- a/include/boost/geometry/algorithms/detail/overlay/graph/assign_counts.hpp +++ b/include/boost/geometry/algorithms/detail/overlay/graph/assign_counts.hpp @@ -80,22 +80,22 @@ void assign_counts(Turn& turn) } }; - auto assign_left_for = [&turn, &assign_for](counts_per_op_t const& op1, counts_per_op_t op2) + auto assign_left_for = [&assign_for](counts_per_op_t const& op1, counts_per_op_t op2) { assign_for(op1, op2, [](auto& enriched, auto count) { enriched.count_left = count; }); }; - auto assign_right_for = [&turn, &assign_for](counts_per_op_t const& op1, counts_per_op_t op2) + auto assign_right_for = [&assign_for](counts_per_op_t const& op1, counts_per_op_t op2) { assign_for(op1, op2, [](auto& enriched, auto count) { enriched.count_right = count; }); }; - auto assign_left_incoming_for = [&turn, &assign_for](counts_per_op_t const& op1, counts_per_op_t op2) + auto assign_left_incoming_for = [&assign_for](counts_per_op_t const& op1, counts_per_op_t op2) { assign_for(op1, op2, [](auto& enriched, auto count) { enriched.count_left_incoming = count; }); }; - auto assign_right_incoming_for = [&turn, &assign_for](counts_per_op_t const& op1, counts_per_op_t op2) + auto assign_right_incoming_for = [&assign_for](counts_per_op_t const& op1, counts_per_op_t op2) { assign_for(op1, op2, [](auto& enriched, auto count) { enriched.count_right_incoming = count; }); }; diff --git a/include/boost/geometry/algorithms/detail/overlay/graph/detect_biconnected_components.hpp b/include/boost/geometry/algorithms/detail/overlay/graph/detect_biconnected_components.hpp index c6416124ba..7a34c0bb73 100644 --- a/include/boost/geometry/algorithms/detail/overlay/graph/detect_biconnected_components.hpp +++ b/include/boost/geometry/algorithms/detail/overlay/graph/detect_biconnected_components.hpp @@ -162,7 +162,7 @@ template second.is_extra ? it->second.original_node_id diff --git a/include/boost/geometry/algorithms/validity_failure_type.hpp b/include/boost/geometry/algorithms/validity_failure_type.hpp index 3fbf8027b4..18eff297c0 100644 --- a/include/boost/geometry/algorithms/validity_failure_type.hpp +++ b/include/boost/geometry/algorithms/validity_failure_type.hpp @@ -81,7 +81,29 @@ enum validity_failure_type failure_wrong_corner_order = 50, // for boxes /// The geometry has at least one point with an invalid coordinate /// (for example, the coordinate is a NaN) - failure_invalid_coordinate = 60 + failure_invalid_coordinate = 60, + /// The geometry has colinear points on the same face + /// (applies to polyhedral surfaces only) + failure_collinear_points_on_face = 70, + /// The geometry has non-coplanar points on the same face + /// The points do not lie on the same plane and thus cannot define a face + /// (applies to polyhedral surfaces only) + failure_non_coplanar_points_on_face = 71, + /// The geometry has a very small number of points on the same face + /// The face must have at least 3 points to be defined + /// (applies to polyhedral surfaces only) + failure_few_points_on_face = 72, + /// The geometry has inconsistent orientation of at least one edge + /// The edge has the same orientation in both faces it belongs to + /// (applies to polyhedral surfaces only) + failure_inconsistent_orientation = 73, + /// The geometry has invalid intersection of the faces + /// The faces do not intersect in a valid way, i.e. on a common edge + /// (applies to polyhedral surfaces only) + failure_invalid_intersection = 74, + /// The geometry has disconnected faces, i.e. it is not continuous + /// (applies to polyhedral surfaces only) + failure_disconnected_surface = 75 }; diff --git a/include/boost/geometry/geometries/concepts/polyhedral_surface_concept.hpp b/include/boost/geometry/geometries/concepts/polyhedral_surface_concept.hpp index a78403a609..b051256210 100644 --- a/include/boost/geometry/geometries/concepts/polyhedral_surface_concept.hpp +++ b/include/boost/geometry/geometries/concepts/polyhedral_surface_concept.hpp @@ -11,11 +11,15 @@ #ifndef BOOST_GEOMETRY_GEOMETRIES_CONCEPTS_POLYHEDRAL_SURFACE_CONCEPT_HPP #define BOOST_GEOMETRY_GEOMETRIES_CONCEPTS_POLYHEDRAL_SURFACE_CONCEPT_HPP +#include + #include #include #include +#include #include +#include #include namespace boost { namespace geometry { namespace concepts @@ -29,6 +33,9 @@ class PolyhedralSurface BOOST_CONCEPT_ASSERT( (concepts::Polygon) ); BOOST_CONCEPT_ASSERT( (boost::RandomAccessRangeConcept) ); + BOOST_STATIC_ASSERT( (geometry::dimension::value == 3) ); + BOOST_STATIC_ASSERT( (std::is_same, + geometry::cartesian_tag>::value) ); public: @@ -53,6 +60,9 @@ class ConstPolyhedralSurface BOOST_CONCEPT_ASSERT( (concepts::ConstPolygon) ); BOOST_CONCEPT_ASSERT( (boost::RandomAccessRangeConcept) ); + BOOST_STATIC_ASSERT( (geometry::dimension::value == 3) ); + BOOST_STATIC_ASSERT( (std::is_same, + geometry::cartesian_tag>::value) ); public: diff --git a/include/boost/geometry/geometries/polyhedral_surface.hpp b/include/boost/geometry/geometries/polyhedral_surface.hpp index 0dc60819d5..7dc8c99b36 100644 --- a/include/boost/geometry/geometries/polyhedral_surface.hpp +++ b/include/boost/geometry/geometries/polyhedral_surface.hpp @@ -44,7 +44,6 @@ namespace model */ -//TODO: add is_valid() check template < typename Polygon, diff --git a/include/boost/geometry/strategies/cartesian/side_3d_rounded_input.hpp b/include/boost/geometry/strategies/cartesian/side_3d_rounded_input.hpp new file mode 100644 index 0000000000..dda48287c6 --- /dev/null +++ b/include/boost/geometry/strategies/cartesian/side_3d_rounded_input.hpp @@ -0,0 +1,98 @@ +// Boost.Geometry + +// Copyright (c) 2025 Tinko Bartels, Berlin, Germany. +// Copyright (c) 2025 Oracle and/or its affiliates. +// Contributed and/or modified by Vissarion Fisikopoulos, on behalf of Oracle + +// This file was modified by Oracle on 2025. +// Modifications copyright (c) 2025 Oracle and/or its affiliates. +// Contributed and/or modified by Vissarion Fysikopoulos, on behalf of Oracle + +// Use, modification and distribution is subject to the Boost Software License, +// Version 1.0. (See accompanying file LICENSE_1_0.txt or copy at +// http://www.boost.org/LICENSE_1_0.txt) + +#ifndef BOOST_GEOMETRY_STRATEGY_CARTESIAN_SIDE_3D_ROUNDED_INPUT_HPP +#define BOOST_GEOMETRY_STRATEGY_CARTESIAN_SIDE_3D_ROUNDED_INPUT_HPP + +#include + +#include +#include + +#include + +#include + +#include + +namespace boost { namespace geometry +{ + +namespace strategy { namespace side +{ + +// This strategy is used for 3D side calculations. +// It is used to determine the side of a point p with respect to a triangle/plane defined by three +// points in 3D space namely p1, p2, p3. +// The following determinant is used to calculate the side: +// | (p_x - p1_x) (p_y - p1_y) (p_z - p1_z) | +// det = | (p2_x - p1_x) (p2_y - p1_y) (p2_z - p1_z) | +// | (p3_x - p1_x) (p3_y - p1_y) (p3_z - p1_z) | +// To calculate we use cofactor expansion along the first row. +// Performance note: +// If this function is called repeatedly with the same p1, p2, and p3, +// the 2x2 cofactors can be precomputed and reused for efficiency. + +template +struct side_3d_rounded_input +{ + using cs_tag = cartesian_tag; + + template + static inline int apply(P1 const& p1, P2 const& p2, P3 const& p3, P const& p) + { + using coor_t = typename select_calculation_type_alt::type; + + coor_t const p1_x = geometry::get<0>(p1); + coor_t const p1_y = geometry::get<1>(p1); + coor_t const p1_z = geometry::get<2>(p1); + coor_t const p2_x = geometry::get<0>(p2); + coor_t const p2_y = geometry::get<1>(p2); + coor_t const p2_z = geometry::get<2>(p2); + coor_t const p3_x = geometry::get<0>(p3); + coor_t const p3_y = geometry::get<1>(p3); + coor_t const p3_z = geometry::get<2>(p3); + coor_t const p_x = geometry::get<0>(p); + coor_t const p_y = geometry::get<1>(p); + coor_t const p_z = geometry::get<2>(p); + + static coor_t const eps = std::numeric_limits::epsilon() / 2; + coor_t const det = (p_x - p1_x) * ((p2_y - p1_y) * (p3_z - p1_z) - (p3_y - p1_y) * (p2_z - p1_z)) + - (p_y - p1_y) * ((p2_x - p1_x) * (p3_z - p1_z) - (p3_x - p1_x) * (p2_z - p1_z)) + + (p_z - p1_z) * ((p2_x - p1_x) * (p3_y - p1_y) - (p3_x - p1_x) * (p2_y - p1_y)); + coor_t const err_bound = (Coeff1 * eps) * + ( (geometry::math::abs(p_x) + geometry::math::abs(p1_x)) + * ((geometry::math::abs(p2_y) + geometry::math::abs(p1_y)) + * (geometry::math::abs(p3_z) + geometry::math::abs(p1_z)) + + (geometry::math::abs(p3_y) + geometry::math::abs(p1_y)) + * (geometry::math::abs(p2_z) + geometry::math::abs(p1_z))) + + (geometry::math::abs(p_y) + geometry::math::abs(p1_y)) + * ((geometry::math::abs(p2_x) + geometry::math::abs(p1_x)) + * (geometry::math::abs(p3_z) + geometry::math::abs(p1_z)) + + (geometry::math::abs(p3_x) + geometry::math::abs(p1_x)) + * (geometry::math::abs(p2_z) + geometry::math::abs(p1_z))) + + (geometry::math::abs(p_z) + geometry::math::abs(p1_z)) + * ((geometry::math::abs(p2_x) + geometry::math::abs(p1_x)) + * (geometry::math::abs(p3_y) + geometry::math::abs(p1_y)) + + (geometry::math::abs(p3_x) + geometry::math::abs(p1_x)) + * (geometry::math::abs(p2_y) + geometry::math::abs(p1_y)))); + return (det > err_bound) - (det < -err_bound); + } +}; + +}} // namespace strategy::side + +}} // namespace boost::geometry + +#endif // BOOST_GEOMETRY_STRATEGY_CARTESIAN_SIDE_3D_ROUNDED_INPUT_HPP diff --git a/include/boost/geometry/strategies/cartesian/side_rounded_input.hpp b/include/boost/geometry/strategies/cartesian/side_rounded_input.hpp index 9283a2b845..091ee81420 100644 --- a/include/boost/geometry/strategies/cartesian/side_rounded_input.hpp +++ b/include/boost/geometry/strategies/cartesian/side_rounded_input.hpp @@ -2,8 +2,8 @@ // Copyright (c) 2021 Tinko Bartels, Berlin, Germany. -// This file was modified by Oracle on 2023. -// Modifications copyright (c) 2023 Oracle and/or its affiliates. +// This file was modified by Oracle on 2025. +// Modifications copyright (c) 2025 Oracle and/or its affiliates. // Contributed and/or modified by Vissarion Fysikopoulos, on behalf of Oracle // Use, modification and distribution is subject to the Boost Software License, @@ -33,8 +33,35 @@ namespace strategy { namespace side template struct side_rounded_input { +private: + template + static inline auto get_coordinate(Point const& point, std::size_t i) + { + switch(i) { + case 0: return geometry::get<0>(point); + case 1: return geometry::get<1>(point); + case 2: return geometry::get<2>(point); + default: BOOST_ASSERT(false); + } + return geometry::get<0>(point); + } +public: using cs_tag = cartesian_tag; + template + static inline int apply(P1 const& p1, P2 const& p2, P const& p, int i, int j) + { + using coor_t = typename select_calculation_type_alt::type; + coor_t const p1_x = get_coordinate(p1, i); + coor_t const p1_y = get_coordinate(p1, j); + coor_t const p2_x = get_coordinate(p2, i); + coor_t const p2_y = get_coordinate(p2, j); + coor_t const p_x = get_coordinate(p, i); + coor_t const p_y = get_coordinate(p, j); + + return apply(p1_x, p1_y, p2_x, p2_y, p_x, p_y); + } + template static inline int apply(P1 const& p1, P2 const& p2, P const& p) { @@ -47,9 +74,17 @@ struct side_rounded_input coor_t const p_x = geometry::get<0>(p); coor_t const p_y = geometry::get<1>(p); - static coor_t const eps = std::numeric_limits::epsilon() / 2; - coor_t const det = (p1_x - p_x) * (p2_y - p_y) - (p1_y - p_y) * (p2_x - p_x); - coor_t const err_bound = (Coeff1 * eps + Coeff2 * eps * eps) * + return apply(p1_x, p1_y, p2_x, p2_y, p_x, p_y); + } + + template + static inline int apply(CT const& p1_x, CT const& p1_y, + CT const& p2_x, CT const& p2_y, + CT const& p_x, CT const& p_y) + { + static CT const eps = std::numeric_limits::epsilon() / 2; + CT const det = (p1_x - p_x) * (p2_y - p_y) - (p1_y - p_y) * (p2_x - p_x); + CT const err_bound = (Coeff1 * eps + Coeff2 * eps * eps) * ( (geometry::math::abs(p1_x) + geometry::math::abs(p_x)) * (geometry::math::abs(p2_y) + geometry::math::abs(p_y)) + (geometry::math::abs(p2_x) + geometry::math::abs(p_x)) diff --git a/include/boost/geometry/strategies/relate3/cartesian.hpp b/include/boost/geometry/strategies/relate3/cartesian.hpp new file mode 100644 index 0000000000..4dccf953c2 --- /dev/null +++ b/include/boost/geometry/strategies/relate3/cartesian.hpp @@ -0,0 +1,57 @@ +// Boost.Geometry + +// Copyright (c) 2025, Oracle and/or its affiliates. + +// Contributed and/or modified by Vissarion Fysikopoulos, on behalf of Oracle + +// Use, modification and distribution is subject to the Boost Software License, +// Version 1.0. (See accompanying file LICENSE_1_0.txt or copy at +// http://www.boost.org/LICENSE_1_0.txt) + +#ifndef BOOST_GEOMETRY_STRATEGIES_RELATE3_CARTESIAN_HPP +#define BOOST_GEOMETRY_STRATEGIES_RELATE3_CARTESIAN_HPP + +#include +#include +#include +#include + +namespace boost { namespace geometry +{ + +namespace strategies { namespace relate3 +{ + +template +class cartesian + : public strategies::relate::cartesian +{ +public: + + static auto side3() + { + return strategy::side::side_3d_rounded_input<>(); + } + + static auto side() + { + return strategy::side::side_rounded_input<>(); + } +}; + +namespace services +{ + +template +struct default_strategy +{ + using type = strategies::relate3::cartesian<>; +}; + +} // namespace services + +}} // namespace strategies::relate3 + +}} // namespace boost::geometry + +#endif // BOOST_GEOMETRY_STRATEGIES_RELATE_CARTESIAN_HPP diff --git a/include/boost/geometry/strategies/relate3/services.hpp b/include/boost/geometry/strategies/relate3/services.hpp new file mode 100644 index 0000000000..c5e071b690 --- /dev/null +++ b/include/boost/geometry/strategies/relate3/services.hpp @@ -0,0 +1,58 @@ +// Boost.Geometry + +// Copyright (c) 2025, Oracle and/or its affiliates. + +// Contributed and/or modified by Vissarion Fysikopoulos, on behalf of Oracle + +// Use, modification and distribution is subject to the Boost Software License, +// Version 1.0. (See accompanying file LICENSE_1_0.txt or copy at +// http://www.boost.org/LICENSE_1_0.txt) + +#ifndef BOOST_GEOMETRY_STRATEGIES_RELATE3_SERVICES_HPP +#define BOOST_GEOMETRY_STRATEGIES_RELATE3_SERVICES_HPP + + +#include +#include + + +namespace boost { namespace geometry +{ + + +namespace strategies { namespace relate3 { + +namespace services +{ + +template +< + typename Geometry1, + typename Geometry2, + typename CSTag1 = geometry::cs_tag_t, + typename CSTag2 = geometry::cs_tag_t +> +struct default_strategy +{ + BOOST_GEOMETRY_STATIC_ASSERT_FALSE( + "Not implemented for this Geometry's coordinate system.", + Geometry1, Geometry2, CSTag1, CSTag2); +}; + +template +struct strategy_converter +{ + BOOST_GEOMETRY_STATIC_ASSERT_FALSE( + "Not implemented for this Strategy.", + Strategy); +}; + + +} // namespace services + +}} // namespace strategies::relate3 + + +}} // namespace boost::geometry + +#endif // BOOST_GEOMETRY_STRATEGIES_RELATE3_SERVICES_HPP diff --git a/test/algorithms/is_valid.cpp b/test/algorithms/is_valid.cpp index 408d541536..9816f108ab 100644 --- a/test/algorithms/is_valid.cpp +++ b/test/algorithms/is_valid.cpp @@ -1,8 +1,9 @@ // Boost.Geometry (aka GGL, Generic Geometry Library) // Unit Test -// Copyright (c) 2014-2021, Oracle and/or its affiliates. +// Copyright (c) 2014-2025, Oracle and/or its affiliates. +// Contributed and/or modified by Vissarion Fysikopoulos, on behalf of Oracle // Contributed and/or modified by Menelaos Karavelas, on behalf of Oracle // Contributed and/or modified by Adam Wulkiewicz, on behalf of Oracle @@ -13,6 +14,8 @@ #define BOOST_TEST_MODULE test_is_valid #endif +//#define BOOST_GEOMETRY_ALLOW_INCONSISTENT_ORIENTATION + #include #include @@ -35,6 +38,7 @@ #include #include +#include template void geometry_to_svg(Geometry const& geometry, const std::string& case_id) @@ -1547,3 +1551,343 @@ BOOST_AUTO_TEST_CASE( test_is_valid_geometry_collection ) gc = {valid_linestring, invalid_polygon}; test::apply("gc03", gc, false); } + + +BOOST_AUTO_TEST_CASE(test_is_valid_polyhedral_surface) +{ + using point_type = bg::model::point; + using polygon_type = bg::model::polygon; + using polyhedral_surface_type = bg::model::polyhedral_surface; + + // Valid polyhedral surface + bg::validity_failure_type failure; + polyhedral_surface_type valid_surface; + bg::read_wkt( + "POLYHEDRALSURFACE(((0 0 0,0 1 0,1 1 0,1 0 0,0 0 0)),((0 0 0,0 0 1,0 1 1,0 1 0,0 0 0)),\ + ((0 0 0,1 0 0,1 0 1,0 0 1,0 0 0)),((1 1 1,0 1 1,0 0 1,1 0 1,1 1 1)),\ + ((1 1 1,1 0 1,1 0 0,1 1 0,1 1 1)),((1 1 1,1 1 0,0 1 0,0 1 1,1 1 1)))", + valid_surface); + BOOST_CHECK(bg::is_valid(valid_surface, failure)); + + // Invalid polyhedral surface with a small distortion + polyhedral_surface_type valid_surface_distortion; + bg::read_wkt( + "POLYHEDRALSURFACE(((0 0 1e-20,0 1 0,1 1 0,1 0 0,1e-20 0 0)),((0 0 0,0 0 1,0 1 1,0 1 0,0 0 0)),\ + ((0 0 0,1 1e-20 0,1 0 1,0 0 1,0 0 0)),((1 1 1,0 1 1,0 1e-20 1,1 0 1,1 1 1)),\ + ((1 1 1,1 0 1,1 0 0,1 1 1e-18,1 1 1)),((1 1 1,1 1 0,0 1 0,0 1 1,1 1 1)))", + valid_surface_distortion); + BOOST_CHECK(!bg::is_valid(valid_surface_distortion, failure)); + BOOST_CHECK(failure == bg::failure_non_coplanar_points_on_face); + + // Valid polyhedral surface different order + polyhedral_surface_type valid_surface_out; + bg::read_wkt( + "POLYHEDRALSURFACE(((0 0 0,1 0 0,1 1 0,0 1 0,0 0 0)),((0 0 0,0 1 0,0 1 1,0 0 1,0 0 0)),\ + ((0 0 0,0 0 1,1 0 1,1 0 0,0 0 0)),((1 1 1,1 0 1,0 0 1,0 1 1,1 1 1)),\ + ((1 1 1,1 1 0,1 0 0,1 0 1,1 1 1)),((1 1 1,0 1 1,0 1 0,1 1 0,1 1 1)))", + valid_surface_out); + BOOST_CHECK(bg::is_valid(valid_surface_out, failure)); + + // A valid polyhedral surface which is open and has non-convex faces + polyhedral_surface_type valid_surface_non_convex; + bg::read_wkt( + "POLYHEDRALSURFACE(((0 0 0,0 1 0,1 1 0,1 0 0,0 0 0)),((0 0 0,0 0 1,0 1 1,0 1 0,0 0 0)),\ + ((0 0 0,1 0 0,1 0 1,0 0 1,0 0 0)),((1 1 1,0 1 1,0 0 1,1 0 1,1 1 1)),\ + ((1 1 1,1 0 1,1 0 0,1 1 0,1 .5 .5,1 1 1)))", + valid_surface_non_convex); + BOOST_CHECK(bg::is_valid(valid_surface_non_convex, failure)); + + // A valid polyhedral surface which is open and has holes on the faces + polyhedral_surface_type valid_surface_holes; + bg::read_wkt( + "POLYHEDRALSURFACE(((0 0 0,0 1 0,1 1 0,1 0 0,0 0 0),(0.2 0.2 0,0.2 0.5 0,0.5 0.5 0,0.5 0.2 0,0.2 0.2 0)),\ + ((0 0 0,0 0 1,0 1 1,0 1 0,0 0 0)),((0 0 0,1 0 0,1 0 1,0 0 1,0 0 0)),\ + ((1 1 1,0 1 1,0 0 1,1 0 1,1 1 1)),((1 1 1,1 0 1,1 0 0,1 1 0,1 1 1)),\ + ((1 1 1,1 1 0,0 1 0,0 1 1,1 1 1)))", + valid_surface_holes); + BOOST_CHECK(bg::is_valid(valid_surface_holes, failure)); + + /* NOT SUPPORTED YET + polyhedral_surface_type valid_surface_holes2; + bg::read_wkt( + "POLYHEDRALSURFACE(((0 0 0,0 1 0,1 1 0,1 0 0,0 0 0),(0.25 0.25 0,0.75 0.25 0,0.75 0.75 0,0.25 0.75 0,0.25 0.25 0)),\ + ((0.25 0.25 0,0.75 0.25 0,0.75 0.25 0.75,0.25 0.25 0.75,0.25 0.25 0)),\ + ((0.75 0.75 0,0.75 0.25 0,0.75 0.25 0.75,0.75 0.75 0.75,0.75 0.75 0)),\ + ((0.25 0.75 0,0.75 0.75 0,0.75 0.75 0.75,0.25 0.75 0.75,0.25 0.75 0)),\ + ((0.25 0.75 0,0.25 0.25 0,0.25 0.25 0.75,0.25 0.75 0.75,0.25 0.75 0)),\ + ((0.25 0.25 0.75,0.75 0.25 0.75,0.75 0.75 0.75,0.25 0.75 0.75,0.25 0.25 0.75)),\ + ((0 0 0,0 0 1,0 1 1,0 1 0,0 0 0)),\ + ((0 0 0,1 0 0,1 0 1,0 0 1,0 0 0)),\ + ((1 1 1,0 1 1,0 0 1,1 0 1,1 1 1)),\ + ((1 1 1,1 0 1,1 0 0,1 1 0,1 1 1)))", + valid_surface_holes2); + BOOST_CHECK(bg::is_valid(valid_surface_holes2, failure)); +*/ + // Invalid polyhedral surface: repeated points on face (polygon) + polyhedral_surface_type invalid_surface_repeated_points; + bg::read_wkt( + "POLYHEDRALSURFACE(((0 0 0,0 1 0,0 1 0,1 1 0,1 0 0,0 0 0)),((0 0 0,0 0 1,0 1 1,0 1 0,0 0 0)))", + invalid_surface_repeated_points); + //Repeated points are allowed by default, so we have to use a user defined policy + bg::failure_type_policy visitor; + BOOST_CHECK(!bg::is_valid(invalid_surface_repeated_points, visitor, bg::default_strategy())); + BOOST_CHECK(visitor.failure() == bg::failure_duplicate_points); + + // Invalid polyhedral surface: non planar face (polygon) + polyhedral_surface_type invalid_surface_non_planar_face; + bg::read_wkt( + "POLYHEDRALSURFACE(((0 0 0,1 0 0,0 1 0,0 0 1,0 0 0)),((1 0 0,1 1 0,0 1 0,1 0 0)))", + invalid_surface_non_planar_face); + BOOST_CHECK(!bg::is_valid(invalid_surface_non_planar_face, failure)); + BOOST_CHECK(failure == bg::failure_non_coplanar_points_on_face); + + // Invalid polyhedral surface: colinear points on face (polygon) + polyhedral_surface_type invalid_surface_collinear_points; + bg::read_wkt( + "POLYHEDRALSURFACE(((1 0 0,1 1 0,0 1 0,1 0 0)),((0 0 0,1 0 0,2 0 0,0 0 0)))", + invalid_surface_collinear_points); + BOOST_CHECK(!bg::is_valid(invalid_surface_collinear_points, failure)); + BOOST_CHECK(failure == bg::failure_collinear_points_on_face); + + // Invalid polyhedral surface: few points on face (polygon) + polyhedral_surface_type invalid_surface_few_points; + bg::read_wkt( + "POLYHEDRALSURFACE(((1 0 0,1 1 0,0 1 0,1 0 0)),((0 0 0,1 0 0,0 0 0)))", + invalid_surface_few_points); + BOOST_CHECK(!bg::is_valid(invalid_surface_few_points, failure)); + BOOST_CHECK(failure == bg::failure_few_points_on_face); + bg::read_wkt( + "POLYHEDRALSURFACE(((0 0 0,1 0 0)),((1 0 0,1 1 0,0 1 0,1 0 0)))", + invalid_surface_few_points); + BOOST_CHECK(!bg::is_valid(invalid_surface_few_points, failure)); + BOOST_CHECK(failure == bg::failure_few_points_on_face); + + // Invalid polyhedral surface; incosistent orientation + polyhedral_surface_type invalid_surface_inconsistent_orientation; + bg::read_wkt( + "POLYHEDRALSURFACE(((0 0 0,0 1 0,1 1 0,1 0 0,0 0 0)),((0 0 0,0 0 1,0 1 1,0 1 0,0 0 0)),\ + ((0 0 0,1 0 0,1 0 1,0 0 1,0 0 0)),((1 1 1,1 0 1,0 0 1,0 1 1,1 1 1)),\ + ((1 1 1,1 0 1,1 0 0,1 1 0,1 1 1)),((1 1 1,1 1 0,0 1 0,0 1 1,1 1 1)))", + invalid_surface_inconsistent_orientation); + BOOST_CHECK(!bg::is_valid(invalid_surface_inconsistent_orientation, failure)); + BOOST_CHECK(failure == bg::failure_inconsistent_orientation); + + // Invalid polyhedral surface: invalid intersection; + // the boundary of a face intersects the interior of another face + polyhedral_surface_type invalid_surface_invalid_intersection_boundary_interior; + bg::read_wkt( + "POLYHEDRALSURFACE(((0 0 0,1 0 0,0 0 1,0 0 0)),((0 1 0,0.5 -0.5 0.5,1 1 0,0 1 0)))", + invalid_surface_invalid_intersection_boundary_interior); + BOOST_CHECK(!bg::is_valid(invalid_surface_invalid_intersection_boundary_interior, failure)); + BOOST_CHECK(failure == bg::failure_invalid_intersection); + + // Invalid polyhedral surface: invalid intersection; + // the boundary of a face intersects the boundary of another face + polyhedral_surface_type invalid_surface_invalid_intersection_boundary_boundary; + bg::read_wkt( + "POLYHEDRALSURFACE(((0 0 0,1 0 0,1 0 1,0 0 1,0 0 0)),((0 1 0,0 -1 1,1 -1 1,1 1 0,0 1 0)))", + invalid_surface_invalid_intersection_boundary_boundary); + BOOST_CHECK(!bg::is_valid(invalid_surface_invalid_intersection_boundary_boundary, failure)); + BOOST_CHECK(failure == bg::failure_invalid_intersection); + + // Invalid polyhedral surface: invalid intersection; + polyhedral_surface_type invalid_surface_invalid_intersection_in_common_vertex; + bg::read_wkt( + "POLYHEDRALSURFACE(((0 0 0,1 0 0,1 0 1,0 0 1,0 0 0)),((0 1 0,1 0 1,1 1 0,0 1 0)))", + invalid_surface_invalid_intersection_in_common_vertex); + BOOST_CHECK(!bg::is_valid(invalid_surface_invalid_intersection_in_common_vertex, failure)); + BOOST_CHECK(failure == bg::failure_disconnected_surface); + + polyhedral_surface_type invalid_surface_invalid_intersection_partial_edge; + bg::read_wkt( + "POLYHEDRALSURFACE(((0 0 0,1 0 0,1 0 1,0 0 1,0 0 0)),((0 1 0,0.5 0 1,1 0 1,1 1 0,0 1 0)))", + invalid_surface_invalid_intersection_partial_edge); + BOOST_CHECK(!bg::is_valid(invalid_surface_invalid_intersection_partial_edge, failure)); + BOOST_CHECK(failure == bg::failure_invalid_intersection); + + // Invalid polyhedral surface: disconnected polygon patches + polyhedral_surface_type invalid_surface_disconnected_polygons; + bg::read_wkt( + "POLYHEDRALSURFACE(((0 0 0,1 0 0,0 1 0,0 0 0)),((1 1 0,2 1 0,2 2 0,1 2 0,1 1 0)))", + invalid_surface_disconnected_polygons); + BOOST_CHECK(!bg::is_valid(invalid_surface_disconnected_polygons, failure)); + BOOST_CHECK(failure == bg::failure_disconnected_surface); + + // Valid polyhedral surface: coplanar polygons + polyhedral_surface_type valid_surface_coplanar_polygons; + bg::read_wkt( + "POLYHEDRALSURFACE(((0 0 0,1 0 0,2 1 0,1 1 0,0 2 0,0 0 0)),((1 1 0,2 1 0,2 2 0,1 2 0,1 1 0)))", + valid_surface_coplanar_polygons); + BOOST_CHECK(bg::is_valid(valid_surface_coplanar_polygons, failure)); + + // Invalid polyhedral surface + // https://github.com/boostorg/geometry/issues/1406 + polyhedral_surface_type invalid_surface_intersection; + bg::read_wkt( + "POLYHEDRALSURFACE(((0 0 0,1 0 0,2 1 0,1 1 0,0.8 0.8 0,0 2 0,0 0 0)),((1 1 0,2 1 0,2 2 0,1 2 0,1 1 0)),((0.8 0.8 0,1 1.5 0,0 2 0,0.8 0.8 0)))", + invalid_surface_intersection); + //BOOST_CHECK(!bg::is_valid(invalid_surface_intersection, failure)); + //BOOST_CHECK(failure == bg::failure_invalid_intersection); + + // Invalid polyhedral surface: disconnected polygon patches + polyhedral_surface_type invalid_surface_disconnected_polygons2; + bg::read_wkt( + "POLYHEDRALSURFACE(((0 0 0,1 0 0,0 1 0,0 0 0)),((0 0 0,0 0 1,1 0 0,0 0 0)),\ + ((1 1 0,2 1 0,2 2 0,1 2 0,1 1 0)),((1 1 0,1 0 2,2 0 2,2 1 0,1 1 0)))", + invalid_surface_disconnected_polygons2); + BOOST_CHECK(!bg::is_valid(invalid_surface_disconnected_polygons2, failure)); + BOOST_CHECK(failure == bg::failure_disconnected_surface); + + // Invalid polyhedral surface: invalid intersection (intersection vertex with edge of another polygon) + // Issue https://github.com/boostorg/geometry/issues/1406 + polyhedral_surface_type invalid_surface_invalid_intersection_vertex_edge; + bg::read_wkt( + "POLYHEDRALSURFACE(((0 0 0,1 0 0,0 1 0,0 0 0)),((0.5 0.5 0,2 1 0,2 2 0,1 2 0,0.5 0.5 0)))", + invalid_surface_invalid_intersection_vertex_edge); + BOOST_CHECK(!bg::is_valid(invalid_surface_invalid_intersection_vertex_edge, failure)); + BOOST_CHECK(failure == bg::failure_disconnected_surface); + + // Invalid polyhedral surface: invalid intersection (intersection vertex with vertex of another polygon) + // Issue 1406 + polyhedral_surface_type invalid_surface_invalid_intersection_vertex_vertex; + bg::read_wkt( + "POLYHEDRALSURFACE(((0 0 0,1 0 0,0 1 0,0 0 0)),((1 0 0,2 1 0,2 2 0,1 2 0,1 0 0)))", + invalid_surface_invalid_intersection_vertex_vertex); + BOOST_CHECK(!bg::is_valid(invalid_surface_invalid_intersection_vertex_vertex, failure)); + BOOST_CHECK(failure == bg::failure_disconnected_surface); + + // Invalid polyhedral surface: invalid intersection (intersection vertex with edge of another polygon) + polyhedral_surface_type invalid_surface_invalid_intersection_vertex_edge2; + bg::read_wkt( + "POLYHEDRALSURFACE(((0 0 0,0 1 0,1 1 0,1 0 0,0 0 0)),((0 0 0,0 0 1,0 1 1,0 1 0,0 0 0)),\ + ((0 0 0,1 0 0,0.5 0 1,0 0 0)),((1 1 1,0 1 1,0 0 1,1 0 1,1 1 1)),\ + ((1 1 1,1 0 1,1 0 0,1 1 0,1 .5 .5,1 1 1)))", + invalid_surface_invalid_intersection_vertex_edge2); + BOOST_CHECK(!bg::is_valid(invalid_surface_invalid_intersection_vertex_edge2, failure)); + BOOST_CHECK(failure == bg::failure_invalid_intersection); + + // Invalid polyhedral surface: invalid intersection + polyhedral_surface_type invalid_surface_invalid_intersection_parallel_edges; + bg::read_wkt( + "POLYHEDRALSURFACE(((0 0 0,1 0 0,0 1 0,0 0 0)),((0 1 0,0.5 0.5 0,1 0 0,2 1 0,2 2 0,1 2 0,0 1 0)))", + invalid_surface_invalid_intersection_parallel_edges); + BOOST_CHECK(!bg::is_valid(invalid_surface_invalid_intersection_parallel_edges, failure)); + BOOST_CHECK(failure == bg::failure_invalid_intersection); + + // Invalid polyhedral surface: invalid intersection (overlapping faces) + polyhedral_surface_type invalid_surface_invalid_intersection_operlapping_faces; + bg::read_wkt( + "POLYHEDRALSURFACE(((0 0 0,1 0.5 0,0.5 1 0,0 0 0)),((0 1 0,1 0 0,2 1 0,2 2 0,1 2 0,0 1 0)))", + invalid_surface_invalid_intersection_operlapping_faces); + BOOST_CHECK(!bg::is_valid(invalid_surface_invalid_intersection_operlapping_faces, failure)); + BOOST_CHECK(failure == bg::failure_invalid_intersection); + + // Invalid polyhedral surface: invalid intersection (overlapping faces with common vertex) + polyhedral_surface_type invalid_surface_invalid_intersection_operlapping_faces2; + bg::read_wkt( + "POLYHEDRALSURFACE(((0 0 0,1 0 0,0.5 1 0,0 0 0)),((0 1 0,1 0 0,2 1 0,2 2 0,1 2 0,0 1 0)))", + invalid_surface_invalid_intersection_operlapping_faces2); + BOOST_CHECK(!bg::is_valid(invalid_surface_invalid_intersection_operlapping_faces2, failure)); + BOOST_CHECK(failure == bg::failure_invalid_intersection); + + // Invalid polyhedral surface: invalid intersection of face + polyhedral_surface_type invalid_surface_invalid_face; + bg::read_wkt( + "POLYHEDRALSURFACE(((0 0 0,1 0 0,0 1 0,0 0 0)),((0 1 0,1 0 0,2 2 0,2 1 0,1 2 0,0 1 0)))", + invalid_surface_invalid_face); + BOOST_CHECK(!bg::is_valid(invalid_surface_invalid_face, failure)); + BOOST_CHECK(failure == bg::failure_self_intersections); + + // Invalid polyhedral surface: non-manifold edge, i.e. an edge belongs to more than two polygons + // This will return an iconsistent orientation failure since if more than two polygons share an + // edge, there is a pair of polygons that share the edge in the same direction + polyhedral_surface_type invalid_surface_non_manifold_edge; + bg::read_wkt( + "POLYHEDRALSURFACE(((0 0 0,1 0 0,0 1 0,0 0 0)),((0 1 0,1 0 0,2 1 0,2 2 0,1 2 0,0 1 0)),\ + ((0 1 0,1 0 0,1 1 1,0 1 0)))", + invalid_surface_non_manifold_edge); + BOOST_CHECK(!bg::is_valid(invalid_surface_non_manifold_edge, failure)); + BOOST_CHECK(failure == bg::failure_inconsistent_orientation); + + polyhedral_surface_type invalid_surface_empty_ring; + bg::read_wkt( + "POLYHEDRALSURFACE(((0 0 0,0 1 0,1 1 0,1 0 0,0 0 0)),((0 0 0,0 0 1,0 1 1,0 1 0,0 0 0)),\ + (()))", + invalid_surface_empty_ring); + BOOST_CHECK(!bg::is_valid(invalid_surface_empty_ring, failure)); + BOOST_CHECK(failure == bg::failure_few_points_on_face); + + // Create a polyhedral surface with 50 triangles forming a "fan" around the origin in the XY plane + polyhedral_surface_type large_surface_tin; + std::ostringstream wkt; + wkt << "POLYHEDRALSURFACE("; + double radius = 10.0; + double z = 0.0; + int n = 50; + for (int i = 0; i < n; ++i) + { + double angle1 = 2.0 * M_PI * i / n; + double angle2 = 2.0 * M_PI * (i + 1) / n; + double x1 = radius * std::cos(angle1); + double y1 = radius * std::sin(angle1); + double x2 = radius * std::cos(angle2); + double y2 = radius * std::sin(angle2); + wkt << "((" << 0 << " " << 0 << " " << z << "," + << x1 << " " << y1 << " " << z << "," + << x2 << " " << y2 << " " << z << "," + << 0 << " " << 0 << " " << z << "))"; + if (i != n - 1) + wkt << ","; + } + wkt << ")"; + bg::read_wkt(wkt.str(), large_surface_tin); + BOOST_CHECK(bg::is_valid(large_surface_tin, failure)); + + // Large polyhedral surface: Triangulated ball (sphere approximation) + polyhedral_surface_type large_surface_ball; + bg::read_wkt( + "POLYHEDRALSURFACE(" + "((0.587785 0.000000 -0.809017, 0.000000 0.000000 -1.000000, 0.181636 0.559017 -0.809017, 0.587785 0.000000 -0.809017))," + "((0.181636 0.559017 0.809017, 0.000000 0.000000 1.000000, 0.587785 -0.000000 0.809017, 0.181636 0.559017 0.809017))," + "((0.181636 -0.559017 0.809017, 0.587785 -0.000000 0.809017, 0.000000 0.000000 1.000000, 0.181636 -0.559017 0.809017))," + "((0.181636 -0.559017 0.809017, 0.000000 0.000000 1.000000, -0.475528 -0.345492 0.809017, 0.181636 -0.559017 0.809017))," + "((0.181636 -0.559017 -0.809017, 0.000000 0.000000 -1.000000, 0.587785 0.000000 -0.809017, 0.181636 -0.559017 -0.809017))," + "((0.181636 -0.559017 -0.809017, -0.475528 -0.345492 -0.809017, 0.000000 0.000000 -1.000000, 0.181636 -0.559017 -0.809017))," + "((-0.475528 0.345492 -0.809017, 0.181636 0.559017 -0.809017, 0.000000 0.000000 -1.000000, -0.475528 0.345492 -0.809017))," + "((-0.475528 0.345492 -0.809017, 0.000000 0.000000 -1.000000, -0.475528 -0.345492 -0.809017, -0.475528 0.345492 -0.809017))," + "((-0.475528 0.345492 0.809017, 0.000000 0.000000 1.000000, 0.181636 0.559017 0.809017, -0.475528 0.345492 0.809017))," + "((-0.475528 0.345492 0.809017, -0.475528 -0.345492 0.809017, 0.000000 0.000000 1.000000, -0.475528 0.345492 0.809017))," + "((0.293893 -0.904508 -0.309017, 0.951057 0.000000 0.309017, 0.293893 -0.904508 0.309017, 0.293893 -0.904508 -0.309017))," + "((0.293893 -0.904508 -0.309017, 0.951057 0.000000 -0.309017, 0.951057 0.000000 0.309017, 0.293893 -0.904508 -0.309017))," + "((0.293893 0.904508 0.309017, 0.951057 0.000000 0.309017, 0.951057 0.000000 -0.309017, 0.293893 0.904508 0.309017))," + "((0.293893 0.904508 0.309017, 0.951057 0.000000 -0.309017, 0.293893 0.904508 -0.309017, 0.293893 0.904508 0.309017))," + "((-0.769421 -0.559017 0.309017, 0.293893 -0.904508 -0.309017, 0.293893 -0.904508 0.309017, -0.769421 -0.559017 0.309017))," + "((-0.769421 -0.559017 0.309017, -0.769421 -0.559017 -0.309017, 0.293893 -0.904508 -0.309017, -0.769421 -0.559017 0.309017))," + "((0.587785 0.000000 -0.809017, 0.293893 0.904508 -0.309017, 0.951057 0.000000 -0.309017, 0.587785 0.000000 -0.809017))," + "((0.587785 0.000000 -0.809017, 0.181636 0.559017 -0.809017, 0.293893 0.904508 -0.309017, 0.587785 0.000000 -0.809017))," + "((-0.769421 0.559017 -0.309017, 0.293893 0.904508 0.309017, 0.293893 0.904508 -0.309017, -0.769421 0.559017 -0.309017))," + "((-0.769421 0.559017 -0.309017, -0.769421 0.559017 0.309017, 0.293893 0.904508 0.309017, -0.769421 0.559017 -0.309017))," + "((-0.769421 0.559017 -0.309017, -0.769421 -0.559017 -0.309017, -0.769421 -0.559017 0.309017, -0.769421 0.559017 -0.309017))," + "((-0.769421 0.559017 -0.309017, -0.769421 -0.559017 0.309017, -0.769421 0.559017 0.309017, -0.769421 0.559017 -0.309017))," + "((0.181636 0.559017 0.809017, 0.951057 0.000000 0.309017, 0.293893 0.904508 0.309017, 0.181636 0.559017 0.809017))," + "((0.181636 0.559017 0.809017, 0.587785 -0.000000 0.809017, 0.951057 0.000000 0.309017, 0.181636 0.559017 0.809017))," + "((0.181636 -0.559017 0.809017, 0.293893 -0.904508 0.309017, 0.951057 0.000000 0.309017, 0.181636 -0.559017 0.809017))," + "((0.181636 -0.559017 0.809017, 0.951057 0.000000 0.309017, 0.587785 -0.000000 0.809017, 0.181636 -0.559017 0.809017))," + "((0.181636 -0.559017 0.809017, -0.769421 -0.559017 0.309017, 0.293893 -0.904508 0.309017, 0.181636 -0.559017 0.809017))," + "((0.181636 -0.559017 0.809017, -0.475528 -0.345492 0.809017, -0.769421 -0.559017 0.309017, 0.181636 -0.559017 0.809017))," + "((0.181636 -0.559017 -0.809017, 0.951057 0.000000 -0.309017, 0.293893 -0.904508 -0.309017, 0.181636 -0.559017 -0.809017))," + "((0.181636 -0.559017 -0.809017, 0.587785 0.000000 -0.809017, 0.951057 0.000000 -0.309017, 0.181636 -0.559017 -0.809017))," + "((0.181636 -0.559017 -0.809017, 0.293893 -0.904508 -0.309017, -0.769421 -0.559017 -0.309017, 0.181636 -0.559017 -0.809017))," + "((0.181636 -0.559017 -0.809017, -0.769421 -0.559017 -0.309017, -0.475528 -0.345492 -0.809017, 0.181636 -0.559017 -0.809017))," + "((-0.475528 0.345492 -0.809017, 0.293893 0.904508 -0.309017, 0.181636 0.559017 -0.809017, -0.475528 0.345492 -0.809017))," + "((-0.475528 0.345492 -0.809017, -0.769421 0.559017 -0.309017, 0.293893 0.904508 -0.309017, -0.475528 0.345492 -0.809017))," + "((-0.475528 0.345492 -0.809017, -0.769421 -0.559017 -0.309017, -0.769421 0.559017 -0.309017, -0.475528 0.345492 -0.809017))," + "((-0.475528 0.345492 -0.809017, -0.475528 -0.345492 -0.809017, -0.769421 -0.559017 -0.309017, -0.475528 0.345492 -0.809017))," + "((-0.475528 0.345492 0.809017, 0.293893 0.904508 0.309017, -0.769421 0.559017 0.309017, -0.475528 0.345492 0.809017))," + "((-0.475528 0.345492 0.809017, 0.181636 0.559017 0.809017, 0.293893 0.904508 0.309017, -0.475528 0.345492 0.809017))," + "((-0.475528 0.345492 0.809017, -0.769421 0.559017 0.309017, -0.769421 -0.559017 0.309017, -0.475528 0.345492 0.809017))," + "((-0.475528 0.345492 0.809017, -0.769421 -0.559017 0.309017, -0.475528 -0.345492 0.809017, -0.475528 0.345492 0.809017))" + ")", + large_surface_ball); + BOOST_CHECK(bg::is_valid(large_surface_ball, failure)); +} diff --git a/test/geometries/CMakeLists.txt b/test/geometries/CMakeLists.txt index 8157cc1cc2..30938c605a 100644 --- a/test/geometries/CMakeLists.txt +++ b/test/geometries/CMakeLists.txt @@ -25,6 +25,7 @@ foreach(item IN ITEMS point_xyz polygon polyhedral_surface + # polyhedral_surface_fail ring segment infinite_line diff --git a/test/geometries/Jamfile b/test/geometries/Jamfile index 69c872464a..ada79e52fd 100644 --- a/test/geometries/Jamfile +++ b/test/geometries/Jamfile @@ -35,6 +35,7 @@ test-suite boost-geometry-geometries [ run point_xyz.cpp : : : : geometries_point_xyz ] [ run polygon.cpp : : : : geometries_polygon ] [ run polyhedral_surface.cpp : : : : geometries_polyhedral_surface ] + [ compile-fail polyhedral_surface_fail.cpp : : geometries_polyhedral_surface_fail ] [ run ring.cpp : : : : geometries_ring ] [ run segment.cpp : : : : geometries_segment ] [ run infinite_line.cpp : : : : geometries_infinite_line ] diff --git a/test/geometries/polyhedral_surface.cpp b/test/geometries/polyhedral_surface.cpp index 3e775b17a0..e36ecf7de9 100644 --- a/test/geometries/polyhedral_surface.cpp +++ b/test/geometries/polyhedral_surface.cpp @@ -30,7 +30,7 @@ int test_main(int, char* []) // Intializing an empty polyhedral surface (default constructor) polyhedral_t polyhedral0; - BOOST_CHECK_EQUAL(boost::size(polyhedral0), 0); + BOOST_CHECK_EQUAL(boost::size(polyhedral0), 0u); // Creating a polyhedral surface using standard initialized list polyhedral_t polyhedral1 = {{{{0, 0, 0}, {0, 1, 0}, {1, 1, 0}, {1, 0, 0}, {0, 0, 0}}}, @@ -49,17 +49,19 @@ int test_main(int, char* []) polyhedral1[0].outer()[0] = point_t(0, 2, 0); // Test that the polyhedral surface has 6 polygons - BOOST_CHECK_EQUAL(boost::size(polyhedral1), 6); + BOOST_CHECK_EQUAL(boost::size(polyhedral1), 6u); // Test that each polygon has 5 points for (const auto& polygon : polyhedral1) { - BOOST_CHECK_EQUAL(boost::size(polygon.outer()), 5); + BOOST_CHECK_EQUAL(boost::size(polygon.outer()), 5u); } // clear polyhedral surface bg::clear(polyhedral1); - BOOST_CHECK_EQUAL(boost::size(polyhedral1), 0); + BOOST_CHECK_EQUAL(boost::size(polyhedral1), 0u); + // Test that the polyhedral surface is empty + BOOST_CHECK(boost::empty(polyhedral1)); // read/write polyhedral surface wkt polyhedral_t polyhedral2; @@ -76,11 +78,25 @@ int test_main(int, char* []) out << bg::wkt(ps1); BOOST_CHECK_EQUAL(out.str(), "POLYHEDRALSURFACE(((0 0 0,0 1 0,1 1 0,1 0 0,0 0 0)))"); // Test that the polyhedral surface has 1 polygon - BOOST_CHECK_EQUAL(boost::size(ps1), 1); + BOOST_CHECK_EQUAL(boost::size(ps1), 1u); // Test concepts BOOST_CONCEPT_ASSERT( (bg::concepts::PolyhedralSurface) ); BOOST_CONCEPT_ASSERT( (bg::concepts::ConstPolyhedralSurface) ); + /* The following tests should fail + // Define a 2-dimensional polyhedral surface + using point2_t = bg::model::point; + using polygon2_t = bg::model::polygon; + using polyhedral2_t = bg::model::polyhedral_surface; + + // Create a 3D polyhedral surface with geographic coordinates + using point_geo_t = bg::model::point>; + using polygon_geo_t = bg::model::polygon; + using polyhedral_geo_t = bg::model::polyhedral_surface; + + BOOST_CONCEPT_ASSERT( (bg::concepts::PolyhedralSurface) ); + BOOST_CONCEPT_ASSERT( (bg::concepts::PolyhedralSurface) ); + */ return 0; } diff --git a/test/geometries/polyhedral_surface_fail.cpp b/test/geometries/polyhedral_surface_fail.cpp new file mode 100644 index 0000000000..d54a67a038 --- /dev/null +++ b/test/geometries/polyhedral_surface_fail.cpp @@ -0,0 +1,43 @@ +// Boost.Geometry +// Unit Test + +// Copyright (c) 2025 Oracle and/or its affiliates. +// Contributed and/or modified by Vissarion Fysikopoulos, on behalf of Oracle + +// Use, modification and distribution is subject to the Boost Software License, +// Version 1.0. (See accompanying file LICENSE_1_0.txt or copy at +// http://www.boost.org/LICENSE_1_0.txt) + +// Polyhedral surface tests invalid concepts + +#include +#include + +#include + +#include +#include +#include +#include + +int test_main(int, char* []) +{ + // The following tests should fail + + namespace bg = boost::geometry; + + // Define a 2-dimensional polyhedral surface + using point2_t = bg::model::point; + using polygon2_t = bg::model::polygon; + using polyhedral2_t = bg::model::polyhedral_surface; + + // Create a 3D polyhedral surface with geographic coordinates + using point_geo_t = bg::model::point>; + using polygon_geo_t = bg::model::polygon; + using polyhedral_geo_t = bg::model::polyhedral_surface; + + BOOST_CONCEPT_ASSERT( (bg::concepts::PolyhedralSurface) ); + BOOST_CONCEPT_ASSERT( (bg::concepts::PolyhedralSurface) ); + + return 0; +}