From acaed28f122b462606298fb0433e7a8bf37122e0 Mon Sep 17 00:00:00 2001 From: Barend Gehrels Date: Sun, 25 Feb 2024 10:33:59 +0000 Subject: [PATCH] fix: set turns in closed clusters as non traversable --- .../detail/buffer/buffer_inserter.hpp | 5 +- .../detail/buffer/buffer_policies.hpp | 10 - .../buffer/buffered_piece_collection.hpp | 114 +++++++---- .../detail/overlay/cluster_info.hpp | 8 +- .../overlay/discard_duplicate_turns.hpp | 7 + .../overlay/enrich_intersection_points.hpp | 47 +++-- .../detail/overlay/handle_colocations.hpp | 19 +- .../algorithms/detail/overlay/traversal.hpp | 54 +++-- .../strategies/cartesian/buffer_end_round.hpp | 2 +- .../geographic/buffer_end_round.hpp | 2 +- .../buffer/buffer_variable_width.cpp | 184 ++++++++++++++++++ 11 files changed, 360 insertions(+), 92 deletions(-) create mode 100644 test/algorithms/buffer/buffer_variable_width.cpp diff --git a/include/boost/geometry/algorithms/detail/buffer/buffer_inserter.hpp b/include/boost/geometry/algorithms/detail/buffer/buffer_inserter.hpp index a3be2858e..2f0c5e3c0 100644 --- a/include/boost/geometry/algorithms/detail/buffer/buffer_inserter.hpp +++ b/include/boost/geometry/algorithms/detail/buffer/buffer_inserter.hpp @@ -972,8 +972,9 @@ inline void buffer_inserter(GeometryInput const& geometry_input, OutputIterator { collection.check_turn_in_original(); } - - collection.verify_turns(); + collection.handle_colocations(); + collection.check_turn_in_pieces(); + collection.make_traversable_consistent_per_cluster(); // Visit the piece collection. This does nothing (by default), but // optionally a debugging tool can be attached (e.g. console or svg), diff --git a/include/boost/geometry/algorithms/detail/buffer/buffer_policies.hpp b/include/boost/geometry/algorithms/detail/buffer/buffer_policies.hpp index e75532c3f..918c2a9ee 100644 --- a/include/boost/geometry/algorithms/detail/buffer/buffer_policies.hpp +++ b/include/boost/geometry/algorithms/detail/buffer/buffer_policies.hpp @@ -281,16 +281,6 @@ struct turn_overlaps_box Strategy const& m_strategy; }; -struct enriched_map_buffer_include_policy -{ - template - static inline bool include(Operation const& op) - { - return op != detail::overlay::operation_intersection - && op != detail::overlay::operation_blocked; - } -}; - }} // namespace detail::buffer #endif // DOXYGEN_NO_DETAIL diff --git a/include/boost/geometry/algorithms/detail/buffer/buffered_piece_collection.hpp b/include/boost/geometry/algorithms/detail/buffer/buffered_piece_collection.hpp index 66943b303..bd39116fc 100644 --- a/include/boost/geometry/algorithms/detail/buffer/buffered_piece_collection.hpp +++ b/include/boost/geometry/algorithms/detail/buffer/buffered_piece_collection.hpp @@ -249,7 +249,8 @@ struct buffered_piece_collection // Offsetted rings, and representations of original ring(s) // both indexed by multi_index - buffered_ring_collection > offsetted_rings; + using ring_collection_t = buffered_ring_collection>; + ring_collection_t offsetted_rings; std::vector original_rings; std::vector m_linear_end_points; @@ -305,29 +306,6 @@ struct buffered_piece_collection } } - inline void verify_turns() - { - typedef detail::overlay::indexed_turn_operation - < - buffer_turn_operation_type - > indexed_turn_operation; - typedef std::map - < - ring_identifier, - std::vector - > mapped_vector_type; - mapped_vector_type mapped_vector; - - detail::overlay::create_map(m_turns, mapped_vector, - enriched_map_buffer_include_policy()); - - // Sort turns over offsetted ring(s) - for (auto& pair : mapped_vector) - { - std::sort(pair.second.begin(), pair.second.end(), buffer_less()); - } - } - inline void deflate_check_turns() { if (! m_has_deflated) @@ -470,24 +448,25 @@ struct buffered_piece_collection } update_turn_administration(); + } - { - // Check if turns are inside pieces - turn_in_piece_visitor - < - typename geometry::cs_tag::type, - turn_vector_type, piece_vector_type, DistanceStrategy, Strategy - > visitor(m_turns, m_pieces, m_distance_strategy, m_strategy); + inline void check_turn_in_pieces() + { + // Check if turns are inside pieces + turn_in_piece_visitor + < + typename geometry::cs_tag::type, + turn_vector_type, piece_vector_type, DistanceStrategy, Strategy + > visitor(m_turns, m_pieces, m_distance_strategy, m_strategy); - geometry::partition - < - box_type - >::apply(m_turns, m_pieces, visitor, - turn_get_box(m_strategy), - turn_overlaps_box(m_strategy), - piece_get_box(m_strategy), - piece_overlaps_box(m_strategy)); - } + geometry::partition + < + box_type + >::apply(m_turns, m_pieces, visitor, + turn_get_box(m_strategy), + turn_overlaps_box(m_strategy), + piece_get_box(m_strategy), + piece_overlaps_box(m_strategy)); } inline void start_new_ring(bool deflate) @@ -898,6 +877,61 @@ struct buffered_piece_collection //------------------------------------------------------------------------- + inline void handle_colocations() + { + if (! detail::overlay::handle_colocations + < + false, false, overlay_buffer, + ring_collection_t, ring_collection_t + >(m_turns, m_clusters, m_robust_policy)) + { + return; + } + + detail::overlay::gather_cluster_properties + < + false, false, overlay_buffer + >(m_clusters, m_turns, detail::overlay::operation_union, + offsetted_rings, offsetted_rings, m_strategy); + + for (auto const& cluster : m_clusters) + { + if (cluster.second.open_count == 0 && cluster.second.spike_count == 0) + { + // If the cluster is completely closed, mark it as not traversable. + for (auto const& index : cluster.second.turn_indices) + { + m_turns[index].is_turn_traversable = false; + } + } + } + } + + inline void make_traversable_consistent_per_cluster() + { + for (auto const& cluster : m_clusters) + { + bool is_traversable = false; + for (auto const& index : cluster.second.turn_indices) + { + if (m_turns[index].is_turn_traversable) + { + // If there is one turn traversable in the cluster, + // then all turns should be traversable. + is_traversable = true; + break; + } + } + if (is_traversable) + { + for (auto const& index : cluster.second.turn_indices) + { + m_turns[index].is_turn_traversable = true; + } + } + } + } + inline void enrich() { enrich_intersection_points(m_turns, diff --git a/include/boost/geometry/algorithms/detail/overlay/cluster_info.hpp b/include/boost/geometry/algorithms/detail/overlay/cluster_info.hpp index 19343488f..dae519c18 100644 --- a/include/boost/geometry/algorithms/detail/overlay/cluster_info.hpp +++ b/include/boost/geometry/algorithms/detail/overlay/cluster_info.hpp @@ -27,11 +27,11 @@ struct cluster_info std::set turn_indices; //! Number of open spaces (e.g. 2 for touch) - std::size_t open_count; + std::size_t open_count{0}; - inline cluster_info() - : open_count(0) - {} + //! Number of spikes, where a segment goes to the cluster point + //! and leaves immediately in the opposite direction. + std::size_t spike_count{0}; }; diff --git a/include/boost/geometry/algorithms/detail/overlay/discard_duplicate_turns.hpp b/include/boost/geometry/algorithms/detail/overlay/discard_duplicate_turns.hpp index ca537e081..604dd9fd3 100644 --- a/include/boost/geometry/algorithms/detail/overlay/discard_duplicate_turns.hpp +++ b/include/boost/geometry/algorithms/detail/overlay/discard_duplicate_turns.hpp @@ -137,6 +137,13 @@ inline void discard_duplicate_start_turns(Turns& turns, { for (std::size_t const& i : it->second) { + if (turns[i].cluster_id != turn.cluster_id) + { + // The turns are not part of the same cluster, + // or one is clustered and the other is not. + // This is not corresponding. + continue; + } if (corresponding_turn(turn, turns[i], geometry0, geometry1)) { diff --git a/include/boost/geometry/algorithms/detail/overlay/enrich_intersection_points.hpp b/include/boost/geometry/algorithms/detail/overlay/enrich_intersection_points.hpp index 350fc284d..7ce96b91c 100644 --- a/include/boost/geometry/algorithms/detail/overlay/enrich_intersection_points.hpp +++ b/include/boost/geometry/algorithms/detail/overlay/enrich_intersection_points.hpp @@ -41,6 +41,7 @@ #include #include #include +#include #include #ifdef BOOST_GEOMETRY_DEBUG_ENRICH @@ -383,6 +384,7 @@ inline void enrich_intersection_points(Turns& turns, ? detail::overlay::operation_intersection : detail::overlay::operation_union; constexpr bool is_dissolve = OverlayType == overlay_dissolve; + constexpr bool is_buffer = OverlayType == overlay_buffer; using turn_type = typename boost::range_value::type; using indexed_turn_operation = detail::overlay::indexed_turn_operation @@ -396,16 +398,36 @@ inline void enrich_intersection_points(Turns& turns, std::vector >; - // From here on, turn indexes are used (in clusters, next_index, etc) - // and turns may not be DELETED - they may only be flagged as discarded - discard_duplicate_start_turns(turns, geometry1, geometry2); + // Turns are often used by index (in clusters, next_index, etc) + // and turns may therefore NOT be DELETED - they may only be flagged as discarded bool has_cc = false; - bool const has_colocations - = detail::overlay::handle_colocations + bool has_colocations = false; + + if BOOST_GEOMETRY_CONSTEXPR (! is_buffer) + { + // Handle colocations, gathering clusters and (below) their properties. + has_colocations = detail::overlay::handle_colocations + < + Reverse1, Reverse2, OverlayType, Geometry1, Geometry2 + >(turns, clusters, robust_policy); + // Gather cluster properties (using even clusters with + // discarded turns - for open turns) + detail::overlay::gather_cluster_properties < - Reverse1, Reverse2, OverlayType, Geometry1, Geometry2 - >(turns, clusters, robust_policy); + Reverse1, + Reverse2, + OverlayType + >(clusters, turns, target_operation, + geometry1, geometry2, strategy); + } + else + { + // For buffer, this was already done before calling enrich_intersection_points. + has_colocations = ! clusters.empty(); + } + + discard_duplicate_start_turns(turns, geometry1, geometry2); // Discard turns not part of target overlay for (auto& turn : turns) @@ -479,17 +501,8 @@ inline void enrich_intersection_points(Turns& turns, if (has_colocations) { - // First gather cluster properties (using even clusters with - // discarded turns - for open turns), then clean up clusters - detail::overlay::gather_cluster_properties - < - Reverse1, - Reverse2, - OverlayType - >(clusters, turns, target_operation, - geometry1, geometry2, strategy); - detail::overlay::cleanup_clusters(turns, clusters); + detail::overlay::colocate_clusters(clusters, turns); } // After cleaning up clusters assign the next turns diff --git a/include/boost/geometry/algorithms/detail/overlay/handle_colocations.hpp b/include/boost/geometry/algorithms/detail/overlay/handle_colocations.hpp index bb1618ab3..eb0882563 100644 --- a/include/boost/geometry/algorithms/detail/overlay/handle_colocations.hpp +++ b/include/boost/geometry/algorithms/detail/overlay/handle_colocations.hpp @@ -97,7 +97,6 @@ inline void cleanup_clusters(Turns& turns, Clusters& clusters) } remove_clusters(turns, clusters); - colocate_clusters(clusters, turns); } template @@ -463,6 +462,21 @@ inline void gather_cluster_properties(Clusters& clusters, Turns& turns, cinfo.open_count = sbs.open_count(for_operation); + // Determine spikes + cinfo.spike_count = 0; + for (std::size_t i = 0; i + 1 < sbs.m_ranked_points.size(); i++) + { + auto const& current = sbs.m_ranked_points[i]; + auto const& next = sbs.m_ranked_points[i + 1]; + if (current.rank == next.rank + && current.direction == detail::overlay::sort_by_side::dir_from + && next.direction == detail::overlay::sort_by_side::dir_to) + { + // It leaves, from cluster point, and immediately returns. + cinfo.spike_count += 1; + } + } + bool const set_startable = OverlayType != overlay_dissolve; // Unset the startable flag for all 'closed' zones. This does not @@ -475,7 +489,8 @@ inline void gather_cluster_properties(Clusters& clusters, Turns& turns, turn_operation_type& op = turn.operations[ranked.operation_index]; if (set_startable - && for_operation == operation_union && cinfo.open_count == 0) + && for_operation == operation_union + && cinfo.open_count == 0) { op.enriched.startable = false; } diff --git a/include/boost/geometry/algorithms/detail/overlay/traversal.hpp b/include/boost/geometry/algorithms/detail/overlay/traversal.hpp index b5a5d2eae..e027a9bda 100644 --- a/include/boost/geometry/algorithms/detail/overlay/traversal.hpp +++ b/include/boost/geometry/algorithms/detail/overlay/traversal.hpp @@ -559,16 +559,18 @@ public : return true; } + // Returns a priority, the one with the highst priority will be selected + // 0: not OK + // 1: OK following spike out + // 2: OK but next turn is in same cluster + // 3: OK + // 4: OK and start turn matches + // 5: OK and start turn and start operation both match, this is the best inline int priority_of_turn_in_cluster_union(sort_by_side::rank_type selected_rank, typename sbs_type::rp const& ranked_point, - std::set const& cluster_indices, + cluster_info const& cinfo, signed_size_type start_turn_index, int start_op_index) const { - // Returns 0: not OK - // Returns 1: OK but next turn is in same cluster - // Returns 2: OK - // Returns 3: OK and start turn matches - // Returns 4: OK and start turn and start op both match if (ranked_point.rank != selected_rank || ranked_point.direction != sort_by_side::dir_to) { @@ -589,19 +591,23 @@ public : { // Check counts: in some cases interior rings might be generated with // polygons on both sides. For dissolve it can be anything. - return 0; + + // If this forms a spike, going to/from the cluster point in the same + // (opposite) direction, it can still be used. + return cinfo.spike_count > 0 ? 1 : 0; } bool const to_start = ranked_point.turn_index == start_turn_index; bool const to_start_index = ranked_point.operation_index == start_op_index; bool const next_in_same_cluster - = cluster_indices.count(op.enriched.get_next_turn_index()) > 0; + = cinfo.turn_indices.count(op.enriched.get_next_turn_index()) > 0; - return to_start && to_start_index ? 4 - : to_start ? 3 - : next_in_same_cluster ? 1 - : 2 + // Return the priority as described above + return to_start && to_start_index ? 5 + : to_start ? 4 + : next_in_same_cluster ? 2 + : 3 ; } @@ -647,7 +653,7 @@ public : } inline bool select_from_cluster_union(signed_size_type& turn_index, - std::set const& cluster_indices, + cluster_info const& cinfo, int& op_index, sbs_type const& sbs, signed_size_type start_turn_index, int start_op_index) const { @@ -664,7 +670,7 @@ public : } int const priority = priority_of_turn_in_cluster_union(selected_rank, - ranked_point, cluster_indices, start_turn_index, start_op_index); + ranked_point, cinfo, start_turn_index, start_op_index); if (priority > current_priority) { @@ -791,6 +797,24 @@ public : return false; } + if (is_union && cinfo.open_count == 0 && cinfo.spike_count > 0) + { + // Leave the cluster from the spike. + for (std::size_t i = 0; i + 1 < sbs.m_ranked_points.size(); i++) + { + auto const& current = sbs.m_ranked_points[i]; + auto const& next = sbs.m_ranked_points[i + 1]; + if (current.rank == next.rank + && current.direction == detail::overlay::sort_by_side::dir_from + && next.direction == detail::overlay::sort_by_side::dir_to) + { + turn_index = next.turn_index; + op_index = next.operation_index; + return true; + } + } + } + cluster_exits exits(m_turns, cinfo.turn_indices, sbs); if (exits.apply(turn_index, op_index)) @@ -802,7 +826,7 @@ public : if (is_union) { - result = select_from_cluster_union(turn_index, cinfo.turn_indices, + result = select_from_cluster_union(turn_index, cinfo, op_index, sbs, start_turn_index, start_op_index); if (! result) diff --git a/include/boost/geometry/strategies/cartesian/buffer_end_round.hpp b/include/boost/geometry/strategies/cartesian/buffer_end_round.hpp index 47ecb22e7..fd52eb259 100644 --- a/include/boost/geometry/strategies/cartesian/buffer_end_round.hpp +++ b/include/boost/geometry/strategies/cartesian/buffer_end_round.hpp @@ -165,7 +165,7 @@ public : return distance; } - //! Returns the piece_type (flat end) + //! Returns the piece_type (round end) static inline piece_type get_piece_type() { return buffered_round_end; diff --git a/include/boost/geometry/strategies/geographic/buffer_end_round.hpp b/include/boost/geometry/strategies/geographic/buffer_end_round.hpp index 1d6c8d197..2191e96e1 100644 --- a/include/boost/geometry/strategies/geographic/buffer_end_round.hpp +++ b/include/boost/geometry/strategies/geographic/buffer_end_round.hpp @@ -134,7 +134,7 @@ public : return distance; } - //! Returns the piece_type (flat end) + //! Returns the piece_type (round end) static inline piece_type get_piece_type() { return buffered_round_end; diff --git a/test/algorithms/buffer/buffer_variable_width.cpp b/test/algorithms/buffer/buffer_variable_width.cpp new file mode 100644 index 000000000..ee3925512 --- /dev/null +++ b/test/algorithms/buffer/buffer_variable_width.cpp @@ -0,0 +1,184 @@ +// Boost.Geometry + +// Copyright (c) 2024 Barend Gehrels, Amsterdam, the Netherlands. + +// 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) + +#include + +#include +#include +#include +#include + +namespace bg = boost::geometry; + +#if defined(TEST_WITH_SVG) +#include "test_buffer_svg.hpp" +#endif + +// A point with extra info, such that +// - it can influence the buffer with dynamically (input) +// - it can receive the side at which the buffer was (output) +struct specific_point +{ + double x{0.0}; + double y{0.0}; + double distance_left{0.0}; + double distance_right{0.0}; + boost::optional side{}; +}; + +BOOST_GEOMETRY_REGISTER_POINT_2D(specific_point, double, cs::cartesian, x, y) + +struct specific_buffer_side_strategy +{ + bool equidistant() const + { + return true; + } + + template + bg::strategy::buffer::result_code apply(Point const& input_p1, Point const& input_p2, + bg::strategy::buffer::buffer_side_selector side, + DistanceStrategy const& distance, + OutputRange &output_range) const + { + const auto result = + bg::strategy::buffer::side_straight::apply(input_p1, input_p2, side, distance, output_range); + + for (auto& point : output_range) + { + point.side = side; + } + return result; + } +}; + +struct specific_buffer_distance_strategy +{ + /// Returns the distance-value. + template + double apply(Point const& p, Point const& q, + bg::strategy::buffer::buffer_side_selector side) const + { + return bg::strategy::buffer::buffer_side_left == side + ? std::max(p.distance_left, q.distance_left) + : std::max(p.distance_right, q.distance_right); + } + + bool negative() const + { + return false; + } + + bool empty(bg::strategy::buffer::buffer_side_selector) const + { + return false; + } + + /// Returns the max distance distance up to the buffer will reach. + template + double max_distance(JoinStrategy const &, EndStrategy const &) const + { + constexpr double unit_buffer_width{3.0}; + return 6.0 * unit_buffer_width; + } + + /// Returns the distance at which the input is simplified before the buffer process. + double simplify_distance() const + { + constexpr double unit_buffer_width{3.0}; + return unit_buffer_width / 1000.0; + } +}; + +void test_buffer(std::string const& caseid, std::string const& wkt, std::vector const& widths, double expected_area) +{ + using ls_t = boost::geometry::model::linestring; + using mls_t = boost::geometry::model::multi_linestring; + using polygon_t = boost::geometry::model::polygon; + + mls_t mls; + boost::geometry::read_wkt(wkt, mls); + + if (mls.size() != widths.size()) + { + throw std::runtime_error("There should be correct widths"); + } + + using point_type = specific_point; + using strategy_t = bg::strategies::buffer::services::default_strategy< + mls_t>::type; + strategy_t strategy; + +#if defined(TEST_WITH_SVG) + bg::model::box envelope; + bg::envelope(mls, envelope, strategy); + + buffer_svg_mapper buffer_mapper(caseid); + + std::ostringstream filename; + filename << "/tmp/buffer_variable_width_" << caseid << ".svg"; + std::ofstream svg(filename.str().c_str()); + typedef bg::svg_mapper mapper_type; + mapper_type mapper(svg, 1000, 800); + + svg_visitor> visitor(mapper); + + // Set the SVG boundingbox, with a margin. The margin is necessary because + // drawing is already started before the buffer is finished. It is not + // possible to "add" the buffer (unless we buffer twice). + buffer_mapper.prepare(mapper, visitor, envelope, 2.0); +#else + bg::detail::buffer::visit_pieces_default_policy visitor; +#endif + + for (std::size_t i = 0; i < mls.size(); i++) + { + for (auto& point : mls[i]) + { + point.distance_left = widths[i]; + point.distance_right = widths[i]; + } + } + + specific_buffer_side_strategy buffer_side_strategy; + specific_buffer_distance_strategy distance_strategy; + boost::geometry::strategy::buffer::join_miter join_strategy; + boost::geometry::strategy::buffer::end_flat end_strategy; + boost::geometry::strategy::buffer::point_circle point_strategy; + boost::geometry::model::multi_polygon result; + boost::geometry::detail::buffer::buffer_inserter + < + polygon_t + >(mls, + std::back_inserter(result), + distance_strategy, buffer_side_strategy, join_strategy, + end_strategy, point_strategy, + strategy, + bg::detail::no_rescale_policy(), + visitor); + +#if defined(TEST_WITH_SVG) + buffer_mapper.map_input_output(mapper, mls, result, false); +#endif + + std::cout << caseid << " : " << boost::geometry::area(result) << std::endl; + BOOST_CHECK_CLOSE_FRACTION(boost::geometry::area(result), expected_area, 0.001); +} + +int test_main(int argc, char **argv) +{ + test_buffer("case1", "MULTILINESTRING((-2.99999999998012700785920969792641699314117431640625 10.4999999997948929575386500800959765911102294921875,18.00000000002045652536253328435122966766357421875 10.4999999997948947338954894803464412689208984375),(-2.99999999998012700785920969792641699314117431640625 10.4999999997948929575386500800959765911102294921875,17.99999999995061017443731543608009815216064453125 -4.5000000001943778471513724070973694324493408203125),(-2.99999999998012700785920969792641699314117431640625 10.4999999997948929575386500800959765911102294921875,11.99999999996175148453403380699455738067626953125 -10.49999999983214848953139153309166431427001953125))", + {4.5, 1.5, 1.5}, 304.294); + test_buffer("case2", "MULTILINESTRING((-18.00000000002045652536253328435122966766357421875 10.4999999997948929575386500800959765911102294921875,-2.99999999998012700785920969792641699314117431640625 10.4999999997948929575386500800959765911102294921875),(-2.99999999998012700785920969792641699314117431640625 10.4999999997948929575386500800959765911102294921875,17.99999999995061017443731543608009815216064453125 -4.5000000001943778471513724070973694324493408203125),(-2.99999999998012700785920969792641699314117431640625 10.4999999997948929575386500800959765911102294921875,11.99999999996175148453403380699455738067626953125 -10.49999999983214848953139153309166431427001953125))", + {6.0, 1.5, 1.5}, 319.767); + test_buffer("case3", "MULTILINESTRING((-18.00000000002045652536253328435122966766357421875 10.4999999997948929575386500800959765911102294921875,-2.99999999998012700785920969792641699314117431640625 10.4999999997948929575386500800959765911102294921875),(-2.99999999998012700785920969792641699314117431640625 10.4999999997948929575386500800959765911102294921875,18.00000000002045652536253328435122966766357421875 10.4999999997948947338954894803464412689208984375),(-2.99999999998012700785920969792641699314117431640625 10.4999999997948929575386500800959765911102294921875,11.99999999996175148453403380699455738067626953125 -10.49999999983214848953139153309166431427001953125))", + {6.0, 4.5, 1.5}, 429.831); + test_buffer("case4", "MULTILINESTRING((-18.00000000002045652536253328435122966766357421875 10.4999999997948929575386500800959765911102294921875,-2.99999999998012700785920969792641699314117431640625 10.4999999997948929575386500800959765911102294921875),(-2.99999999998012700785920969792641699314117431640625 10.4999999997948929575386500800959765911102294921875,18.00000000002045652536253328435122966766357421875 10.4999999997948947338954894803464412689208984375),(-2.99999999998012700785920969792641699314117431640625 10.4999999997948929575386500800959765911102294921875,17.99999999995061017443731543608009815216064453125 -4.5000000001943778471513724070973694324493408203125))", + {6.0, 4.5, 1.5}, 423.195); + return 0; +}