From git at osgeo.org Tue Jan 6 05:51:20 2026 From: git at osgeo.org (git at osgeo.org) Date: Tue, 6 Jan 2026 05:51:20 -0800 (PST) Subject: [geos-commits] [SCM] GEOS branch main updated. 02cbfe81143f6303f234f8a9ce3d9694ddcec381 Message-ID: <20260106135120.ED33F188988@trac.osgeo.org> This is an automated email from the git hooks/post-receive script. It was generated because a ref change was pushed to the repository containing the project "GEOS". The branch, main has been updated via 02cbfe81143f6303f234f8a9ce3d9694ddcec381 (commit) from 158c588566e08fd2f31f8c9e7ae069518e8264c5 (commit) Those revisions listed above that are new to this repository have not appeared on any other notification email; so we list those revisions in full, below. - Log ----------------------------------------------------------------- commit 02cbfe81143f6303f234f8a9ce3d9694ddcec381 Author: arriopolis Date: Tue Jan 6 14:50:56 2026 +0100 Improve OverlayNG performance (#1353) * Incorporated JTS PR 906 * EdgeRing: Add const qualifiers, use std::make_shared --------- Co-authored-by: arriopolis Co-authored-by: Dan Baston diff --git a/include/geos/geom/Envelope.h b/include/geos/geom/Envelope.h index 687dbbae1..15d360030 100644 --- a/include/geos/geom/Envelope.h +++ b/include/geos/geom/Envelope.h @@ -575,6 +575,19 @@ public: std::isless(p.y, maxy) && std::isgreater(p.y, miny); } + /** \brief + * Tests if an envelope is properly contained in this one. + * The envelope is properly contained if it is contained + * by this one but not equal to it. + * + * @param other the envelope to test + * @return true if the envelope is properly contained + */ + bool containsProperly(const Envelope& other) const { + if (equals(&other)) return false; + return covers(other); + } + /** \brief * Check if the point p intersects (lies inside) the region of this Envelope. * diff --git a/include/geos/operation/overlayng/OverlayEdgeRing.h b/include/geos/operation/overlayng/OverlayEdgeRing.h index b2509f02c..f0e94445a 100644 --- a/include/geos/operation/overlayng/OverlayEdgeRing.h +++ b/include/geos/operation/overlayng/OverlayEdgeRing.h @@ -28,6 +28,7 @@ class PointOnGeometryLocator; } namespace geom { class Coordinate; +class CoordinateXY; class CoordinateSequence; class GeometryFactory; class LinearRing; @@ -46,6 +47,7 @@ namespace overlayng { // geos.operation.overlayng class GEOS_DLL OverlayEdgeRing { using Coordinate = geos::geom::Coordinate; + using CoordinateXY = geos::geom::CoordinateXY; using CoordinateSequence = geos::geom::CoordinateSequence; using GeometryFactory = geos::geom::GeometryFactory; using LinearRing = geos::geom::LinearRing; @@ -59,23 +61,25 @@ private: OverlayEdge* startEdge; std::unique_ptr ring; bool m_isHole; - std::unique_ptr locator; + mutable std::unique_ptr locator; OverlayEdgeRing* shell; // a list of EdgeRings which are holes in this EdgeRing std::vector holes; // Methods void computeRingPts(OverlayEdge* start, CoordinateSequence& pts); - void computeRing(std::unique_ptr && ringPts, const GeometryFactory* geometryFactory); + void computeRing(const std::shared_ptr & ringPts, const GeometryFactory* geometryFactory); /** * Computes the list of coordinates which are contained in this ring. * The coordinates are computed once only and cached. * @return an array of the {@link Coordinate}s in this ring */ - const CoordinateSequence& getCoordinates(); - PointOnGeometryLocator* getLocator(); + const CoordinateSequence& getCoordinates() const; + PointOnGeometryLocator* getLocator() const; static void closeRing(CoordinateSequence& pts); + bool contains(const OverlayEdgeRing& otherRing) const; + bool isPointInOrOut(const OverlayEdgeRing& otherRing) const; public: @@ -85,6 +89,8 @@ public: std::unique_ptr getRing(); const LinearRing* getRingPtr() const; + const geom::Envelope& getEnvelope() const; + /** * Tests whether this ring is a hole. * @return true if this ring is a hole @@ -114,9 +120,9 @@ public: void addHole(OverlayEdgeRing* ring); - bool isInRing(const Coordinate& pt); + geom::Location locate(const CoordinateXY& pt) const; - const Coordinate& getCoordinate(); + const Coordinate& getCoordinate() const; /** * Computes the {@link Polygon} formed by this ring and any contained holes. @@ -144,7 +150,7 @@ public: * @return containing EdgeRing, if there is one * or null if no containing EdgeRing is found */ - OverlayEdgeRing* findEdgeRingContaining(const std::vector& erList); + OverlayEdgeRing* findEdgeRingContaining(const std::vector& erList) const; }; diff --git a/include/geos/operation/polygonize/EdgeRing.h b/include/geos/operation/polygonize/EdgeRing.h index 30f5be286..18646ee5d 100644 --- a/include/geos/operation/polygonize/EdgeRing.h +++ b/include/geos/operation/polygonize/EdgeRing.h @@ -64,9 +64,9 @@ private: DeList deList; // cache the following data for efficiency - std::unique_ptr ring; - std::unique_ptr ringPts; - std::unique_ptr ringLocator; + mutable std::unique_ptr ring; + mutable std::shared_ptr ringPts; + mutable std::unique_ptr ringLocator; std::unique_ptr>> holes; @@ -84,19 +84,26 @@ private: * * @return an array of the Coordinate in this ring */ - const geom::CoordinateSequence* getCoordinates(); + const geom::CoordinateSequence* getCoordinates() const; + + const geom::Envelope& getEnvelope() const { + return *getRingInternal()->getEnvelopeInternal(); + } static void addEdge(const geom::CoordinateSequence* coords, bool isForward, geom::CoordinateSequence* coordList); - algorithm::locate::PointOnGeometryLocator* getLocator() { + algorithm::locate::PointOnGeometryLocator* getLocator() const { if (ringLocator == nullptr) { ringLocator.reset(new algorithm::locate::IndexedPointInAreaLocator(*getRingInternal())); } return ringLocator.get(); } + bool contains(const EdgeRing& otherRing) const; + bool isPointInOrOut(const EdgeRing& otherRing) const; + public: /** \brief * Adds a DirectedEdge which is known to form part of this ring. @@ -324,7 +331,7 @@ public: * Ownership of ring is retained by the object. * Details of problems are written to standard output. */ - geom::LinearRing* getRingInternal(); + const geom::LinearRing* getRingInternal() const; /** \brief * Returns this ring as a LinearRing, or null if an Exception @@ -335,8 +342,8 @@ public: */ std::unique_ptr getRingOwnership(); - bool isInRing(const geom::Coordinate & pt) { - return geom::Location::EXTERIOR != getLocator()->locate(&pt); + geom::Location locate(const geom::CoordinateXY& pt) const { + return getLocator()->locate(&pt); } }; diff --git a/src/operation/overlayng/OverlayEdgeRing.cpp b/src/operation/overlayng/OverlayEdgeRing.cpp index f45d46418..b9ac01518 100644 --- a/src/operation/overlayng/OverlayEdgeRing.cpp +++ b/src/operation/overlayng/OverlayEdgeRing.cpp @@ -42,9 +42,9 @@ OverlayEdgeRing::OverlayEdgeRing(OverlayEdge* start, const GeometryFactory* geom , locator(nullptr) , shell(nullptr) { - auto ringPts = detail::make_unique(0u, start->getCoordinatesRO()->hasZ(), start->getCoordinatesRO()->hasM()); + auto ringPts = std::make_shared(0u, start->getCoordinatesRO()->hasZ(), start->getCoordinatesRO()->hasM()); computeRingPts(start, *ringPts); - computeRing(std::move(ringPts), geometryFactory); + computeRing(ringPts, geometryFactory); } /*public*/ @@ -60,6 +60,12 @@ OverlayEdgeRing::getRingPtr() const return ring.get(); } +const Envelope& +OverlayEdgeRing::getEnvelope() const +{ + return *ring->getEnvelopeInternal(); +} + /** * Tests whether this ring is a hole. * @return true if this ring is a hole @@ -149,10 +155,10 @@ OverlayEdgeRing::computeRingPts(OverlayEdge* start, CoordinateSequence& pts) /*private*/ void -OverlayEdgeRing::computeRing(std::unique_ptr && p_ringPts, const GeometryFactory* geometryFactory) +OverlayEdgeRing::computeRing(const std::shared_ptr & p_ringPts, const GeometryFactory* geometryFactory) { if (ring != nullptr) return; // don't compute more than once - ring = geometryFactory->createLinearRing(std::move(p_ringPts)); + ring = geometryFactory->createLinearRing(p_ringPts); m_isHole = algorithm::Orientation::isCCW(ring->getCoordinatesRO()); } @@ -164,9 +170,9 @@ OverlayEdgeRing::computeRing(std::unique_ptr && p_ringPts, c */ /*private*/ const CoordinateSequence& -OverlayEdgeRing::getCoordinates() +OverlayEdgeRing::getCoordinates() const { - return *(detail::down_cast(ring->getCoordinatesRO())); + return *ring->getCoordinatesRO(); } /** @@ -189,39 +195,23 @@ OverlayEdgeRing::getCoordinates() */ /*public*/ OverlayEdgeRing* -OverlayEdgeRing::findEdgeRingContaining(const std::vector& erList) +OverlayEdgeRing::findEdgeRingContaining(const std::vector& erList) const { - const LinearRing* testRing = ring.get(); - const Envelope* testEnv = testRing->getEnvelopeInternal(); - - OverlayEdgeRing* minRing = nullptr; - const Envelope* minRingEnv = nullptr; - for (auto tryEdgeRing: erList) { - const LinearRing* tryRing = tryEdgeRing->getRingPtr(); - const Envelope* tryShellEnv = tryRing->getEnvelopeInternal(); - // the hole envelope cannot equal the shell envelope - // (also guards against testing rings against themselves) - if (tryShellEnv->equals(testEnv)) continue; - - // hole must be contained in shell - if (! tryShellEnv->contains(testEnv)) continue; - - const Coordinate& testPt = EdgeRing::ptNotInList(testRing->getCoordinatesRO(), tryRing->getCoordinatesRO()); - bool isContained = tryEdgeRing->isInRing(testPt); - // check if the new containing ring is smaller than the current minimum ring - if (isContained) { - if (minRing == nullptr || minRingEnv->contains(tryShellEnv)) { - minRing = tryEdgeRing; - minRingEnv = minRing->getRingPtr()->getEnvelopeInternal(); + OverlayEdgeRing* minContainingRing = nullptr; + for (OverlayEdgeRing* edgeRing : erList) { + if (edgeRing->contains(*this)) { + if (minContainingRing == nullptr + || minContainingRing->getEnvelope().contains(edgeRing->getEnvelope())) { + minContainingRing = edgeRing; } } } - return minRing; + return minContainingRing; } /*private*/ PointOnGeometryLocator* -OverlayEdgeRing::getLocator() +OverlayEdgeRing::getLocator() const { if (locator == nullptr) { locator.reset(new IndexedPointInAreaLocator(*(getRingPtr()))); @@ -230,18 +220,54 @@ OverlayEdgeRing::getLocator() } /*public*/ -bool -OverlayEdgeRing::isInRing(const Coordinate& pt) +geom::Location +OverlayEdgeRing::locate(const CoordinateXY& pt) const { /** * Use an indexed point-in-polygon for performance */ - return Location::EXTERIOR != getLocator()->locate(&pt); + return getLocator()->locate(&pt); +} + +/** + * Tests if an edgeRing is properly contained in this ring. + * Relies on property that edgeRings never overlap (although they may + * touch at single vertices). + * + * @param ring ring to test + * @return true if ring is properly contained + */ +bool +OverlayEdgeRing::contains(const OverlayEdgeRing& otherRing) const { + // the test envelope must be properly contained + // (guards against testing rings against themselves) + const Envelope& env = getEnvelope(); + const Envelope& testEnv = otherRing.getEnvelope(); + if (! env.containsProperly(testEnv)) { + return false; + } + return isPointInOrOut(otherRing); +} + +bool +OverlayEdgeRing::isPointInOrOut(const OverlayEdgeRing& otherRing) const { + // in most cases only one or two points will be checked + for (const CoordinateXY& pt : otherRing.getCoordinates().items()) { + geom::Location loc = locate(pt); + if (loc == geom::Location::INTERIOR) { + return true; + } + if (loc == geom::Location::EXTERIOR) { + return false; + } + // pt is on BOUNDARY, so keep checking for a determining location + } + return false; } /*public*/ const Coordinate& -OverlayEdgeRing::getCoordinate() +OverlayEdgeRing::getCoordinate() const { return ring->getCoordinatesRO()->getAt(0); } diff --git a/src/operation/polygonize/EdgeRing.cpp b/src/operation/polygonize/EdgeRing.cpp index 7bff9c787..4f4252a8d 100644 --- a/src/operation/polygonize/EdgeRing.cpp +++ b/src/operation/polygonize/EdgeRing.cpp @@ -51,39 +51,16 @@ namespace polygonize { // geos.operation.polygonize EdgeRing* EdgeRing::findEdgeRingContaining(const std::vector & erList) { - const LinearRing* testRing = getRingInternal(); - if(! testRing) { - return nullptr; - } - const Envelope* testEnv = testRing->getEnvelopeInternal(); - EdgeRing* minRing = nullptr; - const Envelope* minRingEnv = nullptr; - - for(auto& tryEdgeRing : erList) { - auto tryRing = tryEdgeRing->getRingInternal(); - auto tryShellEnv = tryRing->getEnvelopeInternal(); - // the hole envelope cannot equal the shell envelope - // (also guards against testing rings against themselves) - if (tryShellEnv->equals(testEnv)) { - continue; - } - // hole must be contained in shell - if (!tryShellEnv->contains(testEnv)) { - continue; - } - - auto tryCoords = tryRing->getCoordinatesRO(); - const Coordinate& testPt = ptNotInList(testRing->getCoordinatesRO(), tryCoords); - - // check if this new containing ring is smaller than the current minimum ring - if(tryEdgeRing->isInRing(testPt)) { - if(minRing == nullptr || minRingEnv->contains(tryShellEnv)) { - minRing = tryEdgeRing; - minRingEnv = minRing->getRingInternal()->getEnvelopeInternal(); + EdgeRing* minContainingRing = nullptr; + for (auto& edgeRing : erList) { + if (edgeRing->contains(*this)) { + if (minContainingRing == nullptr + || minContainingRing->getEnvelope().contains(edgeRing->getEnvelope())) { + minContainingRing = edgeRing; } } } - return minRing; + return minContainingRing; } std::vector @@ -214,12 +191,40 @@ EdgeRing::computeValid() is_valid = ring->isValid(); } +bool +EdgeRing::contains(const EdgeRing& otherRing) const { + // the test envelope must be properly contained + // (guards against testing rings against themselves) + const Envelope& env = getEnvelope(); + const Envelope& testEnv = otherRing.getEnvelope(); + if (! env.containsProperly(testEnv)) { + return false; + } + return isPointInOrOut(otherRing); + } + +bool +EdgeRing::isPointInOrOut(const EdgeRing& otherRing) const { + // in most cases only one or two points will be checked + for (const CoordinateXY& pt : otherRing.getCoordinates()->items()) { + geom::Location loc = locate(pt); + if (loc == geom::Location::INTERIOR) { + return true; + } + if (loc == geom::Location::EXTERIOR) { + return false; + } + // pt is on BOUNDARY, so keep checking for a determining location + } + return false; +} + /*private*/ const CoordinateSequence* -EdgeRing::getCoordinates() +EdgeRing::getCoordinates() const { if(ringPts == nullptr) { - ringPts = detail::make_unique(0u, 0u); + ringPts = std::make_shared(0u, 0u); for(const auto& de : deList) { auto edge = dynamic_cast(de->getEdge()); addEdge(edge->getLine()->getCoordinatesRO(), @@ -234,12 +239,12 @@ std::unique_ptr EdgeRing::getLineString() { getCoordinates(); - return std::unique_ptr(factory->createLineString(*ringPts)); + return factory->createLineString(ringPts); } /*public*/ -LinearRing* -EdgeRing::getRingInternal() +const LinearRing* +EdgeRing::getRingInternal() const { if(ring != nullptr) { return ring.get(); ----------------------------------------------------------------------- Summary of changes: include/geos/geom/Envelope.h | 13 +++ include/geos/operation/overlayng/OverlayEdgeRing.h | 20 +++-- include/geos/operation/polygonize/EdgeRing.h | 23 ++++-- src/operation/overlayng/OverlayEdgeRing.cpp | 96 ++++++++++++++-------- src/operation/polygonize/EdgeRing.cpp | 75 +++++++++-------- 5 files changed, 142 insertions(+), 85 deletions(-) hooks/post-receive -- GEOS From git at osgeo.org Tue Jan 6 08:41:48 2026 From: git at osgeo.org (git at osgeo.org) Date: Tue, 6 Jan 2026 08:41:48 -0800 (PST) Subject: [geos-commits] [SCM] GEOS branch main updated. 9e2348082d2ecd02f8241a90e62954a17f8773af Message-ID: <20260106164148.B2852188757@trac.osgeo.org> This is an automated email from the git hooks/post-receive script. It was generated because a ref change was pushed to the repository containing the project "GEOS". The branch, main has been updated via 9e2348082d2ecd02f8241a90e62954a17f8773af (commit) from 02cbfe81143f6303f234f8a9ce3d9694ddcec381 (commit) Those revisions listed above that are new to this repository have not appeared on any other notification email; so we list those revisions in full, below. - Log ----------------------------------------------------------------- commit 9e2348082d2ecd02f8241a90e62954a17f8773af Author: Sven Jensen Date: Tue Jan 6 08:41:28 2026 -0800 Add new CAPI functions for Hausdorff distance (#1352) * Add new CAPI functions for Hausdorff distance Introduce two new CAPI functions: - GEOSHausdorffDistanceWithPoints - GEOSHausdorffDistanceDensifyWithPoints These functions take four additional double pointers (pt1_x, pt1_y, pt2_x, pt2_y) and fill them with the coordinates used in the Hausdorff distance calculation via getCoordinates(). Also add a unit test verifying the new function behavior. * typo in function out parameter documentation * GEOSHausdorffDistanceWithPoints: simplify implementation --------- Co-authored-by: Dan Baston diff --git a/capi/geos_c.cpp b/capi/geos_c.cpp index d26db0966..9a832a64b 100644 --- a/capi/geos_c.cpp +++ b/capi/geos_c.cpp @@ -317,12 +317,24 @@ extern "C" { return GEOSHausdorffDistance_r(handle, g1, g2, dist); } + int + GEOSHausdorffDistanceWithPoints(const Geometry* g1, const Geometry* g2, double* dist, double* p1x, double* p1y, double* p2x, double* p2y) + { + return GEOSHausdorffDistanceWithPoints_r(handle, g1, g2, dist, p1x, p1y, p2x, p2y); + } + int GEOSHausdorffDistanceDensify(const Geometry* g1, const Geometry* g2, double densifyFrac, double* dist) { return GEOSHausdorffDistanceDensify_r(handle, g1, g2, densifyFrac, dist); } + int + GEOSHausdorffDistanceDensifyWithPoints(const Geometry* g1, const Geometry* g2, double densifyFrac, double* dist, double* p1x, double* p1y, double* p2x, double* p2y) + { + return GEOSHausdorffDistanceDensifyWithPoints_r(handle, g1, g2, densifyFrac, dist, p1x, p1y, p2x, p2y); + } + int GEOSFrechetDistance(const Geometry* g1, const Geometry* g2, double* dist) { diff --git a/capi/geos_c.h.in b/capi/geos_c.h.in index 942663671..44bb6d3ed 100644 --- a/capi/geos_c.h.in +++ b/capi/geos_c.h.in @@ -1954,6 +1954,15 @@ extern int GEOS_DLL GEOSHausdorffDistance_r( const GEOSGeometry *g2, double *dist); +/** \see GEOSHausdorffDistanceWithPoints */ +extern int GEOS_DLL GEOSHausdorffDistanceWithPoints_r( + GEOSContextHandle_t handle, + const GEOSGeometry *g1, + const GEOSGeometry *g2, + double *dist, + double *p1x, double *p1y, + double *p2x, double *p2y); + /** \see GEOSHausdorffDistanceDensify */ extern int GEOS_DLL GEOSHausdorffDistanceDensify_r( GEOSContextHandle_t handle, @@ -1961,6 +1970,15 @@ extern int GEOS_DLL GEOSHausdorffDistanceDensify_r( const GEOSGeometry *g2, double densifyFrac, double *dist); +/** \see GEOSHausdorffDistanceDensifyWithPoints */ +extern int GEOS_DLL GEOSHausdorffDistanceDensifyWithPoints_r( + GEOSContextHandle_t handle, + const GEOSGeometry *g1, + const GEOSGeometry *g2, + double densifyFrac, double* dist, + double *p1x, double *p1y, + double *p2x, double *p2y); + /** \see GEOSFrechetDistance */ extern int GEOS_DLL GEOSFrechetDistance_r( GEOSContextHandle_t handle, @@ -3704,6 +3722,30 @@ extern int GEOS_DLL GEOSHausdorffDistance( const GEOSGeometry *g2, double *dist); + +/** +* Calculate the Hausdorff distance between two geometries and return +* the coordinates of the points that determine the distance. +* \param[in] g1 Input geometry +* \param[in] g2 Input geometry +* \param[out] dist Pointer to be filled in with distance result +* \param[out] p1x X coordinate of point on g1 +* \param[out] p1y Y coordinate of point on g1 +* \param[out] p2x X coordinate of point on g2 +* \param[out] p2y Y coordinate of point on g2 +* \return 1 on success, 0 on exception. +* \see geos::algorithm::distance::DiscreteHausdorffDistance +* \since 3.15 +*/ +extern int GEOS_DLL GEOSHausdorffDistanceWithPoints( + const GEOSGeometry *g1, + const GEOSGeometry *g2, + double *dist, + double *p1x, double *p1y, + double *p2x, double *p2y); + + + /** * Calculate a more precise Hausdorff distance between two geometries, * by densifying the inputs before computation. @@ -3724,6 +3766,33 @@ extern int GEOS_DLL GEOSHausdorffDistanceDensify( double densifyFrac, double *dist); +/** + * Calculate a more precise Hausdorff distance between two geometries, + * by densifying the inputs before computation and return the points + * that determine the distance. + * [Hausdorff distance](https://en.wikipedia.org/wiki/Hausdorff_distance) + * is the largest distance between two geometries. + * \param[in] g1 Input geometry + * \param[in] g2 Input geometry + * \param[in] densifyFrac The largest % of the overall line length that + * any given two point segment should be. + * \param[out] dist Pointer to be filled in with distance result + * \param[out] p1x X coordinate of point on g1 + * \param[out] p1y Y coordinate of point on g1 + * \param[out] p2x X coordinate of point on g2 + * \param[out] p2y Y coordinate of point on g2 + * \return 1 on success, 0 on exception. + * \see goes::algorithm::distance::DiscreteHausdorffDistance + * \since 3.15 + */ +extern int GEOS_DLL GEOSHausdorffDistanceDensifyWithPoints( + const GEOSGeometry *g1, + const GEOSGeometry *g2, + double densifyFrac, + double *dist, + double* p1x, double* p1y, + double* p2x, double* p2y); + /** * Calculate the * [Frechet distance](https://en.wikipedia.org/wiki/Fr%C3%A9chet_distance) diff --git a/capi/geos_ts_c.cpp b/capi/geos_ts_c.cpp index d8b57fd87..e082f799c 100644 --- a/capi/geos_ts_c.cpp +++ b/capi/geos_ts_c.cpp @@ -968,6 +968,24 @@ extern "C" { }); } + int + GEOSHausdorffDistanceWithPoints_r(GEOSContextHandle_t extHandle, + const Geometry* g1, const Geometry* g2, + double* dist, double* p1x, double* p1y, + double* p2x, double* p2y) + { + return execute(extHandle, 0, [&]() { + DiscreteHausdorffDistance dhd(*g1, *g2); + *dist = dhd.distance(); + const auto& pts = dhd.getCoordinates(); + *p1x = pts[0].x; + *p1y = pts[0].y; + *p2x = pts[1].x; + *p2y = pts[1].y; + return 1; + }); + } + int GEOSHausdorffDistanceDensify_r(GEOSContextHandle_t extHandle, const Geometry* g1, const Geometry* g2, double densifyFrac, double* dist) @@ -978,6 +996,23 @@ extern "C" { }); } + int + GEOSHausdorffDistanceDensifyWithPoints_r(GEOSContextHandle_t extHandle, const Geometry* g1, const Geometry* g2, + double densifyFrac, double* dist, double* p1x, double* p1y, double* p2x, double* p2y) + { + return execute(extHandle, 0, [&]() { + DiscreteHausdorffDistance dhd(*g1, *g2); + dhd.setDensifyFraction(densifyFrac); + *dist = dhd.distance(); + const auto& pts = dhd.getCoordinates(); + *p1x = pts[0].x; + *p1y = pts[0].y; + *p2x = pts[1].x; + *p2y = pts[1].y; + return 1; + }); + } + int GEOSFrechetDistance_r(GEOSContextHandle_t extHandle, const Geometry* g1, const Geometry* g2, double* dist) { diff --git a/include/geos/algorithm/distance/DiscreteHausdorffDistance.h b/include/geos/algorithm/distance/DiscreteHausdorffDistance.h index be136edee..74876ec7b 100644 --- a/include/geos/algorithm/distance/DiscreteHausdorffDistance.h +++ b/include/geos/algorithm/distance/DiscreteHausdorffDistance.h @@ -96,7 +96,7 @@ public: static double distance(const geom::Geometry& g0, const geom::Geometry& g1, double densifyFrac); - + DiscreteHausdorffDistance(const geom::Geometry& p_g0, const geom::Geometry& p_g1) : @@ -130,7 +130,7 @@ public: return ptDist.getDistance(); } - const std::array + const std::array& getCoordinates() const { return ptDist.getCoordinates(); diff --git a/src/algorithm/distance/DiscreteHausdorffDistance.cpp b/src/algorithm/distance/DiscreteHausdorffDistance.cpp index 3712b0a40..6b0e7914d 100644 --- a/src/algorithm/distance/DiscreteHausdorffDistance.cpp +++ b/src/algorithm/distance/DiscreteHausdorffDistance.cpp @@ -79,8 +79,8 @@ DiscreteHausdorffDistance::distance(const geom::Geometry& g0, return dist.distance(); } -/* public */ +/* public */ void DiscreteHausdorffDistance::setDensifyFraction(double dFrac) { // !(dFrac > 0) written that way to catch NaN diff --git a/tests/unit/capi/GEOSHausdorffDistanceTest.cpp b/tests/unit/capi/GEOSHausdorffDistanceTest.cpp index eb569c60a..747d2a9df 100644 --- a/tests/unit/capi/GEOSHausdorffDistanceTest.cpp +++ b/tests/unit/capi/GEOSHausdorffDistanceTest.cpp @@ -72,4 +72,45 @@ void object::test<3>() ensure_equals("curved geometry not supported", GEOSHausdorffDistance(geom2_, geom1_, &dist), 0); } +template<> +template<> +void object::test<4>() +{ + set_test_name("GEOSHausdorffDistanceWithPoints"); + + geom1_ = fromWKT("LINEARRING (1 1, 1 2, 5 1, 1 1)"); + geom2_ = fromWKT("LINEARRING (0 0, -5 0, 0 -1, 0 0)"); + + double dist; + double p1x, p1y, p2x, p2y; + ensure_equals(GEOSHausdorffDistanceWithPoints(geom1_, geom2_, &dist, &p1x, &p1y, &p2x, &p2y), 1); + + ensure_equals("dist", dist, 6.082763, 1e-5); + ensure_equals(p1x, 1); + ensure_equals(p1y, 1); + ensure_equals(p2x, -5); + ensure_equals(p2y, 0); +} + +template<> +template<> +void object::test<5>() +{ + set_test_name("GEOSHausdorffDistanceDensifyWithPoints"); + + constexpr double densifyFrac = 0.001; + geom1_ = fromWKT("LINEARRING (1 1, 1 2, 5 1, 1 1)"); + geom2_ = fromWKT("LINEARRING (0 0, -5 0, 0 -1, 0 0)"); + + double dist; + double p1x, p1y, p2x, p2y; + ensure_equals(GEOSHausdorffDistanceDensifyWithPoints(geom1_, geom2_, densifyFrac, &dist, &p1x, &p1y, &p2x, &p2y), 1); + + ensure_equals("dist", dist, 6.082763, 1e-5); + ensure_equals(p1x, 1); + ensure_equals(p1y, 1); + ensure_equals(p2x, -5); + ensure_equals(p2y, 0); +} + } // namespace tut ----------------------------------------------------------------------- Summary of changes: capi/geos_c.cpp | 12 ++++ capi/geos_c.h.in | 69 ++++++++++++++++++++++ capi/geos_ts_c.cpp | 35 +++++++++++ .../algorithm/distance/DiscreteHausdorffDistance.h | 4 +- .../distance/DiscreteHausdorffDistance.cpp | 2 +- tests/unit/capi/GEOSHausdorffDistanceTest.cpp | 41 +++++++++++++ 6 files changed, 160 insertions(+), 3 deletions(-) hooks/post-receive -- GEOS From git at osgeo.org Wed Jan 7 07:31:13 2026 From: git at osgeo.org (git at osgeo.org) Date: Wed, 7 Jan 2026 07:31:13 -0800 (PST) Subject: [geos-commits] [SCM] GEOS branch main updated. eb6121ae303477279d4a571c54ae9f0435e120bb Message-ID: <20260107153114.45A04173D4B@trac.osgeo.org> This is an automated email from the git hooks/post-receive script. It was generated because a ref change was pushed to the repository containing the project "GEOS". The branch, main has been updated via eb6121ae303477279d4a571c54ae9f0435e120bb (commit) from 9e2348082d2ecd02f8241a90e62954a17f8773af (commit) Those revisions listed above that are new to this repository have not appeared on any other notification email; so we list those revisions in full, below. - Log ----------------------------------------------------------------- commit eb6121ae303477279d4a571c54ae9f0435e120bb Author: Regina Obe Date: Wed Jan 7 10:30:45 2026 -0500 Update ci and version diff --git a/.drone-1.0.yml b/.drone-1.0.yml deleted file mode 100644 index 87e928bfe..000000000 --- a/.drone-1.0.yml +++ /dev/null @@ -1,37 +0,0 @@ -# Syntax documentation: -# https://docs.drone.io/pipeline/overview/ - -# See https://git.osgeo.org/gitea/geos/geos-docker -test-image: &test-image docker.osgeo.org/geos/build-test:alpine - -kind: pipeline -name: default - -clone: - image: plugins/git - depth: 50 - recursive: false - -steps: - - name: build - image: *test-image - pull: always - commands: - - mkdir build - - cd build - - cmake --version - - CXXFLAGS="-O0" cmake .. - - make V=0 - - name: check - image: *test-image - commands: - - cd build - - ctest --output-on-failure . - # - name: docs - # image: *test-image - # commands: - # - make doxygen-checked - # - name: valgrindcheck - # image: *test-image - # commands: - # - CXXFLAGS="-O0" make valgrindcheck V=0 diff --git a/web/config.toml b/web/config.toml index b568af506..a3dc2641b 100644 --- a/web/config.toml +++ b/web/config.toml @@ -10,7 +10,7 @@ enableEmoji = true enableGitInfo = false [params] - current_release = "3.14.0" + current_release = "3.14.1" geekdocLogo = "web-logo.png" # turn these on when we move to the gh/main branch diff --git a/web/content/project/development/ci_status.md b/web/content/project/development/ci_status.md index cf8f931c1..bca99bab5 100644 --- a/web/content/project/development/ci_status.md +++ b/web/content/project/development/ci_status.md @@ -5,24 +5,22 @@ draft: false weight: 50 --- -| CI | main | 3.13 | 3.12 | 3.11 | 3.10 | 3.9 | -| ------ | ------ | ------ | ------ | ------ | ------ | ------ | -| **GitHub** | [![GitHub](https://github.com/libgeos/geos/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/libgeos/geos/actions?query=workflow%3ACI+branch%3Amain) | [![GitHub](https://github.com/libgeos/geos/actions/workflows/ci.yml/badge.svg?branch=3.13)](https://github.com/libgeos/geos/actions?query=branch%3A3.13) | [![GitHub](https://github.com/libgeos/geos/actions/workflows/ci.yml/badge.svg?branch=3.12)](https://github.com/libgeos/geos/actions?query=branch%3A3.12) | [![GitHub](https://github.com/libgeos/geos/actions/workflows/ci.yml/badge.svg?branch=3.11)](https://github.com/libgeos/geos/actions?query=branch%3A3.11) | [![GitHub](https://github.com/libgeos/geos/actions/workflows/ci.yml/badge.svg?branch=3.10)](https://github.com/libgeos/geos/actions?query=branch%3A3.10) | [![GitHub](https://github.com/libgeos/geos/actions/workflows/ci.yml/badge.svg?branch=3.9)](https://github.com/libgeos/geos/actions?query=branch%3A3.9)| -| **Debbie** | [![Debbie](https://debbie.postgis.net/buildStatus/icon?job=GEOS_Master)](https://debbie.postgis.net/view/GEOS/job/GEOS_Master/)| [![Debbie](https://debbie.postgis.net/buildStatus/icon?job=GEOS_Branch_3.13)](https://debbie.postgis.net/view/job/GEOS_Branch_3.13/) |[![Debbie](https://debbie.postgis.net/buildStatus/icon?job=GEOS_Branch_3.12)](https://debbie.postgis.net/view/job/GEOS_Branch_3.12/) | [![Debbie](https://debbie.postgis.net/buildStatus/icon?job=GEOS_Branch_3.11)](https://debbie.postgis.net/view/job/GEOS_Branch_3.11/) | [![Debbie](https://debbie.postgis.net/buildStatus/icon?job=GEOS_Branch_3.10)](https://debbie.postgis.net/view/job/GEOS_Branch_3.10/)| [![Debbie](https://debbie.postgis.net/buildStatus/icon?job=GEOS_Branch_3.9)](https://debbie.postgis.net/view/job/GEOS_Branch_3.9/)| -| **Winnie** | [![Winnie](https://winnie.postgis.net:444/view/GEOS/job/GEOS_Master/badge/icon)](https://winnie.postgis.net:444/view/GEOS/job/GEOS_Master/) | [![Winnie](https://winnie.postgis.net:444/view/GEOS/job/GEOS_Branch_3.13/badge/icon)](https://winnie.postgis.net:444/view/GEOS/job/GEOS_Branch_3.13/) | [![Winnie](https://winnie.postgis.net:444/view/GEOS/job/GEOS_Branch_3.12/badge/icon)](https://winnie.postgis.net:444/view/GEOS/job/GEOS_Branch_3.12/) | [![Winnie](https://winnie.postgis.net:444/view/GEOS/job/GEOS_Branch_3.11/badge/icon)](https://winnie.postgis.net:444/view/GEOS/job/GEOS_Branch_3.11/) | [![Winnie](https://winnie.postgis.net:444/view/GEOS/job/GEOS_Branch_3.10/badge/icon)](https://winnie.postgis.net:444/view/GEOS/job/GEOS_Branch_3.10/) | [![Winnie](https://winnie.postgis.net:444/view/GEOS/job/GEOS_Branch_3.9/badge/icon)](https://winnie.postgis.net:444/view/GEOS/job/GEOS_Branch_3.9/) | -| **Dronie** | [![Dronie](https://dronie.osgeo.org/api/badges/geos/geos/status.svg?ref=refs/heads/main)](https://dronie.osgeo.org/geos/geos?ref=refs/heads/main) | [![Dronie](https://dronie.osgeo.org/api/badges/geos/geos/status.svg?ref=refs/heads/3.13)](https://dronie.osgeo.org/geos/geos?refs/heads/3.14) | [![Dronie](https://dronie.osgeo.org/api/badges/geos/geos/status.svg?ref=refs/heads/3.12)](https://dronie.osgeo.org/geos/geos?refs/heads/3.12) | [![Dronie](https://dronie.osgeo.org/api/badges/geos/geos/status.svg?ref=refs/heads/3.11)](https://dronie.osgeo.org/geos/geos?refs/heads/3.11) | [![Dronie](https://dronie.osgeo.org/api/badges/geos/geos/status.svg?ref=refs/heads/3.10)](https://dronie.osgeo.org/geos/geos?refs/heads/3.10) | [![Dronie](https://dronie.osgeo.org/api/badges/geos/geos/status.svg?ref=refs/heads/3.9)](https://dronie.osgeo.org/geos/geos?refs/heads/3.9) | -| **Bessie** | [![Bessie](https://debbie.postgis.net/buildStatus/icon?job=GEOS_Worker_Run/label=bessie&BRANCH=main)](https://debbie.postgis.net/view/GEOS/job/GEOS_Worker_Run/label=bessie) | [![Bessie](https://debbie.postgis.net/buildStatus/icon?job=GEOS_Worker_Run/label=bessie&build=last:${params.reference=refs/heads/3.13})](https://debbie.postgis.net/view/GEOS/job/GEOS_Worker_Run/label=bessie) |[![Bessie](https://debbie.postgis.net/buildStatus/icon?job=GEOS_Worker_Run/label=bessie&build=last:${params.reference=refs/heads/3.12})](https://debbie.postgis.net/view/GEOS/job/GEOS_Worker_Run/label=bessie) | [![Bessie](https://debbie.postgis.net/buildStatus/icon?job=GEOS_Worker_Run/label=bessie&build=last:${params.reference=refs/heads/3.11})](https://debbie.postgis.net/view/GEOS/job/GEOS_Worker_Run/label=bessie) | [![Bessie](https://debbie.postgis.net/buildStatus/icon?job=GEOS_Worker_Run/label=bessie&build=last:${params.reference=refs/heads/3.10})](https://debbie.postgis.net/view/GEOS/job/ GEOS_Worker_Run/label=bessie)| [![Bessie](https://debbie.postgis.net/buildStatus/icon?job=GEOS_Worker_Run/label=bessie&build=last:${params.reference=refs/heads/3.9})](https://debbie.postgis.net/view/GEOS/job/GEOS_Worker_Run/label=bessie)| -| **Berrie** | [![Berrie](https://debbie.postgis.net/buildStatus/icon?job=GEOS_Worker_Run/label=berrie&BRANCH=main)](https://debbie.postgis.net/view/GEOS/job/GEOS_Worker_Run/label=berrie) |[![Berrie64](https://debbie.postgis.net/buildStatus/icon?job=GEOS_Worker_Run/label=berrie&build=last:${params.reference=refs/heads/3.13})](https://debbie.postgis.net/view/GEOS/job/GEOS_Worker_Run/label=berrie)|[![Berrie64](https://debbie.postgis.net/buildStatus/icon?job=GEOS_Worker_Run/label=berrie&build=last:${params.reference=refs/heads/3.12})](https://debbie.postgis.net/view/GEOS/job/GEOS_Worker_Run/label=berrie) |[![Berrie64](https://debbie.postgis.net/buildStatus/icon?job=GEOS_Worker_Run/label=berrie&build=last:${params.reference=refs/heads/3.11})](https://debbie.postgis.net/view/GEOS/job/GEOS_Worker_Run/label=berrie) |[![Berrie64](https://debbie.postgis.net/buildStatus/icon?job=GEOS_Worker_Run/label=berrie&build=last:${params.reference=refs/heads/3.10})](https://debbie.postgis.net/view/GEOS/ job/GEOS_Worker_Run/label=berrie) | | -| **Berrie64** | [![Berrie64](https://debbie.postgis.net/buildStatus/icon?job=GEOS_Worker_Run/label=berrie64&BRANCH=main)](https://debbie.postgis.net/view/GEOS/job/GEOS_Worker_Run/label=berrie64) | [![Berrie64](https://debbie.postgis.net/buildStatus/icon?job=GEOS_Worker_Run/label=berrie64&build=last:${params.reference=refs/heads/3.13})](https://debbie.postgis.net/view/GEOS/job/GEOS_Worker_Run/label=berrie64) | [![Berrie64](https://debbie.postgis.net/buildStatus/icon?job=GEOS_Worker_Run/label=berrie64&build=last:${params.reference=refs/heads/3.12})](https://debbie.postgis.net/view/GEOS/job/GEOS_Worker_Run/label=berrie64) | [![Berrie64](https://debbie.postgis.net/buildStatus/icon?job=GEOS_Worker_Run/label=berrie64&build=last:${params.reference=refs/heads/3.11})](https://debbie.postgis.net/view/GEOS/job/GEOS_Worker_Run/label=berrie64) | [![Berrie64](https://debbie.postgis.net/buildStatus/icon?job=GEOS_Worker_Run/label=berrie64&build=last:${params.reference=refs/heads/3.10})](https://de bbie.postgis.net/view/GEOS/job/GEOS_Worker_Run/label=berrie64)| | +| CI | main | 3.14 | 3.13 | 3.12 | 3.11 | 3.10 | +| ------ | ------ | ------ | ------ | ------ | ------ | ------ | +| **GitHub** | [![GitHub](https://github.com/libgeos/geos/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/libgeos/geos/actions?query=workflow%3ACI+branch%3Amain) | [![GitHub](https://github.com/libgeos/geos/actions/workflows/ci.yml/badge.svg?branch=3.14)](https://github.com/libgeos/geos/actions?query=branch%3A3.14) | [![GitHub](https://github.com/libgeos/geos/actions/workflows/ci.yml/badge.svg?branch=3.13)](https://github.com/libgeos/geos/actions?query=branch%3A3.13) | [![GitHub](https://github.com/libgeos/geos/actions/workflows/ci.yml/badge.svg?branch=3.12)](https://github.com/libgeos/geos/actions?query=branch%3A3.12) | [![GitHub](https://github.com/libgeos/geos/actions/workflows/ci.yml/badge.svg?branch=3.11)](https://github.com/libgeos/geos/actions?query=branch%3A3.11) | [![GitHub](https://github.com/libgeos/geos/actions/workflows/ci.yml/badge.svg?branch=3.10)](https://github.com/libgeos/geos/actions?query=branch%3A3.10)| +| **Debbie** | [![Debbie](https://debbie.postgis.net/buildStatus/icon?job=GEOS_Master)](https://debbie.postgis.net/view/GEOS/job/GEOS_Master/)| [![Debbie](https://debbie.postgis.net/buildStatus/icon?job=GEOS_Branch_3.14)](https://debbie.postgis.net/view/job/GEOS_Branch_3.14/) |[![Debbie](https://debbie.postgis.net/buildStatus/icon?job=GEOS_Branch_3.13)](https://debbie.postgis.net/view/job/GEOS_Branch_3.13/) | [![Debbie](https://debbie.postgis.net/buildStatus/icon?job=GEOS_Branch_3.12)](https://debbie.postgis.net/view/job/GEOS_Branch_3.12/) | [![Debbie](https://debbie.postgis.net/buildStatus/icon?job=GEOS_Branch_3.11)](https://debbie.postgis.net/view/job/GEOS_Branch_3.11/)| [![Debbie](https://debbie.postgis.net/buildStatus/icon?job=GEOS_Branch_3.10)](https://debbie.postgis.net/view/job/GEOS_Branch_3.10/)| +| **Winnie** | [![Winnie](https://winnie.postgis.net:444/view/GEOS/job/GEOS_Master/badge/icon)](https://winnie.postgis.net:444/view/GEOS/job/GEOS_Master/) | [![Winnie](https://winnie.postgis.net:444/view/GEOS/job/GEOS_Branch_3.14/badge/icon)](https://winnie.postgis.net:444/view/GEOS/job/GEOS_Branch_3.14/) | [![Winnie](https://winnie.postgis.net:444/view/GEOS/job/GEOS_Branch_3.13/badge/icon)](https://winnie.postgis.net:444/view/GEOS/job/GEOS_Branch_3.13/) | [![Winnie](https://winnie.postgis.net:444/view/GEOS/job/GEOS_Branch_3.12/badge/icon)](https://winnie.postgis.net:444/view/GEOS/job/GEOS_Branch_3.12/) | [![Winnie](https://winnie.postgis.net:444/view/GEOS/job/GEOS_Branch_3.11/badge/icon)](https://winnie.postgis.net:444/view/GEOS/job/GEOS_Branch_3.11/) | [![Winnie](https://winnie.postgis.net:444/view/GEOS/job/GEOS_Branch_3.10/badge/icon)](https://winnie.postgis.net:444/view/GEOS/job/GEOS_Branch_3.10/) | +| **Bessie** | [![Bessie](https://debbie.postgis.net/buildStatus/icon?job=GEOS_Worker_Run/label=bessie&BRANCH=main)](https://debbie.postgis.net/view/GEOS/job/GEOS_Worker_Run/label=bessie) | [![Bessie](https://debbie.postgis.net/buildStatus/icon?job=GEOS_Worker_Run/label=bessie&build=last:${params.reference=refs/heads/3.14})](https://debbie.postgis.net/view/GEOS/job/GEOS_Worker_Run/label=bessie) |[![Bessie](https://debbie.postgis.net/buildStatus/icon?job=GEOS_Worker_Run/label=bessie&build=last:${params.reference=refs/heads/3.13})](https://debbie.postgis.net/view/GEOS/job/GEOS_Worker_Run/label=bessie) | [![Bessie](https://debbie.postgis.net/buildStatus/icon?job=GEOS_Worker_Run/label=bessie&build=last:${params.reference=refs/heads/3.12})](https://debbie.postgis.net/view/GEOS/job/GEOS_Worker_Run/label=bessie) | [![Bessie](https://debbie.postgis.net/buildStatus/icon?job=GEOS_Worker_Run/label=bessie&build=last:${params.reference=refs/heads/3.11})](https://debbie.postgis.net/view/GEOS/job/ GEOS_Worker_Run/label=bessie)| [![Bessie](https://debbie.postgis.net/buildStatus/icon?job=GEOS_Worker_Run/label=bessie&build=last:${params.reference=refs/heads/3.10})](https://debbie.postgis.net/view/GEOS/job/GEOS_Worker_Run/label=bessie)| +| **Berrie** | [![Berrie](https://debbie.postgis.net/buildStatus/icon?job=GEOS_Worker_Run/label=berrie&BRANCH=main)](https://debbie.postgis.net/view/GEOS/job/GEOS_Worker_Run/label=berrie) |[![Berrie64](https://debbie.postgis.net/buildStatus/icon?job=GEOS_Worker_Run/label=berrie&build=last:${params.reference=refs/heads/3.14})](https://debbie.postgis.net/view/GEOS/job/GEOS_Worker_Run/label=berrie)|[![Berrie64](https://debbie.postgis.net/buildStatus/icon?job=GEOS_Worker_Run/label=berrie&build=last:${params.reference=refs/heads/3.13})](https://debbie.postgis.net/view/GEOS/job/GEOS_Worker_Run/label=berrie) |[![Berrie64](https://debbie.postgis.net/buildStatus/icon?job=GEOS_Worker_Run/label=berrie&build=last:${params.reference=refs/heads/3.12})](https://debbie.postgis.net/view/GEOS/job/GEOS_Worker_Run/label=berrie) |[![Berrie64](https://debbie.postgis.net/buildStatus/icon?job=GEOS_Worker_Run/label=berrie&build=last:${params.reference=refs/heads/3.11})](https://debbie.postgis.net/view/GEOS/ job/GEOS_Worker_Run/label=berrie) | | +| **Berrie64** | [![Berrie64](https://debbie.postgis.net/buildStatus/icon?job=GEOS_Worker_Run/label=berrie64&BRANCH=main)](https://debbie.postgis.net/view/GEOS/job/GEOS_Worker_Run/label=berrie64) | [![Berrie64](https://debbie.postgis.net/buildStatus/icon?job=GEOS_Worker_Run/label=berrie64&build=last:${params.reference=refs/heads/3.14})](https://debbie.postgis.net/view/GEOS/job/GEOS_Worker_Run/label=berrie64) | [![Berrie64](https://debbie.postgis.net/buildStatus/icon?job=GEOS_Worker_Run/label=berrie64&build=last:${params.reference=refs/heads/3.13})](https://debbie.postgis.net/view/GEOS/job/GEOS_Worker_Run/label=berrie64) | [![Berrie64](https://debbie.postgis.net/buildStatus/icon?job=GEOS_Worker_Run/label=berrie64&build=last:${params.reference=refs/heads/3.12})](https://debbie.postgis.net/view/GEOS/job/GEOS_Worker_Run/label=berrie64) | [![Berrie64](https://debbie.postgis.net/buildStatus/icon?job=GEOS_Worker_Run/label=berrie64&build=last:${params.reference=refs/heads/3.11})](https://de bbie.postgis.net/view/GEOS/job/GEOS_Worker_Run/label=berrie64)| | ## Runners * **GitHub** - Ubuntu various, Windows various, MacOS various, CMake -* **Debbie** - Debian sid (bullseye), GNU/Linux 64bit, GCC Debian 10.2.1, cmake (3.18.4) >= 3.9, autotools < 3.8 +* **Debbie** - Debian sid (bullseye), GNU/Linux 64bit, GCC Debian 10.2.1, cmake (3.18.4) >= 3.10, autotools < 3.8 * **Winnie** - Windows Mingw64, 32bit GCC 8.1.0, 64bit GCC 8.1.0, MSys CMake (3.21.3), 64-bit GCC 12.1 (cmake 3.23.2 ninja) -* **Dronie** - Alpine Linux 3.14 (alpine.latest), 64bit, GCC 10.3.1, CMake (3.20.3) , automake < 3.10 -* **Bessie** - FreeBSD 14.1, 64-bit CMake (3.30.5) >=3.9, clang version 18.1.5 +* **Bessie** - FreeBSD 14.1, 64-bit CMake (3.30.5) >=3.10, clang version 18.1.5 * **Berrie** - Raspberry Pi (debian bullseye (11)), 32-bit gcc 10.2.1, CMake (3.18.4) * **Berrie64** - Raspberry Pi Arm (debian bookworm (12)), 64-bit gcc 10.2.1, CMake (3.18.4) ----------------------------------------------------------------------- Summary of changes: .drone-1.0.yml | 37 ---------------------------- web/config.toml | 2 +- web/content/project/development/ci_status.md | 22 ++++++++--------- 3 files changed, 11 insertions(+), 50 deletions(-) delete mode 100644 .drone-1.0.yml hooks/post-receive -- GEOS From git at osgeo.org Mon Jan 12 07:31:34 2026 From: git at osgeo.org (git at osgeo.org) Date: Mon, 12 Jan 2026 07:31:34 -0800 (PST) Subject: [geos-commits] [SCM] GEOS branch main updated. 3f3a3e911f259b6fe76e18e05abe03c062d89aeb Message-ID: <20260112153134.6E01F20E53@trac.osgeo.org> This is an automated email from the git hooks/post-receive script. It was generated because a ref change was pushed to the repository containing the project "GEOS". The branch, main has been updated via 3f3a3e911f259b6fe76e18e05abe03c062d89aeb (commit) from eb6121ae303477279d4a571c54ae9f0435e120bb (commit) Those revisions listed above that are new to this repository have not appeared on any other notification email; so we list those revisions in full, below. - Log ----------------------------------------------------------------- commit 3f3a3e911f259b6fe76e18e05abe03c062d89aeb Author: Daniel Baston Date: Mon Jan 12 10:31:09 2026 -0500 Simplify: Preserve dimensions of empty geometries (#1357) * DouglasPeuckerSimplifier: Preserve dimension of empty geometries * TopologyPreservingSimplifier: Add test for preservation of empty geometry dims diff --git a/include/geos/simplify/DouglasPeuckerSimplifier.h b/include/geos/simplify/DouglasPeuckerSimplifier.h index 97fa32ca4..57fe5404e 100644 --- a/include/geos/simplify/DouglasPeuckerSimplifier.h +++ b/include/geos/simplify/DouglasPeuckerSimplifier.h @@ -68,7 +68,7 @@ public: */ void setDistanceTolerance(double tolerance); - std::unique_ptr getResultGeometry(); + std::unique_ptr getResultGeometry() const; private: diff --git a/src/simplify/DouglasPeuckerSimplifier.cpp b/src/simplify/DouglasPeuckerSimplifier.cpp index 893d785a2..05b32b588 100644 --- a/src/simplify/DouglasPeuckerSimplifier.cpp +++ b/src/simplify/DouglasPeuckerSimplifier.cpp @@ -201,8 +201,12 @@ DouglasPeuckerSimplifier::setDistanceTolerance(double tol) } Geometry::Ptr -DouglasPeuckerSimplifier::getResultGeometry() +DouglasPeuckerSimplifier::getResultGeometry() const { + if (inputGeom->isEmpty()) { + return inputGeom->clone(); + } + DPTransformer t(distanceTolerance); return t.transform(inputGeom); diff --git a/tests/unit/simplify/DouglasPeuckerSimplifierTest.cpp b/tests/unit/simplify/DouglasPeuckerSimplifierTest.cpp index ee3257a90..50c0bf308 100644 --- a/tests/unit/simplify/DouglasPeuckerSimplifierTest.cpp +++ b/tests/unit/simplify/DouglasPeuckerSimplifierTest.cpp @@ -37,7 +37,7 @@ struct test_dpsimp_data { {} void - checkDP(const std::string& wkt, double tolerance, const std::string& wkt_expected) + checkDP(const std::string& wkt, double tolerance, const std::string& wkt_expected) const { GeomPtr g(wktreader.read(wkt)); GeomPtr simplified = DouglasPeuckerSimplifier::simplify(g.get(), tolerance); @@ -49,7 +49,7 @@ struct test_dpsimp_data { } void - checkDPNoChange(const std::string& wkt, double tolerance) + checkDPNoChange(const std::string& wkt, double tolerance) const { checkDP(wkt, tolerance, wkt); } @@ -382,4 +382,24 @@ void object::test<23>() "POLYGON ZM ((2 0 10 9, 2 2 15 11, 0 2 20 13, 0 0 25 15, 2 0 10 9))"); } +template<> +template<> +void object::test<24>() +{ + set_test_name("simplification preserves dimensions of empty geometries"); + + const std::vector geomTypes{"POINT", "LINESTRING", "POLYGON"}; + const std::vector dimensions{"", "Z", "M", "ZM"}; + const std::vector mods{"", "MULTI"}; + + for (const auto& geomType : geomTypes) { + for (const auto& dimension: dimensions) { + for (const auto& mod: mods) { + std::string wkt = mod + geomType + " " + dimension + " EMPTY"; + checkDP(wkt, 0, wkt); + } + } + } +} + } // namespace tut diff --git a/tests/unit/simplify/TopologyPreservingSimplifierTest.cpp b/tests/unit/simplify/TopologyPreservingSimplifierTest.cpp index d8ab8d615..651627b05 100644 --- a/tests/unit/simplify/TopologyPreservingSimplifierTest.cpp +++ b/tests/unit/simplify/TopologyPreservingSimplifierTest.cpp @@ -34,17 +34,17 @@ struct test_tpsimp_data { } void - checkTPS(const std::string& wkt, double tolerance, const std::string& wkt_expected) + checkTPS(const std::string& wkt, double tolerance, const std::string& wkt_expected) const { GeomPtr g(wktreader.read(wkt)); GeomPtr simplified = TopologyPreservingSimplifier::simplify(g.get(), tolerance); GeomPtr exp(wktreader.read(wkt_expected)); - ensure_equals_geometry(exp.get(), simplified.get(),0.00001); + ensure_equals_geometry_xyzm(exp.get(), simplified.get(),0.00001); ensure("Simplified geometry is invalid!", simplified->isValid()); } void - checkTPSNoChange(const std::string& wkt, double tolerance) + checkTPSNoChange(const std::string& wkt, double tolerance) const { checkTPS(wkt, tolerance, wkt); } @@ -453,4 +453,24 @@ void object::test<35>() "MULTIPOLYGON (((689.300102 5733.615673, 689.46186 5733.617409, 689.458981 5733.646089, 689.300102 5733.615673)), ((689.488158 5733.746304, 689.23796 5733.680098, 689.253227 5733.613915, 689.467162 5733.67151, 689.679568 5733.588383, 689.488158 5733.746304)))"); } +template<> +template<> +void object::test<36>() +{ + set_test_name("simplification preserves dimensions of empty geometries"); + + const std::vector geomTypes{"POINT", "LINESTRING", "POLYGON"}; + const std::vector dimensions{"", "Z", "M", "ZM"}; + const std::vector mods{"", "MULTI"}; + + for (const auto& geomType : geomTypes) { + for (const auto& dimension: dimensions) { + for (const auto& mod: mods) { + std::string wkt = mod + geomType + " " + dimension + " EMPTY"; + checkTPSNoChange(wkt, 0); + } + } + } +} + } // namespace tut ----------------------------------------------------------------------- Summary of changes: include/geos/simplify/DouglasPeuckerSimplifier.h | 2 +- src/simplify/DouglasPeuckerSimplifier.cpp | 6 ++++- .../unit/simplify/DouglasPeuckerSimplifierTest.cpp | 24 ++++++++++++++++++-- .../simplify/TopologyPreservingSimplifierTest.cpp | 26 +++++++++++++++++++--- 4 files changed, 51 insertions(+), 7 deletions(-) hooks/post-receive -- GEOS From git at osgeo.org Mon Jan 12 08:09:25 2026 From: git at osgeo.org (git at osgeo.org) Date: Mon, 12 Jan 2026 08:09:25 -0800 (PST) Subject: [geos-commits] [SCM] GEOS branch main updated. d5d89d025fcd877fc96b80cd6a325b572a1b16f3 Message-ID: <20260112160925.F1E4F20ED0@trac.osgeo.org> This is an automated email from the git hooks/post-receive script. It was generated because a ref change was pushed to the repository containing the project "GEOS". The branch, main has been updated via d5d89d025fcd877fc96b80cd6a325b572a1b16f3 (commit) from 3f3a3e911f259b6fe76e18e05abe03c062d89aeb (commit) Those revisions listed above that are new to this repository have not appeared on any other notification email; so we list those revisions in full, below. - Log ----------------------------------------------------------------- commit d5d89d025fcd877fc96b80cd6a325b572a1b16f3 Author: Daniel Baston Date: Mon Jan 12 11:09:06 2026 -0500 Fix crash in ConvexHull::preSort (#1358) * Add test for GH-1072 Resolves https://github.com/libgeos/geos/issues/1072 * ConvexHull: improve robustness of polarCompare diff --git a/src/algorithm/ConvexHull.cpp b/src/algorithm/ConvexHull.cpp index 7d8e34374..1ed617438 100644 --- a/src/algorithm/ConvexHull.cpp +++ b/src/algorithm/ConvexHull.cpp @@ -63,13 +63,21 @@ private: polarCompare(const Coordinate* o, const Coordinate* p, const Coordinate* q) { - int orient = Orientation::index(*o, *p, *q); + // To use this comparator in std::sort it must provide a stable ordering, such that + // if cmp(a, b) is true then cmp(b, a) is false. Unfortunately Orientation::index may + // not provide this guarantee when the inputs differ by many orders of magnitude. To + // guard against this, we normalize the order of P and Q before calling OrientationIndex + // and flip the result if the inputs were flipped. + const bool swap = geom::CoordinateLessThan()(p, q); + + const int orient = swap ? Orientation::index(*o, *q, *p) : Orientation::index(*o, *p, *q); if(orient == Orientation::COUNTERCLOCKWISE) { - return 1; + return swap ? -1 : 1; } + if(orient == Orientation::CLOCKWISE) { - return -1; + return swap ? 1 : -1; } /** @@ -102,7 +110,7 @@ public: RadiallyLessThen(const Coordinate* c): origin(c) {} bool - operator()(const Coordinate* p1, const Coordinate* p2) + operator()(const Coordinate* p1, const Coordinate* p2) const { return (polarCompare(origin, p1, p2) == -1); } diff --git a/tests/unit/capi/GEOSMinimumBoundingCircleTest.cpp b/tests/unit/capi/GEOSMinimumBoundingCircleTest.cpp index f535fce86..3634752e9 100644 --- a/tests/unit/capi/GEOSMinimumBoundingCircleTest.cpp +++ b/tests/unit/capi/GEOSMinimumBoundingCircleTest.cpp @@ -129,5 +129,20 @@ void object::test<5> ensure("curved geometry not supported", result_ == nullptr); } +template<> +template<> +void object::test<6> +() +{ + set_test_name("https://github.com/libgeos/geos/issues/1072"); + + geom1_ = fromWKT("LINESTRING(7777777777777777770 7777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777770 1, 1 7 1,-2 1 2)"); + geom2_ = GEOSSingleSidedBuffer(geom1_, 1.0, 64, 1, 1.0, 1); + result_ = GEOSMinimumBoundingCircle(geom2_, NULL, NULL); + + // no segfault +} + + } // namespace tut ----------------------------------------------------------------------- Summary of changes: src/algorithm/ConvexHull.cpp | 16 ++++++++++++---- tests/unit/capi/GEOSMinimumBoundingCircleTest.cpp | 15 +++++++++++++++ 2 files changed, 27 insertions(+), 4 deletions(-) hooks/post-receive -- GEOS From git at osgeo.org Tue Jan 13 05:49:38 2026 From: git at osgeo.org (git at osgeo.org) Date: Tue, 13 Jan 2026 05:49:38 -0800 (PST) Subject: [geos-commits] [SCM] GEOS branch main updated. 8e4d950d8071a02748d54772b839623f5e586428 Message-ID: <20260113134938.91A813983D@trac.osgeo.org> This is an automated email from the git hooks/post-receive script. It was generated because a ref change was pushed to the repository containing the project "GEOS". The branch, main has been updated via 8e4d950d8071a02748d54772b839623f5e586428 (commit) from d5d89d025fcd877fc96b80cd6a325b572a1b16f3 (commit) Those revisions listed above that are new to this repository have not appeared on any other notification email; so we list those revisions in full, below. - Log ----------------------------------------------------------------- commit 8e4d950d8071a02748d54772b839623f5e586428 Author: Daniel Baston Date: Tue Jan 13 08:49:15 2026 -0500 Polygonizer: Preserve M values in input (#1363) Resolves https://github.com/libgeos/geos/issues/1360 diff --git a/include/geos/operation/polygonize/PolygonizeEdge.h b/include/geos/operation/polygonize/PolygonizeEdge.h index 718d709b4..d58d9b8f5 100644 --- a/include/geos/operation/polygonize/PolygonizeEdge.h +++ b/include/geos/operation/polygonize/PolygonizeEdge.h @@ -49,7 +49,7 @@ public: PolygonizeEdge(const geom::LineString* newLine); // Just return what it was given initially - const geom::LineString* getLine(); + const geom::LineString* getLine() const; }; } // namespace geos::operation::polygonize diff --git a/src/operation/polygonize/EdgeRing.cpp b/src/operation/polygonize/EdgeRing.cpp index 4f4252a8d..937b92497 100644 --- a/src/operation/polygonize/EdgeRing.cpp +++ b/src/operation/polygonize/EdgeRing.cpp @@ -224,9 +224,19 @@ const CoordinateSequence* EdgeRing::getCoordinates() const { if(ringPts == nullptr) { - ringPts = std::make_shared(0u, 0u); + bool hasZ = false; + bool hasM = false; + for(const auto& de : deList) { - auto edge = dynamic_cast(de->getEdge()); + const auto edge = detail::down_cast(de->getEdge()); + hasZ |= edge->getLine()->hasZ(); + hasM |= edge->getLine()->hasM(); + } + + ringPts = std::make_shared(0u, hasZ, hasM); + + for(const auto& de : deList) { + auto edge = detail::down_cast(de->getEdge()); addEdge(edge->getLine()->getCoordinatesRO(), de->getEdgeDirection(), ringPts.get()); } @@ -281,13 +291,13 @@ EdgeRing::addEdge(const CoordinateSequence* coords, bool isForward, { const std::size_t npts = coords->getSize(); if(isForward) { - for(std::size_t i = 0; i < npts; ++i) { - coordList->add(coords->getAt(i), false); - } + coordList->add(*coords, 0, npts - 1, false); } else { for(std::size_t i = npts; i > 0; --i) { - coordList->add(coords->getAt(i - 1), false); + coords->applyAt(i-1, [coordList](const auto& coord) { + coordList->add(coord, false); + }); } } } diff --git a/src/operation/polygonize/PolygonizeEdge.cpp b/src/operation/polygonize/PolygonizeEdge.cpp index a224ec2cb..d2d210cc5 100644 --- a/src/operation/polygonize/PolygonizeEdge.cpp +++ b/src/operation/polygonize/PolygonizeEdge.cpp @@ -32,7 +32,7 @@ PolygonizeEdge::PolygonizeEdge(const LineString* newLine) } const LineString* -PolygonizeEdge::getLine() +PolygonizeEdge::getLine() const { return line; } diff --git a/tests/unit/operation/polygonize/PolygonizeTest.cpp b/tests/unit/operation/polygonize/PolygonizeTest.cpp index 0a1a9e40e..970fd4797 100644 --- a/tests/unit/operation/polygonize/PolygonizeTest.cpp +++ b/tests/unit/operation/polygonize/PolygonizeTest.cpp @@ -381,5 +381,25 @@ void object::test<10>() INVALID_RING_LINES); } +template<> +template<> +void object::test<11>() +{ + set_test_name("Z/M preserved"); + + doTest( + {"GEOMETRYCOLLECTION (" + "LINESTRING ZM (10 0 3 4, 0 0 0 0, 0 10 1 2, 10 10 2 3)," + "LINESTRING ZM (10 10 2 3, 10 0 3 4)," + "LINESTRING M (10 0 8, 20 0 9, 10 10 11))" + }, + { + "POLYGON ZM ((0 0 0 0, 0 10 1 2, 10 10 2 3, 10 0 3 4, 0 0 0 0))", + "POLYGON ZM ((10 0 3 4, 10 10 2 3, 20 0 NaN 9, 10 0 3 4))" + }, + false, + POLYGONS); +} + } // namespace tut ----------------------------------------------------------------------- Summary of changes: include/geos/operation/polygonize/PolygonizeEdge.h | 2 +- src/operation/polygonize/EdgeRing.cpp | 22 ++++++++++++++++------ src/operation/polygonize/PolygonizeEdge.cpp | 2 +- tests/unit/operation/polygonize/PolygonizeTest.cpp | 20 ++++++++++++++++++++ 4 files changed, 38 insertions(+), 8 deletions(-) hooks/post-receive -- GEOS From git at osgeo.org Tue Jan 13 06:41:51 2026 From: git at osgeo.org (git at osgeo.org) Date: Tue, 13 Jan 2026 06:41:51 -0800 (PST) Subject: [geos-commits] [SCM] GEOS branch main updated. 88e9956280f9dda33447137b0cc1c1ffb62d05ce Message-ID: <20260113144151.B41423AC17@trac.osgeo.org> This is an automated email from the git hooks/post-receive script. It was generated because a ref change was pushed to the repository containing the project "GEOS". The branch, main has been updated via 88e9956280f9dda33447137b0cc1c1ffb62d05ce (commit) from 8e4d950d8071a02748d54772b839623f5e586428 (commit) Those revisions listed above that are new to this repository have not appeared on any other notification email; so we list those revisions in full, below. - Log ----------------------------------------------------------------- commit 88e9956280f9dda33447137b0cc1c1ffb62d05ce Author: Daniel Baston Date: Tue Jan 13 08:59:08 2026 -0500 PointLocation: add test diff --git a/tests/unit/algorithm/LocatePointInRingTest.cpp b/tests/unit/algorithm/LocatePointInRingTest.cpp index b52768380..1a70b9d15 100644 --- a/tests/unit/algorithm/LocatePointInRingTest.cpp +++ b/tests/unit/algorithm/LocatePointInRingTest.cpp @@ -284,6 +284,17 @@ void object::test<11>() runPtLocator(Location::EXTERIOR, CoordinateXY(0, 5), wkt); } +template<> +template<> +void object::test<12>() +{ + set_test_name("robustness test from PostGIS ticket #6023"); + + std::string wkt = "POLYGON ((11.230120879533454 62.84897119848748,11.230120879533905 62.8489711984873,11.23020501303477 62.84900750109812,11.230170431987244 62.84904481447776,11.230117909393426 62.8489943480894,11.230120879533454 62.84897119848748))"; + + runPtLocator(Location::BOUNDARY, CoordinateXY(11.230120879533454, 62.84897119848748), wkt); +} + } // namespace tut ----------------------------------------------------------------------- Summary of changes: tests/unit/algorithm/LocatePointInRingTest.cpp | 11 +++++++++++ 1 file changed, 11 insertions(+) hooks/post-receive -- GEOS From git at osgeo.org Tue Jan 13 13:50:54 2026 From: git at osgeo.org (git at osgeo.org) Date: Tue, 13 Jan 2026 13:50:54 -0800 (PST) Subject: [geos-commits] [SCM] GEOS branch main updated. 2ad56c719b0155256712357aa4f8e8a6e05ce8b7 Message-ID: <20260113215055.EDDA93E02E@trac.osgeo.org> This is an automated email from the git hooks/post-receive script. It was generated because a ref change was pushed to the repository containing the project "GEOS". The branch, main has been updated via 2ad56c719b0155256712357aa4f8e8a6e05ce8b7 (commit) from 88e9956280f9dda33447137b0cc1c1ffb62d05ce (commit) Those revisions listed above that are new to this repository have not appeared on any other notification email; so we list those revisions in full, below. - Log ----------------------------------------------------------------- commit 2ad56c719b0155256712357aa4f8e8a6e05ce8b7 Author: Daniel Baston Date: Tue Jan 13 16:50:29 2026 -0500 LineDissolver: Create geometries with dimensions matching input (#1373) * LineDissolver: Create geometries with dimensions matching input * LineDissolver: Create CoordinateSequence as shared_ptr diff --git a/include/geos/dissolve/LineDissolver.h b/include/geos/dissolve/LineDissolver.h index cfae61f80..404e94bb3 100644 --- a/include/geos/dissolve/LineDissolver.h +++ b/include/geos/dissolve/LineDissolver.h @@ -83,6 +83,8 @@ private: std::vector> lines; std::stack nodeEdgeStack; DissolveHalfEdge* ringStartEdge = nullptr; + bool constructZ = false; + bool constructM = false; void computeResult(); @@ -136,7 +138,7 @@ private: void buildRing(HalfEdge* eStartRing); - void addLine(std::unique_ptr& cs); + void addLine(const std::shared_ptr& cs); public: diff --git a/src/dissolve/LineDissolver.cpp b/src/dissolve/LineDissolver.cpp index 3c1d808fb..257ea181a 100644 --- a/src/dissolve/LineDissolver.cpp +++ b/src/dissolve/LineDissolver.cpp @@ -88,6 +88,8 @@ void LineDissolver::add(const LineString* lineString) { const CoordinateSequence* seq = lineString->getCoordinatesRO(); + constructZ |= seq->hasZ(); + constructM |= seq->hasM(); bool doneStart = false; for (std::size_t i = 1; i < seq->size(); i++) { @@ -200,7 +202,7 @@ LineDissolver::updateRingStartEdge(DissolveHalfEdge* e) void LineDissolver::buildLine(HalfEdge* eStart) { - std::unique_ptr line(new CoordinateSequence(0, 4)); + auto line = std::make_shared(0, constructZ, constructM); DissolveHalfEdge* e = static_cast(eStart); ringStartEdge = nullptr; @@ -238,7 +240,7 @@ LineDissolver::buildLine(HalfEdge* eStart) void LineDissolver::buildRing(HalfEdge* eStartRing) { - std::unique_ptr line(new CoordinateSequence(0, 4)); + auto line = std::make_shared(0, constructZ, constructM); HalfEdge* e = eStartRing; // add first node @@ -265,9 +267,9 @@ LineDissolver::buildRing(HalfEdge* eStartRing) /* private */ void -LineDissolver::addLine(std::unique_ptr& cs) +LineDissolver::addLine(const std::shared_ptr& cs) { - auto ls = factory->createLineString(std::move(cs)); + auto ls = factory->createLineString(cs); lines.emplace_back(ls.release()); } diff --git a/tests/unit/coverage/CoverageCleanerTest.cpp b/tests/unit/coverage/CoverageCleanerTest.cpp index f36f1f887..de0b634db 100644 --- a/tests/unit/coverage/CoverageCleanerTest.cpp +++ b/tests/unit/coverage/CoverageCleanerTest.cpp @@ -77,6 +77,8 @@ struct test_coveragecleaner_data { { ensure_equals("checkEqual sizes", actual.size(), expected.size()); for (std::size_t i = 0; i < actual.size(); i++) { + ensure_equals("hasZ does not match", actual[i]->hasZ(), expected[i]->hasZ()); + ensure_equals("hasM does not match", actual[i]->hasM(), expected[i]->hasM()); ensure_equals_geometry(actual[i], expected[i]); } } ----------------------------------------------------------------------- Summary of changes: include/geos/dissolve/LineDissolver.h | 4 +++- src/dissolve/LineDissolver.cpp | 10 ++++++---- tests/unit/coverage/CoverageCleanerTest.cpp | 2 ++ 3 files changed, 11 insertions(+), 5 deletions(-) hooks/post-receive -- GEOS From git at osgeo.org Tue Jan 13 13:56:47 2026 From: git at osgeo.org (git at osgeo.org) Date: Tue, 13 Jan 2026 13:56:47 -0800 (PST) Subject: [geos-commits] [SCM] GEOS branch main updated. 36d2dcfdea9cf2738dc5a233ee917417511e7945 Message-ID: <20260113215648.168BE3B5FB@trac.osgeo.org> This is an automated email from the git hooks/post-receive script. It was generated because a ref change was pushed to the repository containing the project "GEOS". The branch, main has been updated via 36d2dcfdea9cf2738dc5a233ee917417511e7945 (commit) from 2ad56c719b0155256712357aa4f8e8a6e05ce8b7 (commit) Those revisions listed above that are new to this repository have not appeared on any other notification email; so we list those revisions in full, below. - Log ----------------------------------------------------------------- commit 36d2dcfdea9cf2738dc5a233ee917417511e7945 Author: Daniel Baston Date: Tue Jan 13 16:56:28 2026 -0500 OverlayEdgeRing: Preserve M values when closing ring (#1372) Resolves https://github.com/libgeos/geos/issues/1365 diff --git a/src/operation/overlayng/OverlayEdgeRing.cpp b/src/operation/overlayng/OverlayEdgeRing.cpp index b9ac01518..1587f9e0c 100644 --- a/src/operation/overlayng/OverlayEdgeRing.cpp +++ b/src/operation/overlayng/OverlayEdgeRing.cpp @@ -128,7 +128,7 @@ void OverlayEdgeRing::closeRing(CoordinateSequence& pts) { if(pts.size() > 0) { - pts.add(pts.getAt(0), false); + pts.closeRing(false); } } diff --git a/tests/unit/capi/GEOSGeom_setPrecisionTest.cpp b/tests/unit/capi/GEOSGeom_setPrecisionTest.cpp index 4640b19e7..4c604a183 100644 --- a/tests/unit/capi/GEOSGeom_setPrecisionTest.cpp +++ b/tests/unit/capi/GEOSGeom_setPrecisionTest.cpp @@ -326,5 +326,20 @@ void object::test<23>() checkPrecision(wkt, 100000, "LINESTRING ( 700000 200000, 700000 1400000)"); } +template<> +template<> +void object::test<24>() +{ + // https://github.com/libgeos/geos/issues/1365{ + set_test_name("M value retained on last point"); + + input_ = fromWKT("POLYGON ZM ((0 0 0 0, 0 1 1 1, 1 1 2 3, 1 0 4 5, 0 0 6 7))"); + expected_ = fromWKT("POLYGON ZM ((0 1 1 1, 1 1 2 3, 1 0 4 5, 0 0 6 7, 0 1 1 1))"); + + result_ = GEOSGeom_setPrecision(input_, 0.1, 0); + + ensure(GEOSEqualsIdentical(result_, expected_)); +} + } // namespace tut ----------------------------------------------------------------------- Summary of changes: src/operation/overlayng/OverlayEdgeRing.cpp | 2 +- tests/unit/capi/GEOSGeom_setPrecisionTest.cpp | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) hooks/post-receive -- GEOS From git at osgeo.org Thu Jan 15 16:42:11 2026 From: git at osgeo.org (git at osgeo.org) Date: Thu, 15 Jan 2026 16:42:11 -0800 (PST) Subject: [geos-commits] [SCM] GEOS branch main updated. aefb2e4be129e466e4e75ea52ad1c9b18dd06b9d Message-ID: <20260116004212.26199310BF@trac.osgeo.org> This is an automated email from the git hooks/post-receive script. It was generated because a ref change was pushed to the repository containing the project "GEOS". The branch, main has been updated via aefb2e4be129e466e4e75ea52ad1c9b18dd06b9d (commit) from 36d2dcfdea9cf2738dc5a233ee917417511e7945 (commit) Those revisions listed above that are new to this repository have not appeared on any other notification email; so we list those revisions in full, below. - Log ----------------------------------------------------------------- commit aefb2e4be129e466e4e75ea52ad1c9b18dd06b9d Author: Lo?c Bartoletti Date: Fri Jan 16 01:41:47 2026 +0100 test(bsd): disable FE_INVALID check on EnvelopeTest (#1375) diff --git a/tests/unit/geom/EnvelopeTest.cpp b/tests/unit/geom/EnvelopeTest.cpp index 1e9385e81..7c281aaee 100644 --- a/tests/unit/geom/EnvelopeTest.cpp +++ b/tests/unit/geom/EnvelopeTest.cpp @@ -103,8 +103,12 @@ struct test_envelope_data { //ensure("FE_INEXACT raised", !std::fetestexcept(FE_INEXACT)); //#endif #ifdef FE_INVALID + // Skip FE_INVALID check on FreeBSD and OpenBSD due to platform-specific behavior + // See: https://github.com/libgeos/geos/issues/1206 +#if !defined(__FreeBSD__) && !defined(__OpenBSD__) ensure("FE_INVALID raised", !std::fetestexcept(FE_INVALID)); #endif +#endif #ifdef FE_OVERFLOW ensure("FE_OVERFLOW raised", !std::fetestexcept(FE_OVERFLOW)); #endif ----------------------------------------------------------------------- Summary of changes: tests/unit/geom/EnvelopeTest.cpp | 4 ++++ 1 file changed, 4 insertions(+) hooks/post-receive -- GEOS From git at osgeo.org Thu Jan 15 23:31:11 2026 From: git at osgeo.org (git at osgeo.org) Date: Thu, 15 Jan 2026 23:31:11 -0800 (PST) Subject: [geos-commits] [SCM] GEOS branch main updated. 9563c1805e90523fce74530e8fa3ff4d5e873d35 Message-ID: <20260116073114.A674834259@trac.osgeo.org> This is an automated email from the git hooks/post-receive script. It was generated because a ref change was pushed to the repository containing the project "GEOS". The branch, main has been updated via 9563c1805e90523fce74530e8fa3ff4d5e873d35 (commit) from aefb2e4be129e466e4e75ea52ad1c9b18dd06b9d (commit) Those revisions listed above that are new to this repository have not appeared on any other notification email; so we list those revisions in full, below. - Log ----------------------------------------------------------------- commit 9563c1805e90523fce74530e8fa3ff4d5e873d35 Author: Mike Taves Date: Fri Jan 16 20:30:51 2026 +1300 CI: add FreeBSD build and test (#1376) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 729951169..108465511 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -485,6 +485,46 @@ jobs: working-directory: ./build run: ctest -V --output-on-failure -C ${{ matrix.build_type }} + freebsd: + name: FreeBSD + runs-on: ubuntu-latest + steps: + - uses: actions/checkout at v5 + + - name: Retrieve build cache + uses: actions/cache/restore at v4 + id: restore-cache + with: + path: .ccache + key: freebsd-${{ github.ref_name }}-${{ github.run_id }} + restore-keys: freebsd + + - name: Build and test + uses: vmactions/freebsd-vm at v1 + with: + release: "15.0" + envs: CCACHE_COMPRESS CCACHE_COMPRESSLEVEL CCACHE_MAXSIZE + usesh: true + prepare: | + pkg install -y cmake ccache + run: | + set -e + export CCACHE_DIR=$PWD/.ccache + cmake --version + cmake -S . -B _build -D USE_CCACHE=ON .. + cmake --build _build -j $(nproc) + ccache --show-stats + cd _build + ctest --output-on-failure + cd .. + rm -rf _build + + - name: Save build cache + uses: actions/cache/save at v4 + with: + path: .ccache + key: ${{ steps.restore-cache.outputs.cache-primary-key }} + emscripten: name: Emscripten WASM build runs-on: ubuntu-latest ----------------------------------------------------------------------- Summary of changes: .github/workflows/ci.yml | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) hooks/post-receive -- GEOS From git at osgeo.org Fri Jan 16 08:18:43 2026 From: git at osgeo.org (git at osgeo.org) Date: Fri, 16 Jan 2026 08:18:43 -0800 (PST) Subject: [geos-commits] [SCM] GEOS branch main updated. 3b391278f7844f16968e9397dd507c95589ac647 Message-ID: <20260116161844.2E78C39903@trac.osgeo.org> This is an automated email from the git hooks/post-receive script. It was generated because a ref change was pushed to the repository containing the project "GEOS". The branch, main has been updated via 3b391278f7844f16968e9397dd507c95589ac647 (commit) via 49209692ccda59d4f650001ba7a7f8752d54b98b (commit) via c28e77e9502c7496f90e1402dae6420312a69203 (commit) from 9563c1805e90523fce74530e8fa3ff4d5e873d35 (commit) Those revisions listed above that are new to this repository have not appeared on any other notification email; so we list those revisions in full, below. - Log ----------------------------------------------------------------- commit 3b391278f7844f16968e9397dd507c95589ac647 Author: Daniel Baston Date: Mon Jan 12 10:21:06 2026 -0500 LineMerger: Retain M values Resolves https://github.com/libgeos/geos/issues/1359 diff --git a/src/operation/linemerge/EdgeString.cpp b/src/operation/linemerge/EdgeString.cpp index 7c10a2a2e..88abfadee 100644 --- a/src/operation/linemerge/EdgeString.cpp +++ b/src/operation/linemerge/EdgeString.cpp @@ -27,7 +27,6 @@ #include #include -#include using namespace geos::geom; @@ -60,9 +59,23 @@ EdgeString::getCoordinates() const { int forwardDirectedEdges = 0; int reverseDirectedEdges = 0; - auto coordinates = detail::make_unique(); - for(std::size_t i = 0, e = directedEdges.size(); i < e; ++i) { - LineMergeDirectedEdge* directedEdge = directedEdges[i]; + + bool resultHasZ = false; + bool resultHasM = false; + + for (const LineMergeDirectedEdge* directedEdge : directedEdges) { + const LineMergeEdge* lme = detail::down_cast(directedEdge->getEdge()); + + resultHasZ |= lme->getLine()->hasZ(); + resultHasM |= lme->getLine()->hasM(); + } + + auto coordinates = std::make_unique(0, resultHasZ, resultHasM); + + bool lastPointMissingZ = false; + bool lastPointMissingM = false; + + for (const LineMergeDirectedEdge* directedEdge : directedEdges) { if(directedEdge->getEdgeDirection()) { forwardDirectedEdges++; } @@ -70,15 +83,30 @@ EdgeString::getCoordinates() const reverseDirectedEdges++; } - LineMergeEdge* lme = detail::down_cast(directedEdge->getEdge()); + const LineMergeEdge* lme = detail::down_cast(directedEdge->getEdge()); + const CoordinateSequence* seq = lme->getLine()->getCoordinatesRO(); - coordinates->add(*lme->getLine()->getCoordinatesRO(), + if (lastPointMissingZ && seq->hasZ()) { + const double z = directedEdge->getEdgeDirection() ? seq->getZ(0) : seq->getZ(seq->getSize() - 1); + coordinates->setZ(coordinates->getSize() - 1, z); + } + if (lastPointMissingM && seq->hasM()) { + const double m = directedEdge->getEdgeDirection() ? seq->getM(0) : seq->getM(seq->getSize() - 1); + coordinates->setM(coordinates->getSize() - 1, m); + } + + coordinates->add(*seq, false, directedEdge->getEdgeDirection()); + + lastPointMissingZ = resultHasZ && !seq->hasZ(); + lastPointMissingM = resultHasM && !seq->hasM(); } + if(reverseDirectedEdges > forwardDirectedEdges) { coordinates->reverse(); } + return coordinates; } diff --git a/tests/unit/operation/linemerge/LineMergerTest.cpp b/tests/unit/operation/linemerge/LineMergerTest.cpp index 1fea1cd7b..e99ad4280 100644 --- a/tests/unit/operation/linemerge/LineMergerTest.cpp +++ b/tests/unit/operation/linemerge/LineMergerTest.cpp @@ -17,6 +17,8 @@ #include #include +#include "utility.h" + namespace tut { // // Test Group @@ -492,5 +494,34 @@ void object::test<19> ensure_equals(geom->getLength(), merged[0]->getLength()); } +template<> +template<> +void object::test<20> +() +{ + std::vector wkts{ + "LINESTRING Z (0 0 0, 1 2 3, 2 4 6)", + "LINESTRING M (10 9 8, 2 4 7)", + "LINESTRING Z (10 9 2, 11 12 15)" + }; + std::vector> geoms; + + LineMerger lm; + + for (const auto& wkt : wkts) { + auto geom = wktreader.read(wkt); + lm.add(geom.get()); + geoms.push_back(std::move(geom)); + } + + auto merged = lm.getMergedLineStrings(); + + ensure_equals(merged.size(), 1u); + + auto expected = wktreader.read("LINESTRING ZM (0 0 0 NaN, 1 2 3 NaN, 2 4 6 7, 10 9 2 8, 11 12 15 NaN)"); + + ensure_equals_exact_geometry_xyzm(merged.front().get(), expected.get(), 0.0); +} + } // namespace tut commit 49209692ccda59d4f650001ba7a7f8752d54b98b Author: Daniel Baston Date: Mon Jan 12 10:20:50 2026 -0500 geos_unit: Improve assert failure message in ensure_equals_xyzm diff --git a/tests/unit/utility.h b/tests/unit/utility.h index 562a95dc8..c86dbce56 100644 --- a/tests/unit/utility.h +++ b/tests/unit/utility.h @@ -428,8 +428,8 @@ ensure_equals_exact_xyzm(const geos::geom::CoordinateSequence* seq1, seq2->getAt(i, c2); ensure("xy not in tolerance", c1.distance(c2) <= tol); - ensure_same("z not same", c1.z, c2.z); - ensure_same("z not same", c1.m, c2.m); + ensure_same(("index " + std::to_string(i) + "/" + std::to_string(seq1->getSize() - 1) + " z not same").c_str(), c1.z, c2.z); + ensure_same(("index " + std::to_string(i) + "/" + std::to_string(seq1->getSize() - 1) + " m not same").c_str(), c1.m, c2.m); } } commit c28e77e9502c7496f90e1402dae6420312a69203 Author: Daniel Baston Date: Mon Jan 12 10:20:23 2026 -0500 CoordinateSequence: Add Z/M accessors diff --git a/include/geos/geom/CoordinateSequence.h b/include/geos/geom/CoordinateSequence.h index 5e0a7776b..e4a1c5553 100644 --- a/include/geos/geom/CoordinateSequence.h +++ b/include/geos/geom/CoordinateSequence.h @@ -337,6 +337,80 @@ public: return m_vect[index * stride() + 1]; } + /** + * Returns ordinate Z of the specified coordinate. + * + * @param index + * @return the value of the Z ordinate in the index'th coordinate, or NaN if the + * CoordinateSequence does not store Z values + */ + double getZ(std::size_t index) const + { + return getOrdinate(index, Z); + } + + /** + * Returns ordinate M of the specified coordinate. + * + * @param index + * @return the value of the M ordinate in the index'th coordinate, or NaN if the + * CoordinateSequence does not store M values + */ + double getM(std::size_t index) const + { + return getOrdinate(index, M); + } + + /** + * Set the X value of the specified coordinate. + * + * @param index + * @param x the new X value + */ + void setX(std::size_t index, double x) + { + m_vect[index * stride()] = x; + } + + /** + * Set the Y value of the specified coordinate. + * + * @param index + * @param y the new Y value + */ + void setY(std::size_t index, double y) + { + m_vect[index * stride() + 1] = y; + } + + /** + * Set the Z value of the specified coordinate. + * + * Has no effect if the CoordinateSequence does not store Z values. + * + * @param index + * @param z the new Z value + */ + void setZ(std::size_t index, double z) + { + if (hasZ()) + setOrdinate(index, Z, z); + } + + /** + * Set the M value of the specified coordinate. + * + * Has no effect if the CoordinateSequence does not store M values. + * + * @param index + * @param m the new M value + */ + void setM(std::size_t index, double m) + { + if (hasM()) + setOrdinate(index, M, m); + } + /// Return last Coordinate in the sequence template const T& back() const diff --git a/tests/unit/geom/CoordinateSequenceTest.cpp b/tests/unit/geom/CoordinateSequenceTest.cpp index e2a94fb41..a0c86888e 100644 --- a/tests/unit/geom/CoordinateSequenceTest.cpp +++ b/tests/unit/geom/CoordinateSequenceTest.cpp @@ -1580,4 +1580,41 @@ void object::test<59>() ensure_equals(length, 8); } +template<> +template<> +void object::test<60>() +{ + set_test_name("Z/M accessors on XY sequence"); + + CoordinateSequence seq(0, false, false); + seq.add(CoordinateXY{0, 0}); + seq.add(CoordinateXY{3, 0}); + + seq.setZ(0, 500); + seq.setM(0, 501); + + ensure(std::isnan(seq.getZ(0))); + ensure(std::isnan(seq.getM(0))); +} + +template<> +template<> +void object::test<61>() +{ + set_test_name("Z/M accessors on XYZM sequence"); + + CoordinateSequence seq(0, true, true); + seq.add(CoordinateXY{0, 0}); + seq.add(CoordinateXY{3, 0}); + + seq.setZ(0, 500); + seq.setM(0, 501); + + ensure_equals(seq.getZ(0), 500); + ensure_equals(seq.getM(0), 501); + + ensure(std::isnan(seq.getZ(1))); + ensure(std::isnan(seq.getM(1))); +} + } // namespace tut ----------------------------------------------------------------------- Summary of changes: include/geos/geom/CoordinateSequence.h | 74 +++++++++++++++++++++++ src/operation/linemerge/EdgeString.cpp | 40 ++++++++++-- tests/unit/geom/CoordinateSequenceTest.cpp | 37 ++++++++++++ tests/unit/operation/linemerge/LineMergerTest.cpp | 31 ++++++++++ tests/unit/utility.h | 4 +- 5 files changed, 178 insertions(+), 8 deletions(-) hooks/post-receive -- GEOS From git at osgeo.org Sat Jan 17 06:05:59 2026 From: git at osgeo.org (git at osgeo.org) Date: Sat, 17 Jan 2026 06:05:59 -0800 (PST) Subject: [geos-commits] [SCM] GEOS branch main updated. 3dde4d59eb6fc1d142378642e2affde328e57f0d Message-ID: <20260117140559.AF04111182C@trac.osgeo.org> This is an automated email from the git hooks/post-receive script. It was generated because a ref change was pushed to the repository containing the project "GEOS". The branch, main has been updated via 3dde4d59eb6fc1d142378642e2affde328e57f0d (commit) from 3b391278f7844f16968e9397dd507c95589ac647 (commit) Those revisions listed above that are new to this repository have not appeared on any other notification email; so we list those revisions in full, below. - Log ----------------------------------------------------------------- commit 3dde4d59eb6fc1d142378642e2affde328e57f0d Author: arriopolis Date: Sat Jan 17 06:05:31 2026 -0800 Added a spatial index to the overlayng polygonbuilder (#1379) * added a spatial index to the overlayng polygonbuilder * Updated spelling. * reuse spatial filter result vector diff --git a/src/operation/overlayng/PolygonBuilder.cpp b/src/operation/overlayng/PolygonBuilder.cpp index abdb4a0fb..8cc88d306 100644 --- a/src/operation/overlayng/PolygonBuilder.cpp +++ b/src/operation/overlayng/PolygonBuilder.cpp @@ -168,11 +168,21 @@ PolygonBuilder::assignHoles(OverlayEdgeRing* shell, const std::vector& shells, const std::vector & freeHoles) const { - // TODO: use a spatial index to improve performance + // build spatial index + index::strtree::TemplateSTRtree index; + for (auto& shell : shells) { + index.insert(*shell->getRingPtr()->getEnvelopeInternal(), shell); + } + + std::vector shellListOverlaps; for (OverlayEdgeRing* hole : freeHoles) { // only place this hole if it doesn't yet have a shell if (hole->getShell() == nullptr) { - OverlayEdgeRing* shell = hole->findEdgeRingContaining(shells); + // get list of overlapping shells + shellListOverlaps.clear(); + index.query(*hole->getRingPtr()->getEnvelopeInternal(), shellListOverlaps); + + OverlayEdgeRing* shell = hole->findEdgeRingContaining(shellListOverlaps); // only when building a polygon-valid result if (isEnforcePolygonal && shell == nullptr) { throw util::TopologyException("unable to assign free hole to a shell", hole->getCoordinate()); ----------------------------------------------------------------------- Summary of changes: src/operation/overlayng/PolygonBuilder.cpp | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) hooks/post-receive -- GEOS From git at osgeo.org Tue Jan 20 05:33:35 2026 From: git at osgeo.org (git at osgeo.org) Date: Tue, 20 Jan 2026 05:33:35 -0800 (PST) Subject: [geos-commits] [SCM] GEOS branch main updated. a02e30f462676831a6241630c87c98ab6c1c2d12 Message-ID: <20260120133412.A7A0830831@trac.osgeo.org> This is an automated email from the git hooks/post-receive script. It was generated because a ref change was pushed to the repository containing the project "GEOS". The branch, main has been updated via a02e30f462676831a6241630c87c98ab6c1c2d12 (commit) via bf9238fbe0db7473b087492d4de055d936314569 (commit) via 4e50c4d3c4f5d4b864881930112de7f985624b22 (commit) from 3dde4d59eb6fc1d142378642e2affde328e57f0d (commit) Those revisions listed above that are new to this repository have not appeared on any other notification email; so we list those revisions in full, below. - Log ----------------------------------------------------------------- commit a02e30f462676831a6241630c87c98ab6c1c2d12 Author: Daniel Baston Date: Mon Jan 19 21:25:56 2026 -0500 GeometryFactory: support curved types in createEmpty, createMulti diff --git a/src/geom/GeometryFactory.cpp b/src/geom/GeometryFactory.cpp index 4abc59aac..152dd39a2 100644 --- a/src/geom/GeometryFactory.cpp +++ b/src/geom/GeometryFactory.cpp @@ -774,9 +774,14 @@ GeometryFactory::createEmpty(GeometryTypeId typeId) const case GEOS_POINT: return createPoint(); case GEOS_LINESTRING: return createLineString(); case GEOS_POLYGON: return createPolygon(); + case GEOS_CIRCULARSTRING: return createCircularString(false, false); + case GEOS_COMPOUNDCURVE: return createCompoundCurve(); + case GEOS_CURVEPOLYGON: return createCurvePolygon(false, false); case GEOS_MULTIPOINT: return createMultiPoint(); case GEOS_MULTILINESTRING: return createMultiLineString(); case GEOS_MULTIPOLYGON: return createMultiPolygon(); + case GEOS_MULTICURVE: return createMultiCurve(); + case GEOS_MULTISURFACE: return createMultiSurface(); case GEOS_GEOMETRYCOLLECTION: return createGeometryCollection(); default: throw geos::util::IllegalArgumentException("Invalid GeometryTypeId"); @@ -807,6 +812,11 @@ GeometryFactory::createMulti(std::unique_ptr && geom) const return gf->createMultiLineString(std::move(subgeoms)); case GEOS_POLYGON: return gf->createMultiPolygon(std::move(subgeoms)); + case GEOS_CIRCULARSTRING: + case GEOS_COMPOUNDCURVE: + return gf->createMultiCurve(std::move(subgeoms)); + case GEOS_CURVEPOLYGON: + return gf->createMultiSurface(std::move(subgeoms)); default: throw geos::util::IllegalArgumentException("Unsupported GeometryTypeId"); } diff --git a/tests/unit/geom/GeometryFactoryTest.cpp b/tests/unit/geom/GeometryFactoryTest.cpp index 279ff3580..a0773b21b 100644 --- a/tests/unit/geom/GeometryFactoryTest.cpp +++ b/tests/unit/geom/GeometryFactoryTest.cpp @@ -1418,6 +1418,41 @@ void object::test<41> g2 = reader_.read("MULTIPOINT((1 1))"); ensure_equals_geometry(g2.get(), mg1.get()); ensure_equals_geometry(g2.get(), mg2.get()); + + // CircularString + { + auto cs = factory_->createEmpty(geos::geom::GEOS_CIRCULARSTRING); + ensure_equals(cs->getGeometryTypeId(), geos::geom::GEOS_CIRCULARSTRING); + ensure_equals(factory_->createMulti(std::move(cs))->getGeometryTypeId(), geos::geom::GEOS_MULTICURVE); + } + + // CompoundCurve + { + auto cc = factory_->createEmpty(geos::geom::GEOS_COMPOUNDCURVE); + ensure_equals(cc->getGeometryTypeId(), geos::geom::GEOS_COMPOUNDCURVE); + ensure_equals(factory_->createMulti(std::move(cc))->getGeometryTypeId(), geos::geom::GEOS_MULTICURVE); + } + + // CurvePolygon + { + auto cp = factory_->createEmpty(geos::geom::GEOS_CURVEPOLYGON); + ensure_equals(cp->getGeometryTypeId(), geos::geom::GEOS_CURVEPOLYGON); + ensure_equals(factory_->createMulti(std::move(cp))->getGeometryTypeId(), geos::geom::GEOS_MULTISURFACE); + } + + // MultiCurve + { + auto mc = factory_->createEmpty(geos::geom::GEOS_MULTICURVE); + ensure_equals(mc->getGeometryTypeId(), geos::geom::GEOS_MULTICURVE); + ensure_equals(factory_->createMulti(std::move(mc))->getGeometryTypeId(), geos::geom::GEOS_MULTICURVE); + } + + // MultiSurface + { + auto ms = factory_->createEmpty(geos::geom::GEOS_MULTISURFACE); + ensure_equals(ms->getGeometryTypeId(), geos::geom::GEOS_MULTISURFACE); + ensure_equals(factory_->createMulti(std::move(ms))->getGeometryTypeId(), geos::geom::GEOS_MULTISURFACE); + } } commit bf9238fbe0db7473b087492d4de055d936314569 Author: Daniel Baston Date: Mon Jan 19 21:25:37 2026 -0500 MultiCurve/MultiSurface: fix isCollection result diff --git a/include/geos/geom/Geometry.h b/include/geos/geom/Geometry.h index 4701c762e..dedf10022 100644 --- a/include/geos/geom/Geometry.h +++ b/include/geos/geom/Geometry.h @@ -389,7 +389,9 @@ public: return t == GEOS_GEOMETRYCOLLECTION || t == GEOS_MULTIPOINT || t == GEOS_MULTILINESTRING || - t == GEOS_MULTIPOLYGON; + t == GEOS_MULTIPOLYGON || + t == GEOS_MULTICURVE || + t == GEOS_MULTISURFACE; } static GeometryTypeId multiTypeId(GeometryTypeId typeId) { @@ -397,6 +399,9 @@ public: case GEOS_POINT: return GEOS_MULTIPOINT; case GEOS_LINESTRING: return GEOS_MULTILINESTRING; case GEOS_POLYGON: return GEOS_MULTIPOLYGON; + case GEOS_CIRCULARSTRING: + case GEOS_COMPOUNDCURVE: return GEOS_MULTICURVE; + case GEOS_CURVEPOLYGON: return GEOS_MULTISURFACE; default: return typeId; } } diff --git a/tests/unit/geom/MultiCurveTest.cpp b/tests/unit/geom/MultiCurveTest.cpp index ec7844c8b..09c79fd12 100644 --- a/tests/unit/geom/MultiCurveTest.cpp +++ b/tests/unit/geom/MultiCurveTest.cpp @@ -92,7 +92,7 @@ void object::test<2>() // Geometry type functions ensure_equals("getGeometryType", mc_->getGeometryType(), "MultiCurve"); ensure_equals("getGeometryTypdId", mc_->getGeometryTypeId(), geos::geom::GEOS_MULTICURVE); - ensure("isCollection", !mc_->isCollection()); + ensure("isCollection", mc_->isCollection()); // Geometry size functions ensure("isEmpty", !mc_->isEmpty()); diff --git a/tests/unit/geom/MultiSurfaceTest.cpp b/tests/unit/geom/MultiSurfaceTest.cpp index 1492f2279..58c6ae524 100644 --- a/tests/unit/geom/MultiSurfaceTest.cpp +++ b/tests/unit/geom/MultiSurfaceTest.cpp @@ -81,7 +81,7 @@ void object::test<2>() // Geometry type functions ensure_equals("getGeometryType", ms_->getGeometryType(), "MultiSurface"); ensure_equals("getGeometryTypdId", ms_->getGeometryTypeId(), geos::geom::GEOS_MULTISURFACE); - ensure("isCollection", !ms_->isCollection()); + ensure("isCollection", ms_->isCollection()); // Geometry size functions ensure("isEmpty", !ms_->isEmpty()); commit 4e50c4d3c4f5d4b864881930112de7f985624b22 Author: Daniel Baston Date: Mon Jan 19 20:49:06 2026 -0500 GeometryFactory: support curved types in buildGeometry() diff --git a/include/geos/geom/GeometryFactory.h b/include/geos/geom/GeometryFactory.h index 4dfe6b593..4b5dba01f 100644 --- a/include/geos/geom/GeometryFactory.h +++ b/include/geos/geom/GeometryFactory.h @@ -197,6 +197,10 @@ public: /// Construct an EMPTY MultiCurve std::unique_ptr createMultiCurve() const; + /// Construct a MultiCurve with a deep-copy of given arguments + std::unique_ptr createMultiCurve( + const std::vector& from) const; + /// Construct a MultiCurve taking ownership of given arguments std::unique_ptr createMultiCurve( std::vector> && fromCurves) const; @@ -221,6 +225,10 @@ public: /// Construct an EMPTY MultiSurface std::unique_ptr createMultiSurface() const; + /// Construct a MultiSurface with a deep-copy of given arguments + std::unique_ptr createMultiSurface( + const std::vector& from) const; + /// Construct a MultiSurface taking ownership of given arguments std::unique_ptr createMultiSurface( std::vector> && from) const; diff --git a/src/geom/GeometryFactory.cpp b/src/geom/GeometryFactory.cpp index 458fbe8e1..4abc59aac 100644 --- a/src/geom/GeometryFactory.cpp +++ b/src/geom/GeometryFactory.cpp @@ -321,6 +321,20 @@ GeometryFactory::createMultiCurve() const { return createMultiCurve(std::vector>()); } +/*public*/ +std::unique_ptr +GeometryFactory::createMultiCurve(const std::vector& fromCurves) +const +{ + std::vector> newGeoms(fromCurves.size()); + + for(std::size_t i = 0; i < fromCurves.size(); i++) { + newGeoms[i]= fromCurves[i]->clone(); // MultiCurve constructor will check that it's actually a Curve + } + + return createMultiCurve(std::move(newGeoms)); +} + std::unique_ptr GeometryFactory::createMultiCurve(std::vector> && fromCurves) const { // Can't use make_unique because constructor is protected @@ -433,6 +447,19 @@ GeometryFactory::createMultiSurface(std::vector> && new return std::unique_ptr(new MultiSurface(std::move(newSurfaces), *this)); } +/*public*/ +std::unique_ptr +GeometryFactory::createMultiSurface(const std::vector& from) const +{ + std::vector> newGeoms(from.size()); + + for(std::size_t i = 0; i < from.size(); i++) { + newGeoms[i] = from[i]->clone(); // MultiSurface constructor will check that it's actually a Surface + } + + return createMultiSurface(std::move(newGeoms)); +} + /*public*/ std::unique_ptr GeometryFactory::createLinearRing(std::size_t coordinateDimension) const @@ -795,18 +822,25 @@ GeometryTypeId commonType(const T& geoms) { return geoms[0]->getGeometryTypeId(); } - GeometryTypeId type = geoms[0]->getGeometryTypeId(); + bool hasCurvedComponents = geoms[0]->hasCurvedComponents(); + + const Dimension::DimensionType dim = geoms[0]->getDimension(); for (std::size_t i = 1; i < geoms.size(); i++) { - if (geoms[i]->getGeometryTypeId() != type) { + hasCurvedComponents |= geoms[i]->hasCurvedComponents(); + + if (geoms[i]->getDimension() != dim) { return GEOS_GEOMETRYCOLLECTION; } } switch(geoms[0]->getGeometryTypeId()) { case GEOS_POINT: return GEOS_MULTIPOINT; + case GEOS_COMPOUNDCURVE: + case GEOS_CIRCULARSTRING: return GEOS_MULTICURVE; case GEOS_LINEARRING: - case GEOS_LINESTRING: return GEOS_MULTILINESTRING; - case GEOS_POLYGON: return GEOS_MULTIPOLYGON; + case GEOS_LINESTRING: return hasCurvedComponents ? GEOS_MULTICURVE : GEOS_MULTILINESTRING; + case GEOS_CURVEPOLYGON: return GEOS_MULTISURFACE; + case GEOS_POLYGON: return hasCurvedComponents ? GEOS_MULTISURFACE : GEOS_MULTIPOLYGON; default: return GEOS_GEOMETRYCOLLECTION; } } @@ -826,8 +860,10 @@ GeometryFactory::buildGeometry(std::vector> && geoms) switch(resultType) { case GEOS_MULTIPOINT: return createMultiPoint(std::move(geoms)); + case GEOS_MULTICURVE: return createMultiCurve(std::move(geoms)); case GEOS_MULTILINESTRING: return createMultiLineString(std::move(geoms)); case GEOS_MULTIPOLYGON: return createMultiPolygon(std::move(geoms)); + case GEOS_MULTISURFACE: return createMultiSurface(std::move(geoms)); default: return createGeometryCollection(std::move(geoms)); } } @@ -890,8 +926,10 @@ GeometryFactory::buildGeometry(const std::vector& fromGeoms) co switch(resultType) { case GEOS_MULTIPOINT: return createMultiPoint(fromGeoms); + case GEOS_MULTICURVE: return createMultiCurve(fromGeoms); case GEOS_MULTILINESTRING: return createMultiLineString(fromGeoms); case GEOS_MULTIPOLYGON: return createMultiPolygon(fromGeoms); + case GEOS_MULTISURFACE: return createMultiSurface(fromGeoms); default: return createGeometryCollection(fromGeoms); } } diff --git a/tests/unit/geom/GeometryFactoryTest.cpp b/tests/unit/geom/GeometryFactoryTest.cpp index 9cc8b659a..279ff3580 100644 --- a/tests/unit/geom/GeometryFactoryTest.cpp +++ b/tests/unit/geom/GeometryFactoryTest.cpp @@ -1092,24 +1092,128 @@ void object::test<33> //inform("Test not implemented!"); } -// Test of buildGeometry(std::vector* geoms) const template<> template<> void object::test<34> () { - // TODO - mloskot - //inform("Test not implemented!"); + set_test_name("GeometryFactory::buildGeometry(std::vector>&&)"); + + auto line = reader_.read("LINESTRING(0 0, 10 10)"); + auto point = reader_.read("POINT (3 2)"); + auto poly = reader_.read("POLYGON((0 0, 10 0, 10 10, 0 10, 0 0))"); + auto circstring = reader_.read("CIRCULARSTRING(0 0, 1 1, 2 0)"); + auto curvepoly = reader_.read("CURVEPOLYGON(COMPOUNDCURVE(CIRCULARSTRING(0 0, 1 1, 2 0), (2 0, 0 0)))"); + + // GeometryCollection + { + std::vector> geoms; + geoms.push_back(line->clone()); + geoms.push_back(point->clone()); + ensure_equals(factory_->buildGeometry(std::move(geoms))->getGeometryTypeId(), geos::geom::GEOS_GEOMETRYCOLLECTION); + } + + // MultiPoint + { + std::vector> geoms; + geoms.push_back(point->clone()); + geoms.push_back(point->clone()); + ensure_equals(factory_->buildGeometry(std::move(geoms))->getGeometryTypeId(), geos::geom::GEOS_MULTIPOINT); + } + + // MultiCurve + { + std::vector> geoms; + geoms.push_back(line->clone()); + geoms.push_back(circstring->clone()); + ensure_equals(factory_->buildGeometry(std::move(geoms))->getGeometryTypeId(), geos::geom::GEOS_MULTICURVE); + } + + // MultiLineString + { + std::vector> geoms; + geoms.push_back(line->clone()); + geoms.push_back(line->clone()); + ensure_equals(factory_->buildGeometry(std::move(geoms))->getGeometryTypeId(), geos::geom::GEOS_MULTILINESTRING); + } + + // MultiPolygon + { + std::vector> geoms; + geoms.push_back(poly->clone()); + geoms.push_back(poly->clone()); + ensure_equals(factory_->buildGeometry(std::move(geoms))->getGeometryTypeId(), geos::geom::GEOS_MULTIPOLYGON); + } + + // MultiSurface + { + std::vector> geoms; + geoms.push_back(poly->clone()); + geoms.push_back(curvepoly->clone()); + ensure_equals(factory_->buildGeometry(std::move(geoms))->getGeometryTypeId(), geos::geom::GEOS_MULTISURFACE); + } } -// Test of buildGeometry(const std::vector& geoms) template<> template<> void object::test<35> () { - // TODO - mloskot - //inform("Test not implemented!"); + set_test_name("GeometryFactory::buildGeometry(const std::vector&)"); + + auto line = reader_.read("LINESTRING(0 0, 10 10)"); + auto point = reader_.read("POINT (3 2)"); + auto poly = reader_.read("POLYGON((0 0, 10 0, 10 10, 0 10, 0 0))"); + auto circstring = reader_.read("CIRCULARSTRING(0 0, 1 1, 2 0)"); + auto curvepoly = reader_.read("CURVEPOLYGON(COMPOUNDCURVE(CIRCULARSTRING(0 0, 1 1, 2 0), (2 0, 0 0)))"); + + // GeometryCollection + { + std::vector geoms; + geoms.push_back(line.get()); + geoms.push_back(point.get()); + ensure_equals(factory_->buildGeometry(geoms)->getGeometryTypeId(), geos::geom::GEOS_GEOMETRYCOLLECTION); + } + + // MultiPoint + { + std::vector geoms; + geoms.push_back(point.get()); + geoms.push_back(point.get()); + ensure_equals(factory_->buildGeometry(geoms)->getGeometryTypeId(), geos::geom::GEOS_MULTIPOINT); + } + + // MultiCurve + { + std::vector geoms; + geoms.push_back(line.get()); + geoms.push_back(circstring.get()); + ensure_equals(factory_->buildGeometry(geoms)->getGeometryTypeId(), geos::geom::GEOS_MULTICURVE); + } + + // MultiLineString + { + std::vector geoms; + geoms.push_back(line.get()); + geoms.push_back(line.get()); + ensure_equals(factory_->buildGeometry(geoms)->getGeometryTypeId(), geos::geom::GEOS_MULTILINESTRING); + } + + // MultiPolygon + { + std::vector geoms; + geoms.push_back(poly.get()); + geoms.push_back(poly.get()); + ensure_equals(factory_->buildGeometry(geoms)->getGeometryTypeId(), geos::geom::GEOS_MULTIPOLYGON); + } + + // MultiSurface + { + std::vector geoms; + geoms.push_back(poly.get()); + geoms.push_back(curvepoly.get()); + ensure_equals(factory_->buildGeometry(geoms)->getGeometryTypeId(), geos::geom::GEOS_MULTISURFACE); + } } // Test of ----------------------------------------------------------------------- Summary of changes: include/geos/geom/Geometry.h | 7 +- include/geos/geom/GeometryFactory.h | 8 ++ src/geom/GeometryFactory.cpp | 56 +++++++++++- tests/unit/geom/GeometryFactoryTest.cpp | 151 ++++++++++++++++++++++++++++++-- tests/unit/geom/MultiCurveTest.cpp | 2 +- tests/unit/geom/MultiSurfaceTest.cpp | 2 +- 6 files changed, 213 insertions(+), 13 deletions(-) hooks/post-receive -- GEOS From git at osgeo.org Fri Jan 23 17:13:34 2026 From: git at osgeo.org (git at osgeo.org) Date: Fri, 23 Jan 2026 17:13:34 -0800 (PST) Subject: [geos-commits] [SCM] GEOS branch main updated. 8f2364fc2c3eecc915e224f5d0e8399bb96d4e4c Message-ID: <20260124011335.119A816C635@trac.osgeo.org> This is an automated email from the git hooks/post-receive script. It was generated because a ref change was pushed to the repository containing the project "GEOS". The branch, main has been updated via 8f2364fc2c3eecc915e224f5d0e8399bb96d4e4c (commit) from a02e30f462676831a6241630c87c98ab6c1c2d12 (commit) Those revisions listed above that are new to this repository have not appeared on any other notification email; so we list those revisions in full, below. - Log ----------------------------------------------------------------- commit 8f2364fc2c3eecc915e224f5d0e8399bb96d4e4c Author: Even Rouault Date: Sat Jan 24 02:12:48 2026 +0100 DistanceOp::distance(): make it return +inf instead of 0 if one of the geometry is empty (#1345) * DistanceOp::distance(): make it return NaN instead of 0 if one of the geometry is empty Fixes https://github.com/OSGeo/gdal/issues/12978 * DistanceOp::distance(): make it return inf instead of 0 if one of the geometry is empty diff --git a/capi/geos_c.h.in b/capi/geos_c.h.in index 44bb6d3ed..b31a4290a 100644 --- a/capi/geos_c.h.in +++ b/capi/geos_c.h.in @@ -3405,7 +3405,7 @@ extern char GEOS_DLL GEOSisSimple(const GEOSGeometry* g); * In one step, calculate and return whether a geometry is simple * and one more more points at which the geometry self-intersects * at interior points. -* Caller has the responsibility to destroy 'location' with +* Caller has the responsibility to destroy 'location' with * GEOSGeom_destroy() * * \param g The geometry to test @@ -3649,7 +3649,8 @@ extern int GEOS_DLL GEOSGeomGetLength( * Calculate the distance between two geometries. * \param[in] g1 Input geometry * \param[in] g2 Input geometry -* \param[out] dist Pointer to be filled in with distance result +* \param[out] dist Pointer to be filled in with distance result. Positive +* infinity is returned if one of the geometry is empty. * \return 1 on success, 0 on exception. * \since 2.2 */ @@ -5531,7 +5532,7 @@ extern char GEOS_DLL GEOSIntersects(const GEOSGeometry* g1, const GEOSGeometry* extern char GEOS_DLL GEOSCrosses(const GEOSGeometry* g1, const GEOSGeometry* g2); /** -* Tests if geometry g1 is completely within g2, +* Tests if geometry g1 is completely within g2, * but not wholly contained in the boundary of g2. * \param g1 Input geometry * \param g2 Input geometry diff --git a/src/operation/distance/DistanceOp.cpp b/src/operation/distance/DistanceOp.cpp index c6da7508f..2eb82eaac 100644 --- a/src/operation/distance/DistanceOp.cpp +++ b/src/operation/distance/DistanceOp.cpp @@ -36,6 +36,7 @@ #include #include +#include #include #include @@ -97,7 +98,7 @@ DistanceOp::DistanceOp(const Geometry& g0, const Geometry& g1, double tdist) /** * Report the distance between the closest points on the input geometries. * - * @return the distance between the geometries + * @return the distance between the geometries, or positive infinity if one of them is empty. */ double DistanceOp::distance() @@ -111,7 +112,7 @@ DistanceOp::distance() throw IllegalArgumentException("null geometries are not supported"); } if(geom[0]->isEmpty() || geom[1]->isEmpty()) { - return 0.0; + return std::numeric_limits::infinity(); } if(geom[0]->getGeometryTypeId() == GEOS_POINT && geom[1]->getGeometryTypeId() == GEOS_POINT) { return static_cast(geom[0])->getCoordinate()->distance(*static_cast(geom[1])->getCoordinate()); diff --git a/tests/unit/operation/distance/DistanceOpTest.cpp b/tests/unit/operation/distance/DistanceOpTest.cpp index 32eee5bbf..bbb5e0850 100644 --- a/tests/unit/operation/distance/DistanceOpTest.cpp +++ b/tests/unit/operation/distance/DistanceOpTest.cpp @@ -228,7 +228,7 @@ void object::test<8> DistanceOp dist(g0.get(), g1.get()); - ensure_equals(dist.distance(), 0); + ensure(std::isinf(dist.distance())); ensure(dist.nearestPoints() == nullptr); } @@ -416,7 +416,7 @@ void object::test<16> DistanceOp dist(g0.get(), g1.get()); - ensure_equals(dist.distance(), 0); + ensure(std::isinf(dist.distance())); ensure(dist.nearestPoints() == nullptr); } @@ -497,7 +497,7 @@ void object::test<19> ensure(g1->isValid()); ensure(g2->isValid()); - ensure_equals(g1->distance(g2.get()), 0); + ensure(std::isinf(g1->distance(g2.get()))); } // Test case reported in Shapely @@ -580,7 +580,6 @@ void object::test<22>() ensure_equals(g2->distance(g1.get()), 1); } -// Empty is same as empty so zero...? template<> template<> void object::test<23>() @@ -589,8 +588,8 @@ void object::test<23>() auto g2 = wktreader.read("LINESTRING EMPTY"); ensure(g1 != nullptr && g2 != nullptr); - ensure_equals(g1->distance(g2.get()), 0); - ensure_equals(g2->distance(g1.get()), 0); + ensure(std::isinf(g1->distance(g2.get()))); + ensure(std::isinf(g2->distance(g1.get()))); } template<> @@ -601,8 +600,8 @@ void object::test<24>() auto g2 = wktreader.read("LINESTRING EMPTY"); ensure(g1 != nullptr && g2 != nullptr); - ensure_equals(g1->distance(g2.get()), 0); - ensure_equals(g2->distance(g1.get()), 0); + ensure(std::isinf(g1->distance(g2.get()))); + ensure(std::isinf(g2->distance(g1.get()))); } // But ignore empty if there's a real distance? @@ -638,8 +637,8 @@ void object::test<27>() auto g2 = wktreader.read("GEOMETRYCOLLECTION(POINT(1 0))"); ensure(g1 != nullptr && g2 != nullptr); - ensure_equals(g1->distance(g2.get()), 0); - ensure_equals(g2->distance(g1.get()), 0); + ensure(std::isinf(g1->distance(g2.get()))); + ensure(std::isinf(g2->distance(g1.get()))); } diff --git a/tests/xmltester/XMLTester.cpp b/tests/xmltester/XMLTester.cpp index c3b33b939..be5445813 100644 --- a/tests/xmltester/XMLTester.cpp +++ b/tests/xmltester/XMLTester.cpp @@ -894,8 +894,12 @@ Test::checkResult( bool result ) void Test::checkResult( double result) { - char* rest; - double expectedRes = std::strtod(opResult.c_str(), &rest); + char* rest = nullptr; + const double expectedRes = (opResult == "NaN" || opResult == "nan") ? + std::numeric_limits::quiet_NaN() : + (opResult == "Inf" || opResult == "inf") ? + std::numeric_limits::infinity() : + std::strtod(opResult.c_str(), &rest); if(rest == opResult.c_str()) { throw std::runtime_error("malformed testcase: missing expected double value"); } @@ -904,6 +908,12 @@ Test::checkResult( double result) isSuccess = true; } } + else if (std::isnan(expectedRes)) { + isSuccess = std::isnan(result); + } + else if( expectedRes == result ) { + isSuccess = true; + } else { if (std::abs(expectedRes - result) / expectedRes < 1e-3) { isSuccess = true; diff --git a/tests/xmltester/tests/general/TestDistance.xml b/tests/xmltester/tests/general/TestDistance.xml index e6444ddac..36ddb40c9 100644 --- a/tests/xmltester/tests/general/TestDistance.xml +++ b/tests/xmltester/tests/general/TestDistance.xml @@ -4,8 +4,8 @@ PeP - point to an empty point POINT(10 10) POINT EMPTY - 0.0 - 0.0 + inf + inf @@ -36,8 +36,8 @@ LL - line to empty line LINESTRING (0 0, 0 10) LINESTRING EMPTY - 0.0 - 0.0 + inf + inf @@ -68,8 +68,8 @@ PA - point to empty polygon POINT (240 160) POLYGON EMPTY - 0.0 - 0.0 + inf + inf ----------------------------------------------------------------------- Summary of changes: capi/geos_c.h.in | 7 ++++--- src/operation/distance/DistanceOp.cpp | 5 +++-- tests/unit/operation/distance/DistanceOpTest.cpp | 19 +++++++++---------- tests/xmltester/XMLTester.cpp | 14 ++++++++++++-- tests/xmltester/tests/general/TestDistance.xml | 12 ++++++------ 5 files changed, 34 insertions(+), 23 deletions(-) hooks/post-receive -- GEOS From git at osgeo.org Thu Jan 29 07:11:41 2026 From: git at osgeo.org (git at osgeo.org) Date: Thu, 29 Jan 2026 07:11:41 -0800 (PST) Subject: [geos-commits] [SCM] GEOS branch main updated. 0154023f7e0d15a27d2bf38fe6b78408be560bc8 Message-ID: <20260129151147.403EA1A5A8C@trac.osgeo.org> This is an automated email from the git hooks/post-receive script. It was generated because a ref change was pushed to the repository containing the project "GEOS". The branch, main has been updated via 0154023f7e0d15a27d2bf38fe6b78408be560bc8 (commit) via 79ed12458622b265a89efaa10e680c74442dc5c3 (commit) via 35bd4a66da3dffc85ab4579e7671e3eec8fb6776 (commit) via 5e1bd575034913eb1941b9fcb303cb2be2630bb3 (commit) via 15ce6735d6ba3014fc12fcf86df9eecf92db9739 (commit) via f54ead7568472702e67563ee1d559b5d7c6d6b66 (commit) via bd03a4b050c553f9c6b84ed1554abe5464cd3e20 (commit) via 78881d3917a5b22e916384c496ed15c931acc0f8 (commit) via 8447ff2a32dd20703e0841736a8a3a6bd6be88f5 (commit) via 3efe43f99214c8e3fa3c0e76f3c991331f897c41 (commit) via 114887e4058d97ead9ccc22269300ad51aa51e56 (commit) via 7d701f5e5077fe860298bd29802922a206b6d70e (commit) via 0de2f8fb56f74890702f7abd7532833b4e48f606 (commit) via c181764a77beae6e05529eff91811c6a7c53f4ce (commit) via 4e0f6f89ba90a1940166537cc9449a4bd34e9622 (commit) via bac74b25b9508a54a14fceb6bb9ed172ad10e3b4 (commit) via db93f778ec51fdc1a71886f2b319ec6da35a302d (commit) from 8f2364fc2c3eecc915e224f5d0e8399bb96d4e4c (commit) Those revisions listed above that are new to this repository have not appeared on any other notification email; so we list those revisions in full, below. - Log ----------------------------------------------------------------- commit 0154023f7e0d15a27d2bf38fe6b78408be560bc8 Author: Daniel Baston Date: Thu Jan 29 09:35:19 2026 -0500 ArcNoder: Match semantics of Noder, do not take ownership of ArcIntersector diff --git a/include/geos/noding/ArcNoder.h b/include/geos/noding/ArcNoder.h index 2fd418cfa..e5f46f7fe 100644 --- a/include/geos/noding/ArcNoder.h +++ b/include/geos/noding/ArcNoder.h @@ -29,13 +29,13 @@ class GEOS_DLL ArcNoder : public Noder { public: ArcNoder() = default; - explicit ArcNoder(std::unique_ptr intersector) : - m_intersector(std::move(intersector)) {} + explicit ArcNoder(ArcIntersector& intersector) : + m_intersector(&intersector) {} ~ArcNoder() override; - void setArcIntersector(std::unique_ptr arcIntersector) { - m_intersector = std::move(arcIntersector); + void setArcIntersector(ArcIntersector& arcIntersector) { + m_intersector = &arcIntersector; } void computeNodes(const std::vector& segStrings) override; @@ -47,7 +47,7 @@ public: virtual std::vector> getNodedPaths() = 0; protected: - std::unique_ptr m_intersector; + ArcIntersector* m_intersector; }; } \ No newline at end of file diff --git a/src/noding/GeometryNoder.cpp b/src/noding/GeometryNoder.cpp index dbc02bf25..cab42cb03 100644 --- a/src/noding/GeometryNoder.cpp +++ b/src/noding/GeometryNoder.cpp @@ -157,11 +157,9 @@ GeometryNoder::getNoded() if (argGeomHasCurves) { ArcNoder& p_noder = static_cast(getNoder()); - // TODO: Improve lifecycle here. We have a heap-allocated ArcIntersectionAdder referencing - // a stack-allocated CircularArcIntersector. algorithm::CircularArcIntersector cai(argGeom.getPrecisionModel()); - auto arcIntersector = std::make_unique(cai); - p_noder.setArcIntersector(std::move(arcIntersector)); + ArcIntersectionAdder aia(cai); + p_noder.setArcIntersector(aia); p_noder.computePathNodes(PathString::toRawPointerVector(lineList)); nodedEdges = p_noder.getNodedPaths(); diff --git a/tests/unit/noding/SimpleNoderTest.cpp b/tests/unit/noding/SimpleNoderTest.cpp index 934247541..0625f150d 100644 --- a/tests/unit/noding/SimpleNoderTest.cpp +++ b/tests/unit/noding/SimpleNoderTest.cpp @@ -123,7 +123,8 @@ void object::test<1>() std::vector ss{&ss1, &ss2}; - SimpleNoder noder(std::make_unique(cai)); + geos::noding::ArcIntersectionAdder aia(cai); + SimpleNoder noder(aia); noder.computePathNodes(ss); auto paths = noder.getNodedPaths(); @@ -149,7 +150,8 @@ void object::test<2>() std::vector ss{&as0, &as1}; - SimpleNoder noder(std::make_unique(cai)); + geos::noding::ArcIntersectionAdder aia(cai); + SimpleNoder noder(aia); noder.computePathNodes(ss); auto paths = noder.getNodedPaths(); @@ -175,7 +177,8 @@ void object::test<3>() std::vector ss{&as0, &ss1}; - SimpleNoder noder(std::make_unique(cai)); + geos::noding::ArcIntersectionAdder aia(cai); + SimpleNoder noder(aia); noder.computePathNodes(ss); auto paths = noder.getNodedPaths(); commit 79ed12458622b265a89efaa10e680c74442dc5c3 Author: Daniel Baston Date: Thu Jan 29 09:21:21 2026 -0500 GeometryNoder: Use SimpleNoder directly for arcs diff --git a/include/geos/noding/GeometryNoder.h b/include/geos/noding/GeometryNoder.h index e31fd08cf..0938302a9 100644 --- a/include/geos/noding/GeometryNoder.h +++ b/include/geos/noding/GeometryNoder.h @@ -29,7 +29,7 @@ namespace geom { class Geometry; } namespace noding { -class ArcNoder; +class Noder; } } @@ -56,14 +56,12 @@ private: const geom::Geometry& argGeom; const bool argGeomHasCurves; - SegmentString::NonConstVect lineList; - static void extractPathStrings(const geom::Geometry& g, std::vector>& to); - ArcNoder& getNoder(); + Noder& getNoder(); - std::unique_ptr noder; + std::unique_ptr noder; std::unique_ptr toGeometry(std::vector>& noded) const; diff --git a/include/geos/noding/IteratedNoder.h b/include/geos/noding/IteratedNoder.h index 11a0ab48f..1d2bc296a 100644 --- a/include/geos/noding/IteratedNoder.h +++ b/include/geos/noding/IteratedNoder.h @@ -20,14 +20,12 @@ #include -#include -#include #include +#include -#include #include #include // due to inlines -#include // for inheritance +#include // for inheritance // Forward declarations namespace geos { @@ -51,37 +49,36 @@ namespace noding { // geos::noding * Clients can choose to rerun the noding using a lower precision model. * */ -class GEOS_DLL IteratedNoder : public ArcNoder { // implements Noder +class GEOS_DLL IteratedNoder : public Noder { // implements Noder private: - static constexpr int MAX_ITER = 5; + static const int MAX_ITER = 5; const geom::PrecisionModel* pm; - algorithm::CircularArcIntersector cai; algorithm::LineIntersector li; - std::vector> nodedPaths; + std::vector> nodedSegStrings; int maxIter; - std::function()> m_noderFunction; /** * Node the input segment strings once * and create the split edges between the nodes */ - void node(const std::vector& segStrings, + void node(const std::vector& segStrings, int& numInteriorIntersections, geom::CoordinateXY& intersectionPoint); - static std::unique_ptr createDefaultNoder(); - public: - /** \brief - * Construct an IteratedNoder using a specific precisionModel and underlying Noder. - */ - IteratedNoder(const geom::PrecisionModel* newPm, std::function()> noderFunction = createDefaultNoder); + IteratedNoder(const geom::PrecisionModel* newPm) + : + pm(newPm), + li(pm), + maxIter(MAX_ITER) + { + } - ~IteratedNoder() override; + ~IteratedNoder() override {} /** \brief * Sets the maximum number of noding iterations performed before @@ -99,21 +96,23 @@ public: maxIter = n; } - std::vector> getNodedPaths() override + std::vector> + getNodedSubstrings() override { - return std::move(nodedPaths); + return std::move(nodedSegStrings); } + /** \brief - * Fully nodes a list of {@link PathString}s, i.e. performs noding iteratively + * Fully nodes a list of {@link SegmentString}s, i.e. performs noding iteratively * until no intersections are found between segments. * * Maintains labelling of edges correctly through the noding. * - * @param inputPathStrings a collection of SegmentStrings to be noded + * @param inputSegmentStrings a collection of SegmentStrings to be noded * @throws TopologyException if the iterated noding fails to converge. */ - void computePathNodes(const std::vector& inputPathStrings) override; + void computeNodes(const std::vector& inputSegmentStrings) override; // throw(GEOSException); // Declare type as noncopyable IteratedNoder(IteratedNoder const&) = delete; diff --git a/src/noding/GeometryNoder.cpp b/src/noding/GeometryNoder.cpp index 6103364ea..dbc02bf25 100644 --- a/src/noding/GeometryNoder.cpp +++ b/src/noding/GeometryNoder.cpp @@ -16,6 +16,7 @@ * **********************************************************************/ +#include #include #include #include @@ -42,6 +43,8 @@ #include // for unique_ptr #include +#include "geos/noding/ArcIntersectionAdder.h" + namespace geos { namespace noding { // geos.noding @@ -146,18 +149,30 @@ GeometryNoder::getNoded() if (argGeom.isEmpty()) return argGeom.clone(); - std::vector> p_lineList; - extractPathStrings(argGeom, p_lineList); - - ArcNoder& p_noder = getNoder(); + std::vector> lineList; std::vector> nodedEdges; - try { - p_noder.computePathNodes(PathString::toRawPointerVector(p_lineList)); + extractPathStrings(argGeom, lineList); + + if (argGeomHasCurves) { + ArcNoder& p_noder = static_cast(getNoder()); + + // TODO: Improve lifecycle here. We have a heap-allocated ArcIntersectionAdder referencing + // a stack-allocated CircularArcIntersector. + algorithm::CircularArcIntersector cai(argGeom.getPrecisionModel()); + auto arcIntersector = std::make_unique(cai); + p_noder.setArcIntersector(std::move(arcIntersector)); + + p_noder.computePathNodes(PathString::toRawPointerVector(lineList)); nodedEdges = p_noder.getNodedPaths(); - } - catch(const std::exception&) { - throw; + } else { + Noder& p_noder = getNoder(); + p_noder.computeNodes(SegmentString::toRawPointerVector(lineList)); + auto nodedSegStrings = p_noder.getNodedSubstrings(); + nodedEdges.resize(nodedSegStrings.size()); + for (size_t i = 0; i < nodedSegStrings.size(); i++) { + nodedEdges[i] = std::move(nodedSegStrings[i]); + } } std::unique_ptr noded = toGeometry(nodedEdges); @@ -175,13 +190,13 @@ GeometryNoder::extractPathStrings(const geom::Geometry& g, } /* private */ -ArcNoder& +Noder& GeometryNoder::getNoder() { - if(! noder.get()) { + if(!noder) { const geom::PrecisionModel* pm = argGeom.getFactory()->getPrecisionModel(); if (argGeomHasCurves) { - noder = std::make_unique(pm, []() { return std::make_unique(); }); + noder = std::make_unique(); } else { noder = std::make_unique(pm); } diff --git a/src/noding/IteratedNoder.cpp b/src/noding/IteratedNoder.cpp index df1e9beaf..c88b76387 100644 --- a/src/noding/IteratedNoder.cpp +++ b/src/noding/IteratedNoder.cpp @@ -17,14 +17,11 @@ * **********************************************************************/ -#include #include #include #include #include -#include -#include #include #include #include @@ -34,92 +31,47 @@ #define GEOS_DEBUG 0 #endif +using namespace geos::geom; + namespace geos { namespace noding { // geos.noding -IteratedNoder::IteratedNoder(const geom::PrecisionModel* newPm, - std::function()> noderFunction) - : - pm(newPm), - cai(pm), - li(pm), - maxIter(MAX_ITER), - m_noderFunction(noderFunction) -{ -} - -std::unique_ptr -IteratedNoder::createDefaultNoder() -{ - return std::make_unique(); -} - -IteratedNoder::~IteratedNoder() = default; - /* private */ void -IteratedNoder::node(const std::vector& pathStrings, +IteratedNoder::node(const std::vector& segStrings, int& numInteriorIntersections, - geom::CoordinateXY& intersectionPoint) + CoordinateXY& intersectionPoint) { + IntersectionAdder si(li); + MCIndexNoder noder; + noder.setSegmentIntersector(&si); + noder.computeNodes(segStrings); + auto updatedSegStrings = noder.getNodedSubstrings(); + nodedSegStrings = std::move(updatedSegStrings); + numInteriorIntersections = si.numInteriorIntersections; - auto noder = m_noderFunction(); - if (auto* spn = dynamic_cast(noder.get())) { - IntersectionAdder si(li); - spn->setSegmentIntersector(&si); - // TODO need to have previously checked that all inputs are SegmentStrings - - std::vector segStrings(pathStrings.size()); - for (size_t i = 0; i < pathStrings.size(); i++) { - segStrings[i] = detail::down_cast(pathStrings[i]); - } - - noder->computeNodes(segStrings); - - auto nodedSegStrings = noder->getNodedSubstrings(); - nodedPaths.resize(nodedSegStrings.size()); - for (size_t i = 0; i < nodedSegStrings.size(); i++) { - nodedPaths[i].reset(nodedSegStrings[i].release()); - } - - numInteriorIntersections = si.numInteriorIntersections; - - if (si.hasProperInteriorIntersection()) { - intersectionPoint = si.getProperIntersectionPoint(); - } - } else { - auto* arcNoder = detail::down_cast(noder.get()); - auto aia = std::make_unique(cai); - arcNoder->setArcIntersector(std::move(aia)); - arcNoder->computePathNodes(pathStrings); - nodedPaths = arcNoder->getNodedPaths(); - - // FIXME use actual number! - numInteriorIntersections = 0; - - - // numInteriorIntersections? - // intesectionPoint? + if (si.hasProperInteriorIntersection()) { + intersectionPoint = si.getProperIntersectionPoint(); } } /* public */ void -IteratedNoder::computePathNodes(const std::vector& paths) +IteratedNoder::computeNodes(const std::vector& segStrings) { int numInteriorIntersections; int nodingIterationCount = 0; int lastNodesCreated = -1; - geom::CoordinateXY intersectionPoint = geom::CoordinateXY::getNull(); + CoordinateXY intersectionPoint = CoordinateXY::getNull(); bool firstPass = true; do { // NOTE: will change this.nodedSegStrings if (firstPass) { - node(paths, numInteriorIntersections, intersectionPoint); + node(segStrings, numInteriorIntersections, intersectionPoint); firstPass = false; } else { - auto nodingInput = PathString::toRawPointerVector(nodedPaths); + auto nodingInput = SegmentString::toRawPointerVector(nodedSegStrings); node(nodingInput, numInteriorIntersections, intersectionPoint); } commit 35bd4a66da3dffc85ab4579e7671e3eec8fb6776 Author: Daniel Baston Date: Tue Jan 20 10:57:32 2026 -0500 ArcIntersectionAdder: Accept CircularArcIntersector argument diff --git a/include/geos/noding/ArcIntersectionAdder.h b/include/geos/noding/ArcIntersectionAdder.h index e3e55d904..e71822df4 100644 --- a/include/geos/noding/ArcIntersectionAdder.h +++ b/include/geos/noding/ArcIntersectionAdder.h @@ -23,6 +23,9 @@ namespace geos::noding { class GEOS_DLL ArcIntersectionAdder : public ArcIntersector { public: + explicit ArcIntersectionAdder(algorithm::CircularArcIntersector& cai) : + m_intersector(cai) {} + void processIntersections(ArcString& e0, std::size_t segIndex0, ArcString& e1, std::size_t segIndex1) override; void processIntersections(ArcString& e0, std::size_t segIndex0, SegmentString& e1, std::size_t segIndex1) override; @@ -30,7 +33,7 @@ public: void processIntersections(SegmentString& e0, std::size_t segIndex0, SegmentString& e1, std::size_t segIndex1) override; private: - algorithm::CircularArcIntersector m_intersector; + algorithm::CircularArcIntersector& m_intersector; }; } \ No newline at end of file diff --git a/include/geos/noding/IteratedNoder.h b/include/geos/noding/IteratedNoder.h index 2af2916bc..11a0ab48f 100644 --- a/include/geos/noding/IteratedNoder.h +++ b/include/geos/noding/IteratedNoder.h @@ -24,6 +24,7 @@ #include #include +#include #include #include // due to inlines #include // for inheritance @@ -57,6 +58,7 @@ private: const geom::PrecisionModel* pm; + algorithm::CircularArcIntersector cai; algorithm::LineIntersector li; std::vector> nodedPaths; int maxIter; diff --git a/src/noding/IteratedNoder.cpp b/src/noding/IteratedNoder.cpp index 746828232..df1e9beaf 100644 --- a/src/noding/IteratedNoder.cpp +++ b/src/noding/IteratedNoder.cpp @@ -41,6 +41,7 @@ IteratedNoder::IteratedNoder(const geom::PrecisionModel* newPm, std::function()> noderFunction) : pm(newPm), + cai(pm), li(pm), maxIter(MAX_ITER), m_noderFunction(noderFunction) @@ -88,8 +89,7 @@ IteratedNoder::node(const std::vector& pathStrings, } } else { auto* arcNoder = detail::down_cast(noder.get()); - // FIXME aia should take a PrecsionModel / LineIntersector? - auto aia = std::make_unique(); + auto aia = std::make_unique(cai); arcNoder->setArcIntersector(std::move(aia)); arcNoder->computePathNodes(pathStrings); nodedPaths = arcNoder->getNodedPaths(); diff --git a/tests/unit/noding/SimpleNoderTest.cpp b/tests/unit/noding/SimpleNoderTest.cpp index 06ffb07e2..934247541 100644 --- a/tests/unit/noding/SimpleNoderTest.cpp +++ b/tests/unit/noding/SimpleNoderTest.cpp @@ -13,6 +13,7 @@ using geos::geom::CoordinateXY; using geos::geom::CoordinateSequence; using geos::geom::CircularArc; using geos::geom::Ordinate; +using geos::algorithm::CircularArcIntersector; using geos::algorithm::Orientation; using geos::noding::ArcString; using geos::noding::SegmentString; @@ -108,6 +109,7 @@ template<> void object::test<1>() { set_test_name("segment-segment intersection"); + CircularArcIntersector cai; auto seq1 = std::make_shared(); seq1->add(CoordinateXY{0, 0}); @@ -121,7 +123,7 @@ void object::test<1>() std::vector ss{&ss1, &ss2}; - SimpleNoder noder(std::make_unique()); + SimpleNoder noder(std::make_unique(cai)); noder.computePathNodes(ss); auto paths = noder.getNodedPaths(); @@ -134,6 +136,7 @@ template<> void object::test<2>() { set_test_name("arc-arc intersection"); + CircularArcIntersector cai; std::vector arcs0; arcs0.push_back(makeArc(CoordinateXY{-1, 0}, {1, 0}, {0, 0}, 1, Orientation::CLOCKWISE)); @@ -146,7 +149,7 @@ void object::test<2>() std::vector ss{&as0, &as1}; - SimpleNoder noder(std::make_unique()); + SimpleNoder noder(std::make_unique(cai)); noder.computePathNodes(ss); auto paths = noder.getNodedPaths(); @@ -159,6 +162,7 @@ template<> void object::test<3>() { set_test_name("arc-segment intersection"); + CircularArcIntersector cai; std::vector arcs0; arcs0.push_back(makeArc(CoordinateXY{-1, 0}, {1, 0}, {0, 0}, 1, Orientation::CLOCKWISE)); @@ -171,7 +175,7 @@ void object::test<3>() std::vector ss{&as0, &ss1}; - SimpleNoder noder(std::make_unique()); + SimpleNoder noder(std::make_unique(cai)); noder.computePathNodes(ss); auto paths = noder.getNodedPaths(); commit 5e1bd575034913eb1941b9fcb303cb2be2630bb3 Author: Daniel Baston Date: Tue Jan 20 10:51:53 2026 -0500 CircularArcIntersector: Accept PrecisionModel argument diff --git a/include/geos/algorithm/CircularArcIntersector.h b/include/geos/algorithm/CircularArcIntersector.h index 58f9aae66..8affd3c65 100644 --- a/include/geos/algorithm/CircularArcIntersector.h +++ b/include/geos/algorithm/CircularArcIntersector.h @@ -38,6 +38,10 @@ public: COCIRCULAR_INTERSECTION = 3, }; + explicit CircularArcIntersector(const geom::PrecisionModel* pm = nullptr) + : precisionModel(pm) + {} + intersection_type getResult() const { return result; @@ -146,6 +150,7 @@ private: std::array intPt; std::array intArc; + const geom::PrecisionModel* precisionModel; intersection_type result = NO_INTERSECTION; std::uint8_t nPt = 0; std::uint8_t nArc = 0; diff --git a/src/algorithm/CircularArcIntersector.cpp b/src/algorithm/CircularArcIntersector.cpp index e17699c3e..1b3bedcaf 100644 --- a/src/algorithm/CircularArcIntersector.cpp +++ b/src/algorithm/CircularArcIntersector.cpp @@ -438,7 +438,7 @@ void CircularArcIntersector::intersects(const CoordinateSequence &p, std::size_t p0, std::size_t p1, const CoordinateSequence &q, std::size_t q0, std::size_t q1) { - LineIntersector li; + LineIntersector li(precisionModel); li.computeIntersection(p, p0, p1, q, q0, q1); if (li.getIntersectionNum() == 2) { @@ -570,6 +570,12 @@ CircularArcIntersector::addCocircularIntersection(double startAngle, double endA CoordinateXYZM computedMidPt(CircularArcs::createPoint(center, radius, theta1)); CoordinateXYZM computedEndPt(CircularArcs::createPoint(center, radius, endAngle)); + if (precisionModel) { + precisionModel->makePrecise(computedStartPt); + precisionModel->makePrecise(computedMidPt); + precisionModel->makePrecise(computedEndPt); + } + // Check to see if the endpoints of the intersection match the endpoints of either of // the input arcs. Use angles for the check to avoid missing an endpoint intersection from // inaccuracy in the point construction. @@ -610,6 +616,10 @@ CircularArcIntersector::addArcArcIntersectionPoint(const CoordinateXY& computedI CoordinateXYZM& newIntPt = intPt[nPt++]; newIntPt = computedIntPt; + if (precisionModel) { + precisionModel->makePrecise(newIntPt); + } + if (computedIntPt.equals2D(arc1.p0())) { arc1.applyAt(0, [&newIntPt](const auto& endpoint) { newIntPt.z = Interpolate::zGet(newIntPt, endpoint); @@ -632,6 +642,10 @@ CircularArcIntersector::addArcSegmentIntersectionPoint(const CoordinateXY& compu CoordinateXYZM& newIntPt = intPt[nPt++]; newIntPt = computedIntPt; + if (precisionModel) { + precisionModel->makePrecise(newIntPt); + } + for (int i = 0; i < 2; i++) { if (useSegEndpoints) { if (computedIntPt.equals2D(seq.getAt(pos0))) { diff --git a/tests/unit/algorithm/CircularArcIntersectorTest.cpp b/tests/unit/algorithm/CircularArcIntersectorTest.cpp index dc07a91aa..6ad649d02 100644 --- a/tests/unit/algorithm/CircularArcIntersectorTest.cpp +++ b/tests/unit/algorithm/CircularArcIntersectorTest.cpp @@ -16,6 +16,7 @@ using geos::geom::CoordinateXY; using geos::geom::CoordinateXYM; using geos::geom::CoordinateXYZM; using geos::geom::CircularArc; +using geos::geom::PrecisionModel; using geos::MATH_PI; namespace tut { @@ -1612,6 +1613,86 @@ void object::test<81> ); } +template<> +template<> +void object::test<82>() +{ + set_test_name("arc/segment interior intersection with fixed PrecisionModel"); + + PrecisionModel pm(10); + CircularArcIntersector cai(&pm); + + auto arcPts = CoordinateSequence::XY(3); + arcPts.setAt(XY{-200, 0}, 0); + arcPts.setAt(XY{0, 200}, 1); + arcPts.setAt(XY{200, 0}, 2); + CircularArc arc(arcPts, 0); + + auto segPts = CoordinateSequence::XY(2); + segPts.setAt(XY{0, 0}, 0); + segPts.setAt(XY{200, 200}, 0); + + cai.intersects(arc, segPts, 0, 1, false); + + ensure_equals(cai.getNumPoints(), 1u); + ensure_equals(cai.getPoint(0), XY{141.4, 141.4}); +} + +template<> +template<> +void object::test<83>() +{ + set_test_name("arc/arc interior intersection with fixed PrecisionModel"); + + PrecisionModel pm(10); + CircularArcIntersector cai(&pm); + + auto arcPts1 = CoordinateSequence::XY(3); + arcPts1.setAt(XY{-200, 0}, 0); + arcPts1.setAt(XY{0, 200}, 1); + arcPts1.setAt(XY{200, 0}, 2); + CircularArc arc1(arcPts1, 0); + + auto arcPts2 = CoordinateSequence::XY(3); + arcPts2.setAt(XY{0, 0}, 0); + arcPts2.setAt(XY{200, 200}, 1); + arcPts2.setAt(XY{400, 0}, 2); + CircularArc arc2(arcPts2, 0); + + cai.intersects(arc1, arc2); + + ensure_equals(cai.getNumPoints(), 1u); + ensure_equals(cai.getPoint(0), XY{100, 173.2}); +} + +template<> +template<> +void object::test<84>() +{ + set_test_name("degenerate arc/degenerate arc intersection with fixed PrecisionModel"); + + PrecisionModel pm(10); + CircularArcIntersector cai(&pm); + + auto arcPts1 = CoordinateSequence::XY(3); + arcPts1.setAt(XY{0, 0}, 0); + arcPts1.setAt(XY{35, 75}, 1); + arcPts1.setAt(XY{70, 150}, 2); + CircularArc arc1(arcPts1, 0); + + auto arcPts2 = CoordinateSequence::XY(3); + arcPts2.setAt(XY{0, 100}, 0); + arcPts2.setAt(XY{50, 50}, 1); + arcPts2.setAt(XY{100, 0}, 2); + CircularArc arc2(arcPts2, 0); + + cai.intersects(arc1, arc2); + + ensure_equals(cai.getNumPoints(), 1u); + ensure_equals(cai.getPoint(0), XY{31.8, 68.2}); +} + + // TODO: check Z values of arc result centerpoints // TODO: add tests for seg/seg commit 15ce6735d6ba3014fc12fcf86df9eecf92db9739 Author: Daniel Baston Date: Mon Jan 19 16:56:54 2026 -0500 GeometryNoder: Readability improvements diff --git a/src/noding/GeometryNoder.cpp b/src/noding/GeometryNoder.cpp index 81e759886..6103364ea 100644 --- a/src/noding/GeometryNoder.cpp +++ b/src/noding/GeometryNoder.cpp @@ -65,7 +65,6 @@ public: { if(const auto* ls = dynamic_cast(g)) { auto coord = ls->getSharedCoordinates(); - // coord ownership transferred to SegmentString auto ss = std::make_unique(coord, _constructZ, _constructM, nullptr); _to.push_back(std::move(ss)); } else if (const auto* cs = dynamic_cast(g)) { @@ -119,19 +118,17 @@ GeometryNoder::toGeometry(std::vector>& nodedEdges) bool resultArcs = false; for(auto& path : nodedEdges) { - if (const auto* ss = dynamic_cast(path.get())) { - const auto& coords = ss->getCoordinates(); + const auto& coords = path->getCoordinates(); - // Check if an equivalent edge is known - OrientedCoordinateArray oca1(*coords); - if(ocas.insert(oca1).second) { + OrientedCoordinateArray oca1(*coords); + // Check if an equivalent edge is known + if(ocas.insert(oca1).second) { + if (dynamic_cast(path.get())) { lines.push_back(geomFact->createLineString(coords)); + } else { + resultArcs = true; + lines.push_back(geomFact->createCircularString(coords)); } - } else { - resultArcs = true; - auto* as = dynamic_cast(path.get()); - // FIXME: check for duplicates - lines.push_back(geomFact->createCircularString(as->getCoordinates())); } } commit f54ead7568472702e67563ee1d559b5d7c6d6b66 Author: Daniel Baston Date: Mon Jan 19 16:48:01 2026 -0500 CircularString: Store arcs as a member variable diff --git a/include/geos/geom/CircularString.h b/include/geos/geom/CircularString.h index 6a6d5f8c3..e38a5b640 100644 --- a/include/geos/geom/CircularString.h +++ b/include/geos/geom/CircularString.h @@ -14,6 +14,7 @@ #pragma once +#include #include namespace geos { @@ -30,6 +31,8 @@ public: std::unique_ptr clone() const; + const std::vector& getArcs() const; + std::string getGeometryType() const override; GeometryTypeId getGeometryTypeId() const override; @@ -81,6 +84,11 @@ protected: void validateConstruction(); +private: + void createArcs() const; + + mutable std::vector arcs; + }; diff --git a/src/algorithm/Area.cpp b/src/algorithm/Area.cpp index 94889933d..1311ebd1f 100644 --- a/src/algorithm/Area.cpp +++ b/src/algorithm/Area.cpp @@ -21,8 +21,10 @@ #include #include +#include #include #include +#include #include using geos::geom::CoordinateXY; @@ -128,22 +130,22 @@ Area::ofClosedCurve(const geom::Curve& ring) { const geom::CoordinateSequence& coords = *section.getCoordinatesRO(); if (section.isCurved()) { - for (std::size_t j = 2; j < coords.size(); j += 2) { - const CoordinateXY& p0 = coords.getAt(j-2); - const CoordinateXY& p1 = coords.getAt(j-1); - const CoordinateXY& p2 = coords.getAt(j); + const auto* cs = detail::down_cast(§ion); + + for (const auto& arc : cs->getArcs()) { + const CoordinateXY& p0 = arc.p0(); + const CoordinateXY& p2 = arc.p2(); double triangleArea = 0.5*(p0.x*p2.y - p2.x*p0.y); sum += triangleArea; - geom::CircularArc arc(coords, j-2); if (arc.isLinear()) { continue; } double circularSegmentArea = arc.getArea(); - if (algorithm::Orientation::index(p0, p2, p1) == algorithm::Orientation::CLOCKWISE) { + if (arc.isCCW()) { sum += circularSegmentArea; } else { sum -= circularSegmentArea; diff --git a/src/geom/CircularArc.cpp b/src/geom/CircularArc.cpp index eb2ffe5df..9b5476953 100644 --- a/src/geom/CircularArc.cpp +++ b/src/geom/CircularArc.cpp @@ -381,17 +381,6 @@ CircularArc::equals(const CircularArc &other, double tol) const return true; } - -#if 0 -std::pair -CircularArc::splitAtPoint(const CoordinateXY& q) const { - return std::make_pair( - CircularArc(p0(), q, getCenter(), getRadius(), getOrientation()), - CircularArc(q, p2(), getCenter(), getRadius(), getOrientation()) - ); -} -#endif - std::string CircularArc::toString() const { std::stringstream ss; diff --git a/src/geom/CircularString.cpp b/src/geom/CircularString.cpp index 361f56c56..ac5311905 100644 --- a/src/geom/CircularString.cpp +++ b/src/geom/CircularString.cpp @@ -46,6 +46,23 @@ CircularString::clone() const return std::unique_ptr(cloneImpl()); } +void +CircularString::createArcs() const +{ + for (std::size_t i = 0; i < points->getSize() - 2; i += 2) { + arcs.emplace_back(*points, i); + } +} + +const std::vector& +CircularString::getArcs() const +{ + if (arcs.empty()) { + createArcs(); + } + return arcs; +} + std::string CircularString::getGeometryType() const { @@ -65,13 +82,11 @@ CircularString::getLength() const return 0; } - const CoordinateSequence& coords = *getCoordinatesRO(); - double tot = 0; - for (std::size_t i = 2; i < coords.size(); i += 2) { - auto len = CircularArc(coords, i-2).getLength(); - tot += len; + for (const auto& arc : getArcs()) { + tot += arc.getLength(); } + return tot; } diff --git a/src/noding/GeometryNoder.cpp b/src/noding/GeometryNoder.cpp index 6103a7407..81e759886 100644 --- a/src/noding/GeometryNoder.cpp +++ b/src/noding/GeometryNoder.cpp @@ -48,7 +48,7 @@ namespace noding { // geos.noding namespace { /** - * Add every linear element in a geometry into SegmentString vector + * Add every linear element in a geometry into PathString vector */ class PathStringExtractor: public geom::GeometryComponentFilter { public: @@ -70,12 +70,7 @@ public: _to.push_back(std::move(ss)); } else if (const auto* cs = dynamic_cast(g)) { const auto& coords = cs->getSharedCoordinates(); - - // TODO: Store this vector in the CircularString ? - std::vector arcs; - for (std::size_t i = 0; i < coords->getSize() - 2; i += 2) { - arcs.emplace_back(*coords, i); - } + auto arcs = cs->getArcs(); auto as = std::make_unique(std::move(arcs), coords, _constructZ, _constructM, nullptr); _to.push_back(std::move(as)); commit bd03a4b050c553f9c6b84ed1554abe5464cd3e20 Author: Daniel Baston Date: Mon Jan 19 16:29:39 2026 -0500 NodableArcString: construct from std::shared_ptr diff --git a/include/geos/noding/ArcString.h b/include/geos/noding/ArcString.h index 67285f33c..e758b7307 100644 --- a/include/geos/noding/ArcString.h +++ b/include/geos/noding/ArcString.h @@ -32,9 +32,9 @@ public: explicit ArcString(std::vector arcs) : m_arcs(std::move(arcs)) { } - ArcString(std::vector arcs, std::unique_ptr seq, void* context) + ArcString(std::vector arcs, const std::shared_ptr& seq, void* context) : m_arcs(std::move(arcs)), - m_seq(std::move(seq)), + m_seq(seq), m_context(context) {} diff --git a/include/geos/noding/NodableArcString.h b/include/geos/noding/NodableArcString.h index 2fcd5be2d..b7e438f8f 100644 --- a/include/geos/noding/NodableArcString.h +++ b/include/geos/noding/NodableArcString.h @@ -23,7 +23,7 @@ namespace geos::noding { class GEOS_DLL NodableArcString : public ArcString, public NodablePath { public: - NodableArcString(std::vector arcs, std::unique_ptr coords, bool constructZ, bool constructM, void* context); + NodableArcString(std::vector arcs, const std::shared_ptr& coords, bool constructZ, bool constructM, void* context); std::unique_ptr clone() const; diff --git a/src/noding/GeometryNoder.cpp b/src/noding/GeometryNoder.cpp index d65676f76..6103a7407 100644 --- a/src/noding/GeometryNoder.cpp +++ b/src/noding/GeometryNoder.cpp @@ -69,7 +69,7 @@ public: auto ss = std::make_unique(coord, _constructZ, _constructM, nullptr); _to.push_back(std::move(ss)); } else if (const auto* cs = dynamic_cast(g)) { - auto coords = cs->getCoordinates(); + const auto& coords = cs->getSharedCoordinates(); // TODO: Store this vector in the CircularString ? std::vector arcs; @@ -77,7 +77,7 @@ public: arcs.emplace_back(*coords, i); } - auto as = std::make_unique(std::move(arcs), std::move(coords), _constructZ, _constructM, nullptr); + auto as = std::make_unique(std::move(arcs), coords, _constructZ, _constructM, nullptr); _to.push_back(std::move(as)); } } diff --git a/src/noding/NodableArcString.cpp b/src/noding/NodableArcString.cpp index 96d8e8880..35969cac5 100644 --- a/src/noding/NodableArcString.cpp +++ b/src/noding/NodableArcString.cpp @@ -29,8 +29,8 @@ pseudoAngleDiffCCW(double paStart, double pa) { return diff; } -NodableArcString::NodableArcString(std::vector arcs, std::unique_ptr coords, bool constructZ, bool constructM, void* context) : - ArcString(std::move(arcs), std::move(coords), context), +NodableArcString::NodableArcString(std::vector arcs, const std::shared_ptr& coords, bool constructZ, bool constructM, void* context) : + ArcString(std::move(arcs), coords, context), m_constructZ(constructZ), m_constructM(constructM) { commit 78881d3917a5b22e916384c496ed15c931acc0f8 Author: Daniel Baston Date: Mon Jan 19 16:23:01 2026 -0500 CircularArcIntersector: Readability improvements diff --git a/include/geos/algorithm/Angle.h b/include/geos/algorithm/Angle.h index 192891022..ee786179f 100644 --- a/include/geos/algorithm/Angle.h +++ b/include/geos/algorithm/Angle.h @@ -228,6 +228,25 @@ public: /// @return true if `angle` is within [from, to] static bool isWithinCCW(double angle, double from, double to); + /// Return which of the two provided angles would be encountered first when moving + /// counterclockwise from the specified start angle. + /// + /// @param from the starting angle + /// @param a the first candidate angle + /// @param b the second candidate angle + /// + /// @return a or b + static double nextCCW(double from, double a, double b); + + /// Return the fraction of an angle as a fraction of a larger angle + /// + /// @param x an angle between a and b + /// @param a the starting angle + /// @param b the ending angle + /// + /// @return a value in the range [0, 1] + static double fractionCCW(double x, double a, double b); + /// Computes the unoriented smallest difference between two angles. /// /// The angles are assumed to be normalized to the range [-Pi, Pi]. diff --git a/include/geos/algorithm/CircularArcIntersector.h b/include/geos/algorithm/CircularArcIntersector.h index 80e1543ab..58f9aae66 100644 --- a/include/geos/algorithm/CircularArcIntersector.h +++ b/include/geos/algorithm/CircularArcIntersector.h @@ -103,7 +103,7 @@ private: * When the endpoints of the new arc correspond with those of the inputs, Z/M values * will be preferentially taken from arc1. */ - void addArcIntersection(double startAngle, double endAngle, int orientation, const CircularArc& arc1, const CircularArc& arc2); + void addCocircularIntersection(double startAngle, double endAngle, int orientation, const CircularArc& arc1, const CircularArc& arc2); /** Add a point intersection between two arcs. * @@ -113,7 +113,7 @@ private: * If the intersection point does not equal the endpoint of either arc, its Z/M values * will be interpolated. */ - void addIntersection(const CoordinateXY& computedIntPt, const CircularArc& arc1, const CircularArc& arc2); + void addArcArcIntersectionPoint(const CoordinateXY& computedIntPt, const CircularArc& arc1, const CircularArc& arc2); /** Add a point intersection between an arc and a segment. * @@ -123,7 +123,8 @@ private: * `useSegEndpoint` is true. If the intersection point does not equal the endpoint of the arc * or the segment, its Z/M values will be interpolated. */ - void addIntersection(const CoordinateXY& computedIntPt, const CircularArc& lhs, const geom::CoordinateSequence& seq, std::size_t pos0, std::size_t pos1, bool useSegEndpoints); + void addArcSegmentIntersectionPoint(const CoordinateXY& computedIntPt, const CircularArc& lhs, + const geom::CoordinateSequence& seq, std::size_t pos0, std::size_t pos1, bool useSegEndpoints); /** Determines whether and where two circles intersect a line segment. * @@ -140,6 +141,9 @@ private: void computeCocircularIntersection(const CircularArc& arc1, const CircularArc& arc2); + /** Checks whether the provided point has already been recorded as an intersection point. */ + bool hasIntersection(const geom::CoordinateXY& p) const; + std::array intPt; std::array intArc; intersection_type result = NO_INTERSECTION; diff --git a/src/algorithm/Angle.cpp b/src/algorithm/Angle.cpp index b63dc4db8..e372a4a9a 100644 --- a/src/algorithm/Angle.cpp +++ b/src/algorithm/Angle.cpp @@ -156,6 +156,29 @@ Angle::getTurn(double ang1, double ang2) return NONE; } +double +Angle::nextCCW(double from, double a, double b) +{ + if (normalizePositive(a - from) < normalizePositive(b - from)) { + return a; + } + + return b; +} + +double +Angle::fractionCCW(double x, double a, double b) +{ + if (x < a) { + x += 2*MATH_PI; + } + if (b < a) { + b += 2*MATH_PI; + } + return (x - a) / (b - a); +} + + /* public static */ double Angle::normalize(double angle) diff --git a/src/algorithm/CircularArcIntersector.cpp b/src/algorithm/CircularArcIntersector.cpp index ba0ab0ccd..e17699c3e 100644 --- a/src/algorithm/CircularArcIntersector.cpp +++ b/src/algorithm/CircularArcIntersector.cpp @@ -3,7 +3,7 @@ * GEOS - Geometry Engine Open Source * http://geos.osgeo.org * - * Copyright (C) 2024-2025 ISciences, LLC + * Copyright (C) 2024-2026 ISciences, LLC * * This is free software; you can redistribute and/or modify it under * the terms of the GNU Lesser General Public Licence as published @@ -25,29 +25,6 @@ using geos::geom::CircularArc; namespace geos::algorithm { -static double -nextAngleCCW(double from, double a, double b) -{ - if (Angle::normalizePositive(a - from) < Angle::normalizePositive(b - from)) { - return a; - } - else { - return b; - } -} - -static double -angleFractionCCW(double x, double a, double b) -{ - if (x < a) { - x += 2*MATH_PI; - } - if (b < a) { - b += 2*MATH_PI; - } - return (x - a) / (b - a); -} - static double interpolateValue(double a1, double a2, double frac) { @@ -61,53 +38,50 @@ interpolateValue(double a1, double a2, double frac) return a1 + frac * (a2 - a1); } -static void interpolateZM(const CircularArc& arc, - const CoordinateXY& pt, - double& z, double& m) +static void +interpolateZM(const CircularArc& arc, const CoordinateXY& pt, double& z, double& m) { - using geos::geom::Ordinate; + using geom::Ordinate; - const geom::CoordinateSequence& seq = *arc.getCoordinateSequence(); + const CoordinateSequence& seq = *arc.getCoordinateSequence(); std::size_t i0 = arc.getCoordinatePosition(); - if (arc.p1().equals2D(pt)) { - seq.applyAt(i0 + 1, [&z, &m](const auto& arcPt) { - z = arcPt.template get(); - m = arcPt.template get(); - }); - return; - } - - double z0, m0; - seq.applyAt(i0, [&z0, &m0](const auto& arcPt) { - z0 = arcPt.template get(); - m0 = arcPt.template get(); - }); - if (arc.p0().equals2D(pt)) { - z = z0; - m = m0; - return; - } - + // Read Z, M from control point double z1, m1; seq.applyAt(i0 + 1, [&z1, &m1](const auto& arcPt) { z1 = arcPt.template get(); m1 = arcPt.template get(); }); - + // Test point = control point? + // Take Z, M from the control point if (arc.p1().equals2D(pt)) { z = z1; m = m1; return; } + // Read Z, M from start point + double z0, m0; + seq.applyAt(i0, [&z0, &m0](const auto& arcPt) { + z0 = arcPt.template get(); + m0 = arcPt.template get(); + }); + // Test point = start point? + // Take Z, M from the start point + if (arc.p0().equals2D(pt)) { + z = z0; + m = m0; + return; + } + // Read Z, M from end point double z2, m2; seq.applyAt(i0 + 2, [&z2, &m2](const auto& arcPt) { z2 = arcPt.template get(); m2 = arcPt.template get(); }); - + // Test point = end point? + // Take Z, M from the end point if (arc.p2().equals2D(pt)) { z = z2; m = m2; @@ -127,37 +101,39 @@ static void interpolateZM(const CircularArc& arc, if (std::isnan(z1)) { // Interpolate between p0 / p2 - const double frac = angleFractionCCW(theta, theta0, theta2); + const double frac = Angle::fractionCCW(theta, theta0, theta2); z = interpolateValue(z0, z2, frac); } else if (Angle::isWithinCCW(theta, theta0, theta1)) { // Interpolate between p0 / p1 - const double frac = angleFractionCCW(theta, theta0, theta1); + const double frac = Angle::fractionCCW(theta, theta0, theta1); z = interpolateValue(z0, z1, frac); } else { // Interpolate between p1 / p2 - const double frac = angleFractionCCW(theta, theta1, theta2); + const double frac = Angle::fractionCCW(theta, theta1, theta2); z = interpolateValue(z1, z2, frac); } if (std::isnan(m1)) { // Interpolate between p0 / p2 - const double frac = angleFractionCCW(theta, theta0, theta2); + const double frac = Angle::fractionCCW(theta, theta0, theta2); m = interpolateValue(m0, m2, frac); } else if (Angle::isWithinCCW(theta, theta0, theta1)) { // Interpolate between p0 / p1 - const double frac = angleFractionCCW(theta, theta0, theta1); + const double frac = Angle::fractionCCW(theta, theta0, theta1); m = interpolateValue(m0, m1, frac); } else { // Interpolate between p1 / p2 - const double frac = angleFractionCCW(theta, theta1, theta2); + const double frac = Angle::fractionCCW(theta, theta1, theta2); m = interpolateValue(m1, m2, frac); } } -static void interpolateSegmentZM(const CoordinateSequence& seq, - std::size_t ind0, std::size_t ind1, - geom::CoordinateXY& pt, double& z, double& m) +// Interpolate the Z/M values of a point lying on the provided line segment +static void +interpolateSegmentZM(const CoordinateSequence& seq, + std::size_t ind0, std::size_t ind1, + CoordinateXY& pt, double& z, double& m) { seq.applyAt(ind0, [&seq, &pt, ind1, &z, &m](const auto& p0) { using CoordinateType = std::decay_t; @@ -169,7 +145,9 @@ static void interpolateSegmentZM(const CoordinateSequence& seq, } -static void interpolateZM(const CircularArc& arc0, const CircularArc& arc1, geom::CoordinateXYZM& pt) +// Interpolate the Z/M values of an intersection point between two arcs +static void +interpolateZM(const CircularArc& arc0, const CircularArc& arc1, geom::CoordinateXYZM& pt) { if (!std::isnan(pt.z) && !std::isnan(pt.m)) { return; @@ -188,10 +166,12 @@ static void interpolateZM(const CircularArc& arc0, const CircularArc& arc1, geom } } -static void interpolateZM(const CircularArc& arc0, - const geom::CoordinateSequence& seq, - std::size_t ind0, std::size_t ind1, - geom::CoordinateXYZM& pt) +// Interpolate the Z/M values of an intersection point between an arc and a segment +static void +interpolateZM(const CircularArc& arc0, + const CoordinateSequence& seq, + std::size_t ind0, std::size_t ind1, + geom::CoordinateXYZM& pt) { if (!std::isnan(pt.z) && !std::isnan(pt.m)) { return; @@ -277,6 +257,19 @@ CircularArcIntersector::circleIntersects(const CoordinateXY& center, return n; } +bool +CircularArcIntersector::hasIntersection(const geom::CoordinateXY &p) const { + switch (nPt) { + case 2: return intPt[1].equals2D(p) || intPt[0].equals2D(p); + case 1: return intPt[0].equals2D(p); + case 0: return false; + default: break; + } + + assert(0); + return false; +} + void CircularArcIntersector::intersects(const CircularArc& arc, const CoordinateSequence& seq, std::size_t segPos0, std::size_t segPos1, bool useSegEndpoints) { @@ -297,11 +290,11 @@ CircularArcIntersector::intersects(const CircularArc& arc, const CoordinateSeque auto n = circleIntersects(c, r, seq.getAt(segPos0), seq.getAt(segPos1), isect0, isect1); if (n > 0 && arc.containsPointOnCircle(isect0)) { - addIntersection(isect0, arc, seq, segPos0, segPos1, useSegEndpoints); + addArcSegmentIntersectionPoint(isect0, arc, seq, segPos0, segPos1, useSegEndpoints); } if (n > 1 && arc.containsPointOnCircle(isect1)) { - addIntersection(isect1, arc, seq, segPos0, segPos1, useSegEndpoints); + addArcSegmentIntersectionPoint(isect1, arc, seq, segPos0, segPos1, useSegEndpoints); } switch (nPt) { @@ -370,17 +363,17 @@ CircularArcIntersector::intersects(const CircularArc& arc1, const CircularArc& a computeCocircularIntersection(arc1, arc2); } else { // Explicitly add endpoint intersections that may be missed or inexactly computed. - if (nPt < 2 && arc1.p0().equals2D(arc2.p0()) && (nPt == 0 || !intPt[0].equals2D(arc1.p0()))) { - addIntersection(arc1.p0(), arc1, arc2); + if (arc1.p0().equals2D(arc2.p0()) && !hasIntersection(arc1.p0())) { + addArcArcIntersectionPoint(arc1.p0(), arc1, arc2); } - if (nPt < 2 && arc1.p0().equals2D(arc2.p2()) && (nPt == 0 || !intPt[0].equals2D(arc1.p0()))) { - addIntersection(arc1.p0(), arc1, arc2); + if (arc1.p0().equals2D(arc2.p2()) && !hasIntersection(arc1.p0())) { + addArcArcIntersectionPoint(arc1.p0(), arc1, arc2); } - if (nPt < 2 && arc1.p2().equals2D(arc2.p0()) && (nPt == 0 || !intPt[0].equals2D(arc1.p2()))) { - addIntersection(arc1.p2(), arc1, arc2); + if (arc1.p2().equals2D(arc2.p0()) && !hasIntersection(arc1.p2())) { + addArcArcIntersectionPoint(arc1.p2(), arc1, arc2); } - if (nPt < 2 && arc1.p2().equals2D(arc2.p2()) && (nPt == 0 || !intPt[0].equals2D(arc1.p2()))) { - addIntersection(arc1.p2(), arc1, arc2); + if (arc1.p2().equals2D(arc2.p2()) && !hasIntersection(arc1.p2())) { + addArcArcIntersectionPoint(arc1.p2(), arc1, arc2); } if (nPt < 2) { @@ -399,8 +392,8 @@ CircularArcIntersector::intersects(const CircularArc& arc1, const CircularArc& a CoordinateXY isect1{p.x - h* dy/d, p.y + h* dx/d }; // One of the computed intersection points may be an inexact version of an endpoint. - // If we already have an endpoint intersection, process the farther-away computed - // point first. + // If we already have an endpoint intersection, we need to process the farther-away + // computed point first. if (nPt == 1 && intPt[0].distance(isect0) < intPt[0].distance(isect1)) { std::swap(isect0, isect1); } @@ -415,7 +408,7 @@ CircularArcIntersector::intersects(const CircularArc& arc1, const CircularArc& a } if (arc1.containsPointOnCircle(computedIntPt) && arc2.containsPointOnCircle(computedIntPt)) { - addIntersection(computedIntPt, arc1, arc2); + addArcArcIntersectionPoint(computedIntPt, arc1, arc2); } } } @@ -435,6 +428,8 @@ CircularArcIntersector::intersects(const CircularArc& arc1, const CircularArc& a case 0: result = NO_INTERSECTION; break; + default: + assert(0); } } } @@ -460,6 +455,7 @@ CircularArcIntersector::intersects(const CoordinateSequence &p, std::size_t p0, } } +/// Overwrite X/Y, and NaN Z/M values on the supplied point with those from the coordinate at the specified index static void setFromEndpoint(geom::CoordinateXYZM& pt, const CircularArc& arc, std::size_t index) { @@ -510,7 +506,7 @@ CircularArcIntersector::computeCocircularIntersection(const CircularArc& arc1, c if (Angle::isWithinCCW(bp0, ap0, ap1)) { checkAcontained = false; const double start = bp0; - const double end = nextAngleCCW(start, bp1, ap1); + const double end = Angle::nextCCW(start, bp1, ap1); if (end == bp1) { checkBp1inA = false; @@ -518,14 +514,14 @@ CircularArcIntersector::computeCocircularIntersection(const CircularArc& arc1, c if (start == end) { const CoordinateXY computedIntPt = CircularArcs::createPoint(center, radius, start); - addIntersection(computedIntPt, arc1, arc2); + addArcArcIntersectionPoint(computedIntPt, arc1, arc2); } else { if (resultArcIsCCW) { - addArcIntersection(start, end, Orientation::COUNTERCLOCKWISE, arc1, arc2); + addCocircularIntersection(start, end, Orientation::COUNTERCLOCKWISE, arc1, arc2); } else { - addArcIntersection(end, start, Orientation::CLOCKWISE, arc1, arc2); + addCocircularIntersection(end, start, Orientation::CLOCKWISE, arc1, arc2); } } } @@ -538,30 +534,30 @@ CircularArcIntersector::computeCocircularIntersection(const CircularArc& arc1, c const double end = bp1; if (start == end) { const CoordinateXY computedIntPt = CircularArcs::createPoint(center, radius, start); - addIntersection(computedIntPt, arc1, arc2); + addArcArcIntersectionPoint(computedIntPt, arc1, arc2); } else { if (resultArcIsCCW) { - addArcIntersection(start, end, Orientation::CLOCKWISE, arc1, arc2); + addCocircularIntersection(start, end, Orientation::CLOCKWISE, arc1, arc2); } else { - addArcIntersection(end, start, Orientation::CLOCKWISE, arc1, arc2); + addCocircularIntersection(end, start, Orientation::CLOCKWISE, arc1, arc2); } } } if (checkAcontained && Angle::isWithinCCW(ap0, bp0 , bp1) && ap0 != bp0 && ap0 != bp1 && Angle::isWithinCCW(ap1, bp0, bp1) && ap1 != bp1 && ap1 != bp0) { if (resultArcIsCCW) { - addArcIntersection(ap0, ap1, Orientation::COUNTERCLOCKWISE, arc1, arc2); + addCocircularIntersection(ap0, ap1, Orientation::COUNTERCLOCKWISE, arc1, arc2); } else { - addArcIntersection(ap1, ap0, Orientation::CLOCKWISE, arc1, arc2); + addCocircularIntersection(ap1, ap0, Orientation::CLOCKWISE, arc1, arc2); } } } void -CircularArcIntersector::addArcIntersection(double startAngle, double endAngle, int orientation, const CircularArc& arc1, const CircularArc& arc2) +CircularArcIntersector::addCocircularIntersection(double startAngle, double endAngle, int orientation, const CircularArc& arc1, const CircularArc& arc2) { const auto theta1 = CircularArcs::getMidpointAngle(startAngle, endAngle, orientation == Orientation::COUNTERCLOCKWISE); const CoordinateXY& center = arc1.getCenter(); @@ -575,8 +571,8 @@ CircularArcIntersector::addArcIntersection(double startAngle, double endAngle, i CoordinateXYZM computedEndPt(CircularArcs::createPoint(center, radius, endAngle)); // Check to see if the endpoints of the intersection match the endpoints of either of - // the endpoints. Use angles for the check to avoid missing an endpoint intersection from - // inaccuracy the point construction. + // the input arcs. Use angles for the check to avoid missing an endpoint intersection from + // inaccuracy in the point construction. if (startAngle == arc1.theta0()) { setFromEndpoint(computedStartPt, arc1, 0); } else if (startAngle == arc1.theta2()) { @@ -610,7 +606,7 @@ CircularArcIntersector::addArcIntersection(double startAngle, double endAngle, i } void -CircularArcIntersector::addIntersection(const CoordinateXY& computedIntPt, const CircularArc& arc1, const CircularArc& arc2) { +CircularArcIntersector::addArcArcIntersectionPoint(const CoordinateXY& computedIntPt, const CircularArc& arc1, const CircularArc& arc2) { CoordinateXYZM& newIntPt = intPt[nPt++]; newIntPt = computedIntPt; @@ -630,7 +626,9 @@ CircularArcIntersector::addIntersection(const CoordinateXY& computedIntPt, const } void -CircularArcIntersector::addIntersection(const CoordinateXY& computedIntPt, const CircularArc& arc1, const CoordinateSequence& seq, std::size_t pos0, std::size_t pos1, bool useSegEndpoints) { +CircularArcIntersector::addArcSegmentIntersectionPoint(const CoordinateXY& computedIntPt, const CircularArc& arc1, + const CoordinateSequence& seq, std::size_t pos0, std::size_t pos1, bool useSegEndpoints) +{ CoordinateXYZM& newIntPt = intPt[nPt++]; newIntPt = computedIntPt; diff --git a/tests/unit/algorithm/AngleTest.cpp b/tests/unit/algorithm/AngleTest.cpp index f44d3ee42..17f83056f 100644 --- a/tests/unit/algorithm/AngleTest.cpp +++ b/tests/unit/algorithm/AngleTest.cpp @@ -232,6 +232,30 @@ void object::test<7>() } } +template<> +template<> +void object::test<8>() +{ + set_test_name("Angle::nextCCW"); + + ensure_equals(Angle::nextCCW(Angle::PI_OVER_2, Angle::PI_OVER_4, PI), PI); + ensure_equals(Angle::nextCCW(Angle::PI_OVER_4, Angle::PI_OVER_2, PI), Angle::PI_OVER_2); + ensure_equals(Angle::nextCCW(Angle::PI_OVER_2, Angle::PI_OVER_4, Angle::PI_TIMES_2), Angle::PI_TIMES_2); + ensure_equals(Angle::nextCCW(Angle::PI_TIMES_2, Angle::PI_OVER_4, 0), 0); +} + +template<> +template<> +void object::test<9>() +{ + set_test_name("Angle::fractionCCW"); + + ensure_equals("pi/2 in interval [2pi, pi]", Angle::fractionCCW(Angle::PI_OVER_2, Angle::PI_TIMES_2, PI), 0.5); + ensure_equals("3pi/2 in interval [pi, 0]", Angle::fractionCCW(3*Angle::PI_OVER_2, PI, 0), 0.5); + ensure_equals("3pi/2 in interval [pi, 2pi]", Angle::fractionCCW(3*Angle::PI_OVER_2, PI, Angle::PI_TIMES_2), 0.5); + ensure_equals("0 in interval [3pi/2, pi/2]", Angle::fractionCCW(0, 3*Angle::PI_OVER_2, Angle::PI_OVER_2), 0.5); + ensure_equals("2pi in interval [3pi/2, pi]", Angle::fractionCCW(Angle::PI_TIMES_2, 3*Angle::PI_OVER_2, PI), 1.0/3); +} } // namespace tut commit 8447ff2a32dd20703e0841736a8a3a6bd6be88f5 Author: Daniel Baston Date: Thu Dec 18 14:05:52 2025 -0500 CircularArcIntersector: Remove disabled alternate implementation diff --git a/src/algorithm/CircularArcIntersector.cpp b/src/algorithm/CircularArcIntersector.cpp index 39b0ecead..ba0ab0ccd 100644 --- a/src/algorithm/CircularArcIntersector.cpp +++ b/src/algorithm/CircularArcIntersector.cpp @@ -369,7 +369,7 @@ CircularArcIntersector::intersects(const CircularArc& arc1, const CircularArc& a if (a == 0 || (d == 0 && r1 == r2)) { computeCocircularIntersection(arc1, arc2); } else { - // Add endpoint intersections that may be missed or inexactly computed. + // Explicitly add endpoint intersections that may be missed or inexactly computed. if (nPt < 2 && arc1.p0().equals2D(arc2.p0()) && (nPt == 0 || !intPt[0].equals2D(arc1.p0()))) { addIntersection(arc1.p0(), arc1, arc2); } @@ -384,10 +384,10 @@ CircularArcIntersector::intersects(const CircularArc& arc1, const CircularArc& a } if (nPt < 2) { + // Compute interior intersection points. const double dx = c2.x-c1.x; const double dy = c2.y-c1.y; -#if 1 // point where a line between the two circle center points intersects // the radical line CoordinateXY p{c1.x + a* dx/d, c1.y+a* dy/d}; @@ -418,35 +418,6 @@ CircularArcIntersector::intersects(const CircularArc& arc1, const CircularArc& a addIntersection(computedIntPt, arc1, arc2); } } -#else - // Alternate formulation. - // Instead of calculating the intersection points and determining if they fall on the arc, - // calculate the angles of the intersection points. If they fall on the arc, create intersection points - // at those angles. - - double centerPointAngle = std::atan2(dy, dx); - - double arc1IntPtAngleDeviation = std::acos(a / r1); - - double a11 = Angle::normalize(centerPointAngle - arc1IntPtAngleDeviation); - double a12 = Angle::normalize(centerPointAngle + arc1IntPtAngleDeviation); - - double b = d - a; - double arc2IntPtAngleDeviation = std::acos(b / r2); - - double a21 = Angle::normalize(centerPointAngle + MATH_PI + arc2IntPtAngleDeviation); - double a22 = Angle::normalize(centerPointAngle + MATH_PI - arc2IntPtAngleDeviation); - - if (arc1.containsAngle(a11) && arc2.containsAngle(a21)) { - intPt[nPt++] = CircularArcs::createPoint(arc1.getCenter(), arc1.getRadius(), a11); - } - if (arc1.containsAngle(a12) && arc2.containsAngle(a22)) { - intPt[nPt++] = CircularArcs::createPoint(arc1.getCenter(), arc1.getRadius(), a12); - if (nPt == 2 && intPt[0].equals(intPt[1])) { - nPt = 1; - } - } -#endif } } commit 3efe43f99214c8e3fa3c0e76f3c991331f897c41 Author: Daniel Baston Date: Thu Dec 18 13:48:31 2025 -0500 CircularArcIntersector: Prevent generation of too-close points diff --git a/include/geos/algorithm/CircularArcIntersector.h b/include/geos/algorithm/CircularArcIntersector.h index c0aa3b753..80e1543ab 100644 --- a/include/geos/algorithm/CircularArcIntersector.h +++ b/include/geos/algorithm/CircularArcIntersector.h @@ -138,6 +138,8 @@ private: static int circleIntersects(const CoordinateXY& center, double r, const CoordinateXY& p0, const CoordinateXY& p1, CoordinateXY& isect0, CoordinateXY& isect1); + void computeCocircularIntersection(const CircularArc& arc1, const CircularArc& arc2); + std::array intPt; std::array intArc; intersection_type result = NO_INTERSECTION; diff --git a/src/algorithm/CircularArcIntersector.cpp b/src/algorithm/CircularArcIntersector.cpp index ae9e85758..39b0ecead 100644 --- a/src/algorithm/CircularArcIntersector.cpp +++ b/src/algorithm/CircularArcIntersector.cpp @@ -345,7 +345,7 @@ CircularArcIntersector::intersects(const CircularArc& arc1, const CircularArc& a const auto r1 = arc1.getRadius(); const auto r2 = arc2.getRadius(); - auto d = c1.distance(c2); + const auto d = c1.distance(c2); if (d > r1 + r2) { // Circles are disjoint @@ -360,157 +360,16 @@ CircularArcIntersector::intersects(const CircularArc& arc1, const CircularArc& a } // a: the distance from c1 to the "radical line", which connects the two intersection points - // The following expression was rewritten by - double a = (d*d + r1*r1 - r2*r2) / (2*d); // Expression rewritten by Herbie, https://herbie.uwplse.org/demo/ - // double a = std::fma(r1-r2, (r1 + r2) / (d+d), d*0.5); + // const double a = (d*d + r1*r1 - r2*r2) / (2*d); + const double a = std::fma(r1-r2, (r1 + r2) / (d+d), d*0.5); // TODO because the circle center calculation is inexact we need some kind of tolerance here. // Take a PrecisionModel like LineIntersector? if (a == 0 || (d == 0 && r1 == r2)) { - // COCIRCULAR - - double ap0 = arc1.theta0(); - double ap1 = arc1.theta2(); - double bp0 = arc2.theta0(); - double bp1 = arc2.theta2(); - - // Orientation of the result matches the first input - bool resultArcIsCCW = true; - - // Make both inputs counter-clockwise for the purpose of determining intersections - if (arc1.getOrientation() != Orientation::COUNTERCLOCKWISE) { - std::swap(ap0, ap1); - resultArcIsCCW = false; - } - if (arc2.getOrientation() != Orientation::COUNTERCLOCKWISE) { - std::swap(bp0, bp1); - } - ap0 = Angle::normalizePositive(ap0); - ap1 = Angle::normalizePositive(ap1); - bp0 = Angle::normalizePositive(bp0); - bp1 = Angle::normalizePositive(bp1); - - bool checkBp1inA = true; - bool checkAcontained = true; - - // Possible intersection arrangements: - // A contained within B - // A overlaps B - // B contained within A - - // check start of B within A? - if (Angle::isWithinCCW(bp0, ap0, ap1)) { - checkAcontained = false; - const double start = bp0; - const double end = nextAngleCCW(start, bp1, ap1); - - if (end == bp1) { - checkBp1inA = false; - } - - if (start == end) { - const CoordinateXY computedIntPt = CircularArcs::createPoint(c1, r1, start); - addIntersection(computedIntPt, arc1, arc2); - } - else { - if (resultArcIsCCW) { - addArcIntersection(start, end, Orientation::COUNTERCLOCKWISE, arc1, arc2); - } - else { - addArcIntersection(end, start, Orientation::CLOCKWISE, arc1, arc2); - } - } - } - - if (checkBp1inA && Angle::isWithinCCW(bp1, ap0, ap1)) { - // end of B within A? - checkAcontained = false; - - const double start = ap0; - const double end = bp1; - if (start == end) { - const CoordinateXY computedIntPt = CircularArcs::createPoint(c1, r1, start); - addIntersection(computedIntPt, arc1, arc2); - } - else { - if (resultArcIsCCW) { - addArcIntersection(start, end, Orientation::CLOCKWISE, arc1, arc2); - } - else { - addArcIntersection(end, start, Orientation::CLOCKWISE, arc1, arc2); - } - } - } - - if (checkAcontained && Angle::isWithinCCW(ap0, bp0 , bp1) && ap0 != bp0 && ap0 != bp1 && Angle::isWithinCCW(ap1, bp0, bp1) && ap1 != bp1 && ap1 != bp0) { - if (resultArcIsCCW) { - addArcIntersection(ap0, ap1, Orientation::COUNTERCLOCKWISE, arc1, arc2); - } - else { - addArcIntersection(ap1, ap0, Orientation::CLOCKWISE, arc1, arc2); - } - } + computeCocircularIntersection(arc1, arc2); } else { - // NOT COCIRCULAR - - const double dx = c2.x-c1.x; - const double dy = c2.y-c1.y; - -#if 1 - // point where a line between the two circle center points intersects - // the radical line - CoordinateXY p{c1.x + a* dx/d, c1.y+a* dy/d}; - - // distance from p to the intersection points - const double h = std::sqrt(r1*r1 - a*a); - - const CoordinateXY isect0{p.x + h* dy/d, p.y - h* dx/d }; - const CoordinateXY isect1{p.x - h* dy/d, p.y + h* dx/d }; - - for (const CoordinateXY& computedIntPt : {isect0, isect1}) { - if (nPt > 0 && computedIntPt.equals2D(intPt[0])) { - continue; - } - - if (arc1.containsPointOnCircle(computedIntPt) && arc2.containsPointOnCircle(computedIntPt)) { - addIntersection(computedIntPt, arc1, arc2); - } - } -#else - // Alternate formulation. - // Instead of calculating the intersection points and determining if they fall on the arc, - // calculate the angles of the intersection points. If they fall on the arc, create intersection points - // at those angles. - - double centerPointAngle = std::atan2(dy, dx); - - double arc1IntPtAngleDeviation = std::acos(a / r1); - - double a11 = Angle::normalize(centerPointAngle - arc1IntPtAngleDeviation); - double a12 = Angle::normalize(centerPointAngle + arc1IntPtAngleDeviation); - - double b = d - a; - double arc2IntPtAngleDeviation = std::acos(b / r2); - - double a21 = Angle::normalize(centerPointAngle + MATH_PI + arc2IntPtAngleDeviation); - double a22 = Angle::normalize(centerPointAngle + MATH_PI - arc2IntPtAngleDeviation); - - if (arc1.containsAngle(a11) && arc2.containsAngle(a21)) { - intPt[nPt++] = CircularArcs::createPoint(arc1.getCenter(), arc1.getRadius(), a11); - } - if (arc1.containsAngle(a12) && arc2.containsAngle(a22)) { - intPt[nPt++] = CircularArcs::createPoint(arc1.getCenter(), arc1.getRadius(), a12); - if (nPt == 2 && intPt[0].equals(intPt[1])) { - nPt = 1; - } - } -#endif - - // Add endpoint intersections missed due to precision issues. - // TODO: Add some logic to prevent double-counting of endpoints. Ideally, the endpoint test would happen before - // computing intersection points, so if there is an endpoint intersection we get the exact intersection point - // instead of a computed one. + // Add endpoint intersections that may be missed or inexactly computed. if (nPt < 2 && arc1.p0().equals2D(arc2.p0()) && (nPt == 0 || !intPt[0].equals2D(arc1.p0()))) { addIntersection(arc1.p0(), arc1, arc2); } @@ -523,6 +382,72 @@ CircularArcIntersector::intersects(const CircularArc& arc1, const CircularArc& a if (nPt < 2 && arc1.p2().equals2D(arc2.p2()) && (nPt == 0 || !intPt[0].equals2D(arc1.p2()))) { addIntersection(arc1.p2(), arc1, arc2); } + + if (nPt < 2) { + const double dx = c2.x-c1.x; + const double dy = c2.y-c1.y; + +#if 1 + // point where a line between the two circle center points intersects + // the radical line + CoordinateXY p{c1.x + a* dx/d, c1.y+a* dy/d}; + + // distance from p to the intersection points + const double h = std::sqrt(r1*r1 - a*a); + + CoordinateXY isect0{p.x + h* dy/d, p.y - h* dx/d }; + CoordinateXY isect1{p.x - h* dy/d, p.y + h* dx/d }; + + // One of the computed intersection points may be an inexact version of an endpoint. + // If we already have an endpoint intersection, process the farther-away computed + // point first. + if (nPt == 1 && intPt[0].distance(isect0) < intPt[0].distance(isect1)) { + std::swap(isect0, isect1); + } + + for (const CoordinateXY& computedIntPt : {isect0, isect1}) { + if (nPt > 0 && computedIntPt.equals2D(intPt[0])) { + continue; + } + + if (nPt > 1) { + continue; + } + + if (arc1.containsPointOnCircle(computedIntPt) && arc2.containsPointOnCircle(computedIntPt)) { + addIntersection(computedIntPt, arc1, arc2); + } + } +#else + // Alternate formulation. + // Instead of calculating the intersection points and determining if they fall on the arc, + // calculate the angles of the intersection points. If they fall on the arc, create intersection points + // at those angles. + + double centerPointAngle = std::atan2(dy, dx); + + double arc1IntPtAngleDeviation = std::acos(a / r1); + + double a11 = Angle::normalize(centerPointAngle - arc1IntPtAngleDeviation); + double a12 = Angle::normalize(centerPointAngle + arc1IntPtAngleDeviation); + + double b = d - a; + double arc2IntPtAngleDeviation = std::acos(b / r2); + + double a21 = Angle::normalize(centerPointAngle + MATH_PI + arc2IntPtAngleDeviation); + double a22 = Angle::normalize(centerPointAngle + MATH_PI - arc2IntPtAngleDeviation); + + if (arc1.containsAngle(a11) && arc2.containsAngle(a21)) { + intPt[nPt++] = CircularArcs::createPoint(arc1.getCenter(), arc1.getRadius(), a11); + } + if (arc1.containsAngle(a12) && arc2.containsAngle(a22)) { + intPt[nPt++] = CircularArcs::createPoint(arc1.getCenter(), arc1.getRadius(), a12); + if (nPt == 2 && intPt[0].equals(intPt[1])) { + nPt = 1; + } + } +#endif + } } if (nArc) { @@ -575,6 +500,95 @@ setFromEndpoint(geom::CoordinateXYZM& pt, const CircularArc& arc, std::size_t in }); } +void +CircularArcIntersector::computeCocircularIntersection(const CircularArc& arc1, const CircularArc& arc2) +{ + const auto& center = arc1.getCenter(); + const double radius = arc1.getRadius(); + + double ap0 = arc1.theta0(); + double ap1 = arc1.theta2(); + double bp0 = arc2.theta0(); + double bp1 = arc2.theta2(); + + // Orientation of the result matches the first input + bool resultArcIsCCW = true; + + // Make both inputs counter-clockwise for the purpose of determining intersections + if (arc1.getOrientation() != Orientation::COUNTERCLOCKWISE) { + std::swap(ap0, ap1); + resultArcIsCCW = false; + } + if (arc2.getOrientation() != Orientation::COUNTERCLOCKWISE) { + std::swap(bp0, bp1); + } + ap0 = Angle::normalizePositive(ap0); + ap1 = Angle::normalizePositive(ap1); + bp0 = Angle::normalizePositive(bp0); + bp1 = Angle::normalizePositive(bp1); + + bool checkBp1inA = true; + bool checkAcontained = true; + + // Possible intersection arrangements: + // A contained within B + // A overlaps B + // B contained within A + + // check start of B within A? + if (Angle::isWithinCCW(bp0, ap0, ap1)) { + checkAcontained = false; + const double start = bp0; + const double end = nextAngleCCW(start, bp1, ap1); + + if (end == bp1) { + checkBp1inA = false; + } + + if (start == end) { + const CoordinateXY computedIntPt = CircularArcs::createPoint(center, radius, start); + addIntersection(computedIntPt, arc1, arc2); + } + else { + if (resultArcIsCCW) { + addArcIntersection(start, end, Orientation::COUNTERCLOCKWISE, arc1, arc2); + } + else { + addArcIntersection(end, start, Orientation::CLOCKWISE, arc1, arc2); + } + } + } + + if (checkBp1inA && Angle::isWithinCCW(bp1, ap0, ap1)) { + // end of B within A? + checkAcontained = false; + + const double start = ap0; + const double end = bp1; + if (start == end) { + const CoordinateXY computedIntPt = CircularArcs::createPoint(center, radius, start); + addIntersection(computedIntPt, arc1, arc2); + } + else { + if (resultArcIsCCW) { + addArcIntersection(start, end, Orientation::CLOCKWISE, arc1, arc2); + } + else { + addArcIntersection(end, start, Orientation::CLOCKWISE, arc1, arc2); + } + } + } + + if (checkAcontained && Angle::isWithinCCW(ap0, bp0 , bp1) && ap0 != bp0 && ap0 != bp1 && Angle::isWithinCCW(ap1, bp0, bp1) && ap1 != bp1 && ap1 != bp0) { + if (resultArcIsCCW) { + addArcIntersection(ap0, ap1, Orientation::COUNTERCLOCKWISE, arc1, arc2); + } + else { + addArcIntersection(ap1, ap0, Orientation::CLOCKWISE, arc1, arc2); + } + } +} + void CircularArcIntersector::addArcIntersection(double startAngle, double endAngle, int orientation, const CircularArc& arc1, const CircularArc& arc2) { diff --git a/tests/unit/capi/GEOSNodeTest.cpp b/tests/unit/capi/GEOSNodeTest.cpp index 23b3c2172..721a8c46a 100644 --- a/tests/unit/capi/GEOSNodeTest.cpp +++ b/tests/unit/capi/GEOSNodeTest.cpp @@ -231,8 +231,12 @@ void object::test<10>() ensure(result_ != nullptr); expected_ = fromWKT("MULTICURVE ZM (" - "CIRCULARSTRING ZM (-1 0 3 4, -1 1.2246467991e-16 5 4.75, -1 2.7755575616e-16 7 5.5, -1 1.2246467991e-16 7 5.5, -1 5.5511151231e-17 7 5.5, -0.7071067812 0.7071067812 5.25 7.375, -2.7755575616e-16 1 3.5 9.25, -3.8285686989e-16 1 3.5 9.25, -5.5511151231e-17 1 3.5 9.25, 0.7071067812 0.7071067812 3.75 8.125, 1 0 4 7)," - "CIRCULARSTRING ZM (-1 2 NaN 9, -0.2928932188 1.7071067812 NaN 9.125, -2.7755575616e-16 1 3.5 9.25, 0 1 3.5 9.25, -5.5511151231e-17 1 3.5 9.25, -0.2928932188 0.2928932188 5.25 7.375, -1 2.7755575616e-16 7 5.5, -1 0 7 5.5, -1 5.5511151231e-17 7 5.5, -1 0 NaN 11.25, -1 0 NaN 17))"); + "CIRCULARSTRING ZM (-1 0 3 4, -0.7071067812 0.7071067812 2.5 6.5, -5.5511151231e-17 1 2 9, 0.7071067812 0.7071067812 3 8, 1 0 4 7)," + "CIRCULARSTRING ZM (-1 2 NaN 9, -0.2928932188 1.7071067812 NaN 9, -5.5511151231e-17 1 2 9, -0.2928932188 0.2928932188 2.5 6.5, -1 0 3 4))"); + + ensure_equals(GEOSGetNumGeometries(result_), 2); + ensure_equals("Noded arc 1 should have 5 points", GEOSGeomGetNumPoints(GEOSGetGeometryN(result_, 0)), 5); + ensure_equals("Noded arc 2 should have 5 points", GEOSGeomGetNumPoints(GEOSGetGeometryN(result_, 1)), 5); ensure_equals_exact_geometry_xyzm(reinterpret_cast(result_), reinterpret_cast(expected_), 1e-8); @@ -251,7 +255,7 @@ void object::test<11>() ensure(result_ != nullptr); expected_ = fromWKT("MULTICURVE ZM (" - "CIRCULARSTRING ZM (-5 0 3 4, -3.5355 3.5355 3 6, 0 5 3 8, 3.5355 3.5355 3.5 7.5, 4 3 4 7)," + "CIRCULARSTRING ZM (-5 0 3 4, -3.5355 3.5355 3 6, 0 5 3 8, 2.2361 4.4721 3.5 7.5, 4 3 4 7)," "LINESTRING ZM (0 0 NaN 7, 0 5 3 8)," "LINESTRING ZM (0 5 3 8, 0 10 NaN 13))"); @@ -303,7 +307,7 @@ void object::test<14>() { set_test_name("CIRCULARSTRING Z / LINESTRING Z endpoint intersection"); - input_ = fromWKT("MULTICURVE (CIRCULARSTRING Z (-5 0 3, -4 3 5, 4 3 6), LINESTRING Z (0 0 7, 4 3 13))"); + input_ = fromWKT("MULTICURVE (CIRCULARSTRING Z (-4 3 3, 0 5 5, 4 3 7), LINESTRING Z (0 0 7, 4 3 13))"); ensure(input_); result_ = GEOSNode(input_); commit 114887e4058d97ead9ccc22269300ad51aa51e56 Author: Daniel Baston Date: Thu Dec 18 13:39:09 2025 -0500 geos_unit: prevent asserts from skipping unhandled geometry types diff --git a/tests/unit/utility.h b/tests/unit/utility.h index 6b98a59d0..0a15532ed 100644 --- a/tests/unit/utility.h +++ b/tests/unit/utility.h @@ -388,7 +388,7 @@ ensure_equals_exact_geometry_xyz(const geos::geom::Geometry *lhs_in, assert(nullptr != rhs_in); using geos::geom::Point; - using geos::geom::LineString; + using geos::geom::SimpleCurve; using geos::geom::Polygon; using geos::geom::CoordinateSequence; using geos::geom::GeometryCollection; @@ -401,19 +401,19 @@ ensure_equals_exact_geometry_xyz(const geos::geom::Geometry *lhs_in, const Point *gpt2 = static_cast(rhs_in); return ensure_equals_dims( gpt1->getCoordinatesRO(), gpt2->getCoordinatesRO(), 3, tolerance); } - else if (const LineString* gln1 = dynamic_cast(lhs_in)) { - const LineString *gln2 = static_cast(rhs_in); + else if (const SimpleCurve* gln1 = dynamic_cast(lhs_in)) { + const SimpleCurve *gln2 = static_cast(rhs_in); return ensure_equals_dims( gln1->getCoordinatesRO(), gln2->getCoordinatesRO(), 3, tolerance); } - else if (dynamic_cast(lhs_in)) { - ensure("Not implemented yet", 0); - } else if (const GeometryCollection* gc1 = dynamic_cast(lhs_in)) { const GeometryCollection *gc2 = static_cast(rhs_in); for (unsigned int i = 0; i < gc1->getNumGeometries(); i++) { ensure_equals_exact_geometry_xyz(gc1->getGeometryN(i), gc2->getGeometryN(i), tolerance); } } + else { + fail("Not implemented yet"); + } } inline void @@ -445,8 +445,9 @@ ensure_equals_exact_geometry_xyzm(const geos::geom::Geometry *lhs_in, assert(nullptr != rhs_in); using geos::geom::Point; - using geos::geom::LineString; - using geos::geom::Polygon; + using geos::geom::Curve; + using geos::geom::SimpleCurve; + using geos::geom::Surface; using geos::geom::CoordinateSequence; using geos::geom::GeometryCollection; @@ -457,14 +458,14 @@ ensure_equals_exact_geometry_xyzm(const geos::geom::Geometry *lhs_in, const Point *gpt2 = static_cast(rhs_in); return ensure_equals_exact_xyzm(gpt1->getCoordinatesRO(), gpt2->getCoordinatesRO(), tolerance); } - else if (const LineString* gln1 = dynamic_cast(lhs_in)) { - const LineString *gln2 = static_cast(rhs_in); + else if (const SimpleCurve* gln1 = dynamic_cast(lhs_in)) { + const SimpleCurve *gln2 = static_cast(rhs_in); return ensure_equals_exact_xyzm(gln1->getCoordinatesRO(), gln2->getCoordinatesRO(), tolerance); } - else if (const Polygon* gply1 = dynamic_cast(lhs_in)) { - const Polygon* gply2 = static_cast(rhs_in); - const LinearRing* extRing1 = gply1->getExteriorRing(); - const LinearRing* extRing2 = gply2->getExteriorRing(); + else if (const Surface* gply1 = dynamic_cast(lhs_in)) { + const Surface* gply2 = static_cast(rhs_in); + const Curve* extRing1 = gply1->getExteriorRing(); + const Curve* extRing2 = gply2->getExteriorRing(); ensure_equals_exact_geometry_xyzm(extRing1, extRing2, tolerance); @@ -483,6 +484,8 @@ ensure_equals_exact_geometry_xyzm(const geos::geom::Geometry *lhs_in, for (unsigned int i = 0; i < gc1->getNumGeometries(); i++) { ensure_equals_exact_geometry_xyzm(gc1->getGeometryN(i), gc2->getGeometryN(i), tolerance); } + } else { + fail("Not implemented yet."); } } @@ -548,14 +551,14 @@ ensure_equals_exact_geometry(const geos::geom::Geometry *lhs_in, const LineString *gln2 = static_cast(rhs_in); return ensure_equals_dims( gln1->getCoordinatesRO(), gln2->getCoordinatesRO(), 2, tolerance); } - else if (dynamic_cast(lhs_in)) { - ensure("Not implemented yet", 0); - } else if (const GeometryCollection* gc1 = dynamic_cast(lhs_in)) { - const GeometryCollection *gc2 = static_cast(rhs_in); - for (unsigned int i = 0; i < gc1->getNumGeometries(); i++) { - ensure_equals_exact_geometry(gc1->getGeometryN(i), gc2->getGeometryN(i), tolerance); - } + const GeometryCollection *gc2 = static_cast(rhs_in); + for (unsigned int i = 0; i < gc1->getNumGeometries(); i++) { + ensure_equals_exact_geometry(gc1->getGeometryN(i), gc2->getGeometryN(i), tolerance); + } + } + else { + fail("Not implemented yet"); } } commit 7d701f5e5077fe860298bd29802922a206b6d70e Author: Daniel Baston Date: Mon Dec 15 13:50:17 2025 -0500 ArcString: Adopt shared_ptr representation diff --git a/include/geos/noding/ArcString.h b/include/geos/noding/ArcString.h index c442a26f6..67285f33c 100644 --- a/include/geos/noding/ArcString.h +++ b/include/geos/noding/ArcString.h @@ -62,11 +62,11 @@ public: return m_arcs.end(); } - std::unique_ptr releaseCoordinates(); + const std::shared_ptr& getCoordinates() const override; protected: std::vector m_arcs; - std::unique_ptr m_seq; + std::shared_ptr m_seq; void* m_context; }; diff --git a/include/geos/noding/PathString.h b/include/geos/noding/PathString.h index bc1a66990..0de86c23a 100644 --- a/include/geos/noding/PathString.h +++ b/include/geos/noding/PathString.h @@ -20,11 +20,14 @@ #include #include +namespace geos::geom { + class CoordinateSequence; +} + namespace geos::noding { -/// A PathString represents a contiguous line/arc to the used as an input for -/// noding. To access the coordinates, it is necessary to know whether they -/// represent a set of line segments (SegmentString) or circular arcs (ArcString). +/// A PathString represents a contiguous line/arc to be used as an input or output +/// of a noding process. class GEOS_DLL PathString { public: virtual ~PathString() = default; @@ -33,6 +36,11 @@ public: virtual double getLength() const = 0; + /// \brief + /// Return a pointer to the CoordinateSequence associated + /// with this PathString. + virtual const std::shared_ptr& getCoordinates() const = 0; + std::vector static toRawPointerVector(const std::vector> & segStrings); }; diff --git a/include/geos/noding/SegmentString.h b/include/geos/noding/SegmentString.h index 6648386df..d13c70fda 100644 --- a/include/geos/noding/SegmentString.h +++ b/include/geos/noding/SegmentString.h @@ -111,7 +111,7 @@ public: /// \brief /// Return a pointer to the CoordinateSequence associated /// with this SegmentString. - const std::shared_ptr& getCoordinates() const { + const std::shared_ptr& getCoordinates() const override { return seq; } diff --git a/src/noding/ArcString.cpp b/src/noding/ArcString.cpp index 6095414c2..e0b0ce7df 100644 --- a/src/noding/ArcString.cpp +++ b/src/noding/ArcString.cpp @@ -15,8 +15,9 @@ #include namespace geos::noding { - std::unique_ptr - ArcString::releaseCoordinates() { - return std::move(m_seq); - } +const std::shared_ptr& +ArcString::getCoordinates() const +{ + return m_seq; +} } \ No newline at end of file diff --git a/src/noding/GeometryNoder.cpp b/src/noding/GeometryNoder.cpp index 0aaeeb89e..d65676f76 100644 --- a/src/noding/GeometryNoder.cpp +++ b/src/noding/GeometryNoder.cpp @@ -136,7 +136,7 @@ GeometryNoder::toGeometry(std::vector>& nodedEdges) resultArcs = true; auto* as = dynamic_cast(path.get()); // FIXME: check for duplicates - lines.push_back(geomFact->createCircularString(as->releaseCoordinates())); + lines.push_back(geomFact->createCircularString(as->getCoordinates())); } } commit 0de2f8fb56f74890702f7abd7532833b4e48f606 Author: Daniel Baston Date: Mon Dec 15 13:03:54 2025 -0500 GEOSNode: Add tests for endpoint intersections diff --git a/src/noding/ArcIntersectionAdder.cpp b/src/noding/ArcIntersectionAdder.cpp index b85cbb673..61b9adae1 100644 --- a/src/noding/ArcIntersectionAdder.cpp +++ b/src/noding/ArcIntersectionAdder.cpp @@ -54,8 +54,7 @@ ArcIntersectionAdder::processIntersections(ArcString& e0, std::size_t segIndex0, void ArcIntersectionAdder::processIntersections(ArcString& e0, std::size_t segIndex0, SegmentString& e1, std::size_t segIndex1) { -// don't bother intersecting a segment with itself - + // don't bother intersecting a segment with itself const geom::CircularArc& arc = e0.getArc(segIndex0); // FIXME get useSegEndpoints from somewhere @@ -79,7 +78,7 @@ ArcIntersectionAdder::processIntersections(SegmentString& e0, std::size_t segInd { using geom::CoordinateXY; -// don't bother intersecting a segment with itself + // don't bother intersecting a segment with itself if(&e0 == &e1 && segIndex0 == segIndex1) { return; } @@ -87,20 +86,11 @@ ArcIntersectionAdder::processIntersections(SegmentString& e0, std::size_t segInd m_intersector.intersects(*e0.getCoordinates(), segIndex0, segIndex0 + 1, *e1.getCoordinates(), segIndex1, segIndex1 + 1); -#if 0 - const CoordinateXY& p0 = e0.getCoordinate(segIndex0); - const CoordinateXY& p1 = e0.getCoordinate(segIndex0 + 1); - const CoordinateXY& q0 = e1.getCoordinate(segIndex1); - const CoordinateXY& q1 = e1.getCoordinate(segIndex1 + 1); - - m_intersector.intersects(p0, p1, q0, q1); -#endif - if (m_intersector.getResult() == algorithm::CircularArcIntersector::NO_INTERSECTION) { return; } -// todo collinear? + // todo collinear? static_cast(e0).addIntersection(m_intersector.getPoint(0), segIndex0); diff --git a/tests/unit/capi/GEOSNodeTest.cpp b/tests/unit/capi/GEOSNodeTest.cpp index f2f3d2607..23b3c2172 100644 --- a/tests/unit/capi/GEOSNodeTest.cpp +++ b/tests/unit/capi/GEOSNodeTest.cpp @@ -242,7 +242,7 @@ template<> template<> void object::test<11>() { - set_test_name("CIRCULARSTRING ZM intersecting LINESTRING M"); + set_test_name("CIRCULARSTRING ZM / LINESTRING M interior intersection"); input_ = fromWKT("MULTICURVE (CIRCULARSTRING ZM (-5 0 3 4, -4 3 2 5, 4 3 4 7), LINESTRING M (0 0 7, 0 10 13))"); ensure(input_); @@ -279,5 +279,77 @@ void object::test<12>() reinterpret_cast(expected_), 1e-4); } +template<> +template<> +void object::test<13>() +{ + set_test_name("LINESTRING Z / LINESTRING Z endpoint intersection"); + + input_ = fromWKT("MULTILINESTRING Z ((-5 0 3, 4 3 6), (0 0 7, 4 3 13))"); + ensure(input_); + + result_ = GEOSNode(input_); + ensure(result_ != nullptr); + + expected_ = GEOSGeom_clone(input_); + + ensure_equals_exact_geometry_xyzm(reinterpret_cast(result_), + reinterpret_cast(expected_), 1e-4); +} + +template<> +template<> +void object::test<14>() +{ + set_test_name("CIRCULARSTRING Z / LINESTRING Z endpoint intersection"); + + input_ = fromWKT("MULTICURVE (CIRCULARSTRING Z (-5 0 3, -4 3 5, 4 3 6), LINESTRING Z (0 0 7, 4 3 13))"); + ensure(input_); + + result_ = GEOSNode(input_); + ensure(result_ != nullptr); + + expected_ = GEOSGeom_clone(input_); + + ensure_equals_exact_geometry_xyzm(reinterpret_cast(result_), + reinterpret_cast(expected_), 1e-4); +} + +template<> +template<> +void object::test<15>() +{ + set_test_name("LINESTRING Z / LINESTRING endpoint intersection"); + + input_ = fromWKT("GEOMETRYCOLLECTION (LINESTRING Z (-5 0 3, 4 3 6), LINESTRING (0 0, 4 3))"); + ensure(input_); + + result_ = GEOSNode(input_); + ensure(result_ != nullptr); + + expected_ = fromWKT("MULTILINESTRING Z ((-5 0 3, 4 3 6), (0 0 NaN, 4 3 NaN))"); + + ensure_equals_exact_geometry_xyzm(reinterpret_cast(result_), + reinterpret_cast(expected_), 1e-4); +} + +template<> +template<> +void object::test<16>() +{ + set_test_name("CIRCULARSTRING Z / LINESTRING endpoint intersection"); + + input_ = fromWKT("MULTICURVE (CIRCULARSTRING Z (-5 0 3, -4 3 5, 4 3 6), LINESTRING (0 0, 4 3))"); + ensure(input_); + + result_ = GEOSNode(input_); + ensure(result_ != nullptr); + + expected_ = fromWKT("MULTICURVE Z (CIRCULARSTRING Z (-5 0 3, -1.5811388301 4.7434164903 4.5, 4 3 6), (0 0 NaN, 4 3 NaN))"); + + ensure_equals_exact_geometry_xyzm(reinterpret_cast(result_), + reinterpret_cast(expected_), 1e-4); +} + } // namespace tut commit c181764a77beae6e05529eff91811c6a7c53f4ce Author: Daniel Baston Date: Mon Dec 15 12:42:27 2025 -0500 ArcIntersectionAdder: Handle cocircular intersection diff --git a/include/geos/noding/NodableArcString.h b/include/geos/noding/NodableArcString.h index 207f497b3..2fcd5be2d 100644 --- a/include/geos/noding/NodableArcString.h +++ b/include/geos/noding/NodableArcString.h @@ -23,13 +23,13 @@ namespace geos::noding { class GEOS_DLL NodableArcString : public ArcString, public NodablePath { public: - //using ArcString::ArcString; - NodableArcString(std::vector arcs, std::unique_ptr coords, bool constructZ, bool constructM, void* context); std::unique_ptr clone() const; - void addIntersection(geom::CoordinateXYZM intPt, size_t segmentIndex) override { + using NodablePath::addIntersection; + + void addIntersection(const geom::CoordinateXYZM& intPt, size_t segmentIndex) override { m_adds[segmentIndex].push_back(intPt); } diff --git a/include/geos/noding/NodablePath.h b/include/geos/noding/NodablePath.h index 35a4234c4..58609d637 100644 --- a/include/geos/noding/NodablePath.h +++ b/include/geos/noding/NodablePath.h @@ -21,14 +21,20 @@ namespace geos::noding { /// A NodablePath represents a PathString to which coordinates can be added. class GEOS_DLL NodablePath { - //virtual void addIntersection( const geom::Coordinate& intPt, int segmentIndex) =0; - //virtual void addIntersection( const geom::CoordinateXYM& intPt, int segmentIndex) =0; public: - virtual void addInt(const geom::CoordinateXY& intPt, size_t pathIndex) { + virtual void addIntersection( const geom::Coordinate& intPt, size_t pathIndex) { addIntersection(geom::CoordinateXYZM{intPt}, pathIndex); } - virtual void addIntersection(geom::CoordinateXYZM intPt, size_t pathIndex) =0; + virtual void addIntersection( const geom::CoordinateXYM& intPt, size_t pathIndex) { + addIntersection(geom::CoordinateXYZM{intPt}, pathIndex); + } + + virtual void addIntersection(const geom::CoordinateXY& intPt, size_t pathIndex) { + addIntersection(geom::CoordinateXYZM{intPt}, pathIndex); + } + + virtual void addIntersection(const geom::CoordinateXYZM& intPt, size_t pathIndex) =0; }; } \ No newline at end of file diff --git a/src/noding/ArcIntersectionAdder.cpp b/src/noding/ArcIntersectionAdder.cpp index b6fb218c6..b85cbb673 100644 --- a/src/noding/ArcIntersectionAdder.cpp +++ b/src/noding/ArcIntersectionAdder.cpp @@ -12,7 +12,6 @@ * **********************************************************************/ -#include #include #include #include @@ -36,11 +35,20 @@ ArcIntersectionAdder::processIntersections(ArcString& e0, std::size_t segIndex0, return; } - // TODO handle cocircular intersections for (std::uint8_t i = 0; i < m_intersector.getNumPoints(); i++) { detail::down_cast(&e0)->addIntersection(m_intersector.getPoint(i), segIndex0); detail::down_cast(&e1)->addIntersection(m_intersector.getPoint(i), segIndex1); } + + for (std::uint8_t i = 0; i < m_intersector.getNumArcs(); i++) { + const auto& arc = m_intersector.getArc(i); + for (size_t j : {0u, 2u}) { + arc.applyAt(j, [&e0, &segIndex0, &e1, &segIndex1](const auto& pt) { + detail::down_cast(&e0)->addIntersection(pt, segIndex0); + detail::down_cast(&e1)->addIntersection(pt, segIndex1); + }); + } + } } void diff --git a/src/noding/NodableArcString.cpp b/src/noding/NodableArcString.cpp index 331669349..96d8e8880 100644 --- a/src/noding/NodableArcString.cpp +++ b/src/noding/NodableArcString.cpp @@ -14,6 +14,8 @@ #include +#define DEBUG_NODABLE_ARC_STRING 0 + namespace geos::noding { static double @@ -34,20 +36,11 @@ NodableArcString::NodableArcString(std::vector arcs, std::uni { } - -//std::unique_ptr clone() const { -// -//} - std::unique_ptr NodableArcString::getNoded() { auto dstSeq = std::make_unique(0, m_constructZ, m_constructM); - //if (m_adds.empty()) { - // return clone(); - //} - std::vector arcs; for (size_t i = 0; i < m_arcs.size(); i++) { if (const auto it = m_adds.find(i); it == m_adds.end()) { @@ -80,7 +73,7 @@ NodableArcString::getNoded() { } }); -#if 0 +#if DEBUG_NODABLE_ARC_STRING std::cout << "Splitting " << toSplit.toString() << " " << (isCCW ? "CCW" : "CW") << " paStart " << paStart << " paStop " << geom::Quadrant::pseudoAngle(center, toSplit.p2()) << std::endl; for (const auto& splitPt : splitPoints) { @@ -97,7 +90,11 @@ NodableArcString::getNoded() { // Add intermediate points of split arc for (const auto& splitPoint : splitPoints) { - if (!arcs.empty() && splitPoint.equals2D(arcs.back().p2())) { + if (arcs.empty()) { + if (splitPoint.equals2D(p0)) { + continue; + } + } else if (splitPoint.equals2D(arcs.back().p2())) { continue; } @@ -116,14 +113,15 @@ NodableArcString::getNoded() { // Add last point of split arc toSplit.getCoordinateSequence()->getAt(toSplit.getCoordinatePosition() + 2, p2); + if (arcs.empty() || !arcs.back().p2().equals2D(p2)) { + geom::CoordinateXYZM midpoint(algorithm::CircularArcs::getMidpoint(p0, p2, center, radius, isCCW)); + midpoint.z = (p0.z + p2.z) / 2; + midpoint.m = (p0.m + p2.m) / 2; - geom::CoordinateXYZM midpoint(algorithm::CircularArcs::getMidpoint(p0, p2, center, radius, isCCW)); - midpoint.z = (p0.z + p2.z) / 2; - midpoint.m = (p0.m + p2.m) / 2; - - dstSeq->add(midpoint); - dstSeq->add(p2); - arcs.emplace_back(*dstSeq, dstPos, center, radius, orientation); + dstSeq->add(midpoint); + dstSeq->add(p2); + arcs.emplace_back(*dstSeq, dstPos, center, radius, orientation); + } } } diff --git a/tests/unit/capi/GEOSNodeTest.cpp b/tests/unit/capi/GEOSNodeTest.cpp index 3fe730d9a..f2f3d2607 100644 --- a/tests/unit/capi/GEOSNodeTest.cpp +++ b/tests/unit/capi/GEOSNodeTest.cpp @@ -259,5 +259,25 @@ void object::test<11>() reinterpret_cast(expected_), 1e-4); } +template<> +template<> +void object::test<12>() +{ + set_test_name("two cocircular CIRCULARSTRINGs"); + + input_ = fromWKT("MULTICURVE (" + "CIRCULARSTRING (-5 0, 0 5, 5 0)," + "CIRCULARSTRING (-4 3, 0 5, 4 3))"); + + result_ = GEOSNode(input_); + + expected_ = fromWKT("MULTICURVE (" + "CIRCULARSTRING (-5.0000000000000000 0.0000000000000000, -4.7434164902525691 1.5811388300841900, -4.0000000000000000 3.0000000000000000, 0.0000000000000003 5.0000000000000000, 4.0000000000000000 3.0000000000000000, 4.7434164902525691 1.5811388300841898, 5.0000000000000000 0.0000000000000000)," + "CIRCULARSTRING (-4.0000000000000000 3.0000000000000000, 0.0000000000000003 5.0000000000000000, 4.0000000000000000 3.0000000000000000))"); + + ensure_equals_exact_geometry_xyzm(reinterpret_cast(result_), + reinterpret_cast(expected_), 1e-4); +} + } // namespace tut diff --git a/tests/unit/noding/NodableArcStringTest.cpp b/tests/unit/noding/NodableArcStringTest.cpp index d468b1d46..7c90e3831 100644 --- a/tests/unit/noding/NodableArcStringTest.cpp +++ b/tests/unit/noding/NodableArcStringTest.cpp @@ -26,7 +26,7 @@ struct test_nodablearcstring_data { NodableArcString nas(arcs, nullptr, false, false, nullptr); for (const auto& coord : coords) { - nas.addInt(coord, 0); + nas.addIntersection(coord, 0); } auto noded = nas.getNoded(); diff --git a/tests/unit/noding/SimpleNoderTest.cpp b/tests/unit/noding/SimpleNoderTest.cpp index 401763bef..06ffb07e2 100644 --- a/tests/unit/noding/SimpleNoderTest.cpp +++ b/tests/unit/noding/SimpleNoderTest.cpp @@ -25,18 +25,10 @@ namespace tut { struct test_simplenoder_data { - // FIXME: This is duplicated from CircularArcIntersectorTest template CircularArc makeArc(T p0, T p2, const CoordinateXY& center, double radius, int orientation) { - auto seq = std::make_unique(3, T::template has(), T::template has()); - seq->setAt(p0, 0); - seq->setAt(geos::algorithm::CircularArcs::getMidpoint(p0, p2, center, radius, orientation == Orientation::COUNTERCLOCKWISE), 1); - seq->setAt(p2, 2); - - CircularArc ret(std::move(seq), 0); - - return ret; + return CircularArc::create(p0, p2, center, radius, orientation); } template commit 4e0f6f89ba90a1940166537cc9449a4bd34e9622 Author: Daniel Baston Date: Mon Dec 15 10:00:53 2025 -0500 CircularArcIntersector: Improve endpoint calculation for cocircular arcs diff --git a/src/algorithm/CircularArcIntersector.cpp b/src/algorithm/CircularArcIntersector.cpp index 8f0c5dacb..ae9e85758 100644 --- a/src/algorithm/CircularArcIntersector.cpp +++ b/src/algorithm/CircularArcIntersector.cpp @@ -507,7 +507,6 @@ CircularArcIntersector::intersects(const CircularArc& arc1, const CircularArc& a } #endif -#if 1 // Add endpoint intersections missed due to precision issues. // TODO: Add some logic to prevent double-counting of endpoints. Ideally, the endpoint test would happen before // computing intersection points, so if there is an endpoint intersection we get the exact intersection point @@ -524,7 +523,6 @@ CircularArcIntersector::intersects(const CircularArc& arc1, const CircularArc& a if (nPt < 2 && arc1.p2().equals2D(arc2.p2()) && (nPt == 0 || !intPt[0].equals2D(arc1.p2()))) { addIntersection(arc1.p2(), arc1, arc2); } -#endif } if (nArc) { @@ -566,6 +564,17 @@ CircularArcIntersector::intersects(const CoordinateSequence &p, std::size_t p0, } } +static void +setFromEndpoint(geom::CoordinateXYZM& pt, const CircularArc& arc, std::size_t index) +{ + arc.applyAt(index, [&pt](const auto& endpoint) { + pt.x = endpoint.x; + pt.y = endpoint.y; + pt.z = Interpolate::zGet(pt, endpoint); + pt.m = Interpolate::mGet(pt, endpoint); + }); +} + void CircularArcIntersector::addArcIntersection(double startAngle, double endAngle, int orientation, const CircularArc& arc1, const CircularArc& arc2) { @@ -580,48 +589,27 @@ CircularArcIntersector::addArcIntersection(double startAngle, double endAngle, i CoordinateXYZM computedMidPt(CircularArcs::createPoint(center, radius, theta1)); CoordinateXYZM computedEndPt(CircularArcs::createPoint(center, radius, endAngle)); - if (computedStartPt.equals2D(arc1.p0())) { - arc1.applyAt(0, [&computedStartPt](const auto& endpoint) { - computedStartPt.z = Interpolate::zGet(computedStartPt, endpoint); - computedStartPt.m = Interpolate::mGet(computedStartPt, endpoint); - }); - } else if (computedStartPt.equals2D(arc1.p2())) { - arc1.applyAt(2, [&computedStartPt](const auto& endpoint) { - computedStartPt.z = Interpolate::zGet(computedStartPt, endpoint); - computedStartPt.m = Interpolate::mGet(computedStartPt, endpoint); - }); - } else if (computedStartPt.equals2D(arc2.p0())) { - arc2.applyAt(0, [&computedStartPt](const auto& endpoint) { - computedStartPt.z = Interpolate::zGet(computedStartPt, endpoint); - computedStartPt.m = Interpolate::mGet(computedStartPt, endpoint); - }); - } else if (computedStartPt.equals2D(arc2.p2())) { - arc2.applyAt(2, [&computedStartPt](const auto& endpoint) { - computedStartPt.z = Interpolate::zGet(computedStartPt, endpoint); - computedStartPt.m = Interpolate::mGet(computedStartPt, endpoint); - }); + // Check to see if the endpoints of the intersection match the endpoints of either of + // the endpoints. Use angles for the check to avoid missing an endpoint intersection from + // inaccuracy the point construction. + if (startAngle == arc1.theta0()) { + setFromEndpoint(computedStartPt, arc1, 0); + } else if (startAngle == arc1.theta2()) { + setFromEndpoint(computedStartPt, arc1, 2); + } else if (startAngle == arc2.theta0()) { + setFromEndpoint(computedStartPt, arc2, 0); + } else if (startAngle == arc2.theta2()) { + setFromEndpoint(computedStartPt, arc2, 2); } - if (computedEndPt.equals2D(arc1.p0())) { - arc1.applyAt(0, [&computedEndPt](const auto& endpoint) { - computedEndPt.z = Interpolate::zGet(computedEndPt, endpoint); - computedEndPt.m = Interpolate::mGet(computedEndPt, endpoint); - }); - } else if (computedEndPt.equals2D(arc1.p2())) { - arc1.applyAt(2, [&computedEndPt](const auto& endpoint) { - computedEndPt.z = Interpolate::zGet(computedEndPt, endpoint); - computedEndPt.m = Interpolate::mGet(computedEndPt, endpoint); - }); - } else if (computedEndPt.equals2D(arc2.p0())) { - arc2.applyAt(0, [&computedEndPt](const auto& endpoint) { - computedEndPt.z = Interpolate::zGet(computedEndPt, endpoint); - computedEndPt.m = Interpolate::mGet(computedEndPt, endpoint); - }); - } else if (computedEndPt.equals2D(arc2.p2())) { - arc2.applyAt(2, [&computedEndPt](const auto& endpoint) { - computedEndPt.z = Interpolate::zGet(computedEndPt, endpoint); - computedEndPt.m = Interpolate::mGet(computedEndPt, endpoint); - }); + if (endAngle == arc1.theta0()) { + setFromEndpoint(computedEndPt, arc1, 0); + } else if (endAngle == arc1.theta2()) { + setFromEndpoint(computedEndPt, arc1, 2); + } else if (endAngle == arc2.theta0()) { + setFromEndpoint(computedEndPt, arc2, 0); + } else if (endAngle == arc2.theta2()) { + setFromEndpoint(computedEndPt, arc2, 2); } interpolateZM(arc1, arc2, computedStartPt); commit bac74b25b9508a54a14fceb6bb9ed172ad10e3b4 Author: Daniel Baston Date: Mon Nov 17 13:32:34 2025 -0500 GEOSNode: support arcs diff --git a/include/geos/algorithm/CircularArcIntersector.h b/include/geos/algorithm/CircularArcIntersector.h index c7a8c323b..c0aa3b753 100644 --- a/include/geos/algorithm/CircularArcIntersector.h +++ b/include/geos/algorithm/CircularArcIntersector.h @@ -3,7 +3,7 @@ * GEOS - Geometry Engine Open Source * http://geos.osgeo.org * - * Copyright (C) 2024 ISciences, LLC + * Copyright (C) 2024-2025 ISciences, LLC * * This is free software; you can redistribute and/or modify it under * the terms of the GNU Lesser General Public Licence as published @@ -18,15 +18,16 @@ #include #include +#include #include #include -#include namespace geos::algorithm { class GEOS_DLL CircularArcIntersector { public: using CoordinateXY = geom::CoordinateXY; + using CoordinateXYZM = geom::CoordinateXYZM; using CircularArc = geom::CircularArc; using Envelope = geom::Envelope; @@ -42,7 +43,7 @@ public: return result; } - const CoordinateXY& getPoint(std::uint8_t i) const + const CoordinateXYZM& getPoint(std::uint8_t i) const { return intPt[i]; } @@ -62,28 +63,33 @@ public: return nArc; } - /// Determines whether and where a circular arc intersects a line segment. - /// - /// Sets the appropriate value of intersection_type and stores the intersection - /// points, if any. - void intersects(const CircularArc& arc, const CoordinateXY& p0, const CoordinateXY& p1); + /** Determines whether and where a circular arc intersects a line segment. + * + * Sets the appropriate value of intersection_type and stores the intersection points, if any. + * + * @param arc The circular arc + * @param seq A CoordinateSequence containing the points of the line segment + * @param pos0 The index of the first point in the line segment + * @param pos1 The index of the second point in the line segment + * @param useSegEndpoints Whether to preferentially take Z/M values from + * the endpoints of the line segment rather than the arc. + */ + void intersects(const CircularArc& arc, const geom::CoordinateSequence& seq, std::size_t pos0, std::size_t pos1, bool useSegEndpoints); - void intersects(const CircularArc& arc, const geom::LineSegment& seg) - { - intersects(arc, seg.p0, seg.p1); - } - - /// Determines whether and where two circular arcs intersect. - /// - /// Sets the appropriate value of intersection_type and stores the intersection - /// points and/or arcs, if any. + /** Determines whether and where two circular arcs intersect. + * + * Sets the appropriate value of intersection_type and stores the intersection + * points and/or arcs, if any. + */ void intersects(const CircularArc& arc1, const CircularArc& arc2); - static int - circleIntersects(const CoordinateXY& center, double r, const CoordinateXY& p0, const CoordinateXY& p1, CoordinateXY& isect0, CoordinateXY& isect1); - - - void intersects(const CoordinateXY& p0, const CoordinateXY& p1, const CoordinateXY& q0, const CoordinateXY& q1); + /** Determines whether and where two line segments intersect + * + * Sets the appropriate value of intersection_type and stores the intersection + * points, if any. + */ + void intersects(const geom::CoordinateSequence& p, std::size_t p0, std::size_t p1, + const geom::CoordinateSequence& q, std::size_t q0, std::size_t q1); private: void reset() { @@ -91,12 +97,52 @@ private: nArc = 0; } - std::array intPt; + /** Add an arc intersection of two cocircular arcs between the specified angles. + * + * The input arcs are provided so that Z/M values can be assigned to the created arc. + * When the endpoints of the new arc correspond with those of the inputs, Z/M values + * will be preferentially taken from arc1. + */ + void addArcIntersection(double startAngle, double endAngle, int orientation, const CircularArc& arc1, const CircularArc& arc2); + + /** Add a point intersection between two arcs. + * + * The input arcs are provided so that Z/M values can be assigned to the intersection point. + * When the intersection point corresponds matches one of the input arc endpoints, + * Z/M values will be taken from that endpoint, with arc1 having priority over arc2. + * If the intersection point does not equal the endpoint of either arc, its Z/M values + * will be interpolated. + */ + void addIntersection(const CoordinateXY& computedIntPt, const CircularArc& arc1, const CircularArc& arc2); + + /** Add a point intersection between an arc and a segment. + * + * The input arc and segment are provided so that Z/M values can be assigned to the intersection point. + * When the intersection point corresponds to the arc or segment endpoints, Z/M values + * will be taken from that endpoint. Priority will be given to the arc endpoints unless + * `useSegEndpoint` is true. If the intersection point does not equal the endpoint of the arc + * or the segment, its Z/M values will be interpolated. + */ + void addIntersection(const CoordinateXY& computedIntPt, const CircularArc& lhs, const geom::CoordinateSequence& seq, std::size_t pos0, std::size_t pos1, bool useSegEndpoints); + + /** Determines whether and where two circles intersect a line segment. + * + * @param center The center point of the circle + * @param r The radius of the circle + * @param p0 The first point of the line segment + * @param p1 The second point of the line segment + * @param isect0 Set to the first intersection point, if it exists + * @param isect1 Set to the second intersection point, if it exists + * @return The number of intersection points + */ + static int + circleIntersects(const CoordinateXY& center, double r, const CoordinateXY& p0, const CoordinateXY& p1, CoordinateXY& isect0, CoordinateXY& isect1); + + std::array intPt; std::array intArc; intersection_type result = NO_INTERSECTION; std::uint8_t nPt = 0; std::uint8_t nArc = 0; - }; } diff --git a/include/geos/algorithm/Interpolate.h b/include/geos/algorithm/Interpolate.h index 1768c89a1..aa6276b7e 100644 --- a/include/geos/algorithm/Interpolate.h +++ b/include/geos/algorithm/Interpolate.h @@ -160,7 +160,7 @@ public: return get(p, q); } - /// Return a coordinates's non-NaN Z value or interpolate it from two other coordinates if it is NaN. + /// Return a coordinate's non-NaN Z value or interpolate it from two other coordinates if it is NaN. template static double zGetOrInterpolate(const C1& p, const C2& p1, const C2& p2) @@ -168,7 +168,7 @@ public: return getOrInterpolate(p, p1, p2); } - /// Return a coordinates's non-NaN M value or interpolate it from two other coordinates if it is NaN. + /// Return a coordinate's non-NaN M value or interpolate it from two other coordinates if it is NaN. template static double mGetOrInterpolate(const C1& p, const C2& p1, const C2& p2) @@ -176,6 +176,21 @@ public: return getOrInterpolate(p, p1, p2); } + static double + getOrAverage(double a, double b) + { + if (std::isnan(a)) { + return b; + } + + if (std::isnan(b)) { + return a; + } + + return 0.5*(a+b); + } + + }; } diff --git a/include/geos/algorithm/LineIntersector.h b/include/geos/algorithm/LineIntersector.h index ebf676b10..76d78d83e 100644 --- a/include/geos/algorithm/LineIntersector.h +++ b/include/geos/algorithm/LineIntersector.h @@ -111,7 +111,7 @@ public: * @return true if either intersection point is in * the interior of the input segment */ - bool isInteriorIntersection(std::size_t inputLineIndex) + bool isInteriorIntersection(std::size_t inputLineIndex) const { for(std::size_t i = 0; i < result; ++i) { if(!(intPt[i].equals2D(*inputLines[inputLineIndex][0]) @@ -161,6 +161,10 @@ public: void computeIntersection(const geom::CoordinateSequence& p, std::size_t p0, const geom::CoordinateSequence& q, std::size_t q0); + /// Compute the intersection between two segments, given a sequence and indices of each endpoint + void computeIntersection(const geom::CoordinateSequence& p, std::size_t p0, std::size_t p1, + const geom::CoordinateSequence& q, std::size_t q0, std::size_t q1); + std::string toString() const; /** diff --git a/include/geos/algorithm/RayCrossingCounter.h b/include/geos/algorithm/RayCrossingCounter.h index 2120e2358..2bdbf8c39 100644 --- a/include/geos/algorithm/RayCrossingCounter.h +++ b/include/geos/algorithm/RayCrossingCounter.h @@ -113,9 +113,7 @@ public: void countSegment(const geom::CoordinateXY& p1, const geom::CoordinateXY& p2); - void countArc(const geom::CoordinateXY& p1, - const geom::CoordinateXY& p2, - const geom::CoordinateXY& p3); + void countArc(const geom::CircularArc& arc); /** \brief * Counts all segments or arcs in the sequence diff --git a/include/geos/geom/CircularArc.h b/include/geos/geom/CircularArc.h index be2b2fe43..184b07f46 100644 --- a/include/geos/geom/CircularArc.h +++ b/include/geos/geom/CircularArc.h @@ -3,7 +3,7 @@ * GEOS - Geometry Engine Open Source * http://geos.osgeo.org * - * Copyright (C) 2024 ISciences, LLC + * Copyright (C) 2024-2025 ISciences, LLC * * This is free software; you can redistribute and/or modify it under * the terms of the GNU Lesser General Public Licence as published @@ -20,7 +20,6 @@ #include #include #include -#include namespace geos { namespace geom { @@ -30,46 +29,80 @@ namespace geom { class GEOS_DLL CircularArc { public: - using CoordinateXY = geom::CoordinateXY; + /// Create an empty CircularArc backed by a null CoordinateSequence. A CircularArc created in this way will + /// crash if methods are called on it, but a default constructor is necessary to construct std::array + CircularArc(); - CircularArc() : CircularArc({0, 0}, {0, 0}, {0, 0}) {} + /// Create a CircularArc that refers to points in the provided CoordinateSequence. The CoordinateSequence is not + /// owned by the CircularArc and should remain reachable for the lifetime of the CircularArc. + /// The center, radius, and orientation can be specified to avoid lossy re-computation. They are not checked for + /// consistency with the references coordinates. + CircularArc(const CoordinateSequence&, std::size_t pos); + CircularArc(const CoordinateSequence&, std::size_t pos, const CoordinateXY& center, double radius, int orientation); - CircularArc(const CoordinateXY& q0, const CoordinateXY& q1, const CoordinateXY& q2) - : p0(q0) - , p1(q1) - , p2(q2) - , m_center_known(false) - , m_radius_known(false) - , m_orientation_known(false) - {} + /// Create a CircularArc that refers to points in the provided CoordinateSequence. + /// Ownership of the CoordinateSequence is transferred to the CircularArc. + /// The center, radius, and orientation can be specified to avoid lossy re-computation. They are not checked for + /// consistency with the references coordinates. + CircularArc(std::unique_ptr, std::size_t pos); + CircularArc(std::unique_ptr, std::size_t pos, const CoordinateXY& center, double radius, int orientation); - CircularArc(double theta0, double theta2, const CoordinateXY& center, double radius, int orientation) - : p0(algorithm::CircularArcs::createPoint(center, radius, theta0)), - p1(algorithm::CircularArcs::createPoint(center, radius, algorithm::CircularArcs::getMidpointAngle(theta0, theta2, orientation==algorithm::Orientation::COUNTERCLOCKWISE))), - p2(algorithm::CircularArcs::createPoint(center, radius, theta2)), - m_center(center), - m_radius(radius), - m_orientation(orientation), - m_center_known(true), - m_radius_known(true), - m_orientation_known(true) - {} + CircularArc(const CircularArc& other); - CircularArc(const CoordinateXY& q0, const CoordinateXY& q2, const CoordinateXY& center, double radius, int orientation) - : p0(q0), - p1(algorithm::CircularArcs::getMidpoint(q0, q2, center, radius, orientation==algorithm::Orientation::COUNTERCLOCKWISE)), - p2(q2), - m_center(center), - m_radius(radius), - m_orientation(orientation), - m_center_known(true), - m_radius_known(true), - m_orientation_known(true) - {} + CircularArc(CircularArc&&) noexcept; - CoordinateXY p0; - CoordinateXY p1; - CoordinateXY p2; + CircularArc& operator=(const CircularArc& other); + CircularArc& operator=(CircularArc&&) noexcept; + + ~CircularArc(); + + /// Create a CircularArc from the given coordinates, automatically allocating a CoordinateSequence to store them. + /// Typically, it is more efficient to create a larger CoordinateSequence that multiple CircularArcs refer to. + /// However, the "create" methods are convenient for writing tests. + template + static CircularArc create(const CoordType& p0, const CoordType& p1, const CoordType& p2) + { + auto seq = std::make_unique(3, CoordType::template has(), CoordType::template has()); + seq->setAt(p0, 0); + seq->setAt(p1, 1); + seq->setAt(p2, 2); + + CircularArc ret(std::move(seq), 0); + + return ret; + } + + static CircularArc create(const CoordinateXY& p0, const CoordinateXY& p2, const CoordinateXY& center, double radius, int orientation); + static CircularArc create(const Coordinate& p0, const Coordinate& p2, const CoordinateXY& center, double radius, int orientation); + static CircularArc create(const CoordinateXYM& p0, const CoordinateXYM& p2, const CoordinateXY& center, double radius, int orientation); + static CircularArc create(const CoordinateXYZM& p0, const CoordinateXYZM& p2, const CoordinateXY& center, double radius, int orientation); + + /// Return the inner angle of the sector associated with this arc + double getAngle() const; + + /// Return the area enclosed by the arc p0-p1-p2 and the line segment p2-p0 + double getArea() const; + + /// Return the center point of the circle associated with this arc + const CoordinateXY& getCenter() const { + if (!m_center_known) { + m_center = algorithm::CircularArcs::getCenter(p0(), p1(), p2()); + m_center_known = true; + } + + return m_center; + } + + const CoordinateSequence* getCoordinateSequence() const { + return m_seq; + } + + std::size_t getCoordinatePosition() const { + return m_pos; + } + + /// Return the length of the arc + double getLength() const; /// Return the orientation of the arc as one of: /// - algorithm::Orientation::CLOCKWISE, @@ -77,39 +110,35 @@ public: /// - algorithm::Orientation::COLLINEAR int getOrientation() const { if (!m_orientation_known) { - m_orientation = algorithm::Orientation::index(p0, p1, p2); + m_orientation = algorithm::Orientation::index(p0(), p1(), p2()); m_orientation_known = true; } return m_orientation; } - bool isCCW() const { - return getOrientation() == algorithm::Orientation::COUNTERCLOCKWISE; - } - - /// Return the center point of the circle associated with this arc - const CoordinateXY& getCenter() const { - if (!m_center_known) { - m_center = algorithm::CircularArcs::getCenter(p0, p1, p2); - m_center_known = true; - } - - return m_center; - } - /// Return the radius of the circle associated with this arc double getRadius() const { if (!m_radius_known) { - m_radius = getCenter().distance(p0); + m_radius = getCenter().distance(p0()); m_radius_known = true; } return m_radius; } + /// Return the distance from the centerpoint of the arc to the line segment formed by the end points of the arc. + double getSagitta() const { + CoordinateXY midpoint = algorithm::CircularArcs::getMidpoint(p0(), p2(), getCenter(), getRadius(), isCCW()); + return algorithm::Distance::pointToSegment(midpoint, p0(), p2()); + } + + bool isCCW() const { + return getOrientation() == algorithm::Orientation::COUNTERCLOCKWISE; + } + /// Return whether this arc forms a complete circle bool isCircle() const { - return p0.equals(p2); + return p0().equals(p2()); } /// Returns whether this arc forms a straight line (p0, p1, and p2 are collinear) @@ -117,68 +146,26 @@ public: return !std::isfinite(getRadius()); } - /// Return the inner angle of the sector associated with this arc - double getAngle() const { - if (isCircle()) { - return 2*MATH_PI; - } - - /// Even Rouault: - /// potential optimization?: using crossproduct(p0 - center, p2 - center) = radius * radius * sin(angle) - /// could yield the result by just doing a single asin(), instead of 2 atan2() - /// actually one should also likely compute dotproduct(p0 - center, p2 - center) = radius * radius * cos(angle), - /// and thus angle = atan2(crossproduct(p0 - center, p2 - center) , dotproduct(p0 - center, p2 - center) ) - auto t0 = theta0(); - auto t2 = theta2(); - - if (getOrientation() == algorithm::Orientation::COUNTERCLOCKWISE) { - std::swap(t0, t2); - } - - if (t0 < t2) { - t0 += 2*MATH_PI; - } - - auto diff = t0-t2; - - return diff; - } - - /// Return the length of the arc - double getLength() const { - if (isLinear()) { - return p0.distance(p2); - } - - return getAngle()*getRadius(); - } - - /// Return the area enclosed by the arc p0-p1-p2 and the line segment p2-p0 - double getArea() const { - if (isLinear()) { - return 0; - } - - auto R = getRadius(); - auto theta = getAngle(); - return R*R/2*(theta - std::sin(theta)); - } - - /// Return the distance from the centerpoint of the arc to the line segment formed by the end points of the arc. - double getSagitta() const { - CoordinateXY midpoint = algorithm::CircularArcs::getMidpoint(p0, p2, getCenter(), getRadius(), isCCW()); - return algorithm::Distance::pointToSegment(midpoint, p0, p2); - } - /// Return the angle of p0 double theta0() const { - return algorithm::CircularArcs::getAngle(p0, getCenter()); + return algorithm::CircularArcs::getAngle(p0(), getCenter()); + } + + /// Return the angle of p1 + double theta1() const { + return algorithm::CircularArcs::getAngle(p1(), getCenter()); } /// Return the angle of p2 double theta2() const { - return algorithm::CircularArcs::getAngle(p2, getCenter()); + return algorithm::CircularArcs::getAngle(p2(), getCenter()); } + /// Check to see if a given angle lies on this arc + bool containsAngle(double theta) const; + + /// Check to see if a coordinate lies on the arc, after testing whether + /// it lies on the circle. + bool containsPoint(const CoordinateXY& q) const; /// Check to see if a coordinate lies on the arc /// Only the angle is checked, so it is assumed that the point lies on @@ -188,91 +175,18 @@ public: return containsAngle(theta); } - /// Check to see if a coordinate lies on the arc, after testing whether - /// it lies on the circle. - bool containsPoint(const CoordinateXY& q) const { - if (q == p0 || q == p1 || q == p2) { - return true; - } - - //auto dist = std::abs(q.distance(getCenter()) - getRadius()); - - //if (dist > 1e-8) { - // return false; - //} - - if (triangulate::quadedge::TrianglePredicate::isInCircleRobust(p0, p1, p2, q) != geom::Location::BOUNDARY) { - return false; - } - - return containsPointOnCircle(q); - } - - /// Check to see if a given angle lies on this arc - bool containsAngle(double theta) const { - auto t0 = theta0(); - auto t2 = theta2(); - - if (theta == t0 || theta == t2) { - return true; - } - - if (getOrientation() == algorithm::Orientation::COUNTERCLOCKWISE) { - std::swap(t0, t2); - } - - t2 -= t0; - theta -= t0; - - if (t2 < 0) { - t2 += 2*MATH_PI; - } - if (theta < 0) { - theta += 2*MATH_PI; - } - - return theta >= t2; - } - /// Return true if the arc is pointing positive in the y direction /// at the location of a specified point. The point is assumed to /// be on the arc. - bool isUpwardAtPoint(const CoordinateXY& q) const { - auto quad = geom::Quadrant::quadrant(getCenter(), q); - bool isUpward; + bool isUpwardAtPoint(const CoordinateXY& q) const; - if (getOrientation() == algorithm::Orientation::CLOCKWISE) { - isUpward = (quad == geom::Quadrant::SW || quad == geom::Quadrant::NW); - } else { - isUpward = (quad == geom::Quadrant::SE || quad == geom::Quadrant::NE); - } - - return isUpward; - } - - void reverse() { - std::swap(p0, p2); - if (m_orientation_known) { - if (m_orientation == algorithm::Orientation::COUNTERCLOCKWISE) { - m_orientation = algorithm::Orientation::CLOCKWISE; - } else if (m_orientation == algorithm::Orientation::CLOCKWISE) { - m_orientation = algorithm::Orientation::COUNTERCLOCKWISE; - } - } - } + CircularArc reverse() const; // Split an arc at a specified point. - // The point is assumed to be o the arc. - std::pair splitAtPoint(const CoordinateXY& q) const { - return { - CircularArc(p0, q, getCenter(), getRadius(), getOrientation()), - CircularArc(q, p2, getCenter(), getRadius(), getOrientation()) - }; - } + // The point is assumed to be on the arc. + //std::pair splitAtPoint(const CoordinateXY& q) const; - std::string toString() const { - return "CIRCULARSTRING (" + p0.toString() + ", " + p1.toString() + ", " + p2.toString() + ")"; - } + bool equals(const CircularArc& other, double tol) const; class Iterator { public: @@ -285,7 +199,7 @@ public: Iterator(const CircularArc& arc, int i) : m_arc(arc), m_i(i) {} reference operator*() const { - return m_i == 0 ? m_arc.p0 : (m_i == 1 ? m_arc.p1 : m_arc.p2); + return m_i == 0 ? m_arc.p0() : (m_i == 1 ? m_arc.p1() : m_arc.p2()); } Iterator& operator++() { @@ -321,13 +235,39 @@ public: return Iterator(*this, 3); } + template + const T& p0() const { + return m_seq->getAt(m_pos); + } + + template + const T& p1() const { + return m_seq->getAt(m_pos + 1); + } + + template + const T& p2() const { + return m_seq->getAt(m_pos + 2); + } + + std::string toString() const; + + template + auto applyAt(std::size_t i, F&& f) const { + return m_seq->applyAt(m_pos + i, f); + } + private: + const CoordinateSequence* m_seq; + std::size_t m_pos; + mutable CoordinateXY m_center; mutable double m_radius; mutable int m_orientation; mutable bool m_center_known = false; mutable bool m_radius_known = false; mutable bool m_orientation_known = false; + bool m_own_coordinates; }; } diff --git a/include/geos/geom/Coordinate.h b/include/geos/geom/Coordinate.h index 0d8bced0f..ad0d84530 100644 --- a/include/geos/geom/Coordinate.h +++ b/include/geos/geom/Coordinate.h @@ -78,6 +78,9 @@ public: template double get() const; + template + static constexpr bool has(); + /// x-coordinate double x; @@ -255,6 +258,9 @@ public: , z(DEFAULT_Z) {}; + template + static constexpr bool has(); + template double get() const; @@ -308,6 +314,9 @@ public: double m; + template + static constexpr bool has(); + template double get() const; @@ -366,6 +375,9 @@ public: double m; + template + static constexpr bool has(); + template double get() const; @@ -463,6 +475,30 @@ inline bool operator<(const CoordinateXY& a, const CoordinateXY& b) // Generic accessors, XY +template<> +constexpr bool CoordinateXY::has() +{ + return true; +} + +template<> +constexpr bool CoordinateXY::has() +{ + return true; +} + +template<> +constexpr bool CoordinateXY::has() +{ + return false; +} + +template<> +constexpr bool CoordinateXY::has() +{ + return false; +} + template<> inline double CoordinateXY::get() const { @@ -489,6 +525,30 @@ inline double CoordinateXY::get() const // Generic accessors, XYZ +template<> +constexpr bool Coordinate::has() +{ + return true; +} + +template<> +constexpr bool Coordinate::has() +{ + return true; +} + +template<> +constexpr bool Coordinate::has() +{ + return true; +} + +template<> +constexpr bool Coordinate::has() +{ + return false; +} + template<> inline double Coordinate::get() const { @@ -515,6 +575,30 @@ inline double Coordinate::get() const // Generic accessors, XYM +template<> +constexpr bool CoordinateXYM::has() +{ + return true; +} + +template<> +constexpr bool CoordinateXYM::has() +{ + return true; +} + +template<> +constexpr bool CoordinateXYM::has() +{ + return false; +} + +template<> +constexpr bool CoordinateXYM::has() +{ + return true; +} + template<> inline double CoordinateXYM::get() const { @@ -541,6 +625,30 @@ inline double CoordinateXYM::get() const // Generic accessors, XYZM +template<> +constexpr bool CoordinateXYZM::has() +{ + return true; +} + +template<> +constexpr bool CoordinateXYZM::has() +{ + return true; +} + +template<> +constexpr bool CoordinateXYZM::has() +{ + return true; +} + +template<> +constexpr bool CoordinateXYZM::has() +{ + return true; +} + template<> inline double CoordinateXYZM::get() const { diff --git a/include/geos/geom/CoordinateSequence.h b/include/geos/geom/CoordinateSequence.h index e4a1c5553..249e9ddd7 100644 --- a/include/geos/geom/CoordinateSequence.h +++ b/include/geos/geom/CoordinateSequence.h @@ -681,6 +681,8 @@ public: void sort(); + /// Swap the coordinates at two indices. + void swap(std::size_t i, std::size_t j); /** * Expands the given Envelope to include the coordinates in the diff --git a/include/geos/noding/ArcNoder.h b/include/geos/noding/ArcNoder.h index 4337db295..2fd418cfa 100644 --- a/include/geos/noding/ArcNoder.h +++ b/include/geos/noding/ArcNoder.h @@ -27,11 +27,17 @@ namespace geos::noding { class GEOS_DLL ArcNoder : public Noder { public: + ArcNoder() = default; + explicit ArcNoder(std::unique_ptr intersector) : m_intersector(std::move(intersector)) {} ~ArcNoder() override; + void setArcIntersector(std::unique_ptr arcIntersector) { + m_intersector = std::move(arcIntersector); + } + void computeNodes(const std::vector& segStrings) override; std::vector> getNodedSubstrings() override; diff --git a/include/geos/noding/ArcString.h b/include/geos/noding/ArcString.h index 10f254ed2..c442a26f6 100644 --- a/include/geos/noding/ArcString.h +++ b/include/geos/noding/ArcString.h @@ -32,6 +32,12 @@ public: explicit ArcString(std::vector arcs) : m_arcs(std::move(arcs)) { } + ArcString(std::vector arcs, std::unique_ptr seq, void* context) + : m_arcs(std::move(arcs)), + m_seq(std::move(seq)), + m_context(context) + {} + std::size_t getSize() const override { return m_arcs.size(); } @@ -56,8 +62,12 @@ public: return m_arcs.end(); } + std::unique_ptr releaseCoordinates(); + protected: std::vector m_arcs; + std::unique_ptr m_seq; + void* m_context; }; } \ No newline at end of file diff --git a/include/geos/noding/GeometryNoder.h b/include/geos/noding/GeometryNoder.h index ea3b3c347..e31fd08cf 100644 --- a/include/geos/noding/GeometryNoder.h +++ b/include/geos/noding/GeometryNoder.h @@ -29,7 +29,7 @@ namespace geom { class Geometry; } namespace noding { -class Noder; +class ArcNoder; } } @@ -43,6 +43,8 @@ public: GeometryNoder(const geom::Geometry& g); + ~GeometryNoder(); + std::unique_ptr getNoded(); // Declare type as noncopyable @@ -52,17 +54,18 @@ public: private: const geom::Geometry& argGeom; + const bool argGeomHasCurves; SegmentString::NonConstVect lineList; - static void extractSegmentStrings(const geom::Geometry& g, - SegmentString::NonConstVect& to); + static void extractPathStrings(const geom::Geometry& g, + std::vector>& to); - Noder& getNoder(); + ArcNoder& getNoder(); - std::unique_ptr noder; + std::unique_ptr noder; - std::unique_ptr toGeometry(std::vector>& noded); + std::unique_ptr toGeometry(std::vector>& noded) const; }; diff --git a/include/geos/noding/IteratedNoder.h b/include/geos/noding/IteratedNoder.h index 1d2bc296a..2af2916bc 100644 --- a/include/geos/noding/IteratedNoder.h +++ b/include/geos/noding/IteratedNoder.h @@ -20,12 +20,13 @@ #include +#include +#include #include -#include #include #include // due to inlines -#include // for inheritance +#include // for inheritance // Forward declarations namespace geos { @@ -49,36 +50,36 @@ namespace noding { // geos::noding * Clients can choose to rerun the noding using a lower precision model. * */ -class GEOS_DLL IteratedNoder : public Noder { // implements Noder +class GEOS_DLL IteratedNoder : public ArcNoder { // implements Noder private: - static const int MAX_ITER = 5; + static constexpr int MAX_ITER = 5; const geom::PrecisionModel* pm; algorithm::LineIntersector li; - std::vector> nodedSegStrings; + std::vector> nodedPaths; int maxIter; + std::function()> m_noderFunction; /** * Node the input segment strings once * and create the split edges between the nodes */ - void node(const std::vector& segStrings, + void node(const std::vector& segStrings, int& numInteriorIntersections, geom::CoordinateXY& intersectionPoint); + static std::unique_ptr createDefaultNoder(); + public: - IteratedNoder(const geom::PrecisionModel* newPm) - : - pm(newPm), - li(pm), - maxIter(MAX_ITER) - { - } + /** \brief + * Construct an IteratedNoder using a specific precisionModel and underlying Noder. + */ + IteratedNoder(const geom::PrecisionModel* newPm, std::function()> noderFunction = createDefaultNoder); - ~IteratedNoder() override {} + ~IteratedNoder() override; /** \brief * Sets the maximum number of noding iterations performed before @@ -96,23 +97,21 @@ public: maxIter = n; } - std::vector> - getNodedSubstrings() override + std::vector> getNodedPaths() override { - return std::move(nodedSegStrings); + return std::move(nodedPaths); } - /** \brief - * Fully nodes a list of {@link SegmentString}s, i.e. performs noding iteratively + * Fully nodes a list of {@link PathString}s, i.e. performs noding iteratively * until no intersections are found between segments. * * Maintains labelling of edges correctly through the noding. * - * @param inputSegmentStrings a collection of SegmentStrings to be noded + * @param inputPathStrings a collection of SegmentStrings to be noded * @throws TopologyException if the iterated noding fails to converge. */ - void computeNodes(const std::vector& inputSegmentStrings) override; // throw(GEOSException); + void computePathNodes(const std::vector& inputPathStrings) override; // Declare type as noncopyable IteratedNoder(IteratedNoder const&) = delete; diff --git a/include/geos/noding/NodableArcString.h b/include/geos/noding/NodableArcString.h index e1b612261..207f497b3 100644 --- a/include/geos/noding/NodableArcString.h +++ b/include/geos/noding/NodableArcString.h @@ -23,82 +23,22 @@ namespace geos::noding { class GEOS_DLL NodableArcString : public ArcString, public NodablePath { public: - using ArcString::ArcString; + //using ArcString::ArcString; + + NodableArcString(std::vector arcs, std::unique_ptr coords, bool constructZ, bool constructM, void* context); + + std::unique_ptr clone() const; void addIntersection(geom::CoordinateXYZM intPt, size_t segmentIndex) override { m_adds[segmentIndex].push_back(intPt); } - static double pseudoAngleDiffCCW(double paStart, double pa) { - double diff = pa - paStart; - - if (diff < 0) { - diff += 4; - } - - return diff; - } - - std::unique_ptr getNoded() { - if (m_adds.empty()) { - // use std::move ? - return std::make_unique(*this); - } - - std::vector arcs; - for (size_t i = 0; i < m_arcs.size(); i++) { - auto it = m_adds.find(i); - if (it == m_adds.end()) { - arcs.push_back(m_arcs[i]); - } else { - std::vector& splitPoints = it->second; - - // TODO check split point actually inside arc - // TODO ignore duplicate splitpoints - - geom::CircularArc remainder = m_arcs[i]; - const bool isCCW = remainder.getOrientation() == algorithm::Orientation::COUNTERCLOCKWISE; - const double paStart = geom::Quadrant::pseudoAngle(remainder.getCenter(), remainder.p0); - //double paStart = isCCW ? geom::Quadrant::pseudoAngle(remainder.getCenter(), remainder.p0) : - // geom::Quadrant::pseudoAngle(remainder.getCenter(), remainder.p2); - - // Need a function for lengthFraction (pt); - std::sort(splitPoints.begin(), splitPoints.end(), [&remainder, paStart, isCCW](const auto& p0, const auto& p1) { - double pa0 = geom::Quadrant::pseudoAngle(remainder.getCenter(), p0); - double pa1 = geom::Quadrant::pseudoAngle(remainder.getCenter(), p1); - - // FIXME check this comparator... - if (isCCW) { - return pseudoAngleDiffCCW(paStart, pa0) < pseudoAngleDiffCCW(paStart, pa1); - } else { - return pseudoAngleDiffCCW(paStart, pa0) > pseudoAngleDiffCCW(paStart, pa1); - } - }); - - std::cout << "arc " << remainder.toString() << " " << (isCCW ? "CCW" : "CW") << std::endl; - std::cout << " paStart " << paStart << std::endl; - for (const auto& pt : splitPoints) { - const double pa = geom::Quadrant::pseudoAngle(remainder.getCenter(), pt); - std::cout << " " << pt << " pa " << pa << " diff " << pseudoAngleDiffCCW(paStart, pa) << std::endl; - } - - for (const auto& splitPoint : splitPoints) { - if (!arcs.empty() && splitPoint.equals2D(arcs.back().p2)) { - continue; - } - auto [a, b] = remainder.splitAtPoint(splitPoint); - arcs.push_back(a); - remainder = b; - } - arcs.push_back(remainder); - } - } - - return std::make_unique(std::move(arcs)); - } + std::unique_ptr getNoded(); private: std::map> m_adds; + bool m_constructZ = false; + bool m_constructM = false; }; } \ No newline at end of file diff --git a/include/geos/noding/PathString.h b/include/geos/noding/PathString.h index e1a3ba4b3..bc1a66990 100644 --- a/include/geos/noding/PathString.h +++ b/include/geos/noding/PathString.h @@ -17,6 +17,8 @@ #include #include +#include +#include namespace geos::noding { @@ -30,6 +32,9 @@ public: virtual std::size_t getSize() const = 0; virtual double getLength() const = 0; + + std::vector + static toRawPointerVector(const std::vector> & segStrings); }; } diff --git a/include/geos/noding/SegmentString.h b/include/geos/noding/SegmentString.h index d1aa26834..6648386df 100644 --- a/include/geos/noding/SegmentString.h +++ b/include/geos/noding/SegmentString.h @@ -181,6 +181,8 @@ public: static std::vector toRawPointerVector(const std::vector> & segStrings); + static std::vector toRawPointerVector(const std::vector> & segStrings); + virtual std::ostream& print(std::ostream& os) const; protected: diff --git a/include/geos/noding/SimpleNoder.h b/include/geos/noding/SimpleNoder.h index 3be9796ec..3eb348e05 100644 --- a/include/geos/noding/SimpleNoder.h +++ b/include/geos/noding/SimpleNoder.h @@ -50,7 +50,7 @@ private: public: - SimpleNoder(std::unique_ptr(nSegInt)) : ArcNoder(std::move(nSegInt)) {} + using ArcNoder::ArcNoder; void computePathNodes(const std::vector& inputSegmentStrings) override; diff --git a/src/algorithm/Area.cpp b/src/algorithm/Area.cpp index 2be5b5316..94889933d 100644 --- a/src/algorithm/Area.cpp +++ b/src/algorithm/Area.cpp @@ -136,7 +136,7 @@ Area::ofClosedCurve(const geom::Curve& ring) { double triangleArea = 0.5*(p0.x*p2.y - p2.x*p0.y); sum += triangleArea; - geom::CircularArc arc(p0, p1, p2); + geom::CircularArc arc(coords, j-2); if (arc.isLinear()) { continue; } diff --git a/src/algorithm/CircularArcIntersector.cpp b/src/algorithm/CircularArcIntersector.cpp index 2ebbd247d..8f0c5dacb 100644 --- a/src/algorithm/CircularArcIntersector.cpp +++ b/src/algorithm/CircularArcIntersector.cpp @@ -3,7 +3,7 @@ * GEOS - Geometry Engine Open Source * http://geos.osgeo.org * - * Copyright (C) 2024 ISciences, LLC + * Copyright (C) 2024-2025 ISciences, LLC * * This is free software; you can redistribute and/or modify it under * the terms of the GNU Lesser General Public Licence as published @@ -15,6 +15,13 @@ #include #include #include +#include + +#include + +using geos::geom::CoordinateSequence; +using geos::geom::CoordinateXY; +using geos::geom::CircularArc; namespace geos::algorithm { @@ -29,8 +36,187 @@ nextAngleCCW(double from, double a, double b) } } +static double +angleFractionCCW(double x, double a, double b) +{ + if (x < a) { + x += 2*MATH_PI; + } + if (b < a) { + b += 2*MATH_PI; + } + return (x - a) / (b - a); +} + +static double +interpolateValue(double a1, double a2, double frac) +{ + frac = std::clamp(frac, 0.0, 1.0); + if (std::isnan(a1)) { + return a2; + } + if (std::isnan(a2)) { + return a1; + } + return a1 + frac * (a2 - a1); +} + +static void interpolateZM(const CircularArc& arc, + const CoordinateXY& pt, + double& z, double& m) +{ + using geos::geom::Ordinate; + + const geom::CoordinateSequence& seq = *arc.getCoordinateSequence(); + std::size_t i0 = arc.getCoordinatePosition(); + + if (arc.p1().equals2D(pt)) { + seq.applyAt(i0 + 1, [&z, &m](const auto& arcPt) { + z = arcPt.template get(); + m = arcPt.template get(); + }); + return; + } + + double z0, m0; + seq.applyAt(i0, [&z0, &m0](const auto& arcPt) { + z0 = arcPt.template get(); + m0 = arcPt.template get(); + }); + if (arc.p0().equals2D(pt)) { + z = z0; + m = m0; + return; + } + + double z1, m1; + seq.applyAt(i0 + 1, [&z1, &m1](const auto& arcPt) { + z1 = arcPt.template get(); + m1 = arcPt.template get(); + }); + + if (arc.p1().equals2D(pt)) { + z = z1; + m = m1; + return; + } + + + double z2, m2; + seq.applyAt(i0 + 2, [&z2, &m2](const auto& arcPt) { + z2 = arcPt.template get(); + m2 = arcPt.template get(); + }); + + if (arc.p2().equals2D(pt)) { + z = z2; + m = m2; + return; + } + + double theta0 = arc.theta0(); + const double theta1 = arc.theta1(); + double theta2 = arc.theta2(); + const double theta = CircularArcs::getAngle(pt, arc.getCenter()); + + if (!arc.isCCW()) { + std::swap(theta0, theta2); + std::swap(z0, z2); + std::swap(m0, m2); + } + + if (std::isnan(z1)) { + // Interpolate between p0 / p2 + const double frac = angleFractionCCW(theta, theta0, theta2); + z = interpolateValue(z0, z2, frac); + } else if (Angle::isWithinCCW(theta, theta0, theta1)) { + // Interpolate between p0 / p1 + const double frac = angleFractionCCW(theta, theta0, theta1); + z = interpolateValue(z0, z1, frac); + } else { + // Interpolate between p1 / p2 + const double frac = angleFractionCCW(theta, theta1, theta2); + z = interpolateValue(z1, z2, frac); + } + + if (std::isnan(m1)) { + // Interpolate between p0 / p2 + const double frac = angleFractionCCW(theta, theta0, theta2); + m = interpolateValue(m0, m2, frac); + } else if (Angle::isWithinCCW(theta, theta0, theta1)) { + // Interpolate between p0 / p1 + const double frac = angleFractionCCW(theta, theta0, theta1); + m = interpolateValue(m0, m1, frac); + } else { + // Interpolate between p1 / p2 + const double frac = angleFractionCCW(theta, theta1, theta2); + m = interpolateValue(m1, m2, frac); + } + +} + +static void interpolateSegmentZM(const CoordinateSequence& seq, + std::size_t ind0, std::size_t ind1, + geom::CoordinateXY& pt, double& z, double& m) +{ + seq.applyAt(ind0, [&seq, &pt, ind1, &z, &m](const auto& p0) { + using CoordinateType = std::decay_t; + + const auto& p1 = seq.getAt(ind1); + z = Interpolate::zGetOrInterpolate(pt, p0, p1); + m = Interpolate::mGetOrInterpolate(pt, p0, p1); + }); +} + + +static void interpolateZM(const CircularArc& arc0, const CircularArc& arc1, geom::CoordinateXYZM& pt) +{ + if (!std::isnan(pt.z) && !std::isnan(pt.m)) { + return; + } + + double z0, m0; + double z1, m1; + interpolateZM(arc0, pt, z0, m0); + interpolateZM(arc1, pt, z1, m1); + + if (std::isnan(pt.z)) { + pt.z = Interpolate::getOrAverage(z0, z1); + } + if (std::isnan(pt.m)) { + pt.m = Interpolate::getOrAverage(m0, m1); + } +} + +static void interpolateZM(const CircularArc& arc0, + const geom::CoordinateSequence& seq, + std::size_t ind0, std::size_t ind1, + geom::CoordinateXYZM& pt) +{ + if (!std::isnan(pt.z) && !std::isnan(pt.m)) { + return; + } + + double z0, m0; + double z1, m1; + interpolateZM(arc0, pt, z0, m0); + interpolateSegmentZM(seq, ind0, ind1, pt, z1, m1); + + if (std::isnan(pt.z)) { + pt.z = Interpolate::getOrAverage(z0, z1); + } + if (std::isnan(pt.m)) { + pt.m = Interpolate::getOrAverage(m0, m1); + } +} + int -CircularArcIntersector::circleIntersects(const CoordinateXY& center, double r, const CoordinateXY& p0, const CoordinateXY& p1, CoordinateXY& ret0, CoordinateXY& ret1) +CircularArcIntersector::circleIntersects(const CoordinateXY& center, + double r, + const CoordinateXY& p0, + const CoordinateXY& p1, + CoordinateXY& ret0, + CoordinateXY& ret1) { const double& x0 = center.x; const double& y0 = center.y; @@ -92,10 +278,12 @@ CircularArcIntersector::circleIntersects(const CoordinateXY& center, double r, c } void -CircularArcIntersector::intersects(const CircularArc& arc, const CoordinateXY& p0, const CoordinateXY& p1) +CircularArcIntersector::intersects(const CircularArc& arc, const CoordinateSequence& seq, std::size_t segPos0, std::size_t segPos1, bool useSegEndpoints) { if (arc.isLinear()) { - intersects(arc.p0, arc.p2, p0, p1); + std::size_t arcPos0 = arc.getCoordinatePosition(); + intersects(*arc.getCoordinateSequence(), arcPos0, arcPos0 + 2, + seq, segPos0, segPos1); return; } @@ -105,15 +293,15 @@ CircularArcIntersector::intersects(const CircularArc& arc, const CoordinateXY& p const CoordinateXY& c = arc.getCenter(); const double r = arc.getRadius(); - CoordinateXY isect0, isect1; - auto n = circleIntersects(c, r, p0, p1, isect0, isect1); + CoordinateXYZM isect0, isect1; + auto n = circleIntersects(c, r, seq.getAt(segPos0), seq.getAt(segPos1), isect0, isect1); if (n > 0 && arc.containsPointOnCircle(isect0)) { - intPt[nPt++] = isect0; + addIntersection(isect0, arc, seq, segPos0, segPos1, useSegEndpoints); } if (n > 1 && arc.containsPointOnCircle(isect1)) { - intPt[nPt++] = isect1; + addIntersection(isect1, arc, seq, segPos0, segPos1, useSegEndpoints); } switch (nPt) { @@ -128,42 +316,24 @@ CircularArcIntersector::intersects(const CircularArc& arc, const CoordinateXY& p } } -void -CircularArcIntersector::intersects(const CoordinateXY& p0, const CoordinateXY& p1, - const CoordinateXY& q0, const CoordinateXY& q1) -{ - reset(); - - algorithm::LineIntersector li; - li.computeIntersection(p0, p1, q0, q1); - if (li.getIntersectionNum() == 2) { - // FIXME this means a collinear intersection, so we should report as cocircular? - intPt[0] = li.getIntersection(0); - intPt[1] = li.getIntersection(1); - result = TWO_POINT_INTERSECTION; - } else if (li.getIntersectionNum() == 1) { - intPt[0] = li.getIntersection(0); - nPt = 1; - result = ONE_POINT_INTERSECTION; - } else { - result = NO_INTERSECTION; - } -} - void CircularArcIntersector::intersects(const CircularArc& arc1, const CircularArc& arc2) { // Handle cases where one or both arcs are degenerate if (arc1.isLinear()) { if (arc2.isLinear()) { - intersects(arc1.p0, arc1.p2, arc2.p0, arc2.p2); + const auto arc1pos = arc1.getCoordinatePosition(); + const auto arc2pos = arc2.getCoordinatePosition(); + + intersects(*arc1.getCoordinateSequence(), arc1pos, arc1pos + 2, + *arc2.getCoordinateSequence(), arc2pos, arc2pos + 2); return; } else { - intersects(arc2, arc1.p0, arc1.p2); + intersects(arc2, *arc1.getCoordinateSequence(), arc1.getCoordinatePosition(), arc1.getCoordinatePosition() + 2, true); return; } } else if (arc2.isLinear()) { - intersects(arc1, arc2.p0, arc2.p2); + intersects(arc1, *arc2.getCoordinateSequence(), arc2.getCoordinatePosition(), arc2.getCoordinatePosition() + 2, false); return; } @@ -205,8 +375,10 @@ CircularArcIntersector::intersects(const CircularArc& arc1, const CircularArc& a double bp0 = arc2.theta0(); double bp1 = arc2.theta2(); + // Orientation of the result matches the first input bool resultArcIsCCW = true; + // Make both inputs counter-clockwise for the purpose of determining intersections if (arc1.getOrientation() != Orientation::COUNTERCLOCKWISE) { std::swap(ap0, ap1); resultArcIsCCW = false; @@ -220,50 +392,70 @@ CircularArcIntersector::intersects(const CircularArc& arc1, const CircularArc& a bp1 = Angle::normalizePositive(bp1); bool checkBp1inA = true; + bool checkAcontained = true; + + // Possible intersection arrangements: + // A contained within B + // A overlaps B + // B contained within A // check start of B within A? if (Angle::isWithinCCW(bp0, ap0, ap1)) { - double start = bp0; - double end = nextAngleCCW(start, bp1, ap1); + checkAcontained = false; + const double start = bp0; + const double end = nextAngleCCW(start, bp1, ap1); if (end == bp1) { checkBp1inA = false; } if (start == end) { - intPt[nPt++] = CircularArcs::createPoint(c1, r1, start); + const CoordinateXY computedIntPt = CircularArcs::createPoint(c1, r1, start); + addIntersection(computedIntPt, arc1, arc2); } else { if (resultArcIsCCW) { - intArc[nArc++] = CircularArc(start, end, c1, r1, Orientation::COUNTERCLOCKWISE); + addArcIntersection(start, end, Orientation::COUNTERCLOCKWISE, arc1, arc2); } else { - intArc[nArc++] = CircularArc(end, start, c1, r1, Orientation::CLOCKWISE); + addArcIntersection(end, start, Orientation::CLOCKWISE, arc1, arc2); } } } if (checkBp1inA && Angle::isWithinCCW(bp1, ap0, ap1)) { // end of B within A? - double start = ap0; - double end = bp1; + checkAcontained = false; + + const double start = ap0; + const double end = bp1; if (start == end) { - intPt[nPt++] = CircularArcs::createPoint(c1, r1, start); + const CoordinateXY computedIntPt = CircularArcs::createPoint(c1, r1, start); + addIntersection(computedIntPt, arc1, arc2); } else { if (resultArcIsCCW) { - intArc[nArc++] = CircularArc(start, end, c1, r1, Orientation::COUNTERCLOCKWISE); + addArcIntersection(start, end, Orientation::CLOCKWISE, arc1, arc2); } else { - intArc[nArc++] = CircularArc(end, start, c1, r1, Orientation::CLOCKWISE); + addArcIntersection(end, start, Orientation::CLOCKWISE, arc1, arc2); } } } + + if (checkAcontained && Angle::isWithinCCW(ap0, bp0 , bp1) && ap0 != bp0 && ap0 != bp1 && Angle::isWithinCCW(ap1, bp0, bp1) && ap1 != bp1 && ap1 != bp0) { + if (resultArcIsCCW) { + addArcIntersection(ap0, ap1, Orientation::COUNTERCLOCKWISE, arc1, arc2); + } + else { + addArcIntersection(ap1, ap0, Orientation::CLOCKWISE, arc1, arc2); + } + } } else { // NOT COCIRCULAR - double dx = c2.x-c1.x; - double dy = c2.y-c1.y; + const double dx = c2.x-c1.x; + const double dy = c2.y-c1.y; #if 1 // point where a line between the two circle center points intersects @@ -271,16 +463,19 @@ CircularArcIntersector::intersects(const CircularArc& arc1, const CircularArc& a CoordinateXY p{c1.x + a* dx/d, c1.y+a* dy/d}; // distance from p to the intersection points - double h = std::sqrt(r1*r1 - a*a); + const double h = std::sqrt(r1*r1 - a*a); - CoordinateXY isect0{p.x + h* dy/d, p.y - h* dx/d }; - CoordinateXY isect1{p.x - h* dy/d, p.y + h* dx/d }; + const CoordinateXY isect0{p.x + h* dy/d, p.y - h* dx/d }; + const CoordinateXY isect1{p.x - h* dy/d, p.y + h* dx/d }; - if (arc1.containsPointOnCircle(isect0) && arc2.containsPointOnCircle(isect0)) { - intPt[nPt++] = isect0; - } - if (!isect1.equals2D(isect0) && arc1.containsPointOnCircle(isect1) && arc2.containsPointOnCircle(isect1)) { - intPt[nPt++] = isect1; + for (const CoordinateXY& computedIntPt : {isect0, isect1}) { + if (nPt > 0 && computedIntPt.equals2D(intPt[0])) { + continue; + } + + if (arc1.containsPointOnCircle(computedIntPt) && arc2.containsPointOnCircle(computedIntPt)) { + addIntersection(computedIntPt, arc1, arc2); + } } #else // Alternate formulation. @@ -311,6 +506,25 @@ CircularArcIntersector::intersects(const CircularArc& arc1, const CircularArc& a } } #endif + +#if 1 + // Add endpoint intersections missed due to precision issues. + // TODO: Add some logic to prevent double-counting of endpoints. Ideally, the endpoint test would happen before + // computing intersection points, so if there is an endpoint intersection we get the exact intersection point + // instead of a computed one. + if (nPt < 2 && arc1.p0().equals2D(arc2.p0()) && (nPt == 0 || !intPt[0].equals2D(arc1.p0()))) { + addIntersection(arc1.p0(), arc1, arc2); + } + if (nPt < 2 && arc1.p0().equals2D(arc2.p2()) && (nPt == 0 || !intPt[0].equals2D(arc1.p0()))) { + addIntersection(arc1.p0(), arc1, arc2); + } + if (nPt < 2 && arc1.p2().equals2D(arc2.p0()) && (nPt == 0 || !intPt[0].equals2D(arc1.p2()))) { + addIntersection(arc1.p2(), arc1, arc2); + } + if (nPt < 2 && arc1.p2().equals2D(arc2.p2()) && (nPt == 0 || !intPt[0].equals2D(arc1.p2()))) { + addIntersection(arc1.p2(), arc1, arc2); + } +#endif } if (nArc) { @@ -331,4 +545,156 @@ CircularArcIntersector::intersects(const CircularArc& arc1, const CircularArc& a } } +void +CircularArcIntersector::intersects(const CoordinateSequence &p, std::size_t p0, std::size_t p1, + const CoordinateSequence &q, std::size_t q0, std::size_t q1) +{ + LineIntersector li; + li.computeIntersection(p, p0, p1, q, q0, q1); + + if (li.getIntersectionNum() == 2) { + // FIXME this means a collinear intersection, so we should report as cocircular? + intPt[0] = li.getIntersection(0); + intPt[1] = li.getIntersection(1); + result = TWO_POINT_INTERSECTION; + } else if (li.getIntersectionNum() == 1) { + intPt[0] = li.getIntersection(0); + nPt = 1; + result = ONE_POINT_INTERSECTION; + } else { + result = NO_INTERSECTION; + } } + +void +CircularArcIntersector::addArcIntersection(double startAngle, double endAngle, int orientation, const CircularArc& arc1, const CircularArc& arc2) +{ + const auto theta1 = CircularArcs::getMidpointAngle(startAngle, endAngle, orientation == Orientation::COUNTERCLOCKWISE); + const CoordinateXY& center = arc1.getCenter(); + const double radius = arc1.getRadius(); + + const bool constructZ = arc1.getCoordinateSequence()->hasZ() || arc2.getCoordinateSequence()->hasZ(); + const bool constructM = arc1.getCoordinateSequence()->hasM() || arc2.getCoordinateSequence()->hasM(); + + CoordinateXYZM computedStartPt(CircularArcs::createPoint(center, radius, startAngle)); + CoordinateXYZM computedMidPt(CircularArcs::createPoint(center, radius, theta1)); + CoordinateXYZM computedEndPt(CircularArcs::createPoint(center, radius, endAngle)); + + if (computedStartPt.equals2D(arc1.p0())) { + arc1.applyAt(0, [&computedStartPt](const auto& endpoint) { + computedStartPt.z = Interpolate::zGet(computedStartPt, endpoint); + computedStartPt.m = Interpolate::mGet(computedStartPt, endpoint); + }); + } else if (computedStartPt.equals2D(arc1.p2())) { + arc1.applyAt(2, [&computedStartPt](const auto& endpoint) { + computedStartPt.z = Interpolate::zGet(computedStartPt, endpoint); + computedStartPt.m = Interpolate::mGet(computedStartPt, endpoint); + }); + } else if (computedStartPt.equals2D(arc2.p0())) { + arc2.applyAt(0, [&computedStartPt](const auto& endpoint) { + computedStartPt.z = Interpolate::zGet(computedStartPt, endpoint); + computedStartPt.m = Interpolate::mGet(computedStartPt, endpoint); + }); + } else if (computedStartPt.equals2D(arc2.p2())) { + arc2.applyAt(2, [&computedStartPt](const auto& endpoint) { + computedStartPt.z = Interpolate::zGet(computedStartPt, endpoint); + computedStartPt.m = Interpolate::mGet(computedStartPt, endpoint); + }); + } + + if (computedEndPt.equals2D(arc1.p0())) { + arc1.applyAt(0, [&computedEndPt](const auto& endpoint) { + computedEndPt.z = Interpolate::zGet(computedEndPt, endpoint); + computedEndPt.m = Interpolate::mGet(computedEndPt, endpoint); + }); + } else if (computedEndPt.equals2D(arc1.p2())) { + arc1.applyAt(2, [&computedEndPt](const auto& endpoint) { + computedEndPt.z = Interpolate::zGet(computedEndPt, endpoint); + computedEndPt.m = Interpolate::mGet(computedEndPt, endpoint); + }); + } else if (computedEndPt.equals2D(arc2.p0())) { + arc2.applyAt(0, [&computedEndPt](const auto& endpoint) { + computedEndPt.z = Interpolate::zGet(computedEndPt, endpoint); + computedEndPt.m = Interpolate::mGet(computedEndPt, endpoint); + }); + } else if (computedEndPt.equals2D(arc2.p2())) { + arc2.applyAt(2, [&computedEndPt](const auto& endpoint) { + computedEndPt.z = Interpolate::zGet(computedEndPt, endpoint); + computedEndPt.m = Interpolate::mGet(computedEndPt, endpoint); + }); + } + + interpolateZM(arc1, arc2, computedStartPt); + interpolateZM(arc1, arc2, computedMidPt); + interpolateZM(arc1, arc2, computedEndPt); + + auto seq = std::make_unique(3, constructZ, constructM); + seq->setAt(computedStartPt, 0); + seq->setAt(computedMidPt, 1); + seq->setAt(computedEndPt, 2); + + intArc[nArc++] = CircularArc(std::move(seq), 0, center, radius, orientation); +} + +void +CircularArcIntersector::addIntersection(const CoordinateXY& computedIntPt, const CircularArc& arc1, const CircularArc& arc2) { + CoordinateXYZM& newIntPt = intPt[nPt++]; + newIntPt = computedIntPt; + + if (computedIntPt.equals2D(arc1.p0())) { + arc1.applyAt(0, [&newIntPt](const auto& endpoint) { + newIntPt.z = Interpolate::zGet(newIntPt, endpoint); + newIntPt.m = Interpolate::mGet(newIntPt, endpoint); + }); + } else if (computedIntPt.equals2D(arc1.p2())) { + arc1.applyAt(2, [&newIntPt](const auto& endpoint) { + newIntPt.z = Interpolate::zGet(newIntPt, endpoint); + newIntPt.m = Interpolate::mGet(newIntPt, endpoint); + }); + } + + interpolateZM(arc1, arc2, newIntPt); +} + +void +CircularArcIntersector::addIntersection(const CoordinateXY& computedIntPt, const CircularArc& arc1, const CoordinateSequence& seq, std::size_t pos0, std::size_t pos1, bool useSegEndpoints) { + CoordinateXYZM& newIntPt = intPt[nPt++]; + newIntPt = computedIntPt; + + for (int i = 0; i < 2; i++) { + if (useSegEndpoints) { + if (computedIntPt.equals2D(seq.getAt(pos0))) { + seq.applyAt(pos0, [&newIntPt](const auto& endpoint) { + newIntPt.z = Interpolate::zGet(newIntPt, endpoint); + newIntPt.m = Interpolate::mGet(newIntPt, endpoint); + }); + } + if (computedIntPt.equals2D(seq.getAt(pos1))) { + seq.applyAt(pos1, [&newIntPt](const auto& endpoint) { + newIntPt.z = Interpolate::zGet(newIntPt, endpoint); + newIntPt.m = Interpolate::mGet(newIntPt, endpoint); + }); + } + } else { + if (computedIntPt.equals2D(arc1.p0())) { + arc1.applyAt(0, [&newIntPt](const auto& endpoint) { + newIntPt.z = Interpolate::zGet(newIntPt, endpoint); + newIntPt.m = Interpolate::mGet(newIntPt, endpoint); + }); + } + if (computedIntPt.equals2D(arc1.p2())) { + arc1.applyAt(2, [&newIntPt](const auto& endpoint) { + newIntPt.z = Interpolate::zGet(newIntPt, endpoint); + newIntPt.m = Interpolate::mGet(newIntPt, endpoint); + }); + } + } + + useSegEndpoints = !useSegEndpoints; + } + + interpolateZM(arc1, seq, pos0, pos1, newIntPt); +} + + +} \ No newline at end of file diff --git a/src/algorithm/LineIntersector.cpp b/src/algorithm/LineIntersector.cpp index b3901b9b3..f4ce40836 100644 --- a/src/algorithm/LineIntersector.cpp +++ b/src/algorithm/LineIntersector.cpp @@ -181,16 +181,35 @@ public: std::size_t i1) : m_li(li), m_seq0(seq0), - m_i0(i0), + m_i00(i0), + m_i01(i0 + 1), m_seq1(seq1), - m_i1(i1) {} + m_i10(i1), + m_i11(i1 + 1) + {} + + DoIntersect(algorithm::LineIntersector& li, + const CoordinateSequence& seq0, + std::size_t i00, + std::size_t i01, + const CoordinateSequence& seq1, + std::size_t i10, + std::size_t i11) : + m_li(li), + m_seq0(seq0), + m_i00(i00), + m_i01(i01), + m_seq1(seq1), + m_i10(i10), + m_i11(i11) + {} template void operator()() { - const T1& p00 = m_seq0.getAt(m_i0); - const T1& p01 = m_seq0.getAt(m_i0 + 1); - const T2& p10 = m_seq1.getAt(m_i1); - const T2& p11 = m_seq1.getAt(m_i1 + 1); + const T1& p00 = m_seq0.getAt(m_i00); + const T1& p01 = m_seq0.getAt(m_i01); + const T2& p10 = m_seq1.getAt(m_i10); + const T2& p11 = m_seq1.getAt(m_i11); m_li.computeIntersection(p00, p01, p10, p11); } @@ -198,9 +217,11 @@ public: private: algorithm::LineIntersector& m_li; const CoordinateSequence& m_seq0; - std::size_t m_i0; + std::size_t m_i00; + std::size_t m_i01; const CoordinateSequence& m_seq1; - std::size_t m_i1; + std::size_t m_i10; + std::size_t m_i11; }; /*public*/ @@ -212,6 +233,15 @@ LineIntersector::computeIntersection(const CoordinateSequence& p, std::size_t p0 CoordinateSequences::binaryDispatch(p, q, dis); } +/*public*/ +void +LineIntersector::computeIntersection(const CoordinateSequence& p, std::size_t p0, std::size_t p1, + const CoordinateSequence& q, std::size_t q0, std::size_t q1) +{ + DoIntersect dis(*this, p, p0, p1 ,q, q0, q1); + CoordinateSequences::binaryDispatch(p, q, dis); +} + /* private static */ const CoordinateXY& LineIntersector::nearestEndpoint(const CoordinateXY& p1, const CoordinateXY& p2, diff --git a/src/algorithm/RayCrossingCounter.cpp b/src/algorithm/RayCrossingCounter.cpp index 9e25deba8..3d212beb6 100644 --- a/src/algorithm/RayCrossingCounter.cpp +++ b/src/algorithm/RayCrossingCounter.cpp @@ -113,11 +113,8 @@ RayCrossingCounter::processSequence(const geom::CoordinateSequence& seq, bool is } } else { for (std::size_t i = 2; i < seq.size(); i += 2) { - const geom::CoordinateXY& p1 = seq.getAt(i-2); - const geom::CoordinateXY& p2 = seq.getAt(i-1); - const geom::CoordinateXY& p3 = seq.getAt(i); - - countArc(p1, p2, p3); + geom::CircularArc arc(seq, i-2); + countArc(arc); if (isOnSegment()) { return; @@ -197,9 +194,9 @@ RayCrossingCounter::shouldCountCrossing(const geom::CircularArc& arc, const geom // a) is in the interior of the arc // b) is at the starting point of the arc, and the arc is directed upward at that point // c) is at the ending point of the arc is directed downward at that point - if (q.equals2D(arc.p0)) { + if (q.equals2D(arc.p0())) { return arc.isUpwardAtPoint(q); - } else if (q.equals2D(arc.p2)) { + } else if (q.equals2D(arc.p2())) { return !arc.isUpwardAtPoint(q); } else { return true; @@ -252,25 +249,22 @@ RayCrossingCounter::pointsIntersectingHorizontalRay(const geom::CircularArc& arc } void -RayCrossingCounter::countArc(const CoordinateXY& p1, - const CoordinateXY& p2, - const CoordinateXY& p3) +RayCrossingCounter::countArc(const geom::CircularArc& arc) { // For each arc, check if it crosses // a horizontal ray running from the test point in // the positive x direction. - geom::CircularArc arc(p1, p2, p3); // If the arc is degenerate, process it is two line segments if (arc.isLinear()) { - countSegment(p1, p2); - countSegment(p2, p3); + countSegment(arc.p0(), arc.p1()); + countSegment(arc.p1(), arc.p2()); return; } // Check if the arc is strictly to the left of the test point geom::Envelope arcEnvelope; - CircularArcs::expandEnvelope(arcEnvelope, p1, p2, p3); + CircularArcs::expandEnvelope(arcEnvelope, arc.p0(), arc.p1(), arc.p2()); if (arcEnvelope.getMaxX() < point.x) { return; diff --git a/src/geom/CircularArc.cpp b/src/geom/CircularArc.cpp new file mode 100644 index 000000000..eb2ffe5df --- /dev/null +++ b/src/geom/CircularArc.cpp @@ -0,0 +1,416 @@ +/********************************************************************** +* + * GEOS - Geometry Engine Open Source + * http://geos.osgeo.org + * + * Copyright (C) 2024-2025 ISciences, LLC + * + * This is free software; you can redistribute and/or modify it under + * the terms of the GNU Lesser General Public Licence as published + * by the Free Software Foundation. + * See the COPYING file for more information. + * + **********************************************************************/ + +#include +#include +#include + +namespace geos::geom { + +CircularArc::CircularArc() : + m_seq(nullptr), + m_center_known(true), + m_radius_known(true), + m_orientation_known(true), + m_own_coordinates(true) +{} + +template +static CircularArc createFromPoints(const CoordType& p0, const CoordType& p2, const CoordinateXY& center, double radius, int orientation) +{ + static_assert(std::is_base_of_v); + + constexpr bool hasZ = CoordType::template has(); + constexpr bool hasM = CoordType::template has(); + + auto seq = std::make_unique(3, hasZ, hasM); + + CoordType p1(geos::algorithm::CircularArcs::getMidpoint(p0, p2, center, radius, orientation == algorithm::Orientation::COUNTERCLOCKWISE)); + if constexpr (hasZ) { + p1.z = 0.5*(p0.z + p2.z); + } + if constexpr (hasM) { + p1.m = 0.5*(p0.m + p2.m); + } + + seq->setAt(p0, 0); + seq->setAt(p1, 1); + seq->setAt(p2, 2); + + CircularArc ret(std::move(seq), 0); + + return ret; +} + +CircularArc +CircularArc::create(const CoordinateXY& p0, const CoordinateXY& p2, const CoordinateXY& center, double radius, int orientation) { + return createFromPoints(p0, p2, center, radius, orientation); +} + +CircularArc +CircularArc::create(const Coordinate& p0, const Coordinate& p2, const CoordinateXY& center, double radius, int orientation) { + return createFromPoints(p0, p2, center, radius, orientation); +} + +CircularArc +CircularArc::create(const CoordinateXYM& p0, const CoordinateXYM& p2, const CoordinateXY& center, double radius, int orientation) { + return createFromPoints(p0, p2, center, radius, orientation); +} + +CircularArc +CircularArc::create(const CoordinateXYZM& p0, const CoordinateXYZM& p2, const CoordinateXY& center, double radius, int orientation) { + return createFromPoints(p0, p2, center, radius, orientation); +} + +CircularArc::CircularArc(const CoordinateSequence& seq, std::size_t pos) : + m_seq(&seq), + m_pos(pos), + m_own_coordinates(false) {} + +CircularArc::CircularArc(std::unique_ptr seq, std::size_t pos) : + CircularArc(*seq, pos) +{ + m_own_coordinates = true; + seq.release(); +} + +CircularArc::CircularArc(const CoordinateSequence& seq, std::size_t pos, const CoordinateXY& center, double radius, int orientation) : + m_seq(&seq), + m_pos(pos), + m_center(center), + m_radius(radius), + m_orientation(orientation), + m_center_known(true), + m_radius_known(true), + m_orientation_known(true), + m_own_coordinates(false) +{} + +CircularArc::CircularArc(std::unique_ptr seq, std::size_t pos, const CoordinateXY& center, double radius, int orientation) : + CircularArc(*seq, pos, center, radius, orientation) +{ + m_own_coordinates = true; + seq.release(); +} + +CircularArc::CircularArc(const CircularArc& other) : + m_seq(new CoordinateSequence(0, other.getCoordinateSequence()->hasZ(), other.getCoordinateSequence()->hasM())), + m_pos(0), + m_center(other.m_center), + m_radius(other.m_radius), + m_orientation(other.m_orientation), + m_center_known(other.m_center_known), + m_radius_known(other.m_radius_known), + m_orientation_known(other.m_orientation_known), + m_own_coordinates(true) +{ + CoordinateSequence* seq = const_cast(m_seq); + seq->reserve(3); + seq->add(*other.getCoordinateSequence(), other.getCoordinatePosition(), other.getCoordinatePosition() + 2); +} + +CircularArc::CircularArc(CircularArc&& other) noexcept { + m_seq = other.m_seq; + m_pos = other.m_pos; + m_center = other.m_center; + m_radius = other.m_radius; + m_orientation = other.m_orientation; + m_center_known = other.m_center_known; + m_radius_known = other.m_radius_known; + m_orientation_known = other.m_orientation_known; + m_own_coordinates = other.m_own_coordinates; + + if (other.m_own_coordinates) { + other.m_own_coordinates = false; + } +} + +CircularArc& +CircularArc::operator=(const CircularArc& other) +{ + if (m_own_coordinates) { + delete m_seq; + } + + m_seq = new CoordinateSequence(0, other.getCoordinateSequence()->hasZ(), other.getCoordinateSequence()->hasM()); + m_pos = other.m_pos; + m_own_coordinates = true; + m_orientation = other.m_orientation; + m_orientation_known = other.m_orientation_known; + m_center = other.m_center; + m_center_known = other.m_center_known; + m_radius = other.m_radius; + m_radius_known = other.m_radius_known; + + CoordinateSequence* seq = const_cast(m_seq); + seq->reserve(3); + seq->add(*other.getCoordinateSequence(), other.getCoordinatePosition(), other.getCoordinatePosition() + 2); + + return *this; +} + +CircularArc& +CircularArc::operator=(CircularArc&& other) noexcept +{ + if (m_own_coordinates) { + delete m_seq; + } + + m_seq = other.m_seq; + m_pos = other.m_pos; + m_own_coordinates = other.m_own_coordinates; + m_orientation = other.m_orientation; + m_orientation_known = other.m_orientation_known; + m_center = other.m_center; + m_center_known = other.m_center_known; + m_radius = other.m_radius; + m_radius_known = other.m_radius_known; + + if (m_own_coordinates) { + other.m_own_coordinates = false; + } + + return *this; +} + +CircularArc::~CircularArc() +{ + if (m_own_coordinates) { + delete m_seq; + } +} + +bool +CircularArc::containsAngle(double theta) const { + auto t0 = theta0(); + auto t2 = theta2(); + + if (theta == t0 || theta == t2) { + return true; + } + + if (getOrientation() == algorithm::Orientation::COUNTERCLOCKWISE) { + std::swap(t0, t2); + } + + t2 -= t0; + theta -= t0; + + if (t2 < 0) { + t2 += 2*MATH_PI; + } + if (theta < 0) { + theta += 2*MATH_PI; + } + + return theta >= t2; +} + +bool +CircularArc::containsPoint(const CoordinateXY& q) const +{ + if (q == p0() || q == p1() || q == p2()) { + return true; + } + + //auto dist = std::abs(q.distance(getCenter()) - getRadius()); + + //if (dist > 1e-8) { + // return false; + //} + + if (triangulate::quadedge::TrianglePredicate::isInCircleRobust(p0(), p1(), p2(), q) != geom::Location::BOUNDARY) { + return false; + } + + return containsPointOnCircle(q); +} + +double +CircularArc::getAngle() const +{ + if (isCircle()) { + return 2*MATH_PI; + } + + /// Even Rouault: + /// potential optimization?: using crossproduct(p0 - center, p2 - center) = radius * radius * sin(angle) + /// could yield the result by just doing a single asin(), instead of 2 atan2() + /// actually one should also likely compute dotproduct(p0 - center, p2 - center) = radius * radius * cos(angle), + /// and thus angle = atan2(crossproduct(p0 - center, p2 - center) , dotproduct(p0 - center, p2 - center) ) + auto t0 = theta0(); + auto t2 = theta2(); + + if (getOrientation() == algorithm::Orientation::COUNTERCLOCKWISE) { + std::swap(t0, t2); + } + + if (t0 < t2) { + t0 += 2*MATH_PI; + } + + auto diff = t0-t2; + + return diff; +} + +double +CircularArc::getArea() const { + if (isLinear()) { + return 0; + } + + auto R = getRadius(); + auto theta = getAngle(); + return R*R/2*(theta - std::sin(theta)); +} + +double +CircularArc::getLength() const { + if (isLinear()) { + return p0().distance(p2()); + } + + return getAngle()*getRadius(); +} + +bool +CircularArc::isUpwardAtPoint(const CoordinateXY& q) const { + auto quad = geom::Quadrant::quadrant(getCenter(), q); + bool isUpward; + + if (getOrientation() == algorithm::Orientation::CLOCKWISE) { + isUpward = (quad == geom::Quadrant::SW || quad == geom::Quadrant::NW); + } else { + isUpward = (quad == geom::Quadrant::SE || quad == geom::Quadrant::NE); + } + + return isUpward; +} + +CircularArc +CircularArc::reverse() const +{ + auto seq = std::make_unique(3, m_seq->hasZ(), m_seq->hasM()); + m_seq->applyAt(m_pos, [&seq](const auto& pt) { + seq->setAt(pt, 2); + }); + m_seq->applyAt(m_pos + 1, [&seq](const auto& pt) { + seq->setAt(pt, 1); + }); + m_seq->applyAt(m_pos + 2, [&seq](const auto& pt) { + seq->setAt(pt, 0); + }); + + CircularArc ret(std::move(seq), 0); + + if (m_orientation_known) { + if (m_orientation == algorithm::Orientation::COUNTERCLOCKWISE) { + ret.m_orientation = algorithm::Orientation::CLOCKWISE; + } else if (m_orientation == algorithm::Orientation::CLOCKWISE) { + ret.m_orientation = algorithm::Orientation::COUNTERCLOCKWISE; + } else { + ret.m_orientation = algorithm::Orientation::COLLINEAR; + } + ret.m_orientation_known = true; + } + + if (m_center_known) { + ret.m_center = m_center; + ret.m_center_known = true; + } + + if (m_radius_known) { + ret.m_radius = m_radius; + ret.m_radius_known = true; + } + + return ret; +} + +bool +CircularArc::equals(const CircularArc &other, double tol) const +{ + if (getCoordinateSequence()->hasZ() != other.getCoordinateSequence()->hasZ()) { + return false; + } + + if (getCoordinateSequence()->hasM() != other.getCoordinateSequence()->hasM()) { + return false; + } + + if (getCenter().distance(other.getCenter()) > tol) { + return false; + } + + if (std::abs(getRadius() - other.getRadius()) > tol) { + return false; + } + + if (getOrientation() != other.getOrientation()) { + return false; + } + + CoordinateXYZM a, b; + getCoordinateSequence()->getAt(getCoordinatePosition(), a); + other.getCoordinateSequence()->getAt(other.getCoordinatePosition(), b); + + if (a.distance(b) > tol) { + return false; + } + + if ((!std::isnan(a.z) || !std::isnan(b.z)) && !(std::abs(a.z - b.z) <= tol)) { + return false; + } + + if ((!std::isnan(a.m) || !std::isnan(b.m)) && !(std::abs(a.m - b.m) <= tol)) { + return false; + } + + return true; +} + + +#if 0 +std::pair +CircularArc::splitAtPoint(const CoordinateXY& q) const { + return std::make_pair( + CircularArc(p0(), q, getCenter(), getRadius(), getOrientation()), + CircularArc(q, p2(), getCenter(), getRadius(), getOrientation()) + ); +} +#endif + +std::string +CircularArc::toString() const { + std::stringstream ss; + ss << "CIRCULARSTRING "; + if (m_seq->hasZ()) { + ss << "Z"; + } + if (m_seq->hasM()) { + ss << "M"; + } + if (m_seq->hasZ() || m_seq->hasM()) { + ss << " "; + } + ss << "("; + m_seq->applyAt(m_pos, [&ss](const auto& pt) { + ss << pt << ", " << *(&pt + 1) << ", " << *(&pt + 2); + }); + ss << ")"; + return ss.str(); +} + +} diff --git a/src/geom/CircularString.cpp b/src/geom/CircularString.cpp index 6e95aa035..361f56c56 100644 --- a/src/geom/CircularString.cpp +++ b/src/geom/CircularString.cpp @@ -69,7 +69,7 @@ CircularString::getLength() const double tot = 0; for (std::size_t i = 2; i < coords.size(); i += 2) { - auto len = CircularArc(coords[i-2], coords[i-1], coords[i]).getLength(); + auto len = CircularArc(coords, i-2).getLength(); tot += len; } return tot; diff --git a/src/geom/CoordinateSequence.cpp b/src/geom/CoordinateSequence.cpp index 1292c2bd1..865f97a44 100644 --- a/src/geom/CoordinateSequence.cpp +++ b/src/geom/CoordinateSequence.cpp @@ -608,6 +608,18 @@ CoordinateSequence::setPoints(const std::vector& v) } } +void +CoordinateSequence::swap(std::size_t i, std::size_t j) +{ + using difference_type = decltype(m_vect)::difference_type; + + if (i != j) { + std::swap_ranges(std::next(m_vect.begin(), static_cast(i*stride())), + std::next(m_vect.begin(), static_cast(i+1) * stride()), + std::next(m_vect.begin(), static_cast(j*stride()))); + } +} + void CoordinateSequence::toVector(std::vector& out) const { diff --git a/src/noding/ArcIntersectionAdder.cpp b/src/noding/ArcIntersectionAdder.cpp index 278228436..b6fb218c6 100644 --- a/src/noding/ArcIntersectionAdder.cpp +++ b/src/noding/ArcIntersectionAdder.cpp @@ -12,6 +12,7 @@ * **********************************************************************/ +#include #include #include #include @@ -27,7 +28,7 @@ ArcIntersectionAdder::processIntersections(ArcString& e0, std::size_t segIndex0, } const geom::CircularArc& arc0 = e0.getArc(segIndex0); - const geom::CircularArc& arc1 = e1.getArc(segIndex0); + const geom::CircularArc& arc1 = e1.getArc(segIndex1); m_intersector.intersects(arc0, arc1); @@ -37,8 +38,8 @@ ArcIntersectionAdder::processIntersections(ArcString& e0, std::size_t segIndex0, // TODO handle cocircular intersections for (std::uint8_t i = 0; i < m_intersector.getNumPoints(); i++) { - detail::down_cast(&e0)->addIntersection(geom::CoordinateXYZM{m_intersector.getPoint(i)}, segIndex0); - detail::down_cast(&e1)->addIntersection(geom::CoordinateXYZM{m_intersector.getPoint(i)}, segIndex1); + detail::down_cast(&e0)->addIntersection(m_intersector.getPoint(i), segIndex0); + detail::down_cast(&e1)->addIntersection(m_intersector.getPoint(i), segIndex1); } } @@ -48,10 +49,10 @@ ArcIntersectionAdder::processIntersections(ArcString& e0, std::size_t segIndex0, // don't bother intersecting a segment with itself const geom::CircularArc& arc = e0.getArc(segIndex0); - const geom::CoordinateXY& q0 = e1.getCoordinate(segIndex1); - const geom::CoordinateXY& q1 = e1.getCoordinate(segIndex1 + 1); - m_intersector.intersects(arc, q0, q1); + // FIXME get useSegEndpoints from somewhere + constexpr bool useSegEndpoints = false; + m_intersector.intersects(arc, *e1.getCoordinates(), segIndex1, segIndex1 + 1, useSegEndpoints); if (m_intersector.getResult() == algorithm::CircularArcIntersector::NO_INTERSECTION) { return; @@ -75,12 +76,17 @@ ArcIntersectionAdder::processIntersections(SegmentString& e0, std::size_t segInd return; } + m_intersector.intersects(*e0.getCoordinates(), segIndex0, segIndex0 + 1, + *e1.getCoordinates(), segIndex1, segIndex1 + 1); + +#if 0 const CoordinateXY& p0 = e0.getCoordinate(segIndex0); const CoordinateXY& p1 = e0.getCoordinate(segIndex0 + 1); const CoordinateXY& q0 = e1.getCoordinate(segIndex1); const CoordinateXY& q1 = e1.getCoordinate(segIndex1 + 1); m_intersector.intersects(p0, p1, q0, q1); +#endif if (m_intersector.getResult() == algorithm::CircularArcIntersector::NO_INTERSECTION) { return; diff --git a/include/geos/noding/PathString.h b/src/noding/ArcString.cpp similarity index 51% copy from include/geos/noding/PathString.h copy to src/noding/ArcString.cpp index e1a3ba4b3..6095414c2 100644 --- a/include/geos/noding/PathString.h +++ b/src/noding/ArcString.cpp @@ -12,24 +12,11 @@ * **********************************************************************/ -#pragma once - -#include - -#include +#include namespace geos::noding { - -/// A PathString represents a contiguous line/arc to the used as an input for -/// noding. To access the coordinates, it is necessary to know whether they -/// represent a set of line segments (SegmentString) or circular arcs (ArcString). -class GEOS_DLL PathString { -public: - virtual ~PathString() = default; - - virtual std::size_t getSize() const = 0; - - virtual double getLength() const = 0; -}; - -} + std::unique_ptr + ArcString::releaseCoordinates() { + return std::move(m_seq); + } +} \ No newline at end of file diff --git a/src/noding/GeometryNoder.cpp b/src/noding/GeometryNoder.cpp index 46a5f0e04..0aaeeb89e 100644 --- a/src/noding/GeometryNoder.cpp +++ b/src/noding/GeometryNoder.cpp @@ -25,9 +25,13 @@ #include #include #include +#include +#include #include #include +#include +#include #include #include @@ -46,9 +50,9 @@ namespace { /** * Add every linear element in a geometry into SegmentString vector */ -class SegmentStringExtractor: public geom::GeometryComponentFilter { +class PathStringExtractor: public geom::GeometryComponentFilter { public: - SegmentStringExtractor(SegmentString::NonConstVect& to, + PathStringExtractor(std::vector> & to, bool constructZ, bool constructM) : _to(to) @@ -59,20 +63,31 @@ public: void filter_ro(const geom::Geometry* g) override { - const geom::LineString* ls = dynamic_cast(g); - if(ls) { + if(const auto* ls = dynamic_cast(g)) { auto coord = ls->getSharedCoordinates(); - SegmentString* ss = new NodedSegmentString(coord, _constructZ, _constructM, nullptr); - _to.push_back(ss); + // coord ownership transferred to SegmentString + auto ss = std::make_unique(coord, _constructZ, _constructM, nullptr); + _to.push_back(std::move(ss)); + } else if (const auto* cs = dynamic_cast(g)) { + auto coords = cs->getCoordinates(); + + // TODO: Store this vector in the CircularString ? + std::vector arcs; + for (std::size_t i = 0; i < coords->getSize() - 2; i += 2) { + arcs.emplace_back(*coords, i); + } + + auto as = std::make_unique(std::move(arcs), std::move(coords), _constructZ, _constructM, nullptr); + _to.push_back(std::move(as)); } } private: - SegmentString::NonConstVect& _to; + std::vector>& _to; bool _constructZ; bool _constructM; - SegmentStringExtractor(SegmentStringExtractor const&); /*= delete*/ - SegmentStringExtractor& operator=(SegmentStringExtractor const&); /*= delete*/ + PathStringExtractor(PathStringExtractor const&); /*= delete*/ + PathStringExtractor& operator=(PathStringExtractor const&); /*= delete*/ }; } @@ -89,14 +104,15 @@ GeometryNoder::node(const geom::Geometry& geom) /* public */ GeometryNoder::GeometryNoder(const geom::Geometry& g) : - argGeom(g) -{ - util::ensureNoCurvedComponents(argGeom); -} + argGeom(g), + argGeomHasCurves(g.hasCurvedComponents()) +{} + +GeometryNoder::~GeometryNoder() = default; /* private */ std::unique_ptr -GeometryNoder::toGeometry(std::vector>& nodedEdges) +GeometryNoder::toGeometry(std::vector>& nodedEdges) const { const geom::GeometryFactory* geomFact = argGeom.getFactory(); @@ -105,17 +121,30 @@ GeometryNoder::toGeometry(std::vector>& nodedEdge // Create a geometry out of the noded substrings. std::vector> lines; lines.reserve(nodedEdges.size()); - for(auto& ss : nodedEdges) { - const auto& coords = ss->getCoordinates(); - // Check if an equivalent edge is known - OrientedCoordinateArray oca1(*coords); - if(ocas.insert(oca1).second) { - lines.push_back(geomFact->createLineString(coords)); + bool resultArcs = false; + for(auto& path : nodedEdges) { + if (const auto* ss = dynamic_cast(path.get())) { + const auto& coords = ss->getCoordinates(); + + // Check if an equivalent edge is known + OrientedCoordinateArray oca1(*coords); + if(ocas.insert(oca1).second) { + lines.push_back(geomFact->createLineString(coords)); + } + } else { + resultArcs = true; + auto* as = dynamic_cast(path.get()); + // FIXME: check for duplicates + lines.push_back(geomFact->createCircularString(as->releaseCoordinates())); } } - return geomFact->createMultiLineString(std::move(lines)); + if (resultArcs) { + return geomFact->createMultiCurve(std::move(lines)); + } else { + return geomFact->createMultiLineString(std::move(lines)); + } } /* public */ @@ -125,49 +154,45 @@ GeometryNoder::getNoded() if (argGeom.isEmpty()) return argGeom.clone(); - std::vector p_lineList; - extractSegmentStrings(argGeom, p_lineList); + std::vector> p_lineList; + extractPathStrings(argGeom, p_lineList); - Noder& p_noder = getNoder(); - std::vector> nodedEdges; + ArcNoder& p_noder = getNoder(); + std::vector> nodedEdges; try { - p_noder.computeNodes(p_lineList); - nodedEdges = p_noder.getNodedSubstrings(); + p_noder.computePathNodes(PathString::toRawPointerVector(p_lineList)); + nodedEdges = p_noder.getNodedPaths(); } catch(const std::exception&) { - for(std::size_t i = 0, n = p_lineList.size(); i < n; ++i) { - delete p_lineList[i]; - } throw; } std::unique_ptr noded = toGeometry(nodedEdges); - for(auto* elem : p_lineList) { - delete elem; - } - return noded; } /* private static */ void -GeometryNoder::extractSegmentStrings(const geom::Geometry& g, - SegmentString::NonConstVect& to) +GeometryNoder::extractPathStrings(const geom::Geometry& g, + std::vector>& to) { - SegmentStringExtractor ex(to, g.hasZ(), g.hasM()); + PathStringExtractor ex(to, g.hasZ(), g.hasM()); g.apply_ro(&ex); } /* private */ -Noder& +ArcNoder& GeometryNoder::getNoder() { if(! noder.get()) { const geom::PrecisionModel* pm = argGeom.getFactory()->getPrecisionModel(); - IteratedNoder* in = new IteratedNoder(pm); - noder.reset(in); + if (argGeomHasCurves) { + noder = std::make_unique(pm, []() { return std::make_unique(); }); + } else { + noder = std::make_unique(pm); + } } return *noder; } diff --git a/src/noding/IteratedNoder.cpp b/src/noding/IteratedNoder.cpp index c88b76387..746828232 100644 --- a/src/noding/IteratedNoder.cpp +++ b/src/noding/IteratedNoder.cpp @@ -17,11 +17,14 @@ * **********************************************************************/ +#include #include #include #include #include +#include +#include #include #include #include @@ -31,47 +34,92 @@ #define GEOS_DEBUG 0 #endif -using namespace geos::geom; - namespace geos { namespace noding { // geos.noding +IteratedNoder::IteratedNoder(const geom::PrecisionModel* newPm, + std::function()> noderFunction) + : + pm(newPm), + li(pm), + maxIter(MAX_ITER), + m_noderFunction(noderFunction) +{ +} + +std::unique_ptr +IteratedNoder::createDefaultNoder() +{ + return std::make_unique(); +} + +IteratedNoder::~IteratedNoder() = default; + /* private */ void -IteratedNoder::node(const std::vector& segStrings, +IteratedNoder::node(const std::vector& pathStrings, int& numInteriorIntersections, - CoordinateXY& intersectionPoint) + geom::CoordinateXY& intersectionPoint) { - IntersectionAdder si(li); - MCIndexNoder noder; - noder.setSegmentIntersector(&si); - noder.computeNodes(segStrings); - auto updatedSegStrings = noder.getNodedSubstrings(); - nodedSegStrings = std::move(updatedSegStrings); - numInteriorIntersections = si.numInteriorIntersections; - if (si.hasProperInteriorIntersection()) { - intersectionPoint = si.getProperIntersectionPoint(); + auto noder = m_noderFunction(); + if (auto* spn = dynamic_cast(noder.get())) { + IntersectionAdder si(li); + spn->setSegmentIntersector(&si); + // TODO need to have previously checked that all inputs are SegmentStrings + + std::vector segStrings(pathStrings.size()); + for (size_t i = 0; i < pathStrings.size(); i++) { + segStrings[i] = detail::down_cast(pathStrings[i]); + } + + noder->computeNodes(segStrings); + + auto nodedSegStrings = noder->getNodedSubstrings(); + nodedPaths.resize(nodedSegStrings.size()); + for (size_t i = 0; i < nodedSegStrings.size(); i++) { + nodedPaths[i].reset(nodedSegStrings[i].release()); + } + + numInteriorIntersections = si.numInteriorIntersections; + + if (si.hasProperInteriorIntersection()) { + intersectionPoint = si.getProperIntersectionPoint(); + } + } else { + auto* arcNoder = detail::down_cast(noder.get()); + // FIXME aia should take a PrecsionModel / LineIntersector? + auto aia = std::make_unique(); + arcNoder->setArcIntersector(std::move(aia)); + arcNoder->computePathNodes(pathStrings); + nodedPaths = arcNoder->getNodedPaths(); + + // FIXME use actual number! + numInteriorIntersections = 0; + + + // numInteriorIntersections? + // intesectionPoint? } } /* public */ void -IteratedNoder::computeNodes(const std::vector& segStrings) +IteratedNoder::computePathNodes(const std::vector& paths) { int numInteriorIntersections; int nodingIterationCount = 0; int lastNodesCreated = -1; - CoordinateXY intersectionPoint = CoordinateXY::getNull(); + geom::CoordinateXY intersectionPoint = geom::CoordinateXY::getNull(); bool firstPass = true; do { // NOTE: will change this.nodedSegStrings if (firstPass) { - node(segStrings, numInteriorIntersections, intersectionPoint); + node(paths, numInteriorIntersections, intersectionPoint); firstPass = false; } else { - auto nodingInput = SegmentString::toRawPointerVector(nodedSegStrings); + auto nodingInput = PathString::toRawPointerVector(nodedPaths); node(nodingInput, numInteriorIntersections, intersectionPoint); } diff --git a/src/noding/NodableArcString.cpp b/src/noding/NodableArcString.cpp new file mode 100644 index 000000000..331669349 --- /dev/null +++ b/src/noding/NodableArcString.cpp @@ -0,0 +1,133 @@ +/********************************************************************** + * + * GEOS - Geometry Engine Open Source + * http://geos.osgeo.org + * + * Copyright (C) 2025 ISciences, LLC + * + * This is free software; you can redistribute and/or modify it under + * the terms of the GNU Lesser General Public Licence as published + * by the Free Software Foundation. + * See the COPYING file for more information. + * + **********************************************************************/ + +#include + +namespace geos::noding { + +static double +pseudoAngleDiffCCW(double paStart, double pa) { + double diff = pa - paStart; + + if (diff <= 0) { + diff += 4; + } + + return diff; +} + +NodableArcString::NodableArcString(std::vector arcs, std::unique_ptr coords, bool constructZ, bool constructM, void* context) : + ArcString(std::move(arcs), std::move(coords), context), + m_constructZ(constructZ), + m_constructM(constructM) +{ +} + + +//std::unique_ptr clone() const { +// +//} + +std::unique_ptr +NodableArcString::getNoded() { + + auto dstSeq = std::make_unique(0, m_constructZ, m_constructM); + + //if (m_adds.empty()) { + // return clone(); + //} + + std::vector arcs; + for (size_t i = 0; i < m_arcs.size(); i++) { + if (const auto it = m_adds.find(i); it == m_adds.end()) { + // No nodes added, just copy the coordinates into the sequence. + const geom::CoordinateSequence* srcSeq = m_arcs[i].getCoordinateSequence(); + std::size_t srcPos = m_arcs[i].getCoordinatePosition(); + std::size_t dstPos = dstSeq->getSize(); + dstSeq->add(*srcSeq, srcPos, srcPos + 2, false); + arcs.emplace_back(*dstSeq, dstPos); + } else { + std::vector& splitPoints = it->second; + + // TODO check split point actually inside arc? + + const geom::CircularArc& toSplit = m_arcs[i]; + const geom::CoordinateXY& center = toSplit.getCenter(); + const double radius = toSplit.getRadius(); + const int orientation = toSplit.getOrientation(); + const bool isCCW = orientation == algorithm::Orientation::COUNTERCLOCKWISE; + const double paStart = geom::Quadrant::pseudoAngle(center, toSplit.p0()); + + std::sort(splitPoints.begin(), splitPoints.end(), [¢er, paStart, isCCW](const auto& p0, const auto& p1) { + double pa0 = geom::Quadrant::pseudoAngle(center, p0); + double pa1 = geom::Quadrant::pseudoAngle(center, p1); + + if (isCCW) { + return pseudoAngleDiffCCW(paStart, pa0) < pseudoAngleDiffCCW(paStart, pa1); + } else { + return pseudoAngleDiffCCW(paStart, pa0) > pseudoAngleDiffCCW(paStart, pa1); + } + }); + +#if 0 + std::cout << "Splitting " << toSplit.toString() << " " << (isCCW ? "CCW" : "CW") << " paStart " << paStart << " paStop " << geom::Quadrant::pseudoAngle(center, toSplit.p2()) << std::endl; + for (const auto& splitPt : splitPoints) + { + const double pa = geom::Quadrant::pseudoAngle(center, splitPt); + std::cout << " " << splitPt.toString() << " (pa " << pa << " diff " << pseudoAngleDiffCCW(paStart, pa) << ")" << std::endl; + } +#endif + + // Add first point of split arc + std::size_t dstPos = dstSeq->getSize(); + dstSeq->add(*toSplit.getCoordinateSequence(), toSplit.getCoordinatePosition(), toSplit.getCoordinatePosition()); + geom::CoordinateXYZM p0, p2; + dstSeq->getAt(dstPos, p0); + + // Add intermediate points of split arc + for (const auto& splitPoint : splitPoints) { + if (!arcs.empty() && splitPoint.equals2D(arcs.back().p2())) { + continue; + } + + geom::CoordinateXYZM midpoint(algorithm::CircularArcs::getMidpoint(p0, splitPoint, center, radius, isCCW)); + midpoint.z = (p0.z + splitPoint.z) / 2; + midpoint.m = (p0.m + splitPoint.m) / 2; + + dstSeq->add(midpoint); + dstSeq->add(splitPoint); + + p0 = splitPoint; + + arcs.emplace_back(*dstSeq, dstPos, center, radius, orientation); + dstPos = dstSeq->getSize() - 1; + } + + // Add last point of split arc + toSplit.getCoordinateSequence()->getAt(toSplit.getCoordinatePosition() + 2, p2); + + geom::CoordinateXYZM midpoint(algorithm::CircularArcs::getMidpoint(p0, p2, center, radius, isCCW)); + midpoint.z = (p0.z + p2.z) / 2; + midpoint.m = (p0.m + p2.m) / 2; + + dstSeq->add(midpoint); + dstSeq->add(p2); + arcs.emplace_back(*dstSeq, dstPos, center, radius, orientation); + } + } + + return std::make_unique(std::move(arcs), std::move(dstSeq), m_constructZ, m_constructM, nullptr); + } + +} \ No newline at end of file diff --git a/include/geos/noding/PathString.h b/src/noding/PathString.cpp similarity index 52% copy from include/geos/noding/PathString.h copy to src/noding/PathString.cpp index e1a3ba4b3..af4760a09 100644 --- a/include/geos/noding/PathString.h +++ b/src/noding/PathString.cpp @@ -12,24 +12,19 @@ * **********************************************************************/ -#pragma once - -#include - -#include +#include +#include namespace geos::noding { -/// A PathString represents a contiguous line/arc to the used as an input for -/// noding. To access the coordinates, it is necessary to know whether they -/// represent a set of line segments (SegmentString) or circular arcs (ArcString). -class GEOS_DLL PathString { -public: - virtual ~PathString() = default; - - virtual std::size_t getSize() const = 0; - - virtual double getLength() const = 0; -}; - +std::vector +PathString::toRawPointerVector(const std::vector> & pathStrings) +{ + std::vector ret(pathStrings.size()); + for (std::size_t i = 0; i < pathStrings.size(); i++) { + ret[i] = pathStrings[i].get(); + } + return ret; } + +} \ No newline at end of file diff --git a/src/noding/SegmentString.cpp b/src/noding/SegmentString.cpp index b5a56c70f..4416116b8 100644 --- a/src/noding/SegmentString.cpp +++ b/src/noding/SegmentString.cpp @@ -62,6 +62,15 @@ SegmentString::toRawPointerVector(const std::vector +SegmentString::toRawPointerVector(const std::vector> & segStrings) { + std::vector ret(segStrings.size()); + for (std::size_t i = 0; i < segStrings.size(); i++) { + ret[i] = detail::down_cast(segStrings[i].get()); + } + return ret; +} + } // namespace geos.noding } // namespace geos diff --git a/src/noding/SimpleNoder.cpp b/src/noding/SimpleNoder.cpp index 7fc2007f3..496de6070 100644 --- a/src/noding/SimpleNoder.cpp +++ b/src/noding/SimpleNoder.cpp @@ -67,7 +67,6 @@ SimpleNoder::computePathNodes(const std::vector& inputPathStrings) for (auto* edge0: m_pathStrings) { for (auto* edge1: m_pathStrings) { - // TODO skip processing against self? computeIntersects(*edge0, *edge1); } } diff --git a/tests/unit/algorithm/CircularArcIntersectorTest.cpp b/tests/unit/algorithm/CircularArcIntersectorTest.cpp index 2f9e51bdc..dc07a91aa 100644 --- a/tests/unit/algorithm/CircularArcIntersectorTest.cpp +++ b/tests/unit/algorithm/CircularArcIntersectorTest.cpp @@ -5,9 +5,16 @@ #include #include +#include "geos/util.h" + using geos::algorithm::CircularArcIntersector; using geos::algorithm::Orientation; +using geos::geom::Ordinate; +using geos::geom::Coordinate; +using geos::geom::CoordinateSequence; using geos::geom::CoordinateXY; +using geos::geom::CoordinateXYM; +using geos::geom::CoordinateXYZM; using geos::geom::CircularArc; using geos::MATH_PI; @@ -15,102 +22,236 @@ namespace tut { struct test_circulararcintersector_data { - using ArcOrPoint = std::variant; + using XY = CoordinateXY; + using XYZ = Coordinate; + using XYM = CoordinateXYM; + using XYZM = CoordinateXYZM; + + static constexpr double NaN = geos::DoubleNotANumber; + + using ArcOrPoint = std::variant; static std::string to_string(CircularArcIntersector::intersection_type t) { switch (t) { - case geos::algorithm::CircularArcIntersector::NO_INTERSECTION: + case CircularArcIntersector::NO_INTERSECTION: return "no intersection"; - case geos::algorithm::CircularArcIntersector::ONE_POINT_INTERSECTION: + case CircularArcIntersector::ONE_POINT_INTERSECTION: return "one-point intersection"; - case geos::algorithm::CircularArcIntersector::TWO_POINT_INTERSECTION: + case CircularArcIntersector::TWO_POINT_INTERSECTION: return "two-point intersection"; - case geos::algorithm::CircularArcIntersector::COCIRCULAR_INTERSECTION: + case CircularArcIntersector::COCIRCULAR_INTERSECTION: return "cocircular intersection"; - break; } return ""; } - static std::string toWKT(const CoordinateXY& pt) + static std::string toWKT(const CoordinateXYZM& pt) { - return "POINT (" + pt.toString() + ")"; + const bool hasZ = !std::isnan(pt.z); + const bool hasM = !std::isnan(pt.m); + + std::stringstream ss; + ss << "POINT "; + if (hasZ) { + ss << "Z"; + } + if (hasM) { + ss << "M"; + } + if (hasZ || hasM) { + ss << " "; + } + ss << "("; + ss << pt.x << " " << pt.y; + if (hasZ) { + ss << " " << pt.z; + } + if (hasM) { + ss << " " << pt.m; + } + ss << ")"; + + return ss.str(); } static std::string toWKT(const CircularArc& arc) { - return "CIRCULARSTRING (" + arc.p0.toString() + ", " + arc.p1.toString() + ", " + arc.p2.toString() + ")"; + return arc.toString(); } - static std::string toWKT(const geos::geom::LineSegment& seg) + static std::string toWKT(const CoordinateSequence & seg) { - return "LINESTRING (" + seg.p0.toString() + ", " + seg.p1.toString() + ")"; + std::stringstream ss; + + ss << "LINESTRING ("; + seg.applyAt(0, [&ss](const auto& pt) { + ss << pt << ", " << *(&pt + 1); + }); + ss << ")"; + + return ss.str(); } - static void checkIntersection(CoordinateXY p0, CoordinateXY p1, CoordinateXY p2, - CoordinateXY q0, CoordinateXY q1, CoordinateXY q2, + template + static void checkIntersection(C1 p0, C1 p1, C1 p2, + C2 q0, C2 q1, C2 q2, CircularArcIntersector::intersection_type result, - ArcOrPoint i0 = CoordinateXY::getNull(), - ArcOrPoint i1 = CoordinateXY::getNull()) + const ArcOrPoint& i0 = CoordinateXYZM::getNull(), + const ArcOrPoint& i1 = CoordinateXYZM::getNull()) { - CircularArc a0(p0, p1, p2); - CircularArc a1(q0, q1, q2); + CoordinateSequence cs1(3, C1::template has(), C1::template has()); + cs1.setAt(p0, 0); + cs1.setAt(p1, 1); + cs1.setAt(p2, 2); + + CoordinateSequence cs2(3, C2::template has(), C2::template has()); + cs2.setAt(q0, 0); + cs2.setAt(q1, 1); + cs2.setAt(q2, 2); + + const CircularArc a0(cs1, 0); + const CircularArc a1(cs2, 0); checkIntersection(a0, a1, result, i0, i1); } - static void checkIntersection(CoordinateXY p0, CoordinateXY p1, CoordinateXY p2, - CoordinateXY q0, CoordinateXY q1, - CircularArcIntersector::intersection_type result, - CoordinateXY i0 = CoordinateXY::getNull(), - CoordinateXY i1 = CoordinateXY::getNull()) + template + static void checkIntersectionArcSeg(C1 p0, C1 p1, C1 p2, + C2 q0, C2 q1, + CircularArcIntersector::intersection_type result, + const ArcOrPoint& i0 = CoordinateXYZM::getNull(), + const ArcOrPoint& i1 = CoordinateXYZM::getNull()) { - CircularArc a(p0, p1, p2); - geos::geom::LineSegment s(geos::geom::Coordinate{q0}, geos::geom::Coordinate{q1}); + CoordinateSequence cs1(3, C1::template has(), C1::template has()); + cs1.setAt(p0, 0); + cs1.setAt(p1, 1); + cs1.setAt(p2, 2); - checkIntersection(a, s, result, i0, i1); + const CircularArc arc(cs1, 0); + + CoordinateSequence seg(2, C2::template has(), C2::template has()); + seg.setAt(q0, 0); + seg.setAt(q1, 1); + + checkIntersection(arc, seg, result, i0, i1); } - static bool pointWithinTolerance(const CoordinateXY& actual, const CoordinateXY& expected, double tol) + template + static void checkIntersectionSegArc(C1 p0, C1 p1, + C2 q0, C2 q1, C2 q2, + CircularArcIntersector::intersection_type result, + const ArcOrPoint& i0 = CoordinateXYZM::getNull(), + const ArcOrPoint& i1 = CoordinateXYZM::getNull()) { + CoordinateSequence seg(2, C1::template has(), C1::template has()); + seg.setAt(p0, 0); + seg.setAt(p1, 1); + + + CoordinateSequence arcSeq(3, C2::template has(), C2::template has()); + arcSeq.setAt(q0, 0); + arcSeq.setAt(q1, 1); + arcSeq.setAt(q2, 2); + const CircularArc arc(arcSeq, 0); + + checkIntersection(arc, seg, result, i0, i1, true); + } + + template + static void checkIntersectionSegSeg(C1 p0, C1 p1, + C2 q0, C2 q1, + CircularArcIntersector::intersection_type result, + const ArcOrPoint& i0 = CoordinateXYZM::getNull(), + const ArcOrPoint& i1 = CoordinateXYZM::getNull()) + { + CoordinateSequence seg0(2, C1::template has(), C1::template has()); + seg0.setAt(p0, 0); + seg0.setAt(p1, 1); + + CoordinateSequence seg1(2, C2::template has(), C2::template has()); + seg1.setAt(q0, 0); + seg1.setAt(q1, 1); + + checkIntersection(seg0, seg1, result, i0, i1, true); + } + + + static bool pointWithinTolerance(const CoordinateXYZM& actual, const CoordinateXYZM& expected, double tol) + { + if (std::isnan(actual.z) != std::isnan(expected.z)) { + return false; + } + + if (std::isnan(actual.m) != std::isnan(expected.m)) { + return false; + } + + if (!std::isnan(expected.z) && std::abs(actual.z - expected.z) > tol * std::abs(expected.z)) { + return false; + } + + if (!std::isnan(expected.m) && std::abs(actual.m - expected.m) > tol * std::abs(expected.m)) { + return false; + } + if (actual.distance(expected) < tol) { return true; } - return std::abs(actual.x - expected.x) < tol * std::abs(actual.x) && - std::abs(actual.y - expected.y) < tol * std::abs(actual.y); + if (std::abs(actual.x - expected.x) > tol * std::abs(expected.x)) { + return false; + } + + if (std::abs(actual.y - expected.y) > tol * std::abs(expected.y)) { + return false; + } + + return true; } - template - static void checkIntersection(const CircularArc& a0, - const CircularArcOrLineSegment& a1, + template + static void checkIntersection(const T1& a0, + const T2& a1, CircularArcIntersector::intersection_type result, - ArcOrPoint p0 = CoordinateXY::getNull(), - ArcOrPoint p1 = CoordinateXY::getNull()) + const ArcOrPoint& p0 = CoordinateXYZM::getNull(), + const ArcOrPoint& p1 = CoordinateXYZM::getNull(), + bool useSegEndpoints=false) { CircularArcIntersector cai; - cai.intersects(a0, a1); + if constexpr (std::is_same_v) + if constexpr (std::is_same_v) { + geos::ignore_unused_variable_warning(useSegEndpoints); // needed for gcc 10 + cai.intersects(a0, a1); + } else { + cai.intersects(a0, a1, 0, 1, useSegEndpoints); + } else { + static_assert(std::is_same_v); + geos::ignore_unused_variable_warning(useSegEndpoints); // needed for gcc 10 + cai.intersects(a0, 0, 1, a1, 0, 1); + } ensure_equals("incorrect intersection type between " + toWKT(a0) + " and " + toWKT(a1), to_string(cai.getResult()), to_string(result)); - std::vector expectedPoints; + std::vector expectedPoints; std::vector expectedArcs; for (const auto& intersection : { p0, p1 }) { - if (std::holds_alternative(intersection)) { - const CoordinateXY& pt = std::get(intersection); - if (!pt.isNull()) { - expectedPoints.push_back(pt); + std::visit([&expectedArcs, &expectedPoints](const auto& isect) { + using IntersectionType = std::decay_t; + + if constexpr (std::is_same_v) { + expectedArcs.push_back(isect); + } else { + if (!isect.isNull()) { + expectedPoints.push_back(CoordinateXYZM(isect)); + } } - } - else { - expectedArcs.push_back(std::get(intersection)); - } + }, intersection); } - std::vector actualPoints; + std::vector actualPoints; std::vector actualArcs; for (std::uint8_t i = 0; i < cai.getNumPoints(); i++) { @@ -122,12 +263,11 @@ struct test_circulararcintersector_data { } auto compareArcs = [](const CircularArc& a, const CircularArc& b) { - int cmp; - cmp = a.p0.compareTo(b.p0); + int cmp = a.p0().compareTo(b.p0()); if (cmp != 0) { return cmp == -1; } - cmp = a.p2.compareTo(b.p2); + cmp = a.p2().compareTo(b.p2()); if (cmp != 0) { return cmp == -1; } @@ -168,15 +308,23 @@ struct test_circulararcintersector_data { equal = false; } - if (!pointWithinTolerance(actualArcs[i].getCenter(), expectedArcs[i].getCenter(), eps)) { + if (!pointWithinTolerance(XYZM(actualArcs[i].getCenter()), XYZM(expectedArcs[i].getCenter()), eps)) { equal = false; } - if (!pointWithinTolerance(actualArcs[i].p0, expectedArcs[i].p0, eps)) { + XYZM actual0, expected0; + actualArcs[i].getCoordinateSequence()->getAt(actualArcs[i].getCoordinatePosition(), actual0); + expectedArcs[i].getCoordinateSequence()->getAt(expectedArcs[i].getCoordinatePosition(), expected0); + + if (!pointWithinTolerance(actual0, expected0, eps)) { equal = false; } - if (!pointWithinTolerance(actualArcs[i].p2, expectedArcs[i].p2, eps)) { + XYZM actual2, expected2; + actualArcs[i].getCoordinateSequence()->getAt(actualArcs[i].getCoordinatePosition(), actual2); + expectedArcs[i].getCoordinateSequence()->getAt(expectedArcs[i].getCoordinatePosition(), expected2); + + if (!pointWithinTolerance(actual2, expected2, eps)) { equal = false; } } @@ -214,17 +362,17 @@ struct test_circulararcintersector_data { expected += toWKT(arc); } - ensure_equals(actual, expected); + ensure_equals("incorrect intersection loc between " + toWKT(a0) + " and " + toWKT(a1), actual, expected); } - const CoordinateXY _NW = { -std::sqrt(2)/2, std::sqrt(2)/2 }; - const CoordinateXY _N = { 0, 1}; - const CoordinateXY _NE = { std::sqrt(2)/2, std::sqrt(2)/2 }; - const CoordinateXY _E = { 1, 0}; - const CoordinateXY _SE = { std::sqrt(2)/2, -std::sqrt(2)/2 }; - const CoordinateXY _S = { 0, -1}; - const CoordinateXY _SW = { -std::sqrt(2)/2, -std::sqrt(2)/2 }; - const CoordinateXY _W = { -1, 0}; + const CoordinateXY NW_ = { -std::sqrt(2)/2, std::sqrt(2)/2 }; + const CoordinateXY N_ = { 0, 1}; + const CoordinateXY NE_ = { std::sqrt(2)/2, std::sqrt(2)/2 }; + const CoordinateXY E_ = { 1, 0}; + const CoordinateXY SE_ = { std::sqrt(2)/2, -std::sqrt(2)/2 }; + const CoordinateXY S_ = { 0, -1}; + const CoordinateXY SW_ = { -std::sqrt(2)/2, -std::sqrt(2)/2 }; + const CoordinateXY W_ = { -1, 0}; }; using group = test_group; @@ -238,10 +386,12 @@ void object::test<1>() { set_test_name("interior/interior intersection (one point)"); - checkIntersection({0, 0}, {1, std::sqrt(3)}, {2, 2}, - {0, 2}, {1, std::sqrt(3)}, {2, 0}, - CircularArcIntersector::ONE_POINT_INTERSECTION, - CoordinateXY{1, std::sqrt(3)}); + checkIntersection( + XY{0, 0}, XY{1, std::sqrt(3)}, XY{2, 2}, + XY{0, 2}, XY{1, std::sqrt(3)}, XY{2, 0}, + CircularArcIntersector::ONE_POINT_INTERSECTION, + XY{1, std::sqrt(3)} + ); } template<> @@ -251,11 +401,13 @@ void object::test<2>() set_test_name("interior/interior intersection (two points)"); // result from CGAL 5.4 - checkIntersection({0, 0}, {2, 2}, {4, 0}, - {0, 1}, {2, -1}, {4, 1}, - CircularArcIntersector::TWO_POINT_INTERSECTION, - CoordinateXY{0.0635083268962914893, 0.5}, - CoordinateXY{3.93649167310370851, 0.5}); + checkIntersection( + XY{0, 0}, XY{2, 2}, XY{4, 0}, + XY{0, 1}, XY{2, -1}, XY{4, 1}, + CircularArcIntersector::TWO_POINT_INTERSECTION, + XY{0.0635083268962914893, 0.5}, + XY{3.93649167310370851, 0.5} + ); } template<> @@ -264,10 +416,12 @@ void object::test<3>() { set_test_name("single endpoint-endpoint intersection"); - checkIntersection({0, 0}, {1, 1}, {2, 0}, - {2, 0}, {3, -1}, {4, 0}, - CircularArcIntersector::ONE_POINT_INTERSECTION, - CoordinateXY{2, 0}); + checkIntersection( + XY{0, 0}, XY{1, 1}, XY{2, 0}, + XY{2, 0}, XY{3, -1}, XY{4, 0}, + CircularArcIntersector::ONE_POINT_INTERSECTION, + XY{2, 0} + ); } template<> @@ -276,10 +430,12 @@ void object::test<4>() { set_test_name("single interior-interior intersection at point of tangency"); - checkIntersection({0, 0}, {1, 1}, {2, 0}, - {0, 2}, {1, 1}, {2, 2}, - CircularArcIntersector::ONE_POINT_INTERSECTION, - CoordinateXY{1, 1}); + checkIntersection( + XY{0, 0}, XY{1, 1}, XY{2, 0}, + XY{0, 2}, XY{1, 1}, XY{2, 2}, + CircularArcIntersector::ONE_POINT_INTERSECTION, + XY{1, 1} + ); } template<> @@ -288,9 +444,11 @@ void object::test<5>() { set_test_name("supporting circles intersect but arcs do not"); - checkIntersection({0, 0}, {2, 2}, {4, 0}, - {1, 1}, {0, -1}, {-1, 1}, - CircularArcIntersector::NO_INTERSECTION); + checkIntersection( + XY{0, 0}, XY{2, 2}, XY{4, 0}, + XY{1, 1}, XY{0, -1}, XY{-1, 1}, + CircularArcIntersector::NO_INTERSECTION + ); } @@ -300,9 +458,11 @@ void object::test<6>() { set_test_name("one circle contained within other"); - checkIntersection({0, 0}, {4, 4}, {8, 0}, - {2, 0}, {4, 2}, {6, 0}, - CircularArcIntersector::NO_INTERSECTION); + checkIntersection( + XY{0, 0}, XY{4, 4}, XY{8, 0}, + XY{2, 0}, XY{4, 2}, XY{6, 0}, + CircularArcIntersector::NO_INTERSECTION + ); } template<> @@ -311,10 +471,12 @@ void object::test<7>() { set_test_name("cocircular with double endpoint intersection"); - checkIntersection({0, 0}, {1, 1}, {2, 0}, - {0, 0}, {1, -1}, {2, 0}, - CircularArcIntersector::TWO_POINT_INTERSECTION, - CoordinateXY{0, 0}, CoordinateXY{2, 0}); + checkIntersection( + XY{0, 0}, XY{1, 1}, XY{2, 0}, + XY{0, 0}, XY{1, -1}, XY{2, 0}, + CircularArcIntersector::TWO_POINT_INTERSECTION, + XY{0, 0}, XY{2, 0} + ); } template<> @@ -323,10 +485,12 @@ void object::test<8>() { set_test_name("cocircular with single endpoint intersection"); - checkIntersection({-2, 0}, {0, 2}, {2, 0}, - {0, -2}, {std::sqrt(2), -std::sqrt(2)}, {2, 0}, - CircularArcIntersector::ONE_POINT_INTERSECTION, - CoordinateXY{2, 0}); + checkIntersection( + XY{-2, 0}, XY{0, 2}, XY{2, 0}, + XY{0, -2}, XY{std::sqrt(2), -std::sqrt(2)}, XY{2, 0}, + CircularArcIntersector::ONE_POINT_INTERSECTION, + XY{2, 0} + ); } template<> @@ -335,8 +499,8 @@ void object::test<9>() { set_test_name("cocircular disjoint"); - checkIntersection(_NW, _N, _NE, - _SW, _S, _SE, + checkIntersection(NW_, N_, NE_, + SW_, S_, SE_, CircularArcIntersector::NO_INTERSECTION); } @@ -346,10 +510,12 @@ void object::test<10>() { set_test_name("cocircular with single arc intersection (clockwise)"); - checkIntersection({-5, 0}, {0, 5}, {5, 0}, // CW - {-4, 3}, {0, 5}, {4, 3}, // CW - CircularArcIntersector::COCIRCULAR_INTERSECTION, - CircularArc{{-4, 3}, {0, 5}, {4, 3}}); // CW + checkIntersection( + XY{-5, 0}, XY{0, 5}, XY{5, 0}, // CW + XY{-4, 3}, XY{0, 5}, XY{4, 3}, // CW + CircularArcIntersector::COCIRCULAR_INTERSECTION, + CircularArc::create(XY{-4, 3}, {0, 5}, {4, 3}) + ); // CW } template<> @@ -358,10 +524,12 @@ void object::test<11>() { set_test_name("cocircular with single arc intersection (counter-clockwise)"); - checkIntersection({5, 0}, {0, 5}, {-5, 0}, // CCW - {-4, 3}, {0, 5}, {4, 3}, // CW - CircularArcIntersector::COCIRCULAR_INTERSECTION, - CircularArc{{4, 3}, {0, 5}, {-4, 3}}); // CCW + checkIntersection( + XY{5, 0}, XY{0, 5}, XY{-5, 0}, // CCW + XY{-4, 3}, XY{0, 5}, XY{4, 3}, // CW + CircularArcIntersector::COCIRCULAR_INTERSECTION, + CircularArc::create(XY{4, 3}, {0, 5}, {-4, 3}) + ); // CCW } template<> @@ -370,11 +538,13 @@ void object::test<12>() { set_test_name("cocircular with arc and point intersections"); - checkIntersection({-5, 0}, {0, 5}, {5, 0}, - {5, 0}, {0, -5}, {0, 5}, - CircularArcIntersector::COCIRCULAR_INTERSECTION, - CircularArc{{-5, 0}, {-5*std::sqrt(2)/2, 5*std::sqrt(2)/2}, {0, 5}}, - CoordinateXY{5, 0}); + checkIntersection( + XY{-5, 0}, XY{0, 5}, XY{5, 0}, + XY{5, 0}, XY{0, -5}, XY{0, 5}, + CircularArcIntersector::COCIRCULAR_INTERSECTION, + CircularArc::create(XY{-5, 0}, {-5 * std::sqrt(2) / 2, 5 * std::sqrt(2) / 2}, {0, 5}), + XY{5, 0} + ); } template<> @@ -383,11 +553,13 @@ void object::test<13>() { set_test_name("cocircular with two arc intersections"); - checkIntersection({-5, 0}, {0, 5}, {5, 0}, - {3, 4}, {0, -5}, {-3, 4}, - CircularArcIntersector::COCIRCULAR_INTERSECTION, - CircularArc{{3, 4}, {4.4721359549995796, 2.2360679774997898}, {5, 0}}, - CircularArc{{-5, 0}, {-4.4721359549995796, 2.2360679774997907}, {-3, 4}}); + checkIntersection( + XY{-5, 0}, XY{0, 5}, XY{5, 0}, + XY{3, 4}, XY{0, -5}, XY{-3, 4}, + CircularArcIntersector::COCIRCULAR_INTERSECTION, + CircularArc::create(XY{3, 4}, {4.4721359549995796, 2.2360679774997898}, {5, 0}), + CircularArc::create(XY{-5, 0}, {-4.4721359549995796, 2.2360679774997907}, {-3, 4}) + ); } template<> @@ -396,15 +568,15 @@ void object::test<20>() { set_test_name("arc - degenerate arc with single interior intersection"); - checkIntersection({0, 0}, {2, 2}, {4, 0}, // CW arc - {-1, -4}, {1, 0}, {3, 4}, // degenerate arc + checkIntersection(XY{0, 0}, XY{2, 2}, XY{4, 0}, // CW arc + XY{-1, -4}, XY{1, 0}, XY{3, 4}, // degenerate arc CircularArcIntersector::ONE_POINT_INTERSECTION, - CoordinateXY{2, 2}); + XY{2, 2}); - checkIntersection({-1, -4}, {1, 0}, {3, 4}, // degenerate arc - {0, 0}, {2, 2}, {4, 0}, // CW arc + checkIntersection(XY{-1, -4}, XY{1, 0}, XY{3, 4}, // degenerate arc + XY{0, 0}, XY{2, 2}, XY{4, 0}, // CW arc CircularArcIntersector::ONE_POINT_INTERSECTION, - CoordinateXY{2, 2}); + XY{2, 2}); } template<> @@ -413,10 +585,10 @@ void object::test<21>() { set_test_name("two degenerate arcs with single interior intersection"); - checkIntersection({0, 0}, {4, 4}, {10, 10}, - {10, 0}, {1, 9}, {0, 10}, + checkIntersection(XY{0, 0}, XY{4, 4}, XY{10, 10}, + XY{10, 0}, XY{1, 9}, XY{0, 10}, CircularArcIntersector::ONE_POINT_INTERSECTION, - CoordinateXY{5, 5}); + XY{5, 5}); } template<> @@ -425,10 +597,12 @@ void object::test<30>() { set_test_name("arc-segment with single interior intersection"); - checkIntersection({0, 0}, {2, 2}, {4, 0}, - {1, 0}, {3, 4}, - CircularArcIntersector::ONE_POINT_INTERSECTION, - {2, 2}); + checkIntersectionArcSeg( + XY{0, 0}, XY{2, 2}, XY{4, 0}, + XY{1, 0}, XY{3, 4}, + CircularArcIntersector::ONE_POINT_INTERSECTION, + XY{2, 2} + ); } template<> @@ -437,10 +611,12 @@ void object::test<31>() { set_test_name("arc-vertical segment with single interior intersection"); - checkIntersection({-2, 0}, {0, 2}, {2, 0}, - {0, 0}, {0, 4}, - CircularArcIntersector::ONE_POINT_INTERSECTION, - {0, 2}); + checkIntersectionArcSeg( + XY{-2, 0}, XY{0, 2}, XY{2, 0}, + XY{0, 0}, XY{0, 4}, + CircularArcIntersector::ONE_POINT_INTERSECTION, + XY{0, 2} + ); } template<> @@ -449,10 +625,12 @@ void object::test<32>() { set_test_name("arc-segment with two interior intersections"); - checkIntersection(_W, _E, _SW, - {-10, 10}, {10, -10}, - CircularArcIntersector::TWO_POINT_INTERSECTION, - _NW, _SE); + checkIntersectionArcSeg( + W_, E_, SW_, + XY{-10, 10}, XY{10, -10}, + CircularArcIntersector::TWO_POINT_INTERSECTION, + NW_, SE_ + ); } template<> @@ -461,10 +639,12 @@ void object::test<33>() { set_test_name("arc-vertical segment with two interior intersections"); - checkIntersection(_W, _E, _SW, - {0, -2}, {0, 2}, - CircularArcIntersector::TWO_POINT_INTERSECTION, - _S, _N); + checkIntersectionArcSeg( + W_, E_, SW_, + XY{0, -2}, XY{0, 2}, + CircularArcIntersector::TWO_POINT_INTERSECTION, + S_, N_ + ); } template<> @@ -473,9 +653,11 @@ void object::test<34>() { set_test_name("arc-segment disjoint with bbox containment"); - checkIntersection(_W, _N, _E, - {0, 0}, {0.2, 0.2}, - CircularArcIntersector::NO_INTERSECTION); + checkIntersectionArcSeg( + W_, N_, E_, + XY{0, 0}, XY{0.2, 0.2}, + CircularArcIntersector::NO_INTERSECTION + ); } template<> @@ -484,10 +666,12 @@ void object::test<35>() { set_test_name("degenerate arc-segment with interior intersection"); - checkIntersection({-5, -5}, {0, 0}, {5, 5}, - {-5, 5}, {5, -5}, - CircularArcIntersector::ONE_POINT_INTERSECTION, - {0, 0}); + checkIntersectionArcSeg( + XY{-5, -5}, XY{0, 0}, XY{5, 5}, + XY{-5, 5}, XY{5, -5}, + CircularArcIntersector::ONE_POINT_INTERSECTION, + XY{0, 0} + ); } template<> @@ -496,10 +680,12 @@ void object::test<36>() { set_test_name("intersection between a segment and a degenerate arc (radius = Infinity)"); - checkIntersection({-5, -5}, {0, 0}, {5, 5 + 1e-14}, - {-5, 5}, {5, -5}, - CircularArcIntersector::ONE_POINT_INTERSECTION, - CoordinateXY{0, 0}); + checkIntersectionArcSeg( + XY{-5, -5}, XY{0, 0}, XY{5, 5 + 1e-14}, + XY{-5, 5}, XY{5, -5}, + CircularArcIntersector::ONE_POINT_INTERSECTION, + XY{0, 0} + ); } template<> @@ -508,10 +694,12 @@ void object::test<37>() { set_test_name("intersection between a segment and a nearly-degenerate arc (radius ~= 1e5)"); - checkIntersection({-5, -5}, {0, 0}, {5, 5 + 1e-4}, - {-5, 5}, {5, -5}, - CircularArcIntersector::ONE_POINT_INTERSECTION, - CoordinateXY{0, 0}); + checkIntersectionArcSeg( + XY{-5, -5}, XY{0, 0}, XY{5, 5 + 1e-4}, + XY{-5, 5}, XY{5, -5}, + CircularArcIntersector::ONE_POINT_INTERSECTION, + XY{0, 0} + ); } template<> @@ -522,62 +710,62 @@ void object::test<38>() // https://github.com/claeis/iox-ili/blob/master/jtsext/src/test/java/ch/interlis/iom_j/itf/impl/hrg/ISCILRTest.java // test_1a - checkIntersection({0, 5}, {5, 0}, {0, -5}, - {20, 5}, {20, -5}, - CircularArcIntersector::NO_INTERSECTION), + checkIntersectionArcSeg(XY{0, 5}, XY{5, 0}, XY{0, -5}, + XY{20, 5}, XY{20, -5}, + CircularArcIntersector::NO_INTERSECTION), // test_2a - checkIntersection({0, 5}, {5, 0}, {0, -5}, - {5, 5}, {5, 0}, + checkIntersectionArcSeg(XY{0, 5}, XY{5, 0}, XY{0, -5}, + XY{5, 5}, XY{5, 0}, CircularArcIntersector::ONE_POINT_INTERSECTION, - {5, 0}); + XY{5, 0}); // test_2b - checkIntersection({0, 5}, {5, 0}, {0, -5}, - {5, 5}, {5, -5}, + checkIntersectionArcSeg(XY{0, 5}, XY{5, 0}, XY{0, -5}, + XY{5, 5}, XY{5, -5}, CircularArcIntersector::ONE_POINT_INTERSECTION, - {5, 0}); + XY{5, 0}); // test_2c - checkIntersection({0, 5}, {4, 3}, {0, -5}, - {5, 5}, {5, 0}, + checkIntersectionArcSeg(XY{0, 5}, XY{4, 3}, XY{0, -5}, + XY{5, 5}, XY{5, 0}, CircularArcIntersector::ONE_POINT_INTERSECTION, - {5, 0}); + XY{5, 0}); // test_2d - checkIntersection({0, 5}, {4, 3}, {0, -5}, - {5, 5}, {5, -5}, + checkIntersectionArcSeg(XY{0, 5}, XY{4, 3}, XY{0, -5}, + XY{5, 5}, XY{5, -5}, CircularArcIntersector::ONE_POINT_INTERSECTION, - {5, 0}); + XY{5, 0}); // test_3a - checkIntersection({0, 5}, {5, 0}, {0, -5}, - {4, 5}, {4, -5}, + checkIntersectionArcSeg(XY{0, 5}, XY{5, 0}, XY{0, -5}, + XY{4, 5}, XY{4, -5}, CircularArcIntersector::TWO_POINT_INTERSECTION, - {4, 3}, {4, -3}); + XY{4, 3}, XY{4, -3}); // test_3b - checkIntersection({0, 5}, {5, 0}, {0, -5}, - {-4, 5}, {-4, -5}, + checkIntersectionArcSeg(XY{0, 5}, XY{5, 0}, XY{0, -5}, + XY{-4, 5}, XY{-4, -5}, CircularArcIntersector::NO_INTERSECTION); // test_3c - checkIntersection({0, 5}, {5, 0}, {0, -5}, - {4, 10}, {4, 5}, + checkIntersectionArcSeg(XY{0, 5}, XY{5, 0}, XY{0, -5}, + XY{4, 10}, XY{4, 5}, CircularArcIntersector::NO_INTERSECTION); // test_3d - checkIntersection({0, 5}, {3, 4}, {5, 0}, - {4, 5}, {4, -5}, + checkIntersectionArcSeg(XY{0, 5}, XY{3, 4}, XY{5, 0}, + XY{4, 5}, XY{4, -5}, CircularArcIntersector::ONE_POINT_INTERSECTION, - {4, 3}); + XY{4, 3}); // test_3e - checkIntersection({0, 5}, {5, 0}, {0, -5}, - {4, 5}, {4, 0}, + checkIntersectionArcSeg(XY{0, 5}, XY{5, 0}, XY{0, -5}, + XY{4, 5}, XY{4, 0}, CircularArcIntersector::ONE_POINT_INTERSECTION, - {4, 3}); + XY{4, 3}); } template<> @@ -588,54 +776,54 @@ void object::test<39>() // https://github.com/claeis/iox-ili/blob/master/jtsext/src/test/java/ch/interlis/iom_j/itf/impl/hrg/ISCICRTest.java // test_1: circles do not overlap - checkIntersection({0, 5}, {5, 0}, {0, -5}, - {20, 5}, {15, 0}, {20, -5}, + checkIntersection(XY{0, 5}, XY{5, 0}, XY{0, -5}, + XY{20, 5}, XY{15, 0}, XY{20, -5}, CircularArcIntersector::NO_INTERSECTION); // test_2a: arcs overlap at a point - checkIntersection({0, 5}, {5, 0}, {0, -5}, - {10, 5}, {5, 0}, {10, -5}, + checkIntersection(XY{0, 5}, XY{5, 0}, XY{0, -5}, + XY{10, 5}, XY{5, 0}, XY{10, -5}, CircularArcIntersector::ONE_POINT_INTERSECTION, - CoordinateXY{5, 0}); + XY{5, 0}); // test_2b: arcs overlap at a point that is not a definition point of either arc - checkIntersection({0, 5}, {4, 3}, {0, -5}, - {10, 5}, {6, 3}, {10, -5}, + checkIntersection(XY{0, 5}, XY{4, 3}, XY{0, -5}, + XY{10, 5}, XY{6, 3}, XY{10, -5}, CircularArcIntersector::ONE_POINT_INTERSECTION, - CoordinateXY{5, 0}); + XY{5, 0}); // test_3a: circles overlap at two points that are within both arcs - checkIntersection({0, 5}, {5, 0}, {0, -5}, - {8, 5}, {3, 0}, {8, -5}, + checkIntersection(XY{0, 5}, XY{5, 0}, XY{0, -5}, + XY{8, 5}, XY{3, 0}, XY{8, -5}, CircularArcIntersector::TWO_POINT_INTERSECTION, - CoordinateXY{4, 3}, CoordinateXY{4, -3}); + XY{4, 3}, XY{4, -3}); // test_3b: circles overlap at two points but neither is on the first arc - checkIntersection({0, 5}, {-5, 0}, {0, -5}, - {8, 5}, {3, 0}, {8, -5}, + checkIntersection(XY{0, 5}, XY{-5, 0}, XY{0, -5}, + XY{8, 5}, XY{3, 0}, XY{8, -5}, CircularArcIntersector::NO_INTERSECTION); // test_3c: circles overlap at two points but neither is on the first or second arc - checkIntersection({0, 5}, {-5, 0}, {0, -5}, - {8, 5}, {13, 0}, {8, -5}, + checkIntersection(XY{0, 5}, XY{-5, 0}, XY{0, -5}, + XY{8, 5}, XY{13, 0}, XY{8, -5}, CircularArcIntersector::NO_INTERSECTION); // test_3d: circles overlap at two points but one is not on the first arc - checkIntersection({5, 0}, {3, -4}, {0, -5}, - {8, 5}, {3, 0}, {8, -5}, + checkIntersection(XY{5, 0}, XY{3, -4}, XY{0, -5}, + XY{8, 5}, XY{3, 0}, XY{8, -5}, CircularArcIntersector::ONE_POINT_INTERSECTION, - CoordinateXY{4, -3}); + XY{4, -3}); // test_3e: circles overlap at two points but one is not on the second arc - checkIntersection({0, 5}, {5, 0}, {0, -5}, - {3, 0}, {5, -4}, {8, -5}, + checkIntersection(XY{0, 5}, XY{5, 0}, XY{0, -5}, + XY{3, 0}, XY{5, -4}, XY{8, -5}, CircularArcIntersector::ONE_POINT_INTERSECTION, - CoordinateXY{4, -3}); + XY{4, -3}); // test_4a: cocircular - checkIntersection({0, 5}, {5, 0}, {0, -5}, - {4, 3}, {5, 0}, {4, -3}, + checkIntersection(XY{0, 5}, XY{5, 0}, XY{0, -5}, + XY{4, 3}, XY{5, 0}, XY{4, -3}, CircularArcIntersector::COCIRCULAR_INTERSECTION, - CircularArc{{4, 3}, {5, 0}, {4, -3}}); + CircularArc::create(XY{4, 3}, {5, 0}, {4, -3})); } #if 0 @@ -646,10 +834,10 @@ void object::test<40>() { set_test_name("intersection between a segment and a nearly-degenerate arc (radius ~= 2e6)"); - checkIntersection({-5, -5}, {0, 0}, {5, 5 + 1e-9}, - {-5, 5}, {5, -5}, + checkIntersection(XY{-5, -5}, XY{0, 0}, XY{5, 5 + 1e-9}, + XY{-5, 5}, XY{5, -5}, CircularArcIntersector::ONE_POINT_INTERSECTION, - CoordinateXY{0, 0}); + XY{0, 0}); } #endif @@ -659,13 +847,13 @@ void object::test<41>() { set_test_name("IOX-ILI: testFastGerade"); - checkIntersection({611770.424, 234251.322}, {611770.171, 234250.059}, {611769.918, 234248.796}, - {611613.84, 233467.819}, - {611610.392, 233468.995}, - CircularArcIntersector::NO_INTERSECTION); + checkIntersectionArcSeg( + XY{611770.424, 234251.322}, XY{611770.171, 234250.059}, XY{611769.918, 234248.796}, + XY{611613.84, 233467.819}, XY{611610.392, 233468.995}, + CircularArcIntersector::NO_INTERSECTION + ); } -#if 0 template<> template<> void object::test<42>() @@ -674,87 +862,91 @@ void object::test<42>() // two nearly-linear arcs touching at a single endpoint // Potential fix is to use tolerance for checking if computed points are within arc. - checkIntersection({645175.553, 248745.374}, { 645092.332, 248711.677}, { 645009.11, 248677.98}, - {645009.11, 248677.98}, {644926.69, 248644.616}, { 644844.269, 248611.253}, - CircularArcIntersector::ONE_POINT_INTERSECTION, - CoordinateXY{645009.110, 248677.980}); + checkIntersection( + XY{645175.553, 248745.374}, XY{ 645092.332, 248711.677}, XY{ 645009.11, 248677.98}, + XY{645009.11, 248677.98}, XY{644926.69, 248644.616}, XY{ 644844.269, 248611.253}, + CircularArcIntersector::ONE_POINT_INTERSECTION, + XY{645009.110, 248677.980} + ); } -#endif template<> template<> void object::test<43>() { - set_test_name("IOX-ILI: overlayTwoARCS_SameEndPoints_SameDirection"); + set_test_name("IOX-ILI: overlayTwoARCS_SameEndPointsS_ameDirection"); // two arcs with same arcPoint and radius. // startPoints and endPoints are same. lines are in same direction checkIntersection( - {100.0, 100.0},{120,150.0},{100.0,200.0}, - {100.0, 100.0},{120,150.0},{100.0,200.0}, - CircularArcIntersector::COCIRCULAR_INTERSECTION, - CircularArc{{100.0, 100.0}, {120, 150}, {100, 200}}); + XY{100.0, 100.0}, XY{120,150.0}, XY{100.0,200.0}, + XY{100.0, 100.0}, XY{120,150.0}, XY{100.0,200.0}, + CircularArcIntersector::COCIRCULAR_INTERSECTION, + CircularArc::create(XY{100.0, 100.0}, {120, 150}, {100, 200}) + ); } template<> template<> void object::test<44>() { - set_test_name("IOX-ILI: overlayTwoARCS_DifferentArcPointOnSameArcLine_SameDirection"); - // two arcs with different arcPoint (on same arcLine) and same radius length. - // startPoints and endPoints are same. lines are in same direction. + set_test_name("IOX-ILI: overlayTwoARCS_DifferentArcPointOnSameArcLineS_ameDirection"); + // two arcs with different arcPoint (on same arcLine) and same radius length. + // startPoints and endPoints are same. lines are in same direction. checkIntersection( - {0.0, 10.0},{4.0,8.0},{0.0,0.0}, - {0.0, 10.0},{4.0,2.0},{0.0,0.0}, - CircularArcIntersector::COCIRCULAR_INTERSECTION, - CircularArc{{0, 10}, {5, 5}, {0, 0}}); + XY{0.0, 10.0}, XY{4.0,8.0}, XY{0.0,0.0}, + XY{0.0, 10.0}, XY{4.0,2.0}, XY{0.0,0.0}, + CircularArcIntersector::COCIRCULAR_INTERSECTION, + CircularArc::create(XY{0, 10}, {5, 5}, {0, 0}) + ); } template<> template<> void object::test<45>() { - set_test_name("IOX-ILI: overlayTwoARCS_SameArcPointOnSameArcLine_OneArcLineIsLonger"); + set_test_name("IOX-ILI: overlayTwoARCSS_ameArcPointOnSameArcLine_OneArcLineIsLonger"); // two arcs with same arcPoint (on same arcLine) and same radius length. // one arc line is longer than the other arc line. // startPoints is same, endPoints are different. lines are in same direction. checkIntersection( - {0.0, 10.0},{4.0,8.0},{0.0,0.0}, - {0.0, 10.0},{4.0,8.0},{4.0,2.0}, - CircularArcIntersector::COCIRCULAR_INTERSECTION, - CircularArc{{0, 10}, {4, 2}, {0, 5}, 5, Orientation::CLOCKWISE}); + XY{0.0, 10.0}, XY{4.0,8.0}, XY{0.0,0.0}, + XY{0.0, 10.0}, XY{4.0,8.0}, XY{4.0,2.0}, + CircularArcIntersector::COCIRCULAR_INTERSECTION, + CircularArc::create(XY{0, 10}, XY{4, 2}, XY{0, 5}, 5, Orientation::CLOCKWISE) + ); } template<> template<> void object::test<46>() { - set_test_name("IOX-ILI: overlayTwoARCS_SameEndPoints_OtherDirection"); + set_test_name("IOX-ILI: overlayTwoARCSS_ameEndPoints_OtherDirection"); // two arcs with same arcPoint and radius // startPoint1 is equal to endPoint2, startPoint2 is equal to endPoint1. checkIntersection( - CircularArc({100.0, 100.0}, {80.0, 150.0}, {100.0, 200.0}), - CircularArc({100.0, 200.0}, {80.0, 150.0}, {100.0, 100.0}), + XY{100.0, 100.0}, XY{80.0, 150.0}, XY{100.0, 200.0}, + XY{100.0, 200.0}, XY{80.0, 150.0}, XY{100.0, 100.0}, CircularArcIntersector::COCIRCULAR_INTERSECTION, - CircularArc({100.0, 100.0}, {80.0, 150.0}, {100.0, 200.0})); + CircularArc::create(XY{100.0, 100.0}, {80.0, 150.0}, {100.0, 200.0})); } template<> template<> void object::test<47>() { - set_test_name("IOX-ILI: overlayTwoARCS_DifferentStartPoints_SameDirection_DifferentLength"); + set_test_name("IOX-ILI: overlayTwoARCS_DifferentStartPointsS_ameDirection_DifferentLength"); // two arcs. ArcPoint is equal. different angle. // startPoints are different. endPoints are same. - CircularArc a({70.0, 60.0}, {50.0, 100.0}, {60.0, 130.0}); - CircularArc b({60.0, 70.0}, {50.0, 100.0}, {60.0, 130.0}); + CircularArc a = CircularArc::create(XY{70.0, 60.0}, {50.0, 100.0}, {60.0, 130.0}); + CircularArc b = CircularArc::create(XY{60.0, 70.0}, {50.0, 100.0}, {60.0, 130.0}); checkIntersection(a, b, CircularArcIntersector::COCIRCULAR_INTERSECTION, - CircularArc({60, 70}, {60, 130}, a.getCenter(), a.getRadius(), a.getOrientation())); + CircularArc::create(XY{60, 70}, {60, 130}, a.getCenter(), a.getRadius(), a.getOrientation())); } template<> @@ -766,12 +958,12 @@ void object::test<48>() // ArcPoint is equal. // startPoints are different. endPoints are different. - CircularArc a({70.0, 60.0}, {50.0, 100.0}, {70.0, 140.0}); - CircularArc b({60.0, 130.0}, {50.0, 100.0}, {60.0, 70.0}); + CircularArc a = CircularArc::create(XY{70.0, 60.0}, {50.0, 100.0}, {70.0, 140.0}); + CircularArc b = CircularArc::create(XY{60.0, 130.0}, {50.0, 100.0}, {60.0, 70.0}); checkIntersection(a, b, CircularArcIntersector::COCIRCULAR_INTERSECTION, - CircularArc({60, 70}, {60, 130}, a.getCenter(), a.getRadius(), a.getOrientation())); + CircularArc::create(XY{60, 70}, XY{60, 130}, a.getCenter(), a.getRadius(), a.getOrientation())); } @@ -779,13 +971,13 @@ template<> template<> void object::test<49>() { - set_test_name("IOX-ILI: overlayTwoARCS_DifferentEndPoints_SameDirection_DifferentLength"); + set_test_name("IOX-ILI: overlayTwoARCS_DifferentEndPointsS_ameDirection_DifferentLength"); // Two arcs with same orientation. // ArcPoint is equal. // startPoints are same, endpoints are different - CircularArc a({70.0, 60.0}, {50.0, 100.0}, {70.0, 140.0}); - CircularArc b({70.0, 60.0}, {50.0, 100.0}, {60.0, 130.0}); + CircularArc a = CircularArc::create(XY{70.0, 60.0}, {50.0, 100.0}, {70.0, 140.0}); + CircularArc b = CircularArc::create(XY{70.0, 60.0}, {50.0, 100.0}, {60.0, 130.0}); checkIntersection(a, b, CircularArcIntersector::COCIRCULAR_INTERSECTION, b); @@ -801,23 +993,23 @@ void object::test<50>() // One endpoint is the same, one is different. - CircularArc a({70.0, 60.0}, {50.0, 100.0}, {70.0, 140.0}); - CircularArc b({60.0, 130.0}, {50.0, 100.0}, {70.0, 60.0}); + CircularArc a = CircularArc::create(XY{70.0, 60.0}, {50.0, 100.0}, {70.0, 140.0}); + CircularArc b = CircularArc::create(XY{60.0, 130.0}, {50.0, 100.0}, {70.0, 60.0}); checkIntersection(a, b, CircularArcIntersector::COCIRCULAR_INTERSECTION, - CircularArc({70, 60}, {60, 130}, a.getCenter(), a.getRadius(), a.getOrientation())); + CircularArc::create(XY{70, 60}, XY{60, 130}, a.getCenter(), a.getRadius(), a.getOrientation())); } template<> template<> void object::test<51>() { - set_test_name("IOX-ILI: twoARCS_SameRadiusAndCenter_DontOverlay"); + set_test_name("IOX-ILI: twoARCSS_ameRadiusAndCenter_DontOverlay"); // two arcs with same center and radius that don't touch each other. - CircularArc a({70.0, 60.0}, {50.0, 100.0}, {70.0, 140.0}); - CircularArc b({140.0, 70.0}, {150.0, 100.0}, {140.0, 130.0}); + CircularArc a = CircularArc::create(XY{70.0, 60.0}, {50.0, 100.0}, {70.0, 140.0}); + CircularArc b = CircularArc::create(XY{140.0, 70.0}, {150.0, 100.0}, {140.0, 130.0}); checkIntersection(a, b, CircularArcIntersector::NO_INTERSECTION); } @@ -826,30 +1018,28 @@ template<> template<> void object::test<52>() { - set_test_name("IOX-ILI: twoARCS_SameRadiusAndCenter_Touch_DontOverlay"); + set_test_name("IOX-ILI: twoARCSS_ameRadiusAndCenter_Touch_DontOverlay"); // Two arcs with same radius and center that touch at the endpoints - CircularArc a({50.0, 100.0}, {100.0, 150.0}, {150.0, 100.0}); - CircularArc b({150.0, 100.0}, {100.0, 50.0}, {50.0, 100.0}); + CircularArc a = CircularArc::create(XY{50.0, 100.0}, {100.0, 150.0}, {150.0, 100.0}); + CircularArc b = CircularArc::create(XY{150.0, 100.0}, {100.0, 50.0}, {50.0, 100.0}); - checkIntersection(a, b, CircularArcIntersector::TWO_POINT_INTERSECTION, a.p0, a.p2); + checkIntersection(a, b, CircularArcIntersector::TWO_POINT_INTERSECTION, a.p0(), a.p2()); } -#if 0 template<> template<> void object::test<53>() { - set_test_name("IOX-ILI: twoARCS_SameRadiusAndCenter_Touch_DontOverlay_real"); + set_test_name("IOX-ILI: twoARCSS_ameRadiusAndCenter_Touch_DontOverlay_real"); // arcs touch at endpoints // Potential fix is to use tolerance for checking if computed points are within arc. - CircularArc a({2654828.912, 1223354.671}, {2654829.982, 1223353.601}, {2654831.052, 1223354.671}); - CircularArc b({2654831.052, 1223354.671}, {2654829.982, 1223355.741}, {2654828.912, 1223354.671}); + CircularArc a = CircularArc::create(XY{2654828.912, 1223354.671}, {2654829.982, 1223353.601}, {2654831.052, 1223354.671}); + CircularArc b = CircularArc::create(XY{2654831.052, 1223354.671}, {2654829.982, 1223355.741}, {2654828.912, 1223354.671}); - checkIntersection(a, b, CircularArcIntersector::TWO_POINT_INTERSECTION, a.p0, a.p2); + checkIntersection(a, b, CircularArcIntersector::TWO_POINT_INTERSECTION, a.p0(), a.p2()); } -#endif template<> template<> @@ -858,13 +1048,16 @@ void object::test<54>() set_test_name("IOX-ILI: twoARCS_intersect0"); // https://github.com/claeis/ilivalidator/issues/186 - CircularArc a({2658317.225, 1250832.586}, {2658262.543, 1250774.465}, {2658210.528, 1250713.944}); - CircularArc b({2658211.456, 1250715.072}, {2658161.386, 1250651.279}, {2658114.283, 1250585.266}); + CircularArc a = CircularArc::create(XY{2658317.225, 1250832.586}, {2658262.543, 1250774.465}, {2658210.528, 1250713.944}); + CircularArc b = CircularArc::create(XY{2658211.456, 1250715.072}, {2658161.386, 1250651.279}, {2658114.283, 1250585.266}); // An intersection is visually apparent in QGIS, but CGAL 5.6 reports no intersections... checkIntersection(a, b, CircularArcIntersector::NO_INTERSECTION); } +#if 0 +// Failing because two intersection points are detected. +// One is and endpoint (exact), the other is an approximation of the same endpoint. template<> template<> void object::test<55>() @@ -878,7 +1071,548 @@ void object::test<55>() // expected result calculated with CGAL 5.6 checkIntersection(a, b, CircularArcIntersector::ONE_POINT_INTERSECTION, - CoordinateXY{2653134.35399999982, 1227788.18800000008}); + XY{2653134.35399999982, 1227788.18800000008}); +} +#endif + +/// Z/M interpolation + +template<> +template<> +void object::test<56>() +{ + set_test_name("arc XYZ / arc XYZ interior intersection"); + // Z value at the intersection point is the average of the interpolated values from each arc + // In both cases the value is interpolated from points 2 3 of the arc + + checkIntersection( + XYZ{-5, 0, 1}, XYZ{-4, 3, 41}, XYZ{4, 3, 53}, + XYZ{-5, 10, 0}, XYZ{-2, 9, 7}, XYZ{-2, 1, 13}, + CircularArcIntersector::ONE_POINT_INTERSECTION, + XYZ{0, 5, 0.25*(41 + 53 + 7 + 13)} + ); } -} \ No newline at end of file +template<> +template<> +void object::test<57>() +{ + set_test_name("arc XYZ / segment XYZ interior intersection"); + // Z value at the intersection point is the average of the interpolated values from the arc and segment + + checkIntersectionArcSeg( + XYZ{-5, 0, 1}, XYZ{-4, 3, 41}, XYZ{4, 3, 53}, + XYZ{0, 0, 7}, XYZ{0, 10, 13}, + CircularArcIntersector::ONE_POINT_INTERSECTION, + XYZ{0, 5, 0.25*(41 + 53 + 7 + 13)} + ); +} + +template<> +template<> +void object::test<58>() +{ + set_test_name("arc XYZ / arc XYM interior intersection"); + // Z value at the intersection point is interpolated from the arc with Z values + // M value at the intersection point is interpolated from the arc with M values + + checkIntersection( + XYZ{-5, 0, 1}, XYZ{-4, 3, 41}, XYZ{4, 3, 53}, + XYM{-5, 10, 0}, XYM{-2, 9, 7}, XYM{-2, 1, 13}, + CircularArcIntersector::ONE_POINT_INTERSECTION, + XYZM{0, 5, 0.5*(41 + 53), 0.5*(7 + 13)} + ); +} + +template<> +template<> +void object::test<59>() +{ + set_test_name("arc XYZ / arc XYZ interior intersection with control point Z = NaN"); + // Z value at the intersection point is the average of the interpolated values from each arc + // Because the control point Z is NaN, interpolation is done from the arc endpoints + + checkIntersection( + XYZ{-5, 0, 41}, XYZ{-4, 3, NaN}, XYZ{5, 0, 53}, + XYZ{-2, 9, 7}, XYZ{-1, 8, NaN}, XYZ{-2, 1, 13}, + CircularArcIntersector::ONE_POINT_INTERSECTION, + XYZ{0, 5, 0.25*(41 + 53 + 7 + 13)} + ); +} + +template<> +template<> +void object::test<60>() +{ + set_test_name("arc XYM / arc XYZ interior intersection with control point Z/M = NaN"); + // Z value at the intersection point is interpolated from the arc with Z values + // Because the control point Z is NaN, interpolation is done from the arc endpoints + // M value at the intersection point is interpolated from the arc with M values + // Because the control point M is NaN, interpolation is done from the arc endpoints + + checkIntersection( + XYM{-5, 0, 41}, XYM{-4, 3, NaN}, XYM{5, 0, 53}, + XYZ{-2, 9, 7}, XYZ{-1, 8, NaN}, XYZ{-2, 1, 13}, + CircularArcIntersector::ONE_POINT_INTERSECTION, + XYZM{0, 5, 0.5*(7 + 13), 0.5*(41 + 53)} + ); +} + +template<> +template<> +void object::test<61>() +{ + set_test_name("arc XYM / arc XYZ interior intersection with endpoint Z/M = NaN"); + // Z value at the intersection point is interpolated from the arc with Z values + // Because the endpoint Z is NaN, the value of the control point is taken + // M value at the intersection point is interpolated from the arc with M values + // Because the endpoint M is NaN, the value of the control point is taken + + checkIntersection( + XYM{-5, 0, 41}, XYM{-4, 3, 53}, XYM{5, 0, NaN}, + XYZ{-2, 9, 7}, XYZ{-1, 8, 13}, XYZ{-2, 1, NaN}, + CircularArcIntersector::ONE_POINT_INTERSECTION, + XYZM{0, 5, 13, 53} + ); +} + +template<> +template<> +void object::test<62>() +{ + set_test_name("arc XYM / arc XYZ interior intersection with control point and endpoint Z/M = NaN"); + // Z value at the intersection point is interpolated from the arc with Z values + // Because the endpoint and control point Z is NaN, the value of the other endpoint is taken + // M value at the intersection point is interpolated from the arc with M values + // Because the endpoint and control point M is NaN, the value of the other endpoint is taken + + checkIntersection( + XYM{-5, 0, 41}, XYM{-4, 3, NaN}, XYM{5, 0, NaN}, + XYZ{-2, 9, 7}, XYZ{-1, 8, NaN}, XYZ{-2, 1, NaN}, + CircularArcIntersector::ONE_POINT_INTERSECTION, + XYZM{0, 5, 7, 41} + ); +} + +template<> +template<> +void object::test<63>() +{ + set_test_name("arc XYZ / segment XYZ interior intersection with control point Z = NaN"); + // Z value at the intersection point is the average of the interpolated values from the arc and the segment + // Because the control point Z is NaN, interpolation is done from the arc endpoints + + checkIntersectionArcSeg( + XYZ{-5, 0, 41}, XYZ{-4, 3, NaN}, XYZ{5, 0, 53}, + XYZ{0, 0, 7}, XYZ{0, 10, 13}, + CircularArcIntersector::ONE_POINT_INTERSECTION, + XYZ{0, 5, 0.25*(41 + 53 + 7 + 13)} + ); +} + +template<> +template<> +void object::test<64>() +{ + set_test_name("arc XYZ / segment XYZ interior intersection, segment endpoint Z = NaN"); + // Z value at the intersection point is the average of the interpolated values from the arc and the other segment endpoint + + checkIntersectionArcSeg( + XYZ{-5, 0, 7}, XYZ{-3, 4, 41}, XYZ{3, 4, 53}, + XYZ{0, 0, NaN}, XYZ{0, 10, 13}, + CircularArcIntersector::ONE_POINT_INTERSECTION, + XYZ{0, 5, 0.25*(41 + 53 + 13 + 13)} + ); +} + +template<> +template<> +void object::test<65>() +{ + set_test_name("arc XYZ / arc XYZ endpoint intersection"); + // Result Z value at intersection point is taken from the first input + + checkIntersection( + XYZ{0, 0, 0}, XYZ{1, 1, 1}, XYZ{2, 0, 2}, + XYZ{2, 0, 500}, XYZ{3, -1, 501}, XYZ{4, 0, 502}, + CircularArcIntersector::ONE_POINT_INTERSECTION, + XYZ{2, 0, 2} + ); +} + +template<> +template<> +void object::test<66>() +{ + set_test_name("arc XYZ / segment XYZ endpoint intersection"); + // Result Z value at intersection point is taken from the first input + // Related: RobustLineIntersectorZTest::testEndpoint + + checkIntersectionArcSeg( + XYZ{0, 0, 0}, XYZ{1, 1, 1}, XYZ{2, 0, 2}, + XYZ{2, 0, 500}, XYZ{4, 0, 502}, + CircularArcIntersector::ONE_POINT_INTERSECTION, + XYZ{2, 0, 2} + ); + + // Same inputs as above but with order reversed + checkIntersectionSegArc( + XYZ{2, 0, 500}, XYZ{4, 0, 502}, + XYZ{0, 0, 0}, XYZ{1, 1, 1}, XYZ{2, 0, 2}, + CircularArcIntersector::ONE_POINT_INTERSECTION, + XYZ{2, 0, 500} + ); +} + +#if 0 +// Fails because computed intersection point is not exactly equal to endpoint, so special handling doesn't apply. +template<> +template<> +void object::test<67>() { + set_test_name("arc XYZ / arc XYZ interior / endpoint intersection"); + // Result Z is taken from the endpoint + // Related: RobustLineIntersectorZTest::testInteriorEndpoint + + checkIntersection( + XYZ{-1, 0, 1}, XYZ{0, 1, 2}, XYZ{1, 0, 3}, + XYZ{-2, 1, 7}, XYZ{-1, 2, 8}, XYZ{0, 1, 9}, + CircularArcIntersector::ONE_POINT_INTERSECTION, + XYZ{0, 1, 9} + ); +} +#endif + +template<> +template<> +void object::test<68>() { + set_test_name("arc XYZ / seg XYZ interior / endpoint intersection"); + // Result Z is taken from the endpoint + // Related: RobustLineIntersectorZTest::testInteriorEndpoint + + checkIntersectionArcSeg( + XYZ{-1, 0, 1}, XYZ{0, 1, 2}, XYZ{1, 0, 3}, + XYZ{0, 0, 5}, {0, 1, 9}, + CircularArcIntersector::ONE_POINT_INTERSECTION, + XYZ{0, 1, 9} + ); + + // Same inputs as above but with order reversed + checkIntersectionSegArc( + XYZ{0, 0, 5}, {0, 1, 9}, + XYZ{-1, 0, 1}, XYZ{0, 1, 2}, XYZ{1, 0, 3}, + CircularArcIntersector::ONE_POINT_INTERSECTION, + XYZ{0, 1, 9} + ); + +} + +template<> +template<> +void object::test<69>() { + set_test_name("arc XYZ / arc XY interior / endpoint intersection"); + // Intersection is at interior of XYZ arc, endpoint of XY arc + // Result Z is interpolated + // Related: RobustLineIntersectorZTest::testInteriorEndpoint3D2D + + checkIntersection( + XYZ{-5, 0, 1}, XYZ{-4, 3, 2}, XYZ{4, 3, 3}, + XY{-10, 5}, XY{-5, 10}, XY{0, 5}, + CircularArcIntersector::ONE_POINT_INTERSECTION, + XYZ{0, 5, 2.5} + ); +} + +template<> +template<> +void object::test<70>() { + set_test_name("arc XYZ / segment XY interior / endpoint intersection"); + // Intersection is at interior of XYZ arc, endpoint of XY segment + // Result Z is interpolated + // Related: RobustLineIntersectorZTest::testInteriorEndpoint3D2D + + checkIntersectionArcSeg( + XYZ{-5, 0, 1}, XYZ{-4, 3, 2}, XYZ{4, 3, 3}, + XY{0, 0}, XY{0, 5}, + CircularArcIntersector::ONE_POINT_INTERSECTION, + XYZ{0, 5, 2.5} + ); +} + +#if 0 +// Fails because computed intersection point is not exactly equal to endpoint, so special handling doesn't apply. +template<> +template<> +void object::test<71>() { + set_test_name("arc XY / arc XYZ interior / endpoint intersection"); + // Intersection is at interior of XY arc, endpoint of XYZ arc + // Result Z is from 3D endpoint + // Related: RobustLineIntersectorZTest::testInteriorEndpoint2D3D + + checkIntersection( + XY{-5, 0}, XY{-4, 3}, XY{4, 3}, + XYZ{-10, 5, 8}, XYZ{-5, 10, 11}, XYZ{0, 5, 17}, + CircularArcIntersector::ONE_POINT_INTERSECTION, + XYZ{0, 5, 17} + ); +} +#endif + +template<> +template<> +void object::test<72>() { + set_test_name("arc XY / segment XYZ interior / endpoint intersection"); + // Intersection is at interior of XY arc, endpoint of XYZ segment + // Result Z is from 3D endpoint + // Related: RobustLineIntersectorZTest::testInteriorEndpoint2D3D + + checkIntersectionArcSeg( + XY{-5, 0}, XY{-4, 3}, XY{4, 3}, + XYZ{0, 0, 3}, XYZ{0, 5, 17}, + CircularArcIntersector::ONE_POINT_INTERSECTION, + XYZ{0, 5, 17} + ); +} + +template<> +template<> +void object::test<73>() { + set_test_name("XYZ arc intersected with itself"); + // Related:: RobustLineIntersectorTest::testCollinearEqual + + // clockwise inputs + checkIntersection( + XYZ{-5, 0, 0}, XYZ{0, 5, 0}, XYZ{5, 0, 15}, + XYZ{-5, 0, 0}, XYZ{0, 5, 0}, XYZ{5, 0, 15}, + CircularArcIntersector::COCIRCULAR_INTERSECTION, + CircularArc::create(XYZ{-5, 0, 0}, XYZ{0, 5, 0}, XYZ{5, 0, 15}) + ); + + // counter-clockwise inputs + checkIntersection( + XYZ{5, 0, 15}, XYZ{0, 5, 0}, XYZ{-5, 0, 0}, + XYZ{5, 0, 15}, XYZ{0, 5, 0}, XYZ{-5, 0, 0}, + CircularArcIntersector::COCIRCULAR_INTERSECTION, + CircularArc::create(XYZ{5, 0, 15}, XYZ{0, 5, 0}, XYZ{-5, 0, 0}) + ); + + // mixed-orientation inputs + checkIntersection( + XYZ{-5, 0, 0}, XYZ{0, 5, 0}, XYZ{5, 0, 15}, + XYZ{5, 0, 15}, XYZ{0, 5, 0}, XYZ{-5, 0, 0}, + CircularArcIntersector::COCIRCULAR_INTERSECTION, + CircularArc::create(XYZ{-5, 0, 0}, XYZ{0, 5, 0}, XYZ{5, 0, 15}) + ); +} + +template<> +template<> +void object::test<74>() { + set_test_name("XYZ arc intersected with 2D version of same arc"); + // Related:: RobustLineIntersectorTest::testCollinearEqual3D2D + + // clockwise inputs + checkIntersection( + XYZ{-5, 0, 0}, XYZ{0, 5, 0}, XYZ{5, 0, 15}, + XY{-5, 0}, XY{0, 5}, XY{5, 0}, + CircularArcIntersector::COCIRCULAR_INTERSECTION, + CircularArc::create(XYZ{-5, 0, 0}, XYZ{0, 5, 0}, XYZ{5, 0, 15}) + ); + + // counter-clockwise input + checkIntersection( + XYZ{5, 0, 15}, XYZ{0, 5, 0}, XYZ{-5, 0, 0}, + XY{5, 0}, XY{0, 5}, XY{-5, 0}, + CircularArcIntersector::COCIRCULAR_INTERSECTION, + CircularArc::create(XYZ{5, 0, 15}, XYZ{0, 5, 0}, XYZ{-5, 0, 0}) + ); +} + +template<> +template<> +void object::test<75>() { + set_test_name("Two cocircular XYZ arcs with endpoint intersections"); + // Z values of inputs are the same and are copied to output + // Related:: RobustLineIntersectorTest::testCollinearEndpoint + + checkIntersection( + XYZ{-5, 0, 3}, XYZ{0, 5, 11}, XYZ{5, 0, 15}, + XYZ{-5, 0, 3}, XYZ{0, -5, 11}, XYZ{5, 0, 15}, + CircularArcIntersector::TWO_POINT_INTERSECTION, + XYZ{-5, 0, 3}, + XYZ{5, 0, 15} + ); + + // Same arguments as above, order reversed + checkIntersection( + XYZ{-5, 0, 3}, XYZ{0, -5, 11}, XYZ{5, 0, 15}, + XYZ{-5, 0, 3}, XYZ{0, 5, 11}, XYZ{5, 0, 15}, + CircularArcIntersector::TWO_POINT_INTERSECTION, + XYZ{-5, 0, 3}, + XYZ{5, 0, 15} + ); +} + +template<> +template<> +void object::test<76>() { + set_test_name("Cocircular XYZ and XY arcs with endpoint intersections"); + // Z values of result is taken from the XYZ input + // Related:: RobustLineIntersectorTest::testCollinearEndpoint3D2D + + // clockwise inputs + checkIntersection( + XYZ{-5, 0, 3}, XYZ{0, 5, 11}, XYZ{5, 0, 15}, + XY{-5, 0}, XY{0, -5}, XY{5, 0}, + CircularArcIntersector::TWO_POINT_INTERSECTION, + XYZ{-5, 0, 3}, + XYZ{5, 0, 15} + ); + + // Same arguments as above, order reversed + checkIntersection( + XY{-5, 0}, XY{0, -5}, XY{5, 0}, + XYZ{-5, 0, 3}, XYZ{0, 5, 11}, XYZ{5, 0, 15}, + CircularArcIntersector::TWO_POINT_INTERSECTION, + XYZ{-5, 0, 3}, + XYZ{5, 0, 15} + ); + + // counter-clockwise inputs + checkIntersection( + XYZ{5, 0, 15}, XYZ{0, 5, 11}, XYZ{-5, 0, 3}, + XY{5, 0}, XY{0, -5}, XY{-5, 0}, + CircularArcIntersector::TWO_POINT_INTERSECTION, + XYZ{-5, 0, 3}, + XYZ{5, 0, 15} + ); + + // Same arguments as above, order reversed + checkIntersection( + XY{5, 0}, XY{0, -5}, XY{-5, 0}, + XYZ{5, 0, 15}, XYZ{0, 5, 11}, XYZ{-5, 0, 3}, + CircularArcIntersector::TWO_POINT_INTERSECTION, + XYZ{-5, 0, 3}, + XYZ{5, 0, 15} + ); +} + +template<> +template<> +void object::test<77>() { + set_test_name("XYZ arc intersected with a subset of itself"); + // Related:: RobustLineIntersectorTest::testCollinearContained + + const double theta = std::atan(3.0 / 4.0); + const double frac = theta / MATH_PI; + + // Clockwise inputs + checkIntersection( + XYZ{-5, 0, 0}, XYZ{0, 5, 7.5}, XYZ{5, 0, 15}, + XYZ{-4, 3, frac*15}, XYZ{0, 5, 7.5}, XYZ{4, 3, (1-frac)*15}, + CircularArcIntersector::COCIRCULAR_INTERSECTION, + CircularArc::create(XYZ{-4, 3, frac*15}, XYZ{0, 5, 0}, XYZ{4, 3, (1-frac)*15}) + ); + + // Counter-clockwise inputs + checkIntersection( + XYZ{5, 0, 15}, XYZ{0, 5, 7.5}, XYZ{-5, 0, 0}, + XYZ{4, 3, (1-frac)*15}, XYZ{0, 5, 7.5}, XYZ{-4, 3, frac*15}, + CircularArcIntersector::COCIRCULAR_INTERSECTION, + CircularArc::create(XYZ{4, 3, (1-frac)*15}, XYZ{0, 5, 0}, XYZ{-4, 3, frac*15}) + ); +} + +template<> +template<> +void object::test<78>() { + set_test_name("XYZ arc intersected with a 2D subset of itself"); + // Related:: RobustLineIntersectorTest::testCollinearContained3D2D + + const double theta = std::atan(3.0 / 4.0); + const double frac = theta / MATH_PI; + + // clockwise inputs + checkIntersection( + XYZ{-5, 0, 0}, XYZ{0, 5, 7.5}, XYZ{5, 0, 15}, + XY{-4, 3}, XY{0, 5}, XY{4, 3}, + CircularArcIntersector::COCIRCULAR_INTERSECTION, + CircularArc::create(XYZ{-4, 3, frac*15}, XYZ{0, 5, 7.5}, XYZ{4, 3, (1-frac)*15}) + ); + + // counter-clockwise inputs + checkIntersection( + XYZ{5, 0, 15}, XYZ{0, 5, 7.5}, XYZ{-5, 0, 0}, + XY{4, 3}, XY{0, 5}, XY{-4, 3}, + CircularArcIntersector::COCIRCULAR_INTERSECTION, + CircularArc::create(XYZ{4, 3, (1-frac)*15}, XYZ{0, 5, 7.5}, XYZ{-4, 3, frac*15}) + ); +} + +template<> +template<> +void object::test<79>() { + set_test_name("XYZ arc intersected with a subset of itself that has different Z values"); + // Related:: RobustLineIntersectorTest::testCollinearContainedDifferentZ + + // clockwise inputs + checkIntersection( + XYZ{-5, 0, 0}, XYZ{0, 5, 7.5}, XYZ{5, 0, 15}, + XYZ{-4, 3, 100}, XYZ{0, 5, 150}, XYZ{4, 3, 200}, + CircularArcIntersector::COCIRCULAR_INTERSECTION, + CircularArc::create(XYZ{-4, 3, 100}, XYZ{0, 5, 150}, XYZ{4, 3, 200}) + ); + + // counter-clockwise inputs + checkIntersection( + XYZ{5, 0, 15}, XYZ{0, 5, 7.5}, XYZ{-5, 0, 0}, + XYZ{4, 3, 200}, XYZ{0, 5, 150}, XYZ{-4, 3, 100}, + CircularArcIntersector::COCIRCULAR_INTERSECTION, + CircularArc::create(XYZ{4, 3, 200}, XYZ{0, 5, 150}, XYZ{-4, 3, 100}) + ); +} + +template<> +template<> +void object::test<80>() { + set_test_name("XYZ arc intersected with a superset of itself that has different Z values"); + + // clockwise inputs + checkIntersection( + XYZ{-4, 3, 100}, XYZ{0, 5, 150}, XYZ{4, 3, 200}, + XYZ{-5, 0, 0}, XYZ{0, 5, 7.5}, XYZ{5, 0, 15}, + CircularArcIntersector::COCIRCULAR_INTERSECTION, + CircularArc::create(XYZ{-4, 3, 100}, XYZ{0, 5, 150}, XYZ{4, 3, 200}) + ); + + // counter-clockwise inputs + checkIntersection( + XYZ{4, 3, 200}, XYZ{0, 5, 150}, XYZ{-4, 3, 100}, + XYZ{5, 0, 15}, XYZ{0, 5, 7.5}, XYZ{-5, 0, 0}, + CircularArcIntersector::COCIRCULAR_INTERSECTION, + CircularArc::create(XYZ{4, 3, 200}, XYZ{0, 5, 150}, XYZ{-4, 3, 100}) + ); + +} + +template<> +template<> +void object::test<81> +() +{ + // Interior intersection of two XYZM lines. + // Result Z and M are the average of the interpolated coordinate values. + set_test_name("XYZM segment and XYZM segment with interior intersection"); + // Related:: RobustLineIntersectorTest::testInteriorXYZM-XYZM + + checkIntersectionSegSeg( + XYZM{1, 1, 1, -1}, XYZM{3, 3, 3, -3}, + XYZM{1, 3, 10, -10}, XYZM{3, 1, 30, -30}, + CircularArcIntersector::ONE_POINT_INTERSECTION, + XYZM{2, 2, 11, -11} + ); +} + +// TODO: check Z values of arc result centerpoints +// TODO: add tests for seg/seg + +} diff --git a/tests/unit/algorithm/CircularArcsTest.cpp b/tests/unit/algorithm/CircularArcsTest.cpp index 96082078b..ccb3e4979 100644 --- a/tests/unit/algorithm/CircularArcsTest.cpp +++ b/tests/unit/algorithm/CircularArcsTest.cpp @@ -15,6 +15,8 @@ namespace tut { struct test_circulararcs_data { const double eps = 1e-8; + using XY = CoordinateXY; + void checkEnvelope(const CoordinateXY& p0, const CoordinateXY& p1, const CoordinateXY& p2, double xmin, double ymin, double xmax, double ymax) { @@ -39,21 +41,17 @@ struct test_circulararcs_data { } } - static std::string toWKT(const CoordinateXY& p0, const CoordinateXY& p1, const CoordinateXY& p2) - { - std::stringstream ss; - ss << "CIRCULARSTRING (" << p0 << ", " << p1 << ", " << p2 << ")"; - return ss.str(); - } - - void checkArc(std::string message, - const CoordinateXY& center, double radius, bool ccw, double from, double to, + void checkArc(const std::string& message, + const CoordinateXY& center, double radius, int orientation, double from, double to, const CoordinateXY& p0, const CoordinateXY& p1, const CoordinateXY& p2) const { - CircularArc arc(from, to, center, radius, ccw); + CoordinateXY fromPt = CircularArcs::createPoint(center, radius, from); + CoordinateXY toPt = CircularArcs::createPoint(center, radius, to); - if (arc.p0.distance(p0) > eps || arc.p1.distance(p1) > eps || arc.p2.distance(p2) > eps) { - ensure_equals(message, toWKT(arc.p0, arc.p1, arc.p2), toWKT(p0, p1, p2)); + auto arc = CircularArc::create(fromPt, toPt, center, radius, orientation); + + if (arc.p0().distance(p0) > eps || arc.p1().distance(p1) > eps || arc.p2().distance(p2) > eps) { + ensure_equals(message, arc.toString(), CircularArc::create(p0, p1, p2).toString()); } } @@ -256,8 +254,8 @@ void object::test<15>() { set_test_name("createArc"); - constexpr bool CCW = true; - constexpr bool CW = false; + auto CCW = geos::algorithm::Orientation::COUNTERCLOCKWISE; + auto CW = geos::algorithm::Orientation::CLOCKWISE; checkArc("CCW: upper half-circle", {0, 0}, 1, CCW, 0, MATH_PI, {1, 0}, {0, 1}, {-1, 0}); checkArc("CCW: lower half-circle", {0, 0}, 1, CCW, MATH_PI, 0, {-1, 0}, {0, -1}, {1, 0}); @@ -270,37 +268,39 @@ void object::test<15>() checkArc("CW: right half-circle", {0, 0}, 1, CW, MATH_PI/2, -MATH_PI/2, {0, 1}, {1, 0}, {0, -1}); } +#if 0 template<> template<> void object::test<16>() { set_test_name("splitAtPoint"); - CircularArc cwArc({-1, 0}, {0, 1}, {1, 0}); + CircularArc cwArc(XY{-1, 0}, XY{0, 1}, XY{1, 0}); auto [arc1, arc2] = cwArc.splitAtPoint({std::sqrt(2)/2, std::sqrt(2)/2}); - ensure_equals(arc1.p0, CoordinateXY{-1, 0}); - ensure_equals(arc1.p2, CoordinateXY{std::sqrt(2)/2, std::sqrt(2)/2}); + ensure_equals(arc1.p0(), CoordinateXY{-1, 0}); + ensure_equals(arc1.p2(), CoordinateXY{std::sqrt(2)/2, std::sqrt(2)/2}); ensure_equals(arc1.getCenter(), cwArc.getCenter()); ensure_equals(arc1.getRadius(), cwArc.getRadius()); - ensure_equals(arc2.p0, CoordinateXY{std::sqrt(2)/2, std::sqrt(2)/2}); - ensure_equals(arc2.p2, CoordinateXY{1, 0}); + ensure_equals(arc2.p0(), CoordinateXY{std::sqrt(2)/2, std::sqrt(2)/2}); + ensure_equals(arc2.p2(), CoordinateXY{1, 0}); ensure_equals(arc2.getCenter(), cwArc.getCenter()); ensure_equals(arc2.getRadius(), cwArc.getRadius()); ensure_equals(cwArc.getLength(), arc1.getLength() + arc2.getLength()); } +#endif template<> template<> void object::test<17>() { set_test_name("getSagitta"); - CircularArc halfCircle({-1, 0}, {0, 1}, {1, 0}); + CircularArc halfCircle = CircularArc::create(XY{-1, 0}, XY{0, 1}, XY{1, 0}); ensure_equals(halfCircle.getSagitta(), 1); - CircularArc quarterCircle({0, 1}, {std::sqrt(2)/2, std::sqrt(2)/2}, {1, 0}); + CircularArc quarterCircle = CircularArc::create(XY{0, 1}, XY{std::sqrt(2)/2, std::sqrt(2)/2}, {1, 0}); ensure_equals(quarterCircle.getSagitta(), CoordinateXY{std::sqrt(2)/2, std::sqrt(2)/2}.distance(CoordinateXY{0.5, 0.5})); } diff --git a/tests/unit/algorithm/RobustLineIntersectorZTest.cpp b/tests/unit/algorithm/RobustLineIntersectorZTest.cpp index 7d7936bc8..2c64c100d 100644 --- a/tests/unit/algorithm/RobustLineIntersectorZTest.cpp +++ b/tests/unit/algorithm/RobustLineIntersectorZTest.cpp @@ -168,7 +168,7 @@ void object::test<4> () { // XY intersects XYZ at interior point. - // Z value at the intersection point is the interpolated value from the XZZ line. + // Z value at the intersection point is the interpolated value from the XYZ line. set_test_name("testInterior2D3D"); checkIntersection( diff --git a/tests/unit/capi/GEOSNodeTest.cpp b/tests/unit/capi/GEOSNodeTest.cpp index 41324bda5..3fe730d9a 100644 --- a/tests/unit/capi/GEOSNodeTest.cpp +++ b/tests/unit/capi/GEOSNodeTest.cpp @@ -1,11 +1,14 @@ // // Test Suite for C-API GEOSNode +#include #include // geos #include +#include #include "capi_test_utils.h" +#include "utility.h" namespace tut { // @@ -200,12 +203,60 @@ template<> template<> void object::test<9>() { + set_test_name("two arcs with two intersection points"); + input_ = fromWKT("MULTICURVE (CIRCULARSTRING (0 0, 1 1, 2 0), CIRCULARSTRING (0 1, 1 0, 2 1))"); ensure(input_); result_ = GEOSNode(input_); + ensure(result_ != nullptr); - ensure("curved geometries not supported", result_ == nullptr); + expected_ = fromWKT("MULTICURVE (" + "CIRCULARSTRING (0 0, 0.0340741737 0.2588190451, 0.1339745962 0.5, 1 1, 1.8660254038 0.5, 1.9659258263 0.2588190451, 2 0)," + "CIRCULARSTRING (0 1, 0.0340741737 0.7411809549, 0.1339745962 0.5, 1 0, 1.8660254038 0.5, 1.9659258263 0.7411809549, 2 1))"); + + ensure_geometry_equals_exact(result_, expected_, 1e-8); +} + +template<> +template<> +void object::test<10>() +{ + set_test_name("CIRCULARSTRING ZM intersecting CIRCULARSTRING M"); + + input_ = fromWKT("MULTICURVE (CIRCULARSTRING ZM (-1 0 3 4, 0 1 2 5, 1 0 4 7), CIRCULARSTRING M (-1 2 9, 0 1 13, -1 0 17))"); + ensure(input_); + + result_ = GEOSNode(input_); + ensure(result_ != nullptr); + + expected_ = fromWKT("MULTICURVE ZM (" + "CIRCULARSTRING ZM (-1 0 3 4, -1 1.2246467991e-16 5 4.75, -1 2.7755575616e-16 7 5.5, -1 1.2246467991e-16 7 5.5, -1 5.5511151231e-17 7 5.5, -0.7071067812 0.7071067812 5.25 7.375, -2.7755575616e-16 1 3.5 9.25, -3.8285686989e-16 1 3.5 9.25, -5.5511151231e-17 1 3.5 9.25, 0.7071067812 0.7071067812 3.75 8.125, 1 0 4 7)," + "CIRCULARSTRING ZM (-1 2 NaN 9, -0.2928932188 1.7071067812 NaN 9.125, -2.7755575616e-16 1 3.5 9.25, 0 1 3.5 9.25, -5.5511151231e-17 1 3.5 9.25, -0.2928932188 0.2928932188 5.25 7.375, -1 2.7755575616e-16 7 5.5, -1 0 7 5.5, -1 5.5511151231e-17 7 5.5, -1 0 NaN 11.25, -1 0 NaN 17))"); + + ensure_equals_exact_geometry_xyzm(reinterpret_cast(result_), + reinterpret_cast(expected_), 1e-8); +} + +template<> +template<> +void object::test<11>() +{ + set_test_name("CIRCULARSTRING ZM intersecting LINESTRING M"); + + input_ = fromWKT("MULTICURVE (CIRCULARSTRING ZM (-5 0 3 4, -4 3 2 5, 4 3 4 7), LINESTRING M (0 0 7, 0 10 13))"); + ensure(input_); + + result_ = GEOSNode(input_); + ensure(result_ != nullptr); + + expected_ = fromWKT("MULTICURVE ZM (" + "CIRCULARSTRING ZM (-5 0 3 4, -3.5355 3.5355 3 6, 0 5 3 8, 3.5355 3.5355 3.5 7.5, 4 3 4 7)," + "LINESTRING ZM (0 0 NaN 7, 0 5 3 8)," + "LINESTRING ZM (0 5 3 8, 0 10 NaN 13))"); + + ensure_equals_exact_geometry_xyzm(reinterpret_cast(result_), + reinterpret_cast(expected_), 1e-4); } } // namespace tut diff --git a/tests/unit/geom/CircularArcTest.cpp b/tests/unit/geom/CircularArcTest.cpp index 024820bb6..d09c9ae5b 100644 --- a/tests/unit/geom/CircularArcTest.cpp +++ b/tests/unit/geom/CircularArcTest.cpp @@ -16,19 +16,19 @@ struct test_circulararc_data { void checkAngle(const CoordinateXY& p0, const CoordinateXY& p1, const CoordinateXY& p2, double expected) { - CircularArc arc(p0, p1, p2); + auto arc = CircularArc::create(p0, p1, p2); ensure_equals(p0.toString() + " / " + p1.toString() + " / " + p2.toString(), arc.getAngle(), expected, eps); - CircularArc rev(p2, p1, p0); + auto rev = CircularArc::create(p2, p1, p0); ensure_equals(p2.toString() + " / " + p1.toString() + " / " + p0.toString(), rev.getAngle(), expected, eps); } void checkLength(const CoordinateXY& p0, const CoordinateXY& p1, const CoordinateXY& p2, double expected) { - CircularArc arc(p0, p1, p2); + auto arc = CircularArc::create(p0, p1, p2); ensure_equals(p0.toString() + " / " + p1.toString() + " / " + p2.toString(), arc.getLength(), expected, eps); - CircularArc rev(p2, p1, p0); + auto rev = CircularArc::create(p2, p1, p0); ensure_equals(p2.toString() + " / " + p1.toString() + " / " + p0.toString(), rev.getLength(), expected, eps); } }; @@ -83,14 +83,14 @@ void object::test<3>() { set_test_name("CircularArc::getArea()"); - ensure_equals("half circle, R=2", CircularArc({-2, 0}, {0, 2}, {2, 0}).getArea(), MATH_PI*2); + ensure_equals("half circle, R=2", CircularArc::create(CoordinateXY{-2, 0}, {0, 2}, {2, 0}).getArea(), MATH_PI*2); - ensure_equals("full circle, R=3", CircularArc({-3, 0}, {3, 0}, {-3, 0}).getArea(), MATH_PI*3*3); + ensure_equals("full circle, R=3", CircularArc::create(CoordinateXY{-3, 0}, {3, 0}, {-3, 0}).getArea(), MATH_PI*3*3); - ensure_equals("3/4, mouth up, R=2", CircularArc({-std::sqrt(2), std::sqrt(2)}, {0, -2}, {std::sqrt(2), std::sqrt(2)}).getArea(), + ensure_equals("3/4, mouth up, R=2", CircularArc::create(CoordinateXY{-std::sqrt(2), std::sqrt(2)}, {0, -2}, {std::sqrt(2), std::sqrt(2)}).getArea(), MATH_PI*4 - 2*(MATH_PI/2-1), 1e-8); - ensure_equals("1/4, pointing up, R=2", CircularArc({-std::sqrt(2), std::sqrt(2)}, {0, 2}, {std::sqrt(2), std::sqrt(2)}).getArea(), + ensure_equals("1/4, pointing up, R=2", CircularArc::create(CoordinateXY{-std::sqrt(2), std::sqrt(2)}, {0, 2}, {std::sqrt(2), std::sqrt(2)}).getArea(), 2*(MATH_PI/2-1), 1e-8); } @@ -100,8 +100,8 @@ void object::test<4>() { set_test_name("CircularArc::isLinear()"); - ensure_equals("not linear", CircularArc({-1, 0}, {0, 1}, {1, 0}).isLinear(), false); - ensure_equals("linear", CircularArc({0, 0}, {1, 1}, {2, 2}).isLinear(), true); + ensure_equals("not linear", CircularArc::create(CoordinateXY{-1, 0}, {0, 1}, {1, 0}).isLinear(), false); + ensure_equals("linear", CircularArc::create(CoordinateXY{0, 0}, {1, 1}, {2, 2}).isLinear(), true); } template<> @@ -110,15 +110,17 @@ void object::test<5>() { set_test_name("CircularArc::containsPointOnCircle"); + // FIXME: Add asserts + // complete circle - CircularArc({5, 0}, {-5, 0}, {5, 0}).containsPointOnCircle({5, 0}); - CircularArc({5, 0}, {-5, 0}, {5, 0}).containsPointOnCircle({4, 3}); + CircularArc::create(CoordinateXY{5, 0}, {-5, 0}, {5, 0}).containsPointOnCircle({5, 0}); + CircularArc::create(CoordinateXY{5, 0}, {-5, 0}, {5, 0}).containsPointOnCircle({4, 3}); // lower semi-circle - CircularArc({-5, 0}, {0, -5}, {5, 0}).containsPointOnCircle({5, 0}); + CircularArc::create(CoordinateXY{-5, 0}, {0, -5}, {5, 0}).containsPointOnCircle({5, 0}); // upper semi-circle - CircularArc({-5, 0}, {0, 5}, {5, 0}).containsPointOnCircle({5, 0}); + CircularArc::create(CoordinateXY{-5, 0}, {0, 5}, {5, 0}).containsPointOnCircle({5, 0}); } } diff --git a/tests/unit/geom/CoordinateSequenceTest.cpp b/tests/unit/geom/CoordinateSequenceTest.cpp index a0c86888e..89c35ba8d 100644 --- a/tests/unit/geom/CoordinateSequenceTest.cpp +++ b/tests/unit/geom/CoordinateSequenceTest.cpp @@ -1617,4 +1617,29 @@ void object::test<61>() ensure(std::isnan(seq.getM(1))); } +template<> +template<> +void object::test<62> +() +{ + set_test_name("CoordinateSequence::swap"); + + CoordinateSequence seq = CoordinateSequence::XYZM(0); + CoordinateXYZM p0{1,2,3,4}; + CoordinateXYZM p1{5,6,7,8}; + CoordinateXYZM p2{9,10,11,12}; + seq.add(p0); + seq.add(p1); + seq.add(p2); + + seq.swap(1, 1); + ensure_equals_xyzm(seq.getAt(0), p0); + ensure_equals_xyzm(seq.getAt(1), p1); + ensure_equals_xyzm(seq.getAt(2), p2); + + seq.swap(2, 1); + ensure_equals_xyzm(seq.getAt(0), p0); + ensure_equals_xyzm(seq.getAt(1), p2); + ensure_equals_xyzm(seq.getAt(2), p1); +} } // namespace tut diff --git a/tests/unit/geom/CoordinateTest.cpp b/tests/unit/geom/CoordinateTest.cpp index 1531fe845..b638552f3 100644 --- a/tests/unit/geom/CoordinateTest.cpp +++ b/tests/unit/geom/CoordinateTest.cpp @@ -439,6 +439,32 @@ void object::test<17>() ensure_same(xyzm.get(), 4); } +template<> +template<> +void object::test<18>() +{ + set_test_name("Coordinate::has()"); + + ensure_equals(CoordinateXY::has(), true); + ensure_equals(CoordinateXY::has(), true); + ensure_equals(CoordinateXY::has(), false); + ensure_equals(CoordinateXY::has(), false); + + ensure_equals(Coordinate::has(), true); + ensure_equals(Coordinate::has(), true); + ensure_equals(Coordinate::has(), true); + ensure_equals(Coordinate::has(), false); + + ensure_equals(CoordinateXYM::has(), true); + ensure_equals(CoordinateXYM::has(), true); + ensure_equals(CoordinateXYM::has(), false); + ensure_equals(CoordinateXYM::has(), true); + + ensure_equals(CoordinateXYZM::has(), true); + ensure_equals(CoordinateXYZM::has(), true); + ensure_equals(CoordinateXYZM::has(), true); + ensure_equals(CoordinateXYZM::has(), true); +} } // namespace tut diff --git a/tests/unit/noding/NodableArcStringTest.cpp b/tests/unit/noding/NodableArcStringTest.cpp index bde0f4970..d468b1d46 100644 --- a/tests/unit/noding/NodableArcStringTest.cpp +++ b/tests/unit/noding/NodableArcStringTest.cpp @@ -5,18 +5,25 @@ using geos::algorithm::Orientation; using geos::geom::CircularArc; +using geos::geom::Ordinate; using geos::geom::CoordinateXY; +using geos::geom::CoordinateXYZM; using geos::noding::NodableArcString; namespace tut { struct test_nodablearcstring_data { + using XY = CoordinateXY; + using XYZ = Coordinate; + using XYM = geos::geom::CoordinateXYM; + using XYZM = CoordinateXYZM; + static void test_add_points(const CircularArc& arc, const std::vector& coords, const std::vector& expected, bool reversed=false) { std::vector arcs; arcs.push_back(arc); - NodableArcString nas(arcs); + NodableArcString nas(arcs, nullptr, false, false, nullptr); for (const auto& coord : coords) { nas.addInt(coord, 0); @@ -27,16 +34,15 @@ struct test_nodablearcstring_data { ensure_equals(noded->getSize(), expected.size()); for (std::size_t i = 0; i < expected.size(); i++) { - ensure_arc_equals(noded->getArc(i), expected[i]); + ensure_arc_equals(noded->getArc(i), expected[i], 1e-8); } if (!reversed) { - auto revArc = arc; - revArc.reverse(); + const auto revArc = arc.reverse(); - auto revExpected = expected; - for (auto& x : revExpected) { - x.reverse(); + std::vector revExpected; + for (const auto& x : expected) { + revExpected.push_back(x.reverse()); } std::reverse(revExpected.begin(), revExpected.end()); @@ -44,12 +50,8 @@ struct test_nodablearcstring_data { } } - static void ensure_arc_equals(const CircularArc& actual, const CircularArc& expected) { - ensure_equals_xy(actual.p0, expected.p0); - ensure_equals_xy(actual.p2, expected.p2); - ensure_equals_xy(actual.getCenter(), expected.getCenter()); - ensure_equals(actual.getRadius(), expected.getRadius()); - ensure_equals(actual.getOrientation(), expected.getOrientation()); + static void ensure_arc_equals(const CircularArc& actual, const CircularArc& expected, double tol) { + ensure(actual.toString() + " does not equal expected " + expected.toString() ,actual.equals(expected, tol)); } }; @@ -64,7 +66,7 @@ void object::test<1>() { set_test_name("CW half-circle, upper half-plane"); - CircularArc in(CoordinateXY{-5, 0}, CoordinateXY{0, 5}, CoordinateXY{5, 0}); + CircularArc in = CircularArc::create(CoordinateXY{-5, 0}, CoordinateXY{0, 5}, CoordinateXY{5, 0}); std::vector coords; coords.emplace_back(4, 3); @@ -73,11 +75,11 @@ void object::test<1>() coords.emplace_back(-4, 3); std::vector expected; - expected.push_back(CircularArc({-5, 0}, {-4, 3}, {0, 0}, 5, Orientation::CLOCKWISE)); - expected.push_back(CircularArc({-4, 3}, {-3, 4}, {0, 0}, 5, Orientation::CLOCKWISE)); - expected.push_back(CircularArc({-3, 4}, {3, 4}, {0, 0}, 5, Orientation::CLOCKWISE)); - expected.push_back(CircularArc({3, 4}, {4, 3}, {0, 0}, 5, Orientation::CLOCKWISE)); - expected.push_back(CircularArc({4, 3}, {5, 0}, {0, 0}, 5, Orientation::CLOCKWISE)); + expected.push_back(CircularArc::create(XY{-5, 0}, {-4, 3}, {0, 0}, 5, Orientation::CLOCKWISE)); + expected.push_back(CircularArc::create(XY{-4, 3}, {-3, 4}, {0, 0}, 5, Orientation::CLOCKWISE)); + expected.push_back(CircularArc::create(XY{-3, 4}, {3, 4}, {0, 0}, 5, Orientation::CLOCKWISE)); + expected.push_back(CircularArc::create(XY{3, 4}, {4, 3}, {0, 0}, 5, Orientation::CLOCKWISE)); + expected.push_back(CircularArc::create(XY{4, 3}, {5, 0}, {0, 0}, 5, Orientation::CLOCKWISE)); test_add_points(in, coords, expected); } @@ -88,7 +90,7 @@ void object::test<2>() { set_test_name("CW half-circle, right half-plane"); - CircularArc in(CoordinateXY{0, 5}, CoordinateXY{5, 0}, CoordinateXY{0, -5}); + CircularArc in = CircularArc::create(CoordinateXY{0, 5}, CoordinateXY{5, 0}, CoordinateXY{0, -5}); std::vector coords; coords.emplace_back(4, -3); @@ -98,14 +100,65 @@ void object::test<2>() coords.emplace_back(5, 0); std::vector expected; - expected.push_back(CircularArc({0, 5}, {3, 4}, {0, 0}, 5, Orientation::CLOCKWISE)); - expected.push_back(CircularArc({3, 4}, {4, 3}, {0, 0}, 5, Orientation::CLOCKWISE)); - expected.push_back(CircularArc({4, 3}, {5, 0}, {0, 0}, 5, Orientation::CLOCKWISE)); - expected.push_back(CircularArc({5, 0}, {4, -3}, {0, 0}, 5, Orientation::CLOCKWISE)); - expected.push_back(CircularArc({4, -3}, {3, -4}, {0, 0}, 5, Orientation::CLOCKWISE)); - expected.push_back(CircularArc({3, -4}, {0, -5}, {0, 0}, 5, Orientation::CLOCKWISE)); + expected.push_back(CircularArc::create(XY{0, 5}, {3, 4}, {0, 0}, 5, Orientation::CLOCKWISE)); + expected.push_back(CircularArc::create(XY{3, 4}, {4, 3}, {0, 0}, 5, Orientation::CLOCKWISE)); + expected.push_back(CircularArc::create(XY{4, 3}, {5, 0}, {0, 0}, 5, Orientation::CLOCKWISE)); + expected.push_back(CircularArc::create(XY{5, 0}, {4, -3}, {0, 0}, 5, Orientation::CLOCKWISE)); + expected.push_back(CircularArc::create(XY{4, -3}, {3, -4}, {0, 0}, 5, Orientation::CLOCKWISE)); + expected.push_back(CircularArc::create(XY{3, -4}, {0, -5}, {0, 0}, 5, Orientation::CLOCKWISE)); test_add_points(in, coords, expected); } +template<> +template<> +void object::test<3>() +{ + set_test_name("no points added"); + CircularArc in = CircularArc::create(CoordinateXY{-1, 0}, CoordinateXY{0, 1}, CoordinateXY{1, 0}); + + std::vector coords; + std::vector expected; + expected.push_back(in); + test_add_points(in, coords, expected); +} + +template<> +template<> +void object::test<4>() +{ + set_test_name("Center point Z/M in constructed arcs interpolated from endpoints"); + + CoordinateSequence seq = CoordinateSequence::XYZM(3); + CoordinateXYZM p0{0, 5, 6, 2}; + CoordinateXYZM p1{5, 0, 7, 3}; + CoordinateXYZM p2{4, -3, 9, 1}; + + seq.setAt(p0, 0); + seq.setAt(p1, 1); + seq.setAt(p2, 2); + + CircularArc arc (seq, 0); + + CoordinateXYZM intPt{4, 3, 13, 5}; + + std::vector in { arc }; + NodableArcString nas(std::move(in), nullptr, true, true, nullptr); + + nas.addIntersection( intPt, 0); + + auto noded = nas.getNoded(); + + ensure_equals(noded->getSize(), 2u); + const CircularArc& arc0 = noded->getArc(0); + ensure_arc_equals(arc0, CircularArc::create(p0, intPt, arc.getCenter(), arc.getRadius(), arc.getOrientation()), 1e-8); + ensure_equals(arc0.p1().z, (p0.z + intPt.z) / 2); + ensure_equals(arc0.p1().m, (p0.m + intPt.m) / 2); + + const CircularArc& arc1 = noded->getArc(1); + ensure_arc_equals(arc1, CircularArc::create(intPt, p2, arc.getCenter(), arc.getRadius(), arc.getOrientation()), 1e-8); + ensure_equals(arc1.p1().z, (intPt.z + p2.z) / 2); + ensure_equals(arc1.p1().m, (intPt.m + p2.m) / 2); +} + } \ No newline at end of file diff --git a/tests/unit/noding/SimpleNoderTest.cpp b/tests/unit/noding/SimpleNoderTest.cpp index 7450724c6..401763bef 100644 --- a/tests/unit/noding/SimpleNoderTest.cpp +++ b/tests/unit/noding/SimpleNoderTest.cpp @@ -12,6 +12,7 @@ using geos::geom::CoordinateXY; using geos::geom::CoordinateSequence; using geos::geom::CircularArc; +using geos::geom::Ordinate; using geos::algorithm::Orientation; using geos::noding::ArcString; using geos::noding::SegmentString; @@ -24,6 +25,20 @@ namespace tut { struct test_simplenoder_data { + // FIXME: This is duplicated from CircularArcIntersectorTest + template + CircularArc makeArc(T p0, T p2, const CoordinateXY& center, double radius, int orientation) + { + auto seq = std::make_unique(3, T::template has(), T::template has()); + seq->setAt(p0, 0); + seq->setAt(geos::algorithm::CircularArcs::getMidpoint(p0, p2, center, radius, orientation == Orientation::COUNTERCLOCKWISE), 1); + seq->setAt(p2, 2); + + CircularArc ret(std::move(seq), 0); + + return ret; + } + template static void check_length_equal(const T1& actual, const T2& expected) { @@ -76,11 +91,11 @@ struct test_simplenoder_data { for (const auto& arc : *arcString) { if (first) { first = false; - std::cout << arc.p0 << ", "; + std::cout << arc.p0() << ", "; } else { std::cout << ", "; } - std::cout << arc.p1 << ", " << arc.p2; + std::cout << arc.p1() << ", " << arc.p2(); } std::cout << ")"; } @@ -128,11 +143,14 @@ void object::test<2>() { set_test_name("arc-arc intersection"); - CircularArc arc0({-1, 0}, {1, 0}, {0, 0}, 1, Orientation::CLOCKWISE); - CircularArc arc1({-1, 1}, {1, 1}, {0, 1}, 1, Orientation::COUNTERCLOCKWISE); + std::vector arcs0; + arcs0.push_back(makeArc(CoordinateXY{-1, 0}, {1, 0}, {0, 0}, 1, Orientation::CLOCKWISE)); - NodableArcString as0({arc0}); - NodableArcString as1({arc1}); + std::vector arcs1; + arcs1.push_back(makeArc(CoordinateXY{-1, 1}, {1, 1}, {0, 1}, 1, Orientation::COUNTERCLOCKWISE)); + + NodableArcString as0(std::move(arcs0), nullptr, false, false, nullptr); + NodableArcString as1(std::move(arcs1), nullptr, false, false, nullptr); std::vector ss{&as0, &as1}; @@ -150,8 +168,9 @@ void object::test<3>() { set_test_name("arc-segment intersection"); - CircularArc arc0({-1, 0}, {1, 0}, {0, 0}, 1, Orientation::CLOCKWISE); - NodableArcString as0({arc0}); + std::vector arcs0; + arcs0.push_back(makeArc(CoordinateXY{-1, 0}, {1, 0}, {0, 0}, 1, Orientation::CLOCKWISE)); + NodableArcString as0(std::move(arcs0), nullptr, false, false, nullptr); auto seq1 = std::make_shared(); seq1->add(CoordinateXY{-1, 0.5}); diff --git a/tests/unit/utility.h b/tests/unit/utility.h index c86dbce56..6b98a59d0 100644 --- a/tests/unit/utility.h +++ b/tests/unit/utility.h @@ -131,45 +131,48 @@ ensure_equals_xy(geos::geom::CoordinateXY const& actual, inline void ensure_equals_xyz(geos::geom::Coordinate const& actual, - geos::geom::Coordinate const& expected) + geos::geom::Coordinate const& expected, + double tol=0) { - ensure_equals("Coordinate X", actual.x, expected.x ); - ensure_equals("Coordinate Y", actual.y, expected.y ); + ensure_equals("Coordinate X", actual.x, expected.x, tol); + ensure_equals("Coordinate Y", actual.y, expected.y, tol); if ( std::isnan(expected.z) ) { ensure("Coordinate Z should be NaN", std::isnan(actual.z) ); } else { - ensure_equals("Coordinate Z", actual.z, expected.z ); + ensure_equals("Coordinate Z", actual.z, expected.z, tol); } } inline void ensure_equals_xym(geos::geom::CoordinateXYM const& actual, - geos::geom::CoordinateXYM const& expected) + geos::geom::CoordinateXYM const& expected, + double tol=0) { - ensure_equals("Coordinate X", actual.x, expected.x ); - ensure_equals("Coordinate Y", actual.y, expected.y ); + ensure_equals("Coordinate X", actual.x, expected.x , tol); + ensure_equals("Coordinate Y", actual.y, expected.y, tol); if ( std::isnan(expected.m) ) { ensure("Coordinate M should be NaN", std::isnan(actual.m) ); } else { - ensure_equals("Coordinate M", actual.m, expected.m ); + ensure_equals("Coordinate M", actual.m, expected.m, tol); } } inline void ensure_equals_xyzm(geos::geom::CoordinateXYZM const& actual, - geos::geom::CoordinateXYZM const& expected) + geos::geom::CoordinateXYZM const& expected, + double tol = 0) { - ensure_equals("Coordinate X", actual.x, expected.x ); - ensure_equals("Coordinate Y", actual.y, expected.y ); + ensure_equals("Coordinate X", actual.x, expected.x, tol); + ensure_equals("Coordinate Y", actual.y, expected.y, tol); if ( std::isnan(expected.z) ) { ensure("Coordinate Z should be NaN", std::isnan(actual.z) ); } else { - ensure_equals("Coordinate Z", actual.z, expected.z ); + ensure_equals("Coordinate Z", actual.z, expected.z, tol); } if ( std::isnan(expected.m) ) { ensure("Coordinate M should be NaN", std::isnan(actual.m) ); } else { - ensure_equals("Coordinate M", actual.m, expected.m ); + ensure_equals("Coordinate M", actual.m, expected.m, tol); } } commit db93f778ec51fdc1a71886f2b319ec6da35a302d Author: Daniel Baston Date: Sun Nov 2 18:15:09 2025 -0500 SimpleNoder: Add Arc support diff --git a/include/geos/algorithm/CircularArcIntersector.h b/include/geos/algorithm/CircularArcIntersector.h index dad551906..c7a8c323b 100644 --- a/include/geos/algorithm/CircularArcIntersector.h +++ b/include/geos/algorithm/CircularArcIntersector.h @@ -82,10 +82,15 @@ public: static int circleIntersects(const CoordinateXY& center, double r, const CoordinateXY& p0, const CoordinateXY& p1, CoordinateXY& isect0, CoordinateXY& isect1); -private: void intersects(const CoordinateXY& p0, const CoordinateXY& p1, const CoordinateXY& q0, const CoordinateXY& q1); +private: + void reset() { + nPt = 0; + nArc = 0; + } + std::array intPt; std::array intArc; intersection_type result = NO_INTERSECTION; diff --git a/include/geos/geom/CircularArc.h b/include/geos/geom/CircularArc.h index 621ab3ec1..be2b2fe43 100644 --- a/include/geos/geom/CircularArc.h +++ b/include/geos/geom/CircularArc.h @@ -250,6 +250,17 @@ public: return isUpward; } + void reverse() { + std::swap(p0, p2); + if (m_orientation_known) { + if (m_orientation == algorithm::Orientation::COUNTERCLOCKWISE) { + m_orientation = algorithm::Orientation::CLOCKWISE; + } else if (m_orientation == algorithm::Orientation::CLOCKWISE) { + m_orientation = algorithm::Orientation::COUNTERCLOCKWISE; + } + } + } + // Split an arc at a specified point. // The point is assumed to be o the arc. std::pair splitAtPoint(const CoordinateXY& q) const { @@ -259,6 +270,10 @@ public: }; } + std::string toString() const { + return "CIRCULARSTRING (" + p0.toString() + ", " + p1.toString() + ", " + p2.toString() + ")"; + } + class Iterator { public: using iterator_category = std::forward_iterator_tag; diff --git a/include/geos/noding/ArcIntersectionAdder.h b/include/geos/noding/ArcIntersectionAdder.h new file mode 100644 index 000000000..e3e55d904 --- /dev/null +++ b/include/geos/noding/ArcIntersectionAdder.h @@ -0,0 +1,36 @@ +/********************************************************************** +* + * GEOS - Geometry Engine Open Source + * http://geos.osgeo.org + * + * Copyright (C) 2025 ISciences, LLC + * + * This is free software; you can redistribute and/or modify it under + * the terms of the GNU Lesser General Public Licence as published + * by the Free Software Foundation. + * See the COPYING file for more information. + * + **********************************************************************/ + +#pragma once + +#include +#include +#include + +namespace geos::noding { + +class GEOS_DLL ArcIntersectionAdder : public ArcIntersector { + +public: + void processIntersections(ArcString& e0, std::size_t segIndex0, ArcString& e1, std::size_t segIndex1) override; + + void processIntersections(ArcString& e0, std::size_t segIndex0, SegmentString& e1, std::size_t segIndex1) override; + + void processIntersections(SegmentString& e0, std::size_t segIndex0, SegmentString& e1, std::size_t segIndex1) override; + +private: + algorithm::CircularArcIntersector m_intersector; +}; + +} \ No newline at end of file diff --git a/include/geos/noding/ArcIntersector.h b/include/geos/noding/ArcIntersector.h new file mode 100644 index 000000000..7b5ee91aa --- /dev/null +++ b/include/geos/noding/ArcIntersector.h @@ -0,0 +1,44 @@ +/********************************************************************** +* + * GEOS - Geometry Engine Open Source + * http://geos.osgeo.org + * + * Copyright (C) 2025 ISciences, LLC + * + * This is free software; you can redistribute and/or modify it under + * the terms of the GNU Lesser General Public Licence as published + * by the Free Software Foundation. + * See the COPYING file for more information. + * + **********************************************************************/ + +#pragma once + +#include +#include +#include + +namespace geos::noding { + +class GEOS_DLL ArcIntersector { +public: + virtual void processIntersections(ArcString& e0, std::size_t arcIndex0, + ArcString& e1, std::size_t arcIndex1) = 0; + virtual void processIntersections(SegmentString& e0, std::size_t segIndex0, + SegmentString& e1, std::size_t segIndex1) = 0; + virtual void processIntersections(ArcString& e0, std::size_t arcIndex0, + SegmentString& e1, std::size_t segIndex1) = 0; + + virtual void processIntersections(SegmentString& e0, std::size_t segIndex0, + ArcString& e1, std::size_t arcIndex1) { + processIntersections(e1, arcIndex1, e0, segIndex0); + } + + virtual bool isDone() const { + return false; + } + + virtual ~ArcIntersector() = default; +}; + +} \ No newline at end of file diff --git a/include/geos/noding/ArcNoder.h b/include/geos/noding/ArcNoder.h new file mode 100644 index 000000000..4337db295 --- /dev/null +++ b/include/geos/noding/ArcNoder.h @@ -0,0 +1,47 @@ +/********************************************************************** + * + * GEOS - Geometry Engine Open Source + * http://geos.osgeo.org + * + * Copyright (C) 2025 ISciences, LLC + * + * This is free software; you can redistribute and/or modify it under + * the terms of the GNU Lesser General Public Licence as published + * by the Free Software Foundation. + * See the COPYING file for more information. + * + **********************************************************************/ + +#pragma once + +#include +#include +#include + +namespace geos::noding { +class PathString; +} + +namespace geos::noding { + +class GEOS_DLL ArcNoder : public Noder { + +public: + explicit ArcNoder(std::unique_ptr intersector) : + m_intersector(std::move(intersector)) {} + + ~ArcNoder() override; + + void computeNodes(const std::vector& segStrings) override; + + std::vector> getNodedSubstrings() override; + + virtual void computePathNodes(const std::vector& inputPaths) = 0; + + virtual std::vector> getNodedPaths() = 0; + +protected: + std::unique_ptr m_intersector; +}; + +} \ No newline at end of file diff --git a/include/geos/noding/ArcString.h b/include/geos/noding/ArcString.h new file mode 100644 index 000000000..10f254ed2 --- /dev/null +++ b/include/geos/noding/ArcString.h @@ -0,0 +1,63 @@ +/********************************************************************** +* + * GEOS - Geometry Engine Open Source + * http://geos.osgeo.org + * + * Copyright (C) 2025 ISciences, LLC + * + * This is free software; you can redistribute and/or modify it under + * the terms of the GNU Lesser General Public Licence as published + * by the Free Software Foundation. + * See the COPYING file for more information. + * + **********************************************************************/ + +#pragma once + +#include +#include +#include + +#include + +namespace geos::noding { + +/** \brief + * An interface for classes which represent a sequence of contiguous + * circular arcs, analogous to the SegmentString for contiguous line + * segments. + */ +class GEOS_DLL ArcString : public PathString { +public: + explicit ArcString(std::vector arcs) : m_arcs(std::move(arcs)) { + } + + std::size_t getSize() const override { + return m_arcs.size(); + } + + double getLength() const override { + double tot = 0; + for (const auto &arc: m_arcs) { + tot += arc.getLength(); + } + return tot; + } + + const geom::CircularArc &getArc(std::size_t i) const { + return m_arcs[i]; + } + + auto begin() const { + return m_arcs.begin(); + } + + auto end() const { + return m_arcs.end(); + } + +protected: + std::vector m_arcs; +}; + +} \ No newline at end of file diff --git a/include/geos/noding/NodableArcString.h b/include/geos/noding/NodableArcString.h new file mode 100644 index 000000000..e1b612261 --- /dev/null +++ b/include/geos/noding/NodableArcString.h @@ -0,0 +1,104 @@ +/********************************************************************** +* + * GEOS - Geometry Engine Open Source + * http://geos.osgeo.org + * + * Copyright (C) 2025 ISciences, LLC + * + * This is free software; you can redistribute and/or modify it under + * the terms of the GNU Lesser General Public Licence as published + * by the Free Software Foundation. + * See the COPYING file for more information. + * + **********************************************************************/ + +#pragma once + +#include +#include +#include + +namespace geos::noding { + +class GEOS_DLL NodableArcString : public ArcString, public NodablePath { + +public: + using ArcString::ArcString; + + void addIntersection(geom::CoordinateXYZM intPt, size_t segmentIndex) override { + m_adds[segmentIndex].push_back(intPt); + } + + static double pseudoAngleDiffCCW(double paStart, double pa) { + double diff = pa - paStart; + + if (diff < 0) { + diff += 4; + } + + return diff; + } + + std::unique_ptr getNoded() { + if (m_adds.empty()) { + // use std::move ? + return std::make_unique(*this); + } + + std::vector arcs; + for (size_t i = 0; i < m_arcs.size(); i++) { + auto it = m_adds.find(i); + if (it == m_adds.end()) { + arcs.push_back(m_arcs[i]); + } else { + std::vector& splitPoints = it->second; + + // TODO check split point actually inside arc + // TODO ignore duplicate splitpoints + + geom::CircularArc remainder = m_arcs[i]; + const bool isCCW = remainder.getOrientation() == algorithm::Orientation::COUNTERCLOCKWISE; + const double paStart = geom::Quadrant::pseudoAngle(remainder.getCenter(), remainder.p0); + //double paStart = isCCW ? geom::Quadrant::pseudoAngle(remainder.getCenter(), remainder.p0) : + // geom::Quadrant::pseudoAngle(remainder.getCenter(), remainder.p2); + + // Need a function for lengthFraction (pt); + std::sort(splitPoints.begin(), splitPoints.end(), [&remainder, paStart, isCCW](const auto& p0, const auto& p1) { + double pa0 = geom::Quadrant::pseudoAngle(remainder.getCenter(), p0); + double pa1 = geom::Quadrant::pseudoAngle(remainder.getCenter(), p1); + + // FIXME check this comparator... + if (isCCW) { + return pseudoAngleDiffCCW(paStart, pa0) < pseudoAngleDiffCCW(paStart, pa1); + } else { + return pseudoAngleDiffCCW(paStart, pa0) > pseudoAngleDiffCCW(paStart, pa1); + } + }); + + std::cout << "arc " << remainder.toString() << " " << (isCCW ? "CCW" : "CW") << std::endl; + std::cout << " paStart " << paStart << std::endl; + for (const auto& pt : splitPoints) { + const double pa = geom::Quadrant::pseudoAngle(remainder.getCenter(), pt); + std::cout << " " << pt << " pa " << pa << " diff " << pseudoAngleDiffCCW(paStart, pa) << std::endl; + } + + for (const auto& splitPoint : splitPoints) { + if (!arcs.empty() && splitPoint.equals2D(arcs.back().p2)) { + continue; + } + auto [a, b] = remainder.splitAtPoint(splitPoint); + arcs.push_back(a); + remainder = b; + } + arcs.push_back(remainder); + } + } + + return std::make_unique(std::move(arcs)); + } + +private: + std::map> m_adds; +}; + +} \ No newline at end of file diff --git a/include/geos/noding/NodablePath.h b/include/geos/noding/NodablePath.h new file mode 100644 index 000000000..35a4234c4 --- /dev/null +++ b/include/geos/noding/NodablePath.h @@ -0,0 +1,34 @@ +/********************************************************************** +* + * GEOS - Geometry Engine Open Source + * http://geos.osgeo.org + * + * Copyright (C) 2025 ISciences, LLC + * + * This is free software; you can redistribute and/or modify it under + * the terms of the GNU Lesser General Public Licence as published + * by the Free Software Foundation. + * See the COPYING file for more information. + * + **********************************************************************/ + +#pragma once + +#include +#include + +namespace geos::noding { + +/// A NodablePath represents a PathString to which coordinates can be added. +class GEOS_DLL NodablePath { + //virtual void addIntersection( const geom::Coordinate& intPt, int segmentIndex) =0; + //virtual void addIntersection( const geom::CoordinateXYM& intPt, int segmentIndex) =0; +public: + virtual void addInt(const geom::CoordinateXY& intPt, size_t pathIndex) { + addIntersection(geom::CoordinateXYZM{intPt}, pathIndex); + } + + virtual void addIntersection(geom::CoordinateXYZM intPt, size_t pathIndex) =0; +}; + +} \ No newline at end of file diff --git a/include/geos/noding/PathString.h b/include/geos/noding/PathString.h new file mode 100644 index 000000000..e1a3ba4b3 --- /dev/null +++ b/include/geos/noding/PathString.h @@ -0,0 +1,35 @@ +/********************************************************************** +* + * GEOS - Geometry Engine Open Source + * http://geos.osgeo.org + * + * Copyright (C) 2025 ISciences, LLC + * + * This is free software; you can redistribute and/or modify it under + * the terms of the GNU Lesser General Public Licence as published + * by the Free Software Foundation. + * See the COPYING file for more information. + * + **********************************************************************/ + +#pragma once + +#include + +#include + +namespace geos::noding { + +/// A PathString represents a contiguous line/arc to the used as an input for +/// noding. To access the coordinates, it is necessary to know whether they +/// represent a set of line segments (SegmentString) or circular arcs (ArcString). +class GEOS_DLL PathString { +public: + virtual ~PathString() = default; + + virtual std::size_t getSize() const = 0; + + virtual double getLength() const = 0; +}; + +} diff --git a/include/geos/noding/SegmentString.h b/include/geos/noding/SegmentString.h index 5b76d8c89..d1aa26834 100644 --- a/include/geos/noding/SegmentString.h +++ b/include/geos/noding/SegmentString.h @@ -21,9 +21,11 @@ #pragma once #include +#include #include #include #include +#include #include @@ -44,7 +46,7 @@ namespace noding { // geos.noding * SegmentStrings can carry a context object, which is useful * for preserving topological or parentage information. */ -class GEOS_DLL SegmentString { +class GEOS_DLL SegmentString : public PathString { public: typedef std::vector ConstVect; typedef std::vector NonConstVect; @@ -89,14 +91,23 @@ public: } std::size_t size() const { + // FIXME: Remove this method, or make consistent with getSize return seq->size(); } + std::size_t getSize() const override{ + return seq->size() - 1; + } + template const CoordType& getCoordinate(std::size_t i) const { return seq->getAt(i); } + double getLength() const override { + return algorithm::Length::ofLine(seq.get()); + } + /// \brief /// Return a pointer to the CoordinateSequence associated /// with this SegmentString. diff --git a/include/geos/noding/SimpleNoder.h b/include/geos/noding/SimpleNoder.h index 70bc4765f..3be9796ec 100644 --- a/include/geos/noding/SimpleNoder.h +++ b/include/geos/noding/SimpleNoder.h @@ -22,22 +22,20 @@ #include -#include -#include // for inlined (FIXME) +#include // Forward declarations namespace geos { namespace noding { -//class SegmentString; +class PathString; } } namespace geos { namespace noding { // geos.noding - /** \brief - * Nodes a set of {@link SegmentString}s by + * Nodes a set of {@link SegmentString}s and/or {@link ArcString}s by * performing a brute-force comparison of every segment to every other one. * * This has n^2 performance, so is too slow for use on large numbers @@ -45,24 +43,21 @@ namespace noding { // geos.noding * * @version 1.7 */ -class GEOS_DLL SimpleNoder: public SinglePassNoder { +class GEOS_DLL SimpleNoder: public ArcNoder { private: - std::vector nodedSegStrings; - void computeIntersects(SegmentString* e0, SegmentString* e1); + std::vector nodedSegStrings; + void computeIntersects(PathString& e0, PathString& e1); public: - SimpleNoder(SegmentIntersector* nSegInt = nullptr) - : - SinglePassNoder(nSegInt) - {} - void computeNodes(const std::vector& inputSegmentStrings) override; + SimpleNoder(std::unique_ptr(nSegInt)) : ArcNoder(std::move(nSegInt)) {} - std::vector> - getNodedSubstrings() override - { - return NodedSegmentString::getNodedSubstrings(nodedSegStrings); - } + void computePathNodes(const std::vector& inputSegmentStrings) override; + + std::vector> getNodedPaths() override; + +private: + std::vector m_pathStrings; }; } // namespace geos.noding diff --git a/src/algorithm/CircularArcIntersector.cpp b/src/algorithm/CircularArcIntersector.cpp index 5ed2a44b7..2ebbd247d 100644 --- a/src/algorithm/CircularArcIntersector.cpp +++ b/src/algorithm/CircularArcIntersector.cpp @@ -99,6 +99,8 @@ CircularArcIntersector::intersects(const CircularArc& arc, const CoordinateXY& p return; } + reset(); + // TODO: envelope check? const CoordinateXY& c = arc.getCenter(); const double r = arc.getRadius(); @@ -130,6 +132,8 @@ void CircularArcIntersector::intersects(const CoordinateXY& p0, const CoordinateXY& p1, const CoordinateXY& q0, const CoordinateXY& q1) { + reset(); + algorithm::LineIntersector li; li.computeIntersection(p0, p1, q0, q1); if (li.getIntersectionNum() == 2) { @@ -163,6 +167,8 @@ CircularArcIntersector::intersects(const CircularArc& arc1, const CircularArc& a return; } + reset(); + const auto& c1 = arc1.getCenter(); const auto& c2 = arc2.getCenter(); diff --git a/src/noding/ArcIntersectionAdder.cpp b/src/noding/ArcIntersectionAdder.cpp new file mode 100644 index 000000000..278228436 --- /dev/null +++ b/src/noding/ArcIntersectionAdder.cpp @@ -0,0 +1,96 @@ +/********************************************************************** +* + * GEOS - Geometry Engine Open Source + * http://geos.osgeo.org + * + * Copyright (C) 2025 ISciences, LLC + * + * This is free software; you can redistribute and/or modify it under + * the terms of the GNU Lesser General Public Licence as published + * by the Free Software Foundation. + * See the COPYING file for more information. + * + **********************************************************************/ + +#include +#include +#include + +namespace geos::noding { + +void +ArcIntersectionAdder::processIntersections(ArcString& e0, std::size_t segIndex0, ArcString& e1, std::size_t segIndex1) +{ + // don't bother intersecting a segment with itself + if(&e0 == &e1 && segIndex0 == segIndex1) { + return; + } + + const geom::CircularArc& arc0 = e0.getArc(segIndex0); + const geom::CircularArc& arc1 = e1.getArc(segIndex0); + + m_intersector.intersects(arc0, arc1); + + if (m_intersector.getResult() == algorithm::CircularArcIntersector::NO_INTERSECTION) { + return; + } + + // TODO handle cocircular intersections + for (std::uint8_t i = 0; i < m_intersector.getNumPoints(); i++) { + detail::down_cast(&e0)->addIntersection(geom::CoordinateXYZM{m_intersector.getPoint(i)}, segIndex0); + detail::down_cast(&e1)->addIntersection(geom::CoordinateXYZM{m_intersector.getPoint(i)}, segIndex1); + } +} + +void +ArcIntersectionAdder::processIntersections(ArcString& e0, std::size_t segIndex0, SegmentString& e1, std::size_t segIndex1) +{ +// don't bother intersecting a segment with itself + + const geom::CircularArc& arc = e0.getArc(segIndex0); + const geom::CoordinateXY& q0 = e1.getCoordinate(segIndex1); + const geom::CoordinateXY& q1 = e1.getCoordinate(segIndex1 + 1); + + m_intersector.intersects(arc, q0, q1); + + if (m_intersector.getResult() == algorithm::CircularArcIntersector::NO_INTERSECTION) { + return; + } + + for (std::uint8_t i = 0; i < m_intersector.getNumPoints(); i++) { + detail::down_cast(&e0)->addIntersection(geom::CoordinateXYZM{m_intersector.getPoint(i)}, segIndex0); + detail::down_cast(&e1)->addIntersection(geom::CoordinateXYZM{m_intersector.getPoint(i)}, segIndex1); + } + + +} + +void +ArcIntersectionAdder::processIntersections(SegmentString& e0, std::size_t segIndex0, SegmentString& e1, std::size_t segIndex1) +{ + using geom::CoordinateXY; + +// don't bother intersecting a segment with itself + if(&e0 == &e1 && segIndex0 == segIndex1) { + return; + } + + const CoordinateXY& p0 = e0.getCoordinate(segIndex0); + const CoordinateXY& p1 = e0.getCoordinate(segIndex0 + 1); + const CoordinateXY& q0 = e1.getCoordinate(segIndex1); + const CoordinateXY& q1 = e1.getCoordinate(segIndex1 + 1); + + m_intersector.intersects(p0, p1, q0, q1); + + if (m_intersector.getResult() == algorithm::CircularArcIntersector::NO_INTERSECTION) { + return; + } + +// todo collinear? + + static_cast(e0).addIntersection(m_intersector.getPoint(0), segIndex0); + + +} + +} \ No newline at end of file diff --git a/src/noding/ArcNoder.cpp b/src/noding/ArcNoder.cpp new file mode 100644 index 000000000..be26617c0 --- /dev/null +++ b/src/noding/ArcNoder.cpp @@ -0,0 +1,48 @@ +/********************************************************************** + * + * GEOS - Geometry Engine Open Source + * http://geos.osgeo.org + * + * Copyright (C) 2025 ISciences, LLC + * + * This is free software; you can redistribute and/or modify it under + * the terms of the GNU Lesser General Public Licence as published + * by the Free Software Foundation. + * See the COPYING file for more information. + * + **********************************************************************/ + +#include +#include +#include + +namespace geos::noding { + +ArcNoder::~ArcNoder() = default; + +void +ArcNoder::computeNodes(const std::vector& segStrings) +{ + std::vector pathStrings(segStrings.size()); + for (size_t i = 0; i < segStrings.size(); ++i) { + pathStrings[i] = segStrings[i]; + } + + computePathNodes(pathStrings); +} + +std::vector> + ArcNoder::getNodedSubstrings() +{ + auto pathStrings(getNodedPaths()); + std::vector> segStrings(pathStrings.size()); + for (size_t i = 0; i < pathStrings.size(); ++i) { + // check inputs to verify they were actually linear, then set a flag that can be + // checked in this method? + segStrings[i].reset(detail::down_cast(pathStrings[i].release())); + } + + return segStrings; +} + +} \ No newline at end of file diff --git a/src/noding/SimpleNoder.cpp b/src/noding/SimpleNoder.cpp index 6719a093e..7fc2007f3 100644 --- a/src/noding/SimpleNoder.cpp +++ b/src/noding/SimpleNoder.cpp @@ -19,44 +19,80 @@ #include #include -#include #include +#include +#include +#include using namespace geos::geom; namespace geos { namespace noding { // geos.noding -/*private*/ -void -SimpleNoder::computeIntersects(SegmentString* e0, SegmentString* e1) +template +static void computeIntersectsImpl(ArcIntersector& intersector, T1& e0, T2& e1) { - assert(segInt); // must provide a segment intersector! - - const CoordinateSequence* pts0 = e0->getCoordinates().get(); - const CoordinateSequence* pts1 = e1->getCoordinates().get(); - for(std::size_t i0 = 0, n0 = pts0->getSize() - 1; i0 < n0; i0++) { - for(std::size_t i1 = 0, n1 = pts1->getSize() - 1; i1 < n1; i1++) { - segInt->processIntersections(e0, i0, e1, i1); + for(std::size_t i = 0; i < e0.getSize(); i++) { + for(std::size_t j = 0; j < e1.getSize(); j++) { + intersector.processIntersections(e0, i, e1, j); } } +} +/*private*/ +void +SimpleNoder::computeIntersects(PathString& e0, PathString& e1) +{ + assert(m_intersector); // must provide a segment intersector! + + SegmentString* linear0 = dynamic_cast(&e0); + SegmentString* linear1 = dynamic_cast(&e1); + + if (linear0 && linear1) { + computeIntersectsImpl(*m_intersector, *linear0, *linear1); + } else if (!linear0 && !linear1) { + computeIntersectsImpl(*m_intersector, *detail::down_cast(&e0), *detail::down_cast(&e1)); + } else if (!linear0) { + computeIntersectsImpl(*m_intersector, *detail::down_cast(&e0), *linear1); + } else { + computeIntersectsImpl(*m_intersector, *linear0, *detail::down_cast(&e1)); + } } /*public*/ void -SimpleNoder::computeNodes(const std::vector& inputSegmentStrings) +SimpleNoder::computePathNodes(const std::vector& inputPathStrings) { - nodedSegStrings = inputSegmentStrings; + m_pathStrings = inputPathStrings; - for (SegmentString* edge0: inputSegmentStrings) { - for (SegmentString* edge1: inputSegmentStrings) { - computeIntersects(edge0, edge1); + for (auto* edge0: m_pathStrings) { + for (auto* edge1: m_pathStrings) { + // TODO skip processing against self? + computeIntersects(*edge0, *edge1); + } + } +} + +std::vector> +SimpleNoder::getNodedPaths() +{ + std::vector> nodedPaths; + + for (PathString* ps : m_pathStrings) { + if (auto* nss = dynamic_cast(ps)) { + std::vector> tmp; + nss->getNodeList().addSplitEdges(tmp); + for (auto& segString : tmp) { + nodedPaths.push_back(std::move(segString)); + } + } else { + auto* nas = detail::down_cast(ps); + nodedPaths.push_back(nas->getNoded()); } } + return nodedPaths; } - } // namespace geos.noding } // namespace geos diff --git a/tests/unit/noding/NodableArcStringTest.cpp b/tests/unit/noding/NodableArcStringTest.cpp new file mode 100644 index 000000000..bde0f4970 --- /dev/null +++ b/tests/unit/noding/NodableArcStringTest.cpp @@ -0,0 +1,111 @@ +#include +#include "utility.h" + +#include + +using geos::algorithm::Orientation; +using geos::geom::CircularArc; +using geos::geom::CoordinateXY; +using geos::noding::NodableArcString; + +namespace tut { + +struct test_nodablearcstring_data { + + static void test_add_points(const CircularArc& arc, const std::vector& coords, + const std::vector& expected, bool reversed=false) { + std::vector arcs; + arcs.push_back(arc); + NodableArcString nas(arcs); + + for (const auto& coord : coords) { + nas.addInt(coord, 0); + } + + auto noded = nas.getNoded(); + + ensure_equals(noded->getSize(), expected.size()); + + for (std::size_t i = 0; i < expected.size(); i++) { + ensure_arc_equals(noded->getArc(i), expected[i]); + } + + if (!reversed) { + auto revArc = arc; + revArc.reverse(); + + auto revExpected = expected; + for (auto& x : revExpected) { + x.reverse(); + } + std::reverse(revExpected.begin(), revExpected.end()); + + test_add_points(revArc, coords, revExpected, true); + } + } + + static void ensure_arc_equals(const CircularArc& actual, const CircularArc& expected) { + ensure_equals_xy(actual.p0, expected.p0); + ensure_equals_xy(actual.p2, expected.p2); + ensure_equals_xy(actual.getCenter(), expected.getCenter()); + ensure_equals(actual.getRadius(), expected.getRadius()); + ensure_equals(actual.getOrientation(), expected.getOrientation()); + } +}; + +typedef test_group group; +typedef group::object object; + +group test_nodablearcstring_group("geos::noding::NodableArcString"); + +template<> +template<> +void object::test<1>() +{ + set_test_name("CW half-circle, upper half-plane"); + + CircularArc in(CoordinateXY{-5, 0}, CoordinateXY{0, 5}, CoordinateXY{5, 0}); + + std::vector coords; + coords.emplace_back(4, 3); + coords.emplace_back(3, 4); + coords.emplace_back(-3, 4); + coords.emplace_back(-4, 3); + + std::vector expected; + expected.push_back(CircularArc({-5, 0}, {-4, 3}, {0, 0}, 5, Orientation::CLOCKWISE)); + expected.push_back(CircularArc({-4, 3}, {-3, 4}, {0, 0}, 5, Orientation::CLOCKWISE)); + expected.push_back(CircularArc({-3, 4}, {3, 4}, {0, 0}, 5, Orientation::CLOCKWISE)); + expected.push_back(CircularArc({3, 4}, {4, 3}, {0, 0}, 5, Orientation::CLOCKWISE)); + expected.push_back(CircularArc({4, 3}, {5, 0}, {0, 0}, 5, Orientation::CLOCKWISE)); + + test_add_points(in, coords, expected); +} + +template<> +template<> +void object::test<2>() +{ + set_test_name("CW half-circle, right half-plane"); + + CircularArc in(CoordinateXY{0, 5}, CoordinateXY{5, 0}, CoordinateXY{0, -5}); + + std::vector coords; + coords.emplace_back(4, -3); + coords.emplace_back(4, 3); + coords.emplace_back(3, -4); + coords.emplace_back(3, 4); + coords.emplace_back(5, 0); + + std::vector expected; + expected.push_back(CircularArc({0, 5}, {3, 4}, {0, 0}, 5, Orientation::CLOCKWISE)); + expected.push_back(CircularArc({3, 4}, {4, 3}, {0, 0}, 5, Orientation::CLOCKWISE)); + expected.push_back(CircularArc({4, 3}, {5, 0}, {0, 0}, 5, Orientation::CLOCKWISE)); + expected.push_back(CircularArc({5, 0}, {4, -3}, {0, 0}, 5, Orientation::CLOCKWISE)); + expected.push_back(CircularArc({4, -3}, {3, -4}, {0, 0}, 5, Orientation::CLOCKWISE)); + expected.push_back(CircularArc({3, -4}, {0, -5}, {0, 0}, 5, Orientation::CLOCKWISE)); + + test_add_points(in, coords, expected); +} + +} \ No newline at end of file diff --git a/tests/unit/noding/SimpleNoderTest.cpp b/tests/unit/noding/SimpleNoderTest.cpp new file mode 100644 index 000000000..7450724c6 --- /dev/null +++ b/tests/unit/noding/SimpleNoderTest.cpp @@ -0,0 +1,171 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +using geos::geom::CoordinateXY; +using geos::geom::CoordinateSequence; +using geos::geom::CircularArc; +using geos::algorithm::Orientation; +using geos::noding::ArcString; +using geos::noding::SegmentString; +using geos::noding::NodableArcString; +using geos::noding::NodedSegmentString; +using geos::noding::PathString; +using geos::noding::SimpleNoder; + +namespace tut { + +struct test_simplenoder_data { + + template + static void + check_length_equal(const T1& actual, const T2& expected) { + double tot_actual = 0.0; + double tot_expected = 0.0; + + for (const auto& path : actual) { + tot_actual += path->getLength(); + } + for (const auto& path : expected) { + tot_expected += path->getLength(); + } + + ensure_equals("length does not match expected", tot_actual, tot_expected, 1e-8); + } + + template + static void printPaths(const T& paths) { + std::cout << "GEOMETRYCOLLECTION ("; + + bool collFirst = true; + + for (const auto& path : paths) { + if (collFirst) { + collFirst = false; + } else { + std::cout << ", "; + } + + if (auto* segString = dynamic_cast(&*path)) { + std::cout << "LINESTRING ("; + + bool first = true; + const CoordinateSequence& seq = *segString->getCoordinates(); + seq.forEach([&first](const auto& pt) { + if (first) { + first = false; + } else { + std::cout << ", "; + } + + std::cout << pt; + }); + + std::cout << ")"; + } else { + auto* arcString = static_cast(&*path); + std::cout << "CIRCULARSTRING ("; + bool first = true; + for (const auto& arc : *arcString) { + if (first) { + first = false; + std::cout << arc.p0 << ", "; + } else { + std::cout << ", "; + } + std::cout << arc.p1 << ", " << arc.p2; + } + std::cout << ")"; + } + + } + + std::cout << ")"; + } +}; + +typedef test_group group; +typedef group::object object; + +group test_simplenoder_group("geos::noding::SimpleNoder"); + +template<> +template<> +void object::test<1>() +{ + set_test_name("segment-segment intersection"); + + auto seq1 = std::make_shared(); + seq1->add(CoordinateXY{0, 0}); + seq1->add(CoordinateXY{1, 1}); + NodedSegmentString ss1(seq1, false, false, nullptr); + + auto seq2 = std::make_shared(); + seq2->add(CoordinateXY{1, 0}); + seq2->add(CoordinateXY{0, 1}); + NodedSegmentString ss2(seq2, false, false, nullptr); + + std::vector ss{&ss1, &ss2}; + + SimpleNoder noder(std::make_unique()); + noder.computePathNodes(ss); + + auto paths = noder.getNodedPaths(); + + check_length_equal(paths, ss); +} + +template<> +template<> +void object::test<2>() +{ + set_test_name("arc-arc intersection"); + + CircularArc arc0({-1, 0}, {1, 0}, {0, 0}, 1, Orientation::CLOCKWISE); + CircularArc arc1({-1, 1}, {1, 1}, {0, 1}, 1, Orientation::COUNTERCLOCKWISE); + + NodableArcString as0({arc0}); + NodableArcString as1({arc1}); + + std::vector ss{&as0, &as1}; + + SimpleNoder noder(std::make_unique()); + noder.computePathNodes(ss); + + auto paths = noder.getNodedPaths(); + + check_length_equal(paths, ss); +} + +template<> +template<> +void object::test<3>() +{ + set_test_name("arc-segment intersection"); + + CircularArc arc0({-1, 0}, {1, 0}, {0, 0}, 1, Orientation::CLOCKWISE); + NodableArcString as0({arc0}); + + auto seq1 = std::make_shared(); + seq1->add(CoordinateXY{-1, 0.5}); + seq1->add(CoordinateXY{1, 0.5}); + NodedSegmentString ss1(seq1, false, false, nullptr); + + std::vector ss{&as0, &ss1}; + + SimpleNoder noder(std::make_unique()); + noder.computePathNodes(ss); + + auto paths = noder.getNodedPaths(); + + check_length_equal(paths, ss); +} + +} ----------------------------------------------------------------------- Summary of changes: include/geos/algorithm/Angle.h | 19 + include/geos/algorithm/CircularArcIntersector.h | 104 +- include/geos/algorithm/Interpolate.h | 19 +- include/geos/algorithm/LineIntersector.h | 6 +- include/geos/algorithm/RayCrossingCounter.h | 4 +- include/geos/geom/CircularArc.h | 311 ++--- include/geos/geom/CircularString.h | 8 + include/geos/geom/Coordinate.h | 108 ++ include/geos/geom/CoordinateSequence.h | 2 + include/geos/noding/ArcIntersectionAdder.h | 39 + include/geos/noding/ArcIntersector.h | 44 + include/geos/noding/ArcNoder.h | 53 + include/geos/noding/ArcString.h | 73 ++ include/geos/noding/GeometryNoder.h | 11 +- include/geos/noding/NodableArcString.h | 44 + include/geos/noding/NodablePath.h | 40 + include/geos/noding/PathString.h | 48 + include/geos/noding/SegmentString.h | 17 +- include/geos/noding/SimpleNoder.h | 31 +- src/algorithm/Angle.cpp | 23 + src/algorithm/Area.cpp | 14 +- src/algorithm/CircularArcIntersector.cpp | 577 +++++++-- src/algorithm/LineIntersector.cpp | 46 +- src/algorithm/RayCrossingCounter.cpp | 22 +- src/geom/CircularArc.cpp | 405 ++++++ src/geom/CircularString.cpp | 25 +- src/geom/CoordinateSequence.cpp | 12 + src/noding/ArcIntersectionAdder.cpp | 100 ++ src/noding/ArcNoder.cpp | 48 + .../{SegmentStringUtil.cpp => ArcString.cpp} | 19 +- src/noding/GeometryNoder.cpp | 114 +- src/noding/NodableArcString.cpp | 131 ++ .../UnionStrategy.cpp => noding/PathString.cpp} | 25 +- src/noding/SegmentString.cpp | 9 + src/noding/SimpleNoder.cpp | 67 +- tests/unit/algorithm/AngleTest.cpp | 24 + .../unit/algorithm/CircularArcIntersectorTest.cpp | 1347 ++++++++++++++++---- tests/unit/algorithm/CircularArcsTest.cpp | 42 +- .../unit/algorithm/RobustLineIntersectorZTest.cpp | 2 +- tests/unit/capi/GEOSNodeTest.cpp | 149 ++- tests/unit/geom/CircularArcTest.cpp | 30 +- tests/unit/geom/CoordinateSequenceTest.cpp | 25 + tests/unit/geom/CoordinateTest.cpp | 26 + tests/unit/noding/NodableArcStringTest.cpp | 164 +++ tests/unit/noding/SimpleNoderTest.cpp | 189 +++ tests/unit/utility.h | 74 +- 46 files changed, 3901 insertions(+), 789 deletions(-) create mode 100644 include/geos/noding/ArcIntersectionAdder.h create mode 100644 include/geos/noding/ArcIntersector.h create mode 100644 include/geos/noding/ArcNoder.h create mode 100644 include/geos/noding/ArcString.h create mode 100644 include/geos/noding/NodableArcString.h create mode 100644 include/geos/noding/NodablePath.h create mode 100644 include/geos/noding/PathString.h create mode 100644 src/geom/CircularArc.cpp create mode 100644 src/noding/ArcIntersectionAdder.cpp create mode 100644 src/noding/ArcNoder.cpp copy src/noding/{SegmentStringUtil.cpp => ArcString.cpp} (67%) create mode 100644 src/noding/NodableArcString.cpp copy src/{operation/union/UnionStrategy.cpp => noding/PathString.cpp} (51%) create mode 100644 tests/unit/noding/NodableArcStringTest.cpp create mode 100644 tests/unit/noding/SimpleNoderTest.cpp hooks/post-receive -- GEOS From git at osgeo.org Thu Jan 29 12:36:18 2026 From: git at osgeo.org (git at osgeo.org) Date: Thu, 29 Jan 2026 12:36:18 -0800 (PST) Subject: [geos-commits] [SCM] GEOS branch main updated. c7ca3dd29bf379c43ae6f97fbc1bdfa10dcf630e Message-ID: <20260129203622.D9FF516CA0A@trac.osgeo.org> This is an automated email from the git hooks/post-receive script. It was generated because a ref change was pushed to the repository containing the project "GEOS". The branch, main has been updated via c7ca3dd29bf379c43ae6f97fbc1bdfa10dcf630e (commit) from 0154023f7e0d15a27d2bf38fe6b78408be560bc8 (commit) Those revisions listed above that are new to this repository have not appeared on any other notification email; so we list those revisions in full, below. - Log ----------------------------------------------------------------- commit c7ca3dd29bf379c43ae6f97fbc1bdfa10dcf630e Author: Paul Ramsey Date: Thu Jan 29 12:35:09 2026 -0800 Port JTS-1177 Simplify FacetSequence implementation diff --git a/include/geos/operation/distance/FacetSequence.h b/include/geos/operation/distance/FacetSequence.h index fbef50e90..61f9fd65b 100644 --- a/include/geos/operation/distance/FacetSequence.h +++ b/include/geos/operation/distance/FacetSequence.h @@ -18,21 +18,27 @@ #pragma once -#include #include #include -#include -#include + +namespace geos { +namespace geom { +class CoordinateSequence; +} +} namespace geos { namespace operation { namespace distance { + class FacetSequence { + private: + const geom::CoordinateSequence* pts; const std::size_t start; const std::size_t end; - const geom::Geometry* geom; + /* * Unlike JTS, we store the envelope in the FacetSequence so * that it has a clear owner. This is helpful when making a @@ -41,25 +47,24 @@ private: geom::Envelope env; double computeDistanceLineLine(const FacetSequence& facetSeq, - std::vector *locs) const; + std::vector *locs) const; double computeDistancePointLine(const geom::Coordinate& pt, const FacetSequence& facetSeq, - std::vector *locs) const; + std::vector *locs) const; void updateNearestLocationsPointLine(const geom::Coordinate& pt, - const FacetSequence& facetSeq, std::size_t i, const geom::Coordinate& q0, const geom::Coordinate &q1, - std::vector *locs) const; + std::vector *locs) const; - void updateNearestLocationsLineLine(std::size_t i, const geom::Coordinate& p0, const geom::Coordinate& p1, - const FacetSequence& facetSeq, - std::size_t j, const geom::Coordinate& q0, const geom::Coordinate &q1, - std::vector *locs) const; + void updateNearestLocationsLineLine(const geom::Coordinate& p0, const geom::Coordinate& p1, + const geom::Coordinate& q0, const geom::Coordinate &q1, + std::vector *locs) const; void computeEnvelope(); public: + const geom::Envelope* getEnvelope() const; const geom::Coordinate* getCoordinate(std::size_t index) const; @@ -72,9 +77,14 @@ public: FacetSequence(const geom::CoordinateSequence* pts, std::size_t start, std::size_t end); - FacetSequence(const geom::Geometry* geom, const geom::CoordinateSequence* pts, std::size_t start, std::size_t end); - - std::vector nearestLocations(const FacetSequence& facetSeq) const; + /** + * Computes the locations of the nearest points between this sequence + * and another sequence. + * The locations are presented in the same order as the input sequences. + * + * @return a pair of {@link Coordinate}s for the nearest points + */ + std::vector nearestLocations(const FacetSequence& facetSeq) const; }; diff --git a/include/geos/operation/distance/FacetSequenceTreeBuilder.h b/include/geos/operation/distance/FacetSequenceTreeBuilder.h index 2a8731fb3..29f7e0589 100644 --- a/include/geos/operation/distance/FacetSequenceTreeBuilder.h +++ b/include/geos/operation/distance/FacetSequenceTreeBuilder.h @@ -18,25 +18,32 @@ #pragma once -#include #include -#include -#include #include + +namespace geos { +namespace geom { +class CoordinateSequence; +class Geometry; +} +} + namespace geos { namespace operation { namespace distance { + class GEOS_DLL FacetSequenceTreeBuilder { + private: + // 6 seems to be a good facet sequence size static const std::size_t FACET_SEQUENCE_SIZE = 6; // Seems to be better to use a minimum node capacity static const std::size_t STR_TREE_NODE_CAPACITY = 4; - static void addFacetSequences(const geom::Geometry* geom, - const geom::CoordinateSequence* pts, + static void addFacetSequences(const geom::CoordinateSequence* pts, std::vector & sections); static std::vector computeFacetSequences(const geom::Geometry* g); @@ -55,6 +62,7 @@ private: }; public: + /** \brief * Return a tree of FacetSequences constructed from the supplied Geometry. * @@ -63,6 +71,7 @@ public: */ static std::unique_ptr> build(const geom::Geometry* g); }; + } } } diff --git a/include/geos/operation/distance/IndexedFacetDistance.h b/include/geos/operation/distance/IndexedFacetDistance.h index 87044c274..6739efbf0 100644 --- a/include/geos/operation/distance/IndexedFacetDistance.h +++ b/include/geos/operation/distance/IndexedFacetDistance.h @@ -94,18 +94,15 @@ public: /// \return true of the geometry lies within the specified distance bool isWithinDistance(const geom::Geometry* g, double maxDistance) const; - /// \brief Computes the nearest locations on the base geometry and the given geometry. - /// - /// \param g the geometry to compute the nearest location to - /// \return the nearest locations - std::vector nearestLocations(const geom::Geometry* g) const; + static bool isWithinDistance(const geom::Geometry* g1, const geom::Geometry* g2, double distance); - /// \brief Compute the nearest locations on the target geometry and the given geometry. + /// \brief Computes the nearest points on the base geometry and the given geometry. /// /// \param g the geometry to compute the nearest point to /// \return the nearest points std::unique_ptr nearestPoints(const geom::Geometry* g) const; + private: struct FacetDistance { double operator()(const FacetSequence* a, const FacetSequence* b) const diff --git a/src/operation/distance/FacetSequence.cpp b/src/operation/distance/FacetSequence.cpp index 4e1f4f496..ebc1501dd 100644 --- a/src/operation/distance/FacetSequence.cpp +++ b/src/operation/distance/FacetSequence.cpp @@ -16,6 +16,8 @@ * **********************************************************************/ +#include +#include #include #include @@ -25,20 +27,11 @@ using namespace geos::geom; using namespace geos::operation::distance; using namespace geos::algorithm; -FacetSequence::FacetSequence(const Geometry *p_geom, const CoordinateSequence* p_pts, std::size_t p_start, std::size_t p_end) : - pts(p_pts), - start(p_start), - end(p_end), - geom(p_geom) -{ - computeEnvelope(); -} -FacetSequence::FacetSequence(const CoordinateSequence* p_pts, std::size_t p_start, std::size_t p_end) : - pts(p_pts), - start(p_start), - end(p_end), - geom(nullptr) +FacetSequence::FacetSequence(const CoordinateSequence* p_pts, std::size_t p_start, std::size_t p_end) + : pts(p_pts) + , start(p_start) + , end(p_end) { computeEnvelope(); } @@ -61,16 +54,16 @@ FacetSequence::distance(const FacetSequence& facetSeq) const bool isPointThis = isPoint(); bool isPointOther = facetSeq.isPoint(); - if(isPointThis && isPointOther) { + if (isPointThis && isPointOther) { const Coordinate& pt = pts->getAt(start); const Coordinate& seqPt = facetSeq.pts->getAt(facetSeq.start); return pt.distance(seqPt); } - else if(isPointThis) { + else if (isPointThis) { const Coordinate& pt = pts->getAt(start); return computeDistancePointLine(pt, facetSeq, nullptr); } - else if(isPointOther) { + else if (isPointOther) { const Coordinate& seqPt = facetSeq.pts->getAt(facetSeq.start); return computeDistancePointLine(seqPt, *this, nullptr); } @@ -84,20 +77,18 @@ FacetSequence::distance(const FacetSequence& facetSeq) const * just return the whole mess, since it only ends up holding two * locations. */ -std::vector +std::vector FacetSequence::nearestLocations(const FacetSequence& facetSeq) const { bool isPointThis = isPoint(); bool isPointOther = facetSeq.isPoint(); - std::vector locs; + std::vector locs; if (isPointThis && isPointOther) { const Coordinate& pt = pts->getAt(start); const Coordinate& seqPt = facetSeq.pts->getAt(facetSeq.start); - GeometryLocation gl1(geom, start, pt); - GeometryLocation gl2(facetSeq.geom, facetSeq.start, seqPt); locs.clear(); - locs.push_back(gl1); - locs.push_back(gl2); + locs.push_back(pt); + locs.push_back(seqPt); } else if (isPointThis) { const Coordinate& pt = pts->getAt(start); @@ -107,7 +98,7 @@ FacetSequence::nearestLocations(const FacetSequence& facetSeq) const const Coordinate& seqPt = facetSeq.pts->getAt(facetSeq.start); computeDistancePointLine(seqPt, *this, &locs); // unflip the locations - GeometryLocation tmp = locs[0]; + Coordinate tmp = locs[0]; locs[0] = locs[1]; locs[1] = tmp; } @@ -120,20 +111,20 @@ FacetSequence::nearestLocations(const FacetSequence& facetSeq) const double FacetSequence::computeDistancePointLine(const Coordinate& pt, const FacetSequence& facetSeq, - std::vector *locs) const + std::vector *locs) const { double minDistance = DoubleInfinity; - for(std::size_t i = facetSeq.start; i < facetSeq.end - 1; i++) { + for (std::size_t i = facetSeq.start; i < facetSeq.end - 1; i++) { const Coordinate& q0 = facetSeq.pts->getAt(i); const Coordinate& q1 = facetSeq.pts->getAt(i + 1); double dist = Distance::pointToSegment(pt, q0, q1); - if(dist < minDistance || (locs != nullptr && locs->empty())) { + if (dist < minDistance || (locs != nullptr && locs->empty())) { minDistance = dist; if (locs != nullptr) { - updateNearestLocationsPointLine(pt, facetSeq, i, q0, q1, locs); + updateNearestLocationsPointLine(pt, q0, q1, locs); } - if(minDistance <= 0.0) { + if (minDistance <= 0.0) { return minDistance; } } @@ -143,22 +134,21 @@ FacetSequence::computeDistancePointLine(const Coordinate& pt, } void -FacetSequence::updateNearestLocationsPointLine(const Coordinate& pt, - const FacetSequence& facetSeq, std::size_t i, - const Coordinate& q0, const Coordinate &q1, - std::vector *locs) const +FacetSequence::updateNearestLocationsPointLine( + const Coordinate& pt, const Coordinate& q0, const Coordinate &q1, + std::vector *locs) const { - geom::LineSegment seg(q0, q1); + LineSegment seg(q0, q1); Coordinate segClosestPoint; seg.closestPoint(pt, segClosestPoint); locs->clear(); - locs->emplace_back(geom, start, pt); - locs->emplace_back(facetSeq.geom, i, segClosestPoint); + locs->push_back(pt); + locs->push_back(segClosestPoint); return; } double -FacetSequence::computeDistanceLineLine(const FacetSequence& facetSeq, std::vector *locs) const +FacetSequence::computeDistanceLineLine(const FacetSequence& facetSeq, std::vector *locs) const { double minDistance = DoubleInfinity; @@ -192,7 +182,7 @@ FacetSequence::computeDistanceLineLine(const FacetSequence& facetSeq, std::vecto if(dist <= minDistance) { minDistance = dist; if(locs != nullptr) { - updateNearestLocationsLineLine(i, p0, p1, facetSeq, j, q0, q1, locs); + updateNearestLocationsLineLine(p0, p1, q0, q1, locs); } if(minDistance <= 0.0) return minDistance; } @@ -203,10 +193,10 @@ FacetSequence::computeDistanceLineLine(const FacetSequence& facetSeq, std::vecto } void -FacetSequence::updateNearestLocationsLineLine(std::size_t i, const Coordinate& p0, const Coordinate& p1, - const FacetSequence& facetSeq, - std::size_t j, const Coordinate& q0, const Coordinate &q1, - std::vector *locs) const +FacetSequence::updateNearestLocationsLineLine( + const Coordinate& p0, const Coordinate& p1, + const Coordinate& q0, const Coordinate &q1, + std::vector *locs) const { LineSegment seg0(p0, p1); LineSegment seg1(q0, q1); @@ -214,8 +204,8 @@ FacetSequence::updateNearestLocationsLineLine(std::size_t i, const Coordinate& p auto closestPts = seg0.closestPoints(seg1); locs->clear(); - locs->emplace_back(geom, i, closestPts[0]); - locs->emplace_back(facetSeq.geom, j, closestPts[1]); + locs->push_back(closestPts[0]); + locs->push_back(closestPts[1]); } void diff --git a/src/operation/distance/FacetSequenceTreeBuilder.cpp b/src/operation/distance/FacetSequenceTreeBuilder.cpp index da91bb66b..ec10c8c57 100644 --- a/src/operation/distance/FacetSequenceTreeBuilder.cpp +++ b/src/operation/distance/FacetSequenceTreeBuilder.cpp @@ -44,21 +44,21 @@ FacetSequenceTreeBuilder::computeFacetSequences(const Geometry* g) class FacetSequenceAdder : public geom::GeometryComponentFilter { std::vector& m_sections; - public : - FacetSequenceAdder(std::vector & p_sections) : - m_sections(p_sections) {} - void - filter_ro(const Geometry* geom) override - { - if(const LineString* ls = dynamic_cast(geom)) { - const CoordinateSequence* seq = ls->getCoordinatesRO(); - addFacetSequences(geom, seq, m_sections); + public : + FacetSequenceAdder(std::vector & p_sections) : + m_sections(p_sections) {} + void + filter_ro(const Geometry* geom) override + { + if(const LineString* ls = dynamic_cast(geom)) { + const CoordinateSequence* seq = ls->getCoordinatesRO(); + addFacetSequences(seq, m_sections); + } + else if(const Point* pt = dynamic_cast(geom)) { + const CoordinateSequence* seq = pt->getCoordinatesRO(); + addFacetSequences(seq, m_sections); + } } - else if(const Point* pt = dynamic_cast(geom)) { - const CoordinateSequence* seq = pt->getCoordinatesRO(); - addFacetSequences(geom, seq, m_sections); - } - } }; FacetSequenceAdder facetSequenceAdder(sections); @@ -68,8 +68,9 @@ FacetSequenceTreeBuilder::computeFacetSequences(const Geometry* g) } void -FacetSequenceTreeBuilder::addFacetSequences(const Geometry* geom, const CoordinateSequence* pts, - std::vector & sections) +FacetSequenceTreeBuilder::addFacetSequences( + const CoordinateSequence* pts, + std::vector& sections) { std::size_t i = 0; std::size_t size = pts->size(); @@ -82,7 +83,7 @@ FacetSequenceTreeBuilder::addFacetSequences(const Geometry* geom, const Coordina if(end >= size - 1) { end = size; } - sections.emplace_back(geom, pts, i, end); + sections.emplace_back(pts, i, end); i += FACET_SEQUENCE_SIZE; } } diff --git a/src/operation/distance/IndexedFacetDistance.cpp b/src/operation/distance/IndexedFacetDistance.cpp index 74beff4e8..6555920cd 100644 --- a/src/operation/distance/IndexedFacetDistance.cpp +++ b/src/operation/distance/IndexedFacetDistance.cpp @@ -27,7 +27,7 @@ namespace geos { namespace operation { namespace distance { -/*public static*/ +/* public static */ double IndexedFacetDistance::distance(const Geometry* g1, const Geometry* g2) { @@ -35,9 +35,17 @@ IndexedFacetDistance::distance(const Geometry* g1, const Geometry* g2) return ifd.distance(g2); } -/*public static*/ +/* public static */ +bool +IndexedFacetDistance::isWithinDistance(const Geometry* g1, const Geometry* g2, double distance) +{ + IndexedFacetDistance ifd(g1); + return ifd.isWithinDistance(g2, distance); +} + +/* public static */ std::unique_ptr -IndexedFacetDistance::nearestPoints(const geom::Geometry* g1, const geom::Geometry* g2) +IndexedFacetDistance::nearestPoints(const Geometry* g1, const Geometry* g2) { IndexedFacetDistance dist(g1); return dist.nearestPoints(g2); @@ -47,15 +55,32 @@ double IndexedFacetDistance::distance(const Geometry* g) const { auto tree2 = FacetSequenceTreeBuilder::build(g); - auto nearest = cachedTree->nearestNeighbour(*tree2); - - if (!nearest.first) { + auto objs = cachedTree->nearestNeighbour(*tree2); + if (!objs.first || !objs.second) { throw util::GEOSException("Cannot calculate IndexedFacetDistance on empty geometries."); } - - return nearest.first->distance(*nearest.second); + auto fs1 = static_cast(objs.first); + auto fs2 = static_cast(objs.second); + return fs1->distance(*fs2); } +std::unique_ptr +IndexedFacetDistance::nearestPoints(const geom::Geometry* g) const +{ + auto tree2 = FacetSequenceTreeBuilder::build(g); + auto objs = cachedTree->nearestNeighbour(*tree2); + if (!objs.first || !objs.second) { + throw util::GEOSException("Cannot calculate IndexedFacetDistance on empty geometries."); + } + auto fs1 = static_cast(objs.first); + auto fs2 = static_cast(objs.second); + auto nearestPts = fs1->nearestLocations(*fs2); + std::unique_ptr cs(new CoordinateSequence()); + cs->setPoints(nearestPts); + return cs; +} + + bool IndexedFacetDistance::isWithinDistance(const Geometry* g, double maxDistance) const { @@ -80,29 +105,6 @@ IndexedFacetDistance::isWithinDistance(const Geometry* g, double maxDistance) co return cachedTree->isWithinDistance(*tree2, maxDistance); } -std::vector -IndexedFacetDistance::nearestLocations(const geom::Geometry* g) const -{ - - auto tree2 = FacetSequenceTreeBuilder::build(g); - auto nearest = cachedTree->nearestNeighbour(*tree2); - - if (!nearest.first) { - throw util::GEOSException("Cannot calculate IndexedFacetDistance on empty geometries."); - } - - return nearest.first->nearestLocations(*nearest.second); -} - -std::unique_ptr -IndexedFacetDistance::nearestPoints(const geom::Geometry* g) const -{ - std::vector minDistanceLocation = nearestLocations(g); - auto nearestPts = detail::make_unique(2u); - nearestPts->setAt(minDistanceLocation[0].getCoordinate(), 0); - nearestPts->setAt(minDistanceLocation[1].getCoordinate(), 1); - return nearestPts; -} } } ----------------------------------------------------------------------- Summary of changes: include/geos/operation/distance/FacetSequence.h | 40 +++++++----- .../operation/distance/FacetSequenceTreeBuilder.h | 19 ++++-- .../geos/operation/distance/IndexedFacetDistance.h | 9 +-- src/operation/distance/FacetSequence.cpp | 76 ++++++++++------------ .../distance/FacetSequenceTreeBuilder.cpp | 35 +++++----- src/operation/distance/IndexedFacetDistance.cpp | 62 +++++++++--------- 6 files changed, 125 insertions(+), 116 deletions(-) hooks/post-receive -- GEOS