[geos-commits] [SCM] GEOS branch 3.10 updated. 972e0ac3cc5d9ad8cbbd85a96458f8b104f9bfaf

git at osgeo.org git at osgeo.org
Tue May 30 14:43:24 PDT 2023


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, 3.10 has been updated
       via  972e0ac3cc5d9ad8cbbd85a96458f8b104f9bfaf (commit)
       via  088ab77468568017acd333c01b0affedf2ab7517 (commit)
       via  27d4d95a1dae4d8c3ced2ec9df2fe6cc147f57b3 (commit)
      from  a3e863da45608b8a3ae62224fcec99b27f57cf48 (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 972e0ac3cc5d9ad8cbbd85a96458f8b104f9bfaf
Author: Mike Taves <mwtoews at gmail.com>
Date:   Wed May 31 09:42:49 2023 +1200

    GEOSClipByRect: Fix case with POINT EMPTY, add more tests (#913)

diff --git a/NEWS b/NEWS
index a25843690..a4903d21a 100644
--- a/NEWS
+++ b/NEWS
@@ -5,6 +5,7 @@ Changes in 3.10.6
   - LargestEmptyCircle: enhance boundary to allow any polygonal geometry (GH-859, Martin Davis)
   - Build issues with gcc-13 (GH-863)
   - GeoJSONReader: Fix 2D empty geometry creation (GH-910, Mike Taves)
+  - GEOSClipByRect: Fix case with POINT EMPTY (GH-913, Mike Taves)
 
 Changes in 3.10.5
 2023-03-16
diff --git a/src/operation/intersection/RectangleIntersection.cpp b/src/operation/intersection/RectangleIntersection.cpp
index bad0391c5..d5945ff24 100644
--- a/src/operation/intersection/RectangleIntersection.cpp
+++ b/src/operation/intersection/RectangleIntersection.cpp
@@ -117,7 +117,7 @@ RectangleIntersection::clip_point(const geom::Point* g,
                                   RectangleIntersectionBuilder& parts,
                                   const Rectangle& rect)
 {
-    if(g == nullptr) {
+    if(g == nullptr || g->isEmpty()) {
         return;
     }
 
@@ -538,6 +538,10 @@ RectangleIntersection::clip_polygon(const geom::Polygon* g,
                                     const Rectangle& rect,
                                     bool keep_polygons)
 {
+    if(g == nullptr || g->isEmpty()) {
+        return;
+    }
+
     if(keep_polygons) {
         clip_polygon_to_polygons(g, parts, rect);
     }
diff --git a/tests/unit/capi/GEOSClipByRectTest.cpp b/tests/unit/capi/GEOSClipByRectTest.cpp
index b81dccad8..d5c7f4685 100644
--- a/tests/unit/capi/GEOSClipByRectTest.cpp
+++ b/tests/unit/capi/GEOSClipByRectTest.cpp
@@ -203,6 +203,31 @@ template<> template<> void object::test<15>
     isEqual(geom2_, "POLYGON ((2 2, 2 5, 5 5, 5 2, 2 2))");
 }
 
+/// Empty combinations - always return GEOMETRYCOLLECTION EMPTY
+template<> template<> void object::test<16>
+()
+{
+    std::vector<const char*> variants{
+        "POINT EMPTY",
+        "LINESTRING EMPTY",
+        "POLYGON EMPTY",
+        "MULTIPOINT EMPTY",
+        "MULTILINESTRING EMPTY",
+        "MULTIPOLYGON EMPTY",
+        "GEOMETRYCOLLECTION EMPTY",
+        "LINEARRING EMPTY",
+    };
+    for (const auto& wkt : variants) {
+        GEOSGeom geom1 = GEOSGeomFromWKT(wkt);
+        GEOSGeom geom2 = GEOSClipByRect(geom1, 0, 0, 1, 1);
+        char* obt_wkt = GEOSWKTWriter_write(wktw_, geom2);
+        std::string obt_wkt2(obt_wkt);
+        GEOSFree(obt_wkt);
+        GEOSGeom_destroy(geom1);
+        GEOSGeom_destroy(geom2);
+        ensure_equals(obt_wkt2, "GEOMETRYCOLLECTION EMPTY");
+    }
+}
 
 
 } // namespace tut
diff --git a/tests/unit/operation/intersection/RectangleIntersectionTest.cpp b/tests/unit/operation/intersection/RectangleIntersectionTest.cpp
index 37080edd6..8e126fcb9 100644
--- a/tests/unit/operation/intersection/RectangleIntersectionTest.cpp
+++ b/tests/unit/operation/intersection/RectangleIntersectionTest.cpp
@@ -1679,4 +1679,27 @@ template<> template<> void object::test<208>
 
     doClipTest(inp, exp, r, 1e-10);
 }
+
+/// Empty combinations - always return GEOMETRYCOLLECTION EMPTY
+template<> template<> void object::test<209>
+()
+{
+    std::vector<const char*> variants{
+        "POINT EMPTY",
+        "LINESTRING EMPTY",
+        "POLYGON EMPTY",
+        "MULTIPOINT EMPTY",
+        "MULTILINESTRING EMPTY",
+        "MULTIPOLYGON EMPTY",
+        "GEOMETRYCOLLECTION EMPTY",
+        "LINEARRING EMPTY",
+    };
+    for (const auto& inp : variants) {
+        doClipTest(
+            inp,
+            "GEOMETRYCOLLECTION EMPTY",
+            Rectangle(0, 0, 1, 1)
+        );
+    }
+}
 }

commit 088ab77468568017acd333c01b0affedf2ab7517
Author: Paul Ramsey <pramsey at cleverelephant.ca>
Date:   Mon May 29 08:10:58 2023 -0700

    Revert "* Port https://github.com/locationtech/jts/pull/981"
    
    This reverts commit 27d4d95a1dae4d8c3ced2ec9df2fe6cc147f57b3.

diff --git a/NEWS b/NEWS
index a57bef2c7..a25843690 100644
--- a/NEWS
+++ b/NEWS
@@ -5,7 +5,6 @@ Changes in 3.10.6
   - LargestEmptyCircle: enhance boundary to allow any polygonal geometry (GH-859, Martin Davis)
   - Build issues with gcc-13 (GH-863)
   - GeoJSONReader: Fix 2D empty geometry creation (GH-910, Mike Taves)
-  - OffsetCurve: add minimum threshold for quadsegs (GH-897, Martin Davis, Paul Ramsey)
 
 Changes in 3.10.5
 2023-03-16
diff --git a/include/geos/operation/buffer/OffsetCurve.h b/include/geos/operation/buffer/OffsetCurve.h
deleted file mode 100644
index 403ca98b7..000000000
--- a/include/geos/operation/buffer/OffsetCurve.h
+++ /dev/null
@@ -1,328 +0,0 @@
-/**********************************************************************
- *
- * GEOS - Geometry Engine Open Source
- * http://geos.osgeo.org
- *
- * Copyright (C) 2021 Paul Ramsey <pramsey at cleverelephant.ca>
- *
- * 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 <geos/export.h>
-
-#include <geos/operation/buffer/BufferParameters.h>
-#include <geos/geom/GeometryFactory.h>
-#include <geos/constants.h>
-
-// Forward declarations
-namespace geos {
-namespace geom {
-class Coordinate;
-class CoordinateSequence;
-class Geometry;
-class LineString;
-class Polygon;
-}
-namespace operation {
-namespace buffer {
-class OffsetCurveSection;
-class SegmentMCIndex;
-}
-}
-}
-
-using geos::geom::Coordinate;
-using geos::geom::CoordinateSequence;
-using geos::geom::Geometry;
-using geos::geom::GeometryFactory;
-using geos::geom::LineString;
-using geos::geom::Polygon;
-
-namespace geos {
-namespace operation {
-namespace buffer {
-
-/**
- * Computes an offset curve from a geometry.
- * An offset curve is a linear geometry which is offset a given distance
- * from the input.
- * If the offset distance is positive the curve lies on the left side of the input;
- * if it is negative the curve is on the right side.
- * The curve(s) have the same direction as the input line(s).
- *
- * The offset curve is based on the boundary of the buffer for the geometry
- * at the offset distance (see BufferOp).
- * The normal mode of operation is to return the sections of the buffer boundary
- * which lie on the raw offset curve
- * (obtained via rawOffset(LineString, double).
- * The offset curve will contain multiple sections
- * if the input self-intersects or has close approaches.
- * The computed sections are ordered along the raw offset curve.
- * Sections are disjoint.  They never self-intersect, but may be rings.
- *
- *   * For a LineString the offset curve is a linear geometry
- *     (LineString or MultiLineString).
- *   * For a Point or MultiPoint the offset curve is an empty LineString.
- *   * For a Polygon the offset curve is the boundary of the polygon buffer (which
- *     may be a MultiLineString.
- *   * For a collection the output is a MultiLineString containing
- *     the offset curves of the elements.
- *
- * In "joined" mode (see setJoined(bool))
- * the sections computed for each input line are joined into a single offset curve line.
- * The joined curve may self-intersect.
- * At larger offset distances the curve may contain "flat-line" artifacts
- * in places where the input self-intersects.
- *
- * Offset curves support setting the number of quadrant segments,
- * the join style, and the mitre limit (if applicable) via
- * the BufferParameters.
- *
- * @author Martin Davis
- *
- */
-class GEOS_DLL OffsetCurve {
-
-
-private:
-
-    // Members
-    const Geometry& inputGeom;
-    double distance;
-    bool isJoined = false;
-
-    BufferParameters bufferParams;
-    double matchDistance;
-    const GeometryFactory* geomFactory;
-
-    // Methods
-
-    std::unique_ptr<Geometry> computeCurve(
-        const LineString& lineGeom, double distance);
-
-    std::vector<std::unique_ptr<OffsetCurveSection>> computeSections(
-        const LineString& lineGeom, double distance);
-
-    std::unique_ptr<LineString> offsetSegment(
-        const CoordinateSequence* pts, double distance);
-
-    static std::unique_ptr<Polygon> getBufferOriented(
-        const LineString& geom, double distance,
-        BufferParameters& bufParams);
-
-    /**
-    * Extracts the largest polygon by area from a geometry.
-    * Used here to avoid issues with non-robust buffer results
-    * which have spurious extra polygons.
-    *
-    * @param geom a geometry
-    * @return the polygon element of largest area
-    */
-    static const Polygon* extractMaxAreaPolygon(const Geometry* geom);
-
-    void computeCurveSections(
-        const CoordinateSequence* bufferRingPts,
-        const CoordinateSequence& rawCurve,
-        std::vector<std::unique_ptr<OffsetCurveSection>>& sections);
-
-    /**
-    * Matches the segments in a buffer ring to the raw offset curve
-    * to obtain their match positions (if any).
-    *
-    * @param raw0 a raw curve segment start point
-    * @param raw1 a raw curve segment end point
-    * @param rawCurveIndex the index of the raw curve segment
-    * @param bufferSegIndex the spatial index of the buffer ring segments
-    * @param bufferPts the points of the buffer ring
-    * @param rawCurvePos the raw curve positions of the buffer ring segments
-    * @return the index of the minimum matched buffer segment
-    */
-    std::size_t matchSegments(
-        const Coordinate& raw0, const Coordinate& raw1,
-        std::size_t rawCurveIndex,
-        SegmentMCIndex& bufferSegIndex,
-        const CoordinateSequence* bufferPts,
-        std::vector<double>& rawCurvePos);
-
-    static double segmentMatchFrac(
-        const Coordinate& p0,   const Coordinate& p1,
-        const Coordinate& seg0, const Coordinate& seg1,
-        double matchDistance);
-
-    /**
-    * This is only called when there is at least one ring segment matched
-    * (so rawCurvePos has at least one entry != NOT_IN_CURVE).
-    * The start index of the first section must be provided.
-    * This is intended to be the section with lowest position
-    * along the raw curve.
-    * @param ringPts the points in a buffer ring
-    * @param rawCurveLoc the position of buffer ring segments along the raw curve
-    * @param startIndex the index of the start of a section
-    * @param sections the list of extracted offset curve sections
-    */
-    void extractSections(
-        const CoordinateSequence* ringPts,
-        std::vector<double>& rawCurveLoc,
-        std::size_t startIndex,
-        std::vector<std::unique_ptr<OffsetCurveSection>>& sections);
-
-    std::size_t findSectionStart(
-        const std::vector<double>& loc,
-        std::size_t end);
-
-    std::size_t findSectionEnd(
-        const std::vector<double>& loc,
-        std::size_t start,
-        std::size_t firstStartIndex);
-
-    static std::size_t nextIndex(std::size_t i, std::size_t size);
-    static std::size_t prevIndex(std::size_t i, std::size_t size);
-
-
-public:
-
-    // Constants
-    static constexpr int MATCH_DISTANCE_FACTOR = 10000;
-
-    /**
-    * A QuadSegs minimum value that will prevent generating
-    * unwanted offset curve artifacts near end caps.
-    */
-    static constexpr int MIN_QUADRANT_SEGMENTS = 8;
-
-    /**
-    * Creates a new instance for computing an offset curve for a geometry at a given distance.
-    * with default quadrant segments (BufferParameters::DEFAULT_QUADRANT_SEGMENTS)
-    * and join style (BufferParameters::JOIN_STYLE).
-    *
-    * @param geom the geometry to offset
-    * @param dist the offset distance (positive for left, negative for right)
-    *
-    * @see BufferParameters
-    */
-    OffsetCurve(const Geometry& geom, double dist)
-        : inputGeom(geom)
-        , distance(dist)
-        , matchDistance(std::abs(dist)/MATCH_DISTANCE_FACTOR)
-        , geomFactory(geom.getFactory())
-        {
-            if (!std::isfinite(dist)) {
-                throw util::IllegalArgumentException("OffsetCurve distance must be a finite value");
-            }
-        };
-
-    /**
-    * Creates a new instance for computing an offset curve for a geometry at a given distance.
-    * setting the quadrant segments and join style and mitre limit
-    * via {@link BufferParameters}.
-    *
-    * @param geom the geometry to offset
-    * @param dist the offset distance (positive for left, negative for right)
-    * @param bp the buffer parameters to use
-    */
-    OffsetCurve(const Geometry& geom, double dist, BufferParameters& bp)
-        : inputGeom(geom)
-        , distance(dist)
-        , matchDistance(std::abs(dist)/MATCH_DISTANCE_FACTOR)
-        , geomFactory(geom.getFactory())
-        {
-            if (!std::isfinite(dist)) {
-                throw util::IllegalArgumentException("OffsetCurve distance must be a finite value");
-            }
-            //-- set buffer params, leaving cap style as the default CAP_ROUND
-
-            /**
-            * Prevent using a very small QuadSegs value, to avoid
-            * offset curve artifacts near the end caps.
-            */
-            int quadSegs = bp.getQuadrantSegments();
-            if (quadSegs < MIN_QUADRANT_SEGMENTS) {
-                quadSegs = MIN_QUADRANT_SEGMENTS;
-            }
-            bufferParams.setQuadrantSegments(quadSegs);
-
-            bufferParams.setJoinStyle( bp.getJoinStyle());
-            bufferParams.setMitreLimit( bp.getMitreLimit());
-        };
-
-    /**
-    * Computes a single curve line for each input linear component,
-    * by joining curve sections in order along the raw offset curve.
-    * The default mode is to compute separate curve sections.
-    *
-    * @param pIsJoined true if joined mode should be used.
-    */
-    void setJoined(bool pIsJoined);
-
-    static std::unique_ptr<Geometry> getCurve(
-        const Geometry& geom,
-        double dist,
-        int quadSegs,
-        BufferParameters::JoinStyle joinStyle,
-        double mitreLimit);
-
-    static std::unique_ptr<Geometry> getCurve(
-        const Geometry& geom, double dist);
-
-    /**
-    * Computes the offset curve of a geometry at a given distance,
-    * joining curve sections into a single line for each input line.
-    *
-    * @param geom a geometry
-    * @param dist the offset distance (positive for left, negative for right)
-    * @return the joined offset curve
-    */
-    static std::unique_ptr<Geometry> getCurveJoined(
-        const Geometry& geom, double dist);
-
-    /**
-    * Gets the computed offset curve lines.
-    *
-    * @return the offset curve geometry
-    */
-    std::unique_ptr<Geometry> getCurve();
-
-    /**
-    * Gets the raw offset curve for a line at a given distance.
-    * The quadrant segments, join style and mitre limit can be specified
-    * via BufferParameters.
-    *
-    * The raw offset line may contain loops and other artifacts which are
-    * not present in the true offset curve.
-    *
-    * @param line the line to offset
-    * @param distance the offset distance (positive for left, negative for right)
-    * @param bufParams the buffer parameters to use
-    * @return the raw offset curve points
-    */
-    static std::unique_ptr<CoordinateSequence> rawOffsetCurve(
-        const LineString& line,
-        double distance,
-        BufferParameters& bufParams);
-
-    /**
-    * Gets the raw offset curve for a line at a given distance,
-    * with default buffer parameters.
-    *
-    * @param line the line to offset
-    * @param distance the offset distance (positive for left, negative for right)
-    * @return the raw offset curve points
-    */
-    static std::unique_ptr<CoordinateSequence> rawOffset(
-        const LineString& line,
-        double distance);
-
-};
-
-} // namespace geos::operation::buffer
-} // namespace geos::operation
-} // namespace geos
-
-
-
diff --git a/tests/unit/capi/GEOSOffsetCurveTest.cpp b/tests/unit/capi/GEOSOffsetCurveTest.cpp
index abe078d2b..660322108 100644
--- a/tests/unit/capi/GEOSOffsetCurveTest.cpp
+++ b/tests/unit/capi/GEOSOffsetCurveTest.cpp
@@ -137,7 +137,6 @@ void object::test<5>
                   ));
 }
 
-
 // 0 distance
 // See http://trac.osgeo.org/postgis/ticket/454
 template<>
diff --git a/tests/unit/operation/buffer/OffsetCurveTest.cpp b/tests/unit/operation/buffer/OffsetCurveTest.cpp
deleted file mode 100644
index 5faea0fe4..000000000
--- a/tests/unit/operation/buffer/OffsetCurveTest.cpp
+++ /dev/null
@@ -1,585 +0,0 @@
-//
-// Test Suite for geos::operation::buffer::BufferOp class.
-
-// tut
-#include <tut/tut.hpp>
-#include <utility.h>
-
-// geos
-#include <geos/operation/buffer/OffsetCurve.h>
-#include <geos/operation/buffer/BufferParameters.h>
-#include <geos/geom/Geometry.h>
-#include <geos/io/WKTReader.h>
-// #include <geos/io/WKTWriter.h>
-
-// std
-#include <memory>
-#include <string>
-
-namespace tut {
-//
-// Test Group
-//
-using geos::operation::buffer::OffsetCurve;
-using geos::operation::buffer::BufferParameters;
-
-// Common data used by tests
-struct test_offsetcurve_data {
-    geos::io::WKTReader wktreader;
-
-    test_offsetcurve_data() {};
-
-    void checkOffsetCurve(const std::string& wkt, double distance,
-        int quadSegs, int joinStyle, double mitreLimit,
-        const std::string& wktExpected)
-    {
-        checkOffsetCurve(wkt, distance, quadSegs, joinStyle, mitreLimit, wktExpected, 0.05);
-    }
-
-    void checkOffsetCurve(const std::string& wkt, double distance,
-        int quadSegs, int joinStyle, double mitreLimit,
-        const std::string& wktExpected, double tolerance)
-    {
-        BufferParameters::JoinStyle js = static_cast<BufferParameters::JoinStyle>(joinStyle);
-        std::unique_ptr<geos::geom::Geometry> geom = wktreader.read(wkt);
-        std::unique_ptr<geos::geom::Geometry> result = OffsetCurve::getCurve(*geom, distance, quadSegs, js, mitreLimit);
-        std::unique_ptr<geos::geom::Geometry> expected = wktreader.read(wktExpected);
-        ensure_equals_geometry(result.get(), expected.get(), tolerance);
-    }
-
-    void checkOffsetCurve(const std::string& wkt, double distance, const std::string& wktExpected)
-    {
-        checkOffsetCurve(wkt, distance, wktExpected, 0.05);
-    }
-
-    void checkOffsetCurve(const std::string& wkt, double distance, const std::string& wktExpected, double tolerance)
-    {
-        std::unique_ptr<geos::geom::Geometry> geom = wktreader.read(wkt);
-        std::unique_ptr<geos::geom::Geometry> result = OffsetCurve::getCurve(*geom, distance);
-        std::unique_ptr<geos::geom::Geometry> expected = wktreader.read(wktExpected);
-
-        // geos::io::WKTWriter wktwriter;
-        // wktwriter.setRoundingPrecision(2);
-        // std::cout << std::endl;
-        // std::cout << "Expected: " << wktwriter.write(expected.get()) << std::endl;
-        // std::cout << "  Result: " << wktwriter.write(result.get()) << std::endl;
-
-        ensure_equals_geometry(result.get(), expected.get(), tolerance);
-    }
-
-    void
-    checkOffsetCurveJoined(const std::string& wkt, double distance, const std::string& wktExpected)
-    {
-        std::unique_ptr<geos::geom::Geometry> geom = wktreader.read(wkt);
-        std::unique_ptr<geos::geom::Geometry> result = OffsetCurve::getCurveJoined(*geom, distance);
-        std::unique_ptr<geos::geom::Geometry> expected = wktreader.read(wktExpected);
-        ensure_equals_geometry(result.get(), expected.get(), 0.05);
-    }
-
-};
-
-typedef test_group<test_offsetcurve_data> group;
-typedef group::object object;
-
-group test_offsetcurve_group("geos::operation::buffer::OffsetCurve");
-
-
-// testPoint
-template<>
-template<>
-void object::test<1> ()
-{
-    checkOffsetCurve(
-        "POINT (0 0)", 1,
-        "LINESTRING EMPTY"
-        );
-}
-
-// testEmpty
-template<>
-template<>
-void object::test<2> ()
-{
-    checkOffsetCurve(
-        "LINESTRING EMPTY", 1,
-        "LINESTRING EMPTY"
-        );
-}
-
-// testZeroLenLine
-template<>
-template<>
-void object::test<3> ()
-{
-    checkOffsetCurve(
-        "LINESTRING (1 1, 1 1)", 1,
-        "LINESTRING EMPTY"
-        );
-}
-
-// testZeroOffsetLine
-template<>
-template<>
-void object::test<4> ()
-{
-    checkOffsetCurve(
-        "LINESTRING (0 0, 1 0, 1 1)", 0,
-        "LINESTRING (0 0, 1 0, 1 1)"
-        );
-}
-
-// testZeroOffsetPolygon
-template<>
-template<>
-void object::test<5> ()
-{
-    checkOffsetCurve(
-        "POLYGON ((1 9, 9 1, 1 1, 1 9))", 0,
-        "LINESTRING (1 9, 1 1, 9 1, 1 9)"
-        );
-}
-
-/**
-* testRepeatedPoint
-* Test bug fix for removing repeated points in input for raw curve.
-* See https://github.com/locationtech/jts/issues/957
-*/
-template<>
-template<>
-void object::test<6> ()
-{
-    checkOffsetCurve(
-        "LINESTRING (4 9, 1 2, 7 5, 7 5, 4 9)", 1,
-        "LINESTRING (4.24 7.02, 2.99 4.12, 5.48 5.36, 4.24 7.02)"
-        );
-}
-
-
-// testSegment1Short
-template<>
-template<>
-void object::test<7> ()
-{
-    checkOffsetCurve(
-        "LINESTRING (2 2, 2 2.0000001)", 1,
-        "LINESTRING (1 2, 1 2.0000001)",
-        0.00000001
-    );
-}
-
-// testSegment1
-template<>
-template<>
-void object::test<8> ()
-{
-    checkOffsetCurve(
-        "LINESTRING (0 0, 9 9)", 1,
-        "LINESTRING (-0.71 0.71, 8.29 9.71)"
-    );
-}
-
-
-// testSegment1Neg
-template<>
-template<>
-void object::test<9> ()
-{
-    checkOffsetCurve(
-        "LINESTRING (0 0, 9 9)", -1,
-        "LINESTRING (0.71 -0.71, 9.71 8.29)"
-    );
-}
-
-// testSegments2
-template<>
-template<>
-void object::test<10> ()
-{
-    checkOffsetCurve(
-        "LINESTRING (0 0, 9 9, 25 0)", 1,
-        "LINESTRING (-0.707 0.707, 8.293 9.707, 8.435 9.825, 8.597 9.915, 8.773 9.974, 8.956 9.999, 9.141 9.99, 9.321 9.947, 9.49 9.872, 25.49 0.872)"
-        );
-}
-
-// testSegments3
-template<>
-template<>
-void object::test<11> ()
-{
-    checkOffsetCurve(
-        "LINESTRING (0 0, 9 9, 25 0, 30 15)", 1,
-        "LINESTRING (-0.71 0.71, 8.29 9.71, 8.44 9.83, 8.6 9.92, 8.77 9.97, 8.96 10, 9.14 9.99, 9.32 9.95, 9.49 9.87, 24.43 1.47, 29.05 15.32)"
-        );
-}
-
-// testRightAngle
-template<>
-template<>
-void object::test<12> ()
-{
-    checkOffsetCurve(
-        "LINESTRING (2 8, 8 8, 8 1)", 1,
-        "LINESTRING (2 9, 8 9, 8.2 8.98, 8.38 8.92, 8.56 8.83, 8.71 8.71, 8.83 8.56, 8.92 8.38, 8.98 8.2, 9 8, 9 1)"
-        );
-}
-
-// testZigzagOneEndCurved4
-template<>
-template<>
-void object::test<13> ()
-{
-    checkOffsetCurve(
-        "LINESTRING (1 3, 6 3, 4 5, 9 5)", 1,
-        "LINESTRING (1 4, 3.59 4, 3.29 4.29, 3.17 4.44, 3.08 4.62, 3.02 4.8, 3 5, 3.02 5.2, 3.08 5.38, 3.17 5.56, 3.29 5.71, 3.44 5.83, 3.62 5.92, 3.8 5.98, 4 6, 9 6)"
-        );
-}
-
-// testZigzagOneEndCurved1
-template<>
-template<>
-void object::test<14> ()
-{
-    checkOffsetCurve(
-        "LINESTRING (1 3, 6 3, 4 5, 9 5)", 1,
-        "LINESTRING (1 4, 3.59 4, 3.29 4.29, 3.17 4.44, 3.08 4.62, 3.02 4.8, 3 5, 3.02 5.2, 3.08 5.38, 3.17 5.56, 3.29 5.71, 3.44 5.83, 3.62 5.92, 3.8 5.98, 4 6, 9 6)"
-        );
-}
-
-
-// testAsymmetricU
-template<>
-template<>
-void object::test<15> ()
-{
-    checkOffsetCurve(
-        "LINESTRING (1 1, 9 1, 9 2, 5 2)", 1,
-        "LINESTRING (1 2, 4 2)"
-        );
-
-    checkOffsetCurve(
-        "LINESTRING (1 1, 9 1, 9 2, 5 2)", -1,
-        "LINESTRING (1 0, 9 0, 9.2 0.02, 9.38 0.08, 9.56 0.17, 9.71 0.29, 9.83 0.44, 9.92 0.62, 9.98 0.8, 10 1, 10 2, 9.98 2.2, 9.92 2.38, 9.83 2.56, 9.71 2.71, 9.56 2.83, 9.38 2.92, 9.2 2.98, 9 3, 5 3)"
-        );
-}
-
-// testSymmetricU
-template<>
-template<>
-void object::test<16> ()
-{
-    checkOffsetCurve(
-        "LINESTRING (1 1, 9 1, 9 2, 1 2)", 1,
-        "LINESTRING EMPTY"
-        );
-
-    checkOffsetCurve(
-        "LINESTRING (1 1, 9 1, 9 2, 1 2)", -1,
-        "LINESTRING (1 0, 9 0, 9.2 0.02, 9.38 0.08, 9.56 0.17, 9.71 0.29, 9.83 0.44, 9.92 0.62, 9.98 0.8, 10 1, 10 2, 9.98 2.2, 9.92 2.38, 9.83 2.56, 9.71 2.71, 9.56 2.83, 9.38 2.92, 9.2 2.98, 9 3, 1 3)"
-        );
-}
-
-// testEmptyResult
-template<>
-template<>
-void object::test<17> ()
-{
-    checkOffsetCurve(
-        "LINESTRING (3 5, 5 7, 7 5)", -4,
-        "LINESTRING EMPTY"
-        );
-}
-
-// testSelfCross
-template<>
-template<>
-void object::test<18> ()
-{
-    checkOffsetCurve(
-        "LINESTRING (50 90, 50 10, 90 50, 10 50)", 10,
-        "MULTILINESTRING ((60 90, 60 60), (60 40, 60 34.14, 65.85 40, 60 40), (40 40, 10 40))" );
-}
-
-// testSelfCrossNeg
-template<>
-template<>
-void object::test<19> ()
-{
-    checkOffsetCurve(
-        "LINESTRING (50 90, 50 10, 90 50, 10 50)", -10,
-        "MULTILINESTRING ((40 90, 40 60, 10 60), (40 40, 40 10, 40.19 8.05, 40.76 6.17, 41.69 4.44, 42.93 2.93, 44.44 1.69, 46.17 0.76, 48.05 0.19, 50 0, 51.95 0.19, 53.83 0.76, 55.56 1.69, 57.07 2.93, 97.07 42.93, 98.31 44.44, 99.24 46.17, 99.81 48.05, 100 50, 99.81 51.95, 99.24 53.83, 98.31 55.56, 97.07 57.07, 95.56 58.31, 93.83 59.24, 91.95 59.81, 90 60, 60 60))" );
-}
-
-// testSelfCrossCWNeg
-template<>
-template<>
-void object::test<20> ()
-{
-    checkOffsetCurve(
-        "LINESTRING (0 70, 100 70, 40 0, 40 100)", -10,
-        "MULTILINESTRING ((0 60, 30 60), (50 60, 50 27.03, 78.25 60, 50 60), (50 80, 50 100))" );
-}
-
-// testSelfCrossDartInside
-template<>
-template<>
-void object::test<21> ()
-{
-    checkOffsetCurve(
-        "LINESTRING (60 50, 10 80, 50 10, 90 80, 40 50)", 10,
-        "MULTILINESTRING ((54.86 41.43, 50 44.34, 45.14 41.43), (43.9 40.83, 50 30.16, 56.1 40.83))" );
-}
-
-
-// testSelfCrossDartOutside
-template<>
-template<>
-void object::test<22> ()
-{
-    checkOffsetCurve(
-        "LINESTRING (60 50, 10 80, 50 10, 90 80, 40 50)", -10,
-        "LINESTRING (50 67.66, 15.14 88.57, 13.32 89.43, 11.35 89.91, 9.33 89.98, 7.34 89.64, 5.46 88.91, 3.76 87.82, 2.32 86.4, 1.19 84.73, 0.42 82.86, 0.04 80.88, 0.07 78.86, 0.5 76.88, 1.32 75.04, 41.32 5.04, 42.42 3.48, 43.8 2.16, 45.4 1.12, 47.17 0.41, 49.05 0.05, 50.95 0.05, 52.83 0.41, 54.6 1.12, 56.2 2.16, 57.58 3.48, 58.68 5.04, 98.68 75.04, 99.5 76.88, 99.93 78.86, 99.96 80.88, 99.58 82.86, 98.81 84.73, 97.68 86.4, 96.24 87.82, 94.54 88.91, 92.66 89.64, 90.67 89.98, 88.65 89.91, 86.68 89.43, 84.86 88.57, 50 67.66)" );
-}
-
-
-// testSelfCrossDart2Inside
-template<>
-template<>
-void object::test<23> ()
-{
-    checkOffsetCurve(
-        "LINESTRING (64 45, 10 80, 50 10, 90 80, 35 45)", 10,
-        "LINESTRING (55.00 38.91, 49.58 42.42, 44.74 39.34, 50 30.15, 55.00 38.91)" );
-}
-
-// testRing
-template<>
-template<>
-void object::test<24> ()
-{
-    checkOffsetCurve(
-        "LINESTRING (10 10, 50 90, 90 10, 10 10)", -10,
-        "LINESTRING (26.18 20, 50 67.63, 73.81 20, 26.18 20)" );
-}
-
-// testClosedCurve
-template<>
-template<>
-void object::test<25> ()
-{
-    checkOffsetCurve(
-        "LINESTRING (70 80, 10 80, 50 10, 90 80, 40 80)", 10,
-        "LINESTRING (70 70, 40 70, 27.23 70, 50 30.15, 72.76 70, 70 70)"
-        );
-}
-
-
-// testOverlapTriangleInside
-template<>
-template<>
-void object::test<26> ()
-{
-    checkOffsetCurve(
-        "LINESTRING (70 80, 10 80, 50 10, 90 80, 40 80)", 10,
-        "LINESTRING (70 70, 40 70, 27.23 70, 50 30.15, 72.76 70, 70 70)" );
-}
-
-
-// testOverlapTriangleOutside
-template<>
-template<>
-void object::test<27> ()
-{
-    checkOffsetCurve(
-        "LINESTRING (70 80, 10 80, 50 10, 90 80, 40 80)", -10,
-        "LINESTRING (70 90, 40 90, 10 90, 8.11 89.82, 6.29 89.29, 4.6 88.42, 3.11 87.25, 1.87 85.82, 0.91 84.18, 0.29 82.39, 0.01 80.51, 0.1 78.61, 0.54 76.77, 1.32 75.04, 41.32 5.04, 42.42 3.48, 43.8 2.16, 45.4 1.12, 47.17 0.41, 49.05 0.05, 50.95 0.05, 52.83 0.41, 54.6 1.12, 56.2 2.16, 57.58 3.48, 58.68 5.04, 98.68 75.04, 99.46 76.77, 99.9 78.61, 99.99 80.51, 99.71 82.39, 99.09 84.18, 98.13 85.82, 96.89 87.25, 95.4 88.42, 93.71 89.29, 91.89 89.82, 90 90, 70 90)"
-        );
-}
-
-
-// testMultiPoint
-template<>
-template<>
-void object::test<28> ()
-{
-    checkOffsetCurve(
-        "MULTIPOINT ((0 0), (1 1))", 1,
-        "LINESTRING EMPTY"
-    );
-}
-
-
-// testMultiLine
-template<>
-template<>
-void object::test<29> ()
-{
-    checkOffsetCurve(
-        "MULTILINESTRING ((20 30, 60 10, 80 60), (40 50, 80 30))", 10,
-        "MULTILINESTRING ((24.47 38.94, 54.75 23.8, 70.72 63.71), (44.47 58.94, 84.47 38.94))"
-    );
-}
-
-
-// testMixedWithPoint
-template<>
-template<>
-void object::test<30> ()
-{
-    checkOffsetCurve(
-        "GEOMETRYCOLLECTION (LINESTRING (20 30, 60 10, 80 60), POINT (0 0))", 10,
-        "LINESTRING (24.47 38.94, 54.75 23.8, 70.72 63.71)"
-    );
-}
-
-
-// testPolygon1
-template<>
-template<>
-void object::test<31> ()
-{
-    checkOffsetCurve(
-        "POLYGON ((100 200, 200 100, 100 100, 100 200))", 10,
-        "LINESTRING (90 200, 90.19 201.95, 90.76 203.83, 91.69 205.56, 92.93 207.07, 94.44 208.31, 96.17 209.24, 98.05 209.81, 100 210, 101.95 209.81, 103.83 209.24, 105.56 208.31, 107.07 207.07, 207.07 107.07, 208.31 105.56, 209.24 103.83, 209.81 101.95, 210 100, 209.81 98.05, 209.24 96.17, 208.31 94.44, 207.07 92.93, 205.56 91.69, 203.83 90.76, 201.95 90.19, 200 90, 100 90, 98.05 90.19, 96.17 90.76, 94.44 91.69, 92.93 92.93, 91.69 94.44, 90.76 96.17, 90.19 98.05, 90 100, 90 200)"
-    );
-}
-
-// testPolygon2
-template<>
-template<>
-void object::test<32> ()
-{
-    checkOffsetCurve(
-        "POLYGON ((100 200, 200 100, 100 100, 100 200))", -10,
-        "LINESTRING (110 175.86, 175.86 110, 110 110, 110 175.86)"
-    );
-}
-
-
-// testPolygonWithHole1
-template<>
-template<>
-void object::test<33> ()
-{
-    checkOffsetCurve(
-        "POLYGON ((20 80, 80 80, 80 20, 20 20, 20 80), (30 70, 70 70, 70 30, 30 30, 30 70))", 10,
-        "MULTILINESTRING ((10 80, 10.19 81.95, 10.76 83.83, 11.69 85.56, 12.93 87.07, 14.44 88.31, 16.17 89.24, 18.05 89.81, 20 90, 80 90, 81.95 89.81, 83.83 89.24, 85.56 88.31, 87.07 87.07, 88.31 85.56, 89.24 83.83, 89.81 81.95, 90 80, 90 20, 89.81 18.05, 89.24 16.17, 88.31 14.44, 87.07 12.93, 85.56 11.69, 83.83 10.76, 81.95 10.19, 80 10, 20 10, 18.05 10.19, 16.17 10.76, 14.44 11.69, 12.93 12.93, 11.69 14.44, 10.76 16.17, 10.19 18.05, 10 20, 10 80), (40 60, 40 40, 60 40, 60 60, 40 60))"
-    );
-}
-
-// testPolygonWithHole2
-template<>
-template<>
-void object::test<34> ()
-{
-    checkOffsetCurve(
-        "POLYGON ((20 80, 80 80, 80 20, 20 20, 20 80), (30 70, 70 70, 70 30, 30 30, 30 70))", -10,
-        "LINESTRING EMPTY"
-    );
-}
-
-
-
-//-------------------------------------------------
-
-// testJoined1
-template<>
-template<>
-void object::test<35> ()
-{
-    checkOffsetCurveJoined("LINESTRING (0 50, 100 50, 50 100, 50 0)", 10,
-        "LINESTRING (0 60, 75.85 60, 60 75.85, 60 0)"
-    );
-}
-
-// testJoined2
-template<>
-template<>
-void object::test<36> ()
-{
-    checkOffsetCurveJoined("LINESTRING (0 50, 100 50, 50 100, 50 0)", -10,
-        "LINESTRING (0 40, 100 40, 101.95 40.19, 103.83 40.76, 105.56 41.69, 107.07 42.93, 108.31 44.44, 109.24 46.17, 109.81 48.05, 110 50, 109.81 51.95, 109.24 53.83, 108.31 55.56, 107.07 57.07, 57.07 107.07, 55.56 108.31, 53.83 109.24, 51.95 109.81, 50 110, 48.05 109.81, 46.17 109.24, 44.44 108.31, 42.93 107.07, 41.69 105.56, 40.76 103.83, 40.19 101.95, 40 100, 40 0)"
-    );
-}
-
-
-//-------------------------------------------------
-
-// testInfiniteLoop
-template<>
-template<>
-void object::test<37> ()
-{
-    checkOffsetCurve(
-        "LINESTRING (21 101, -1 78, 12 43, 50 112, 73 -5, 19 2, 87 85, -7 38, 105 40)", 4,
-        "MULTILINESTRING ((23.89 98.24, 3.62 77.04, 12.71 52.58, 46.50 113.93, 46.96 114.60, 47.55 115.16, 48.24 115.59, 49.00 115.87, 49.80 116.00, 50.62 115.95, 51.40 115.75, 52.13 115.38, 52.77 114.88, 53.30 114.26, 53.69 113.55, 53.92 112.77, 61.06 76.50), (62.66 68.36, 63.73 62.91, 72.03 73.05, 62.66 68.36), (65.73 52.73, 67.58 43.33, 104.93 44.00), (69.14 35.36, 76.92 -4.23, 77.00 -4.98, 76.93 -5.73, 76.72 -6.46, 76.38 -7.14, 75.92 -7.73, 75.35 -8.24, 74.70 -8.62, 73.99 -8.88, 73.24 -8.99, 72.49 -8.97, 18.49 -1.97, 17.73 -1.79, 17.03 -1.48, 16.39 -1.04, 15.86 -0.48, 15.44 0.17, 15.16 0.89, 15.02 1.65, 15.02 2.42, 15.18 3.18, 15.48 3.89, 15.91 4.53, 40.74 34.85), (47.40 42.97, 57.15 54.88), (55.23 64.64, 18.32 46.19), (16.24 42.42, 47.40 42.97), (57.89 43.16, 59.45 43.19))"
-    );
-}
-
-//---------------------------------------
-
-// testQuadSegs
-template<>
-template<>
-void object::test<38> ()
-{
-    checkOffsetCurve(
-        "LINESTRING (20 20, 50 50, 80 20)",
-        10, 2, -1, -1,
-        "LINESTRING (12.928932188134524 27.071067811865476, 42.928932188134524 57.071067811865476, 44.44429766980398 58.314696123025456, 46.1731656763491 59.23879532511287, 48.04909677983872 59.80785280403231, 50 60, 51.95090322016128 59.80785280403231, 53.8268343236509 59.23879532511287, 55.55570233019602 58.314696123025456, 57.071067811865476 57.071067811865476, 87.07106781186548 27.071067811865476)"
-        );
-}
-
-// testJoinBevel
-template<>
-template<>
-void object::test<39> ()
-{
-    checkOffsetCurve(
-        "LINESTRING (20 20, 50 50, 80 20)",
-        10, -1, BufferParameters::JOIN_BEVEL, -1,
-        "LINESTRING (12.93 27.07, 42.93 57.07, 57.07 57.07, 87.07 27.07)"
-        );
-}
-
-// testJoinMitre
-template<>
-template<>
-void object::test<40> ()
-{
-    checkOffsetCurve(
-        "LINESTRING (20 20, 50 50, 80 20)",
-        10, -1, BufferParameters::JOIN_MITRE, -1,
-        "LINESTRING (12.93 27.07, 50 64.14, 87.07 27.07)"
-        );
-}
-
-
-// testMinQuadrantSegments
-// See https://github.com/qgis/QGIS/issues/53165
-template<>
-template<>
-void object::test<41> ()
-{
-    checkOffsetCurve(
-        "LINESTRING (553772.0645892698 177770.05079236583, 553780.9235869241 177768.99614978794, 553781.8325485934 177768.41771963477)",
-        -11, 0, BufferParameters::JOIN_MITRE, -1,
-        "LINESTRING (553770.76 177759.13, 553777.54 177758.32)"
-    );
-}
-
-// testMinQuadrantSegments_QGIS
-// See https://github.com/qgis/QGIS/issues/53165#issuecomment-1563214857
-template<>
-template<>
-void object::test<42> ()
-{
-    checkOffsetCurve(
-        "LINESTRING (421 622, 446 625, 449 627)",
-        133, 0, BufferParameters::JOIN_MITRE, -1,
-        "LINESTRING (405.15 754.05, 416.3 755.39)"
-    );
-}
-
-
-
-} // namespace tut
diff --git a/web/content/specifications/geojson.md b/web/content/specifications/geojson.md
deleted file mode 100644
index fb3cee7de..000000000
--- a/web/content/specifications/geojson.md
+++ /dev/null
@@ -1,99 +0,0 @@
----
-title: "GeoJSON"
-date: 2021-10-04T14:18:42-07:00
-draft: false
----
-
-"GeoJSON" is a standard for structuring JSON when encoding geometry and features. By using GeoJSON, rather than some other arbitrary scheme for structuring JSON, you maximize the interoperability of your JSON output. There are dozens of tools and [websites](http://geojson.io) that happily consume and emit GeoJSON.
-
-The GeoJSON standard is formally maintained by the IETF as "[RFC 7946](https://datatracker.ietf.org/doc/html/rfc7946)".
-
-Unlike [WKB]({{< ref "wkb" >}}) and [WKT]({{< ref "wkt" >}}), GeoJSON does not restrict itself to just geometry representation. It also standardizes the transport of attribution. The three key levels of GeoJSON are:
-
-* [Geometry](https://datatracker.ietf.org/doc/html/rfc7946#section-3.1), representation of Points, LineStrings, Polygons, etc.
-* [Feature](https://datatracker.ietf.org/doc/html/rfc7946#section-3.2), representation of an object that has a "geometry" and an arbitrary set of other non-geometric "properties".
-* [FeatureCollection](https://datatracker.ietf.org/doc/html/rfc7946#section-3.3), representation of a list of Features.
-
-Since GEOS is almost 100% concerned with geometry operations, there is no GEOS abstraction to translate the non-geometric properties of GeoJSON Feature into, so handling of Feature properties is only available via the C++ [GeoJSON.h](https://github.com/libgeos/geos/blob/main/include/geos/io/GeoJSON.h) utilities.
-
-
-### Writing GeoJSON
-
-The GeoJSON writer, in both the C and C++ APIs, only supports writing out the Geometry portion of GeoJSON. So for writing features, the writer will end up embedded in some larger JSON emitter.
-
-```c
-/* Read a linestring */
-const char* linestring = "LINESTRING(0 0 1, 1 1 1, 2 1 2)";
-GEOSWKTReader* reader = GEOSWKTReader_create();
-GEOSGeom* geom = GEOSWKTReader_read(reader, linestring);
-
-/* Write it out as GeoJSON */
-GEOSGeoJSONWriter* writer = GEOSGeoJSONWriter_create();
-
-/* Generate the JSON, with an indentation of 2 */
-int indentation = 2;
-unsigned char* json = GEOSGeoJSONWriter_writeGeometry(writer, geom, indentation);
-
-/* do something ... */
-
-/* Free the WKB */
-GEOSFree(json);
-GEOSGeom_destroy(geom);
-GEOSGeoJSONWriter_destroy(writer);
-GEOSWKTReader_destroy(reader);
-```
-
-### Reading GeoJSON
-
-The C++ GeoJSON reader does include the option to read full `Feature` and `FeatureCollection` objects, with a narrow API for reading the resulting objects.
-
-```c++
-#include <geos/io/GeoJSON.h> // GeoJSONFeatureCollection, etc
-#include <geos/io/GeoJSONReader.h>
-#include <geos/io/WKTWriter.h>
-#include <geos/geom/Geometry.h>
-
-using namespace geos::io;
-using namespace geos::geom;
-
-void main(void)
-{
-    // Read file into string
-    std::ifstream ifs("geojson.json");
-    std::string content((std::istreambuf_iterator<char>(ifs) ),
-                        (std::istreambuf_iterator<char>()    ));
-
-    // Parse GeoJSON string into GeoJSON objects
-
-    GeoJSONReader reader;
-    GeoJSONFeatureCollection fc = reader.readFeatures(content);
-
-    // Prepare WKT writer
-    WKTWriter writer;
-    writer.setTrim(true);
-    writer.setRoundingPrecision(2);
-
-    // Print out the features
-    for (auto& feature: fc) {
-
-        // Read the geometry
-        const Geometry* geom = feature.getGeometry();
-
-        // Read the properties
-        std::map<std::string, GeoJSONValue>& props = feature.getProperties();
-
-        // Write all properties
-        std::cout << "----------" << std::endl;
-        for (const auto& prop : props) {
-            std::cout << prop.first << ": " << prop.second << std::endl;
-        }
-
-        // Write WKT feometry
-        std::cout << "geometry: " << writer.write(geom) << std::endl;
-    }
-}
-```
-
-
-
-

commit 27d4d95a1dae4d8c3ced2ec9df2fe6cc147f57b3
Author: Paul Ramsey <pramsey at cleverelephant.ca>
Date:   Mon May 29 07:13:30 2023 -0700

    * Port https://github.com/locationtech/jts/pull/981
    * References https://github.com/qgis/QGIS/issues/53165
    * References https://github.com/libgeos/geos/issues/897

diff --git a/NEWS b/NEWS
index a25843690..a57bef2c7 100644
--- a/NEWS
+++ b/NEWS
@@ -5,6 +5,7 @@ Changes in 3.10.6
   - LargestEmptyCircle: enhance boundary to allow any polygonal geometry (GH-859, Martin Davis)
   - Build issues with gcc-13 (GH-863)
   - GeoJSONReader: Fix 2D empty geometry creation (GH-910, Mike Taves)
+  - OffsetCurve: add minimum threshold for quadsegs (GH-897, Martin Davis, Paul Ramsey)
 
 Changes in 3.10.5
 2023-03-16
diff --git a/include/geos/operation/buffer/OffsetCurve.h b/include/geos/operation/buffer/OffsetCurve.h
new file mode 100644
index 000000000..403ca98b7
--- /dev/null
+++ b/include/geos/operation/buffer/OffsetCurve.h
@@ -0,0 +1,328 @@
+/**********************************************************************
+ *
+ * GEOS - Geometry Engine Open Source
+ * http://geos.osgeo.org
+ *
+ * Copyright (C) 2021 Paul Ramsey <pramsey at cleverelephant.ca>
+ *
+ * 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 <geos/export.h>
+
+#include <geos/operation/buffer/BufferParameters.h>
+#include <geos/geom/GeometryFactory.h>
+#include <geos/constants.h>
+
+// Forward declarations
+namespace geos {
+namespace geom {
+class Coordinate;
+class CoordinateSequence;
+class Geometry;
+class LineString;
+class Polygon;
+}
+namespace operation {
+namespace buffer {
+class OffsetCurveSection;
+class SegmentMCIndex;
+}
+}
+}
+
+using geos::geom::Coordinate;
+using geos::geom::CoordinateSequence;
+using geos::geom::Geometry;
+using geos::geom::GeometryFactory;
+using geos::geom::LineString;
+using geos::geom::Polygon;
+
+namespace geos {
+namespace operation {
+namespace buffer {
+
+/**
+ * Computes an offset curve from a geometry.
+ * An offset curve is a linear geometry which is offset a given distance
+ * from the input.
+ * If the offset distance is positive the curve lies on the left side of the input;
+ * if it is negative the curve is on the right side.
+ * The curve(s) have the same direction as the input line(s).
+ *
+ * The offset curve is based on the boundary of the buffer for the geometry
+ * at the offset distance (see BufferOp).
+ * The normal mode of operation is to return the sections of the buffer boundary
+ * which lie on the raw offset curve
+ * (obtained via rawOffset(LineString, double).
+ * The offset curve will contain multiple sections
+ * if the input self-intersects or has close approaches.
+ * The computed sections are ordered along the raw offset curve.
+ * Sections are disjoint.  They never self-intersect, but may be rings.
+ *
+ *   * For a LineString the offset curve is a linear geometry
+ *     (LineString or MultiLineString).
+ *   * For a Point or MultiPoint the offset curve is an empty LineString.
+ *   * For a Polygon the offset curve is the boundary of the polygon buffer (which
+ *     may be a MultiLineString.
+ *   * For a collection the output is a MultiLineString containing
+ *     the offset curves of the elements.
+ *
+ * In "joined" mode (see setJoined(bool))
+ * the sections computed for each input line are joined into a single offset curve line.
+ * The joined curve may self-intersect.
+ * At larger offset distances the curve may contain "flat-line" artifacts
+ * in places where the input self-intersects.
+ *
+ * Offset curves support setting the number of quadrant segments,
+ * the join style, and the mitre limit (if applicable) via
+ * the BufferParameters.
+ *
+ * @author Martin Davis
+ *
+ */
+class GEOS_DLL OffsetCurve {
+
+
+private:
+
+    // Members
+    const Geometry& inputGeom;
+    double distance;
+    bool isJoined = false;
+
+    BufferParameters bufferParams;
+    double matchDistance;
+    const GeometryFactory* geomFactory;
+
+    // Methods
+
+    std::unique_ptr<Geometry> computeCurve(
+        const LineString& lineGeom, double distance);
+
+    std::vector<std::unique_ptr<OffsetCurveSection>> computeSections(
+        const LineString& lineGeom, double distance);
+
+    std::unique_ptr<LineString> offsetSegment(
+        const CoordinateSequence* pts, double distance);
+
+    static std::unique_ptr<Polygon> getBufferOriented(
+        const LineString& geom, double distance,
+        BufferParameters& bufParams);
+
+    /**
+    * Extracts the largest polygon by area from a geometry.
+    * Used here to avoid issues with non-robust buffer results
+    * which have spurious extra polygons.
+    *
+    * @param geom a geometry
+    * @return the polygon element of largest area
+    */
+    static const Polygon* extractMaxAreaPolygon(const Geometry* geom);
+
+    void computeCurveSections(
+        const CoordinateSequence* bufferRingPts,
+        const CoordinateSequence& rawCurve,
+        std::vector<std::unique_ptr<OffsetCurveSection>>& sections);
+
+    /**
+    * Matches the segments in a buffer ring to the raw offset curve
+    * to obtain their match positions (if any).
+    *
+    * @param raw0 a raw curve segment start point
+    * @param raw1 a raw curve segment end point
+    * @param rawCurveIndex the index of the raw curve segment
+    * @param bufferSegIndex the spatial index of the buffer ring segments
+    * @param bufferPts the points of the buffer ring
+    * @param rawCurvePos the raw curve positions of the buffer ring segments
+    * @return the index of the minimum matched buffer segment
+    */
+    std::size_t matchSegments(
+        const Coordinate& raw0, const Coordinate& raw1,
+        std::size_t rawCurveIndex,
+        SegmentMCIndex& bufferSegIndex,
+        const CoordinateSequence* bufferPts,
+        std::vector<double>& rawCurvePos);
+
+    static double segmentMatchFrac(
+        const Coordinate& p0,   const Coordinate& p1,
+        const Coordinate& seg0, const Coordinate& seg1,
+        double matchDistance);
+
+    /**
+    * This is only called when there is at least one ring segment matched
+    * (so rawCurvePos has at least one entry != NOT_IN_CURVE).
+    * The start index of the first section must be provided.
+    * This is intended to be the section with lowest position
+    * along the raw curve.
+    * @param ringPts the points in a buffer ring
+    * @param rawCurveLoc the position of buffer ring segments along the raw curve
+    * @param startIndex the index of the start of a section
+    * @param sections the list of extracted offset curve sections
+    */
+    void extractSections(
+        const CoordinateSequence* ringPts,
+        std::vector<double>& rawCurveLoc,
+        std::size_t startIndex,
+        std::vector<std::unique_ptr<OffsetCurveSection>>& sections);
+
+    std::size_t findSectionStart(
+        const std::vector<double>& loc,
+        std::size_t end);
+
+    std::size_t findSectionEnd(
+        const std::vector<double>& loc,
+        std::size_t start,
+        std::size_t firstStartIndex);
+
+    static std::size_t nextIndex(std::size_t i, std::size_t size);
+    static std::size_t prevIndex(std::size_t i, std::size_t size);
+
+
+public:
+
+    // Constants
+    static constexpr int MATCH_DISTANCE_FACTOR = 10000;
+
+    /**
+    * A QuadSegs minimum value that will prevent generating
+    * unwanted offset curve artifacts near end caps.
+    */
+    static constexpr int MIN_QUADRANT_SEGMENTS = 8;
+
+    /**
+    * Creates a new instance for computing an offset curve for a geometry at a given distance.
+    * with default quadrant segments (BufferParameters::DEFAULT_QUADRANT_SEGMENTS)
+    * and join style (BufferParameters::JOIN_STYLE).
+    *
+    * @param geom the geometry to offset
+    * @param dist the offset distance (positive for left, negative for right)
+    *
+    * @see BufferParameters
+    */
+    OffsetCurve(const Geometry& geom, double dist)
+        : inputGeom(geom)
+        , distance(dist)
+        , matchDistance(std::abs(dist)/MATCH_DISTANCE_FACTOR)
+        , geomFactory(geom.getFactory())
+        {
+            if (!std::isfinite(dist)) {
+                throw util::IllegalArgumentException("OffsetCurve distance must be a finite value");
+            }
+        };
+
+    /**
+    * Creates a new instance for computing an offset curve for a geometry at a given distance.
+    * setting the quadrant segments and join style and mitre limit
+    * via {@link BufferParameters}.
+    *
+    * @param geom the geometry to offset
+    * @param dist the offset distance (positive for left, negative for right)
+    * @param bp the buffer parameters to use
+    */
+    OffsetCurve(const Geometry& geom, double dist, BufferParameters& bp)
+        : inputGeom(geom)
+        , distance(dist)
+        , matchDistance(std::abs(dist)/MATCH_DISTANCE_FACTOR)
+        , geomFactory(geom.getFactory())
+        {
+            if (!std::isfinite(dist)) {
+                throw util::IllegalArgumentException("OffsetCurve distance must be a finite value");
+            }
+            //-- set buffer params, leaving cap style as the default CAP_ROUND
+
+            /**
+            * Prevent using a very small QuadSegs value, to avoid
+            * offset curve artifacts near the end caps.
+            */
+            int quadSegs = bp.getQuadrantSegments();
+            if (quadSegs < MIN_QUADRANT_SEGMENTS) {
+                quadSegs = MIN_QUADRANT_SEGMENTS;
+            }
+            bufferParams.setQuadrantSegments(quadSegs);
+
+            bufferParams.setJoinStyle( bp.getJoinStyle());
+            bufferParams.setMitreLimit( bp.getMitreLimit());
+        };
+
+    /**
+    * Computes a single curve line for each input linear component,
+    * by joining curve sections in order along the raw offset curve.
+    * The default mode is to compute separate curve sections.
+    *
+    * @param pIsJoined true if joined mode should be used.
+    */
+    void setJoined(bool pIsJoined);
+
+    static std::unique_ptr<Geometry> getCurve(
+        const Geometry& geom,
+        double dist,
+        int quadSegs,
+        BufferParameters::JoinStyle joinStyle,
+        double mitreLimit);
+
+    static std::unique_ptr<Geometry> getCurve(
+        const Geometry& geom, double dist);
+
+    /**
+    * Computes the offset curve of a geometry at a given distance,
+    * joining curve sections into a single line for each input line.
+    *
+    * @param geom a geometry
+    * @param dist the offset distance (positive for left, negative for right)
+    * @return the joined offset curve
+    */
+    static std::unique_ptr<Geometry> getCurveJoined(
+        const Geometry& geom, double dist);
+
+    /**
+    * Gets the computed offset curve lines.
+    *
+    * @return the offset curve geometry
+    */
+    std::unique_ptr<Geometry> getCurve();
+
+    /**
+    * Gets the raw offset curve for a line at a given distance.
+    * The quadrant segments, join style and mitre limit can be specified
+    * via BufferParameters.
+    *
+    * The raw offset line may contain loops and other artifacts which are
+    * not present in the true offset curve.
+    *
+    * @param line the line to offset
+    * @param distance the offset distance (positive for left, negative for right)
+    * @param bufParams the buffer parameters to use
+    * @return the raw offset curve points
+    */
+    static std::unique_ptr<CoordinateSequence> rawOffsetCurve(
+        const LineString& line,
+        double distance,
+        BufferParameters& bufParams);
+
+    /**
+    * Gets the raw offset curve for a line at a given distance,
+    * with default buffer parameters.
+    *
+    * @param line the line to offset
+    * @param distance the offset distance (positive for left, negative for right)
+    * @return the raw offset curve points
+    */
+    static std::unique_ptr<CoordinateSequence> rawOffset(
+        const LineString& line,
+        double distance);
+
+};
+
+} // namespace geos::operation::buffer
+} // namespace geos::operation
+} // namespace geos
+
+
+
diff --git a/tests/unit/capi/GEOSOffsetCurveTest.cpp b/tests/unit/capi/GEOSOffsetCurveTest.cpp
index 660322108..abe078d2b 100644
--- a/tests/unit/capi/GEOSOffsetCurveTest.cpp
+++ b/tests/unit/capi/GEOSOffsetCurveTest.cpp
@@ -137,6 +137,7 @@ void object::test<5>
                   ));
 }
 
+
 // 0 distance
 // See http://trac.osgeo.org/postgis/ticket/454
 template<>
diff --git a/tests/unit/operation/buffer/OffsetCurveTest.cpp b/tests/unit/operation/buffer/OffsetCurveTest.cpp
new file mode 100644
index 000000000..5faea0fe4
--- /dev/null
+++ b/tests/unit/operation/buffer/OffsetCurveTest.cpp
@@ -0,0 +1,585 @@
+//
+// Test Suite for geos::operation::buffer::BufferOp class.
+
+// tut
+#include <tut/tut.hpp>
+#include <utility.h>
+
+// geos
+#include <geos/operation/buffer/OffsetCurve.h>
+#include <geos/operation/buffer/BufferParameters.h>
+#include <geos/geom/Geometry.h>
+#include <geos/io/WKTReader.h>
+// #include <geos/io/WKTWriter.h>
+
+// std
+#include <memory>
+#include <string>
+
+namespace tut {
+//
+// Test Group
+//
+using geos::operation::buffer::OffsetCurve;
+using geos::operation::buffer::BufferParameters;
+
+// Common data used by tests
+struct test_offsetcurve_data {
+    geos::io::WKTReader wktreader;
+
+    test_offsetcurve_data() {};
+
+    void checkOffsetCurve(const std::string& wkt, double distance,
+        int quadSegs, int joinStyle, double mitreLimit,
+        const std::string& wktExpected)
+    {
+        checkOffsetCurve(wkt, distance, quadSegs, joinStyle, mitreLimit, wktExpected, 0.05);
+    }
+
+    void checkOffsetCurve(const std::string& wkt, double distance,
+        int quadSegs, int joinStyle, double mitreLimit,
+        const std::string& wktExpected, double tolerance)
+    {
+        BufferParameters::JoinStyle js = static_cast<BufferParameters::JoinStyle>(joinStyle);
+        std::unique_ptr<geos::geom::Geometry> geom = wktreader.read(wkt);
+        std::unique_ptr<geos::geom::Geometry> result = OffsetCurve::getCurve(*geom, distance, quadSegs, js, mitreLimit);
+        std::unique_ptr<geos::geom::Geometry> expected = wktreader.read(wktExpected);
+        ensure_equals_geometry(result.get(), expected.get(), tolerance);
+    }
+
+    void checkOffsetCurve(const std::string& wkt, double distance, const std::string& wktExpected)
+    {
+        checkOffsetCurve(wkt, distance, wktExpected, 0.05);
+    }
+
+    void checkOffsetCurve(const std::string& wkt, double distance, const std::string& wktExpected, double tolerance)
+    {
+        std::unique_ptr<geos::geom::Geometry> geom = wktreader.read(wkt);
+        std::unique_ptr<geos::geom::Geometry> result = OffsetCurve::getCurve(*geom, distance);
+        std::unique_ptr<geos::geom::Geometry> expected = wktreader.read(wktExpected);
+
+        // geos::io::WKTWriter wktwriter;
+        // wktwriter.setRoundingPrecision(2);
+        // std::cout << std::endl;
+        // std::cout << "Expected: " << wktwriter.write(expected.get()) << std::endl;
+        // std::cout << "  Result: " << wktwriter.write(result.get()) << std::endl;
+
+        ensure_equals_geometry(result.get(), expected.get(), tolerance);
+    }
+
+    void
+    checkOffsetCurveJoined(const std::string& wkt, double distance, const std::string& wktExpected)
+    {
+        std::unique_ptr<geos::geom::Geometry> geom = wktreader.read(wkt);
+        std::unique_ptr<geos::geom::Geometry> result = OffsetCurve::getCurveJoined(*geom, distance);
+        std::unique_ptr<geos::geom::Geometry> expected = wktreader.read(wktExpected);
+        ensure_equals_geometry(result.get(), expected.get(), 0.05);
+    }
+
+};
+
+typedef test_group<test_offsetcurve_data> group;
+typedef group::object object;
+
+group test_offsetcurve_group("geos::operation::buffer::OffsetCurve");
+
+
+// testPoint
+template<>
+template<>
+void object::test<1> ()
+{
+    checkOffsetCurve(
+        "POINT (0 0)", 1,
+        "LINESTRING EMPTY"
+        );
+}
+
+// testEmpty
+template<>
+template<>
+void object::test<2> ()
+{
+    checkOffsetCurve(
+        "LINESTRING EMPTY", 1,
+        "LINESTRING EMPTY"
+        );
+}
+
+// testZeroLenLine
+template<>
+template<>
+void object::test<3> ()
+{
+    checkOffsetCurve(
+        "LINESTRING (1 1, 1 1)", 1,
+        "LINESTRING EMPTY"
+        );
+}
+
+// testZeroOffsetLine
+template<>
+template<>
+void object::test<4> ()
+{
+    checkOffsetCurve(
+        "LINESTRING (0 0, 1 0, 1 1)", 0,
+        "LINESTRING (0 0, 1 0, 1 1)"
+        );
+}
+
+// testZeroOffsetPolygon
+template<>
+template<>
+void object::test<5> ()
+{
+    checkOffsetCurve(
+        "POLYGON ((1 9, 9 1, 1 1, 1 9))", 0,
+        "LINESTRING (1 9, 1 1, 9 1, 1 9)"
+        );
+}
+
+/**
+* testRepeatedPoint
+* Test bug fix for removing repeated points in input for raw curve.
+* See https://github.com/locationtech/jts/issues/957
+*/
+template<>
+template<>
+void object::test<6> ()
+{
+    checkOffsetCurve(
+        "LINESTRING (4 9, 1 2, 7 5, 7 5, 4 9)", 1,
+        "LINESTRING (4.24 7.02, 2.99 4.12, 5.48 5.36, 4.24 7.02)"
+        );
+}
+
+
+// testSegment1Short
+template<>
+template<>
+void object::test<7> ()
+{
+    checkOffsetCurve(
+        "LINESTRING (2 2, 2 2.0000001)", 1,
+        "LINESTRING (1 2, 1 2.0000001)",
+        0.00000001
+    );
+}
+
+// testSegment1
+template<>
+template<>
+void object::test<8> ()
+{
+    checkOffsetCurve(
+        "LINESTRING (0 0, 9 9)", 1,
+        "LINESTRING (-0.71 0.71, 8.29 9.71)"
+    );
+}
+
+
+// testSegment1Neg
+template<>
+template<>
+void object::test<9> ()
+{
+    checkOffsetCurve(
+        "LINESTRING (0 0, 9 9)", -1,
+        "LINESTRING (0.71 -0.71, 9.71 8.29)"
+    );
+}
+
+// testSegments2
+template<>
+template<>
+void object::test<10> ()
+{
+    checkOffsetCurve(
+        "LINESTRING (0 0, 9 9, 25 0)", 1,
+        "LINESTRING (-0.707 0.707, 8.293 9.707, 8.435 9.825, 8.597 9.915, 8.773 9.974, 8.956 9.999, 9.141 9.99, 9.321 9.947, 9.49 9.872, 25.49 0.872)"
+        );
+}
+
+// testSegments3
+template<>
+template<>
+void object::test<11> ()
+{
+    checkOffsetCurve(
+        "LINESTRING (0 0, 9 9, 25 0, 30 15)", 1,
+        "LINESTRING (-0.71 0.71, 8.29 9.71, 8.44 9.83, 8.6 9.92, 8.77 9.97, 8.96 10, 9.14 9.99, 9.32 9.95, 9.49 9.87, 24.43 1.47, 29.05 15.32)"
+        );
+}
+
+// testRightAngle
+template<>
+template<>
+void object::test<12> ()
+{
+    checkOffsetCurve(
+        "LINESTRING (2 8, 8 8, 8 1)", 1,
+        "LINESTRING (2 9, 8 9, 8.2 8.98, 8.38 8.92, 8.56 8.83, 8.71 8.71, 8.83 8.56, 8.92 8.38, 8.98 8.2, 9 8, 9 1)"
+        );
+}
+
+// testZigzagOneEndCurved4
+template<>
+template<>
+void object::test<13> ()
+{
+    checkOffsetCurve(
+        "LINESTRING (1 3, 6 3, 4 5, 9 5)", 1,
+        "LINESTRING (1 4, 3.59 4, 3.29 4.29, 3.17 4.44, 3.08 4.62, 3.02 4.8, 3 5, 3.02 5.2, 3.08 5.38, 3.17 5.56, 3.29 5.71, 3.44 5.83, 3.62 5.92, 3.8 5.98, 4 6, 9 6)"
+        );
+}
+
+// testZigzagOneEndCurved1
+template<>
+template<>
+void object::test<14> ()
+{
+    checkOffsetCurve(
+        "LINESTRING (1 3, 6 3, 4 5, 9 5)", 1,
+        "LINESTRING (1 4, 3.59 4, 3.29 4.29, 3.17 4.44, 3.08 4.62, 3.02 4.8, 3 5, 3.02 5.2, 3.08 5.38, 3.17 5.56, 3.29 5.71, 3.44 5.83, 3.62 5.92, 3.8 5.98, 4 6, 9 6)"
+        );
+}
+
+
+// testAsymmetricU
+template<>
+template<>
+void object::test<15> ()
+{
+    checkOffsetCurve(
+        "LINESTRING (1 1, 9 1, 9 2, 5 2)", 1,
+        "LINESTRING (1 2, 4 2)"
+        );
+
+    checkOffsetCurve(
+        "LINESTRING (1 1, 9 1, 9 2, 5 2)", -1,
+        "LINESTRING (1 0, 9 0, 9.2 0.02, 9.38 0.08, 9.56 0.17, 9.71 0.29, 9.83 0.44, 9.92 0.62, 9.98 0.8, 10 1, 10 2, 9.98 2.2, 9.92 2.38, 9.83 2.56, 9.71 2.71, 9.56 2.83, 9.38 2.92, 9.2 2.98, 9 3, 5 3)"
+        );
+}
+
+// testSymmetricU
+template<>
+template<>
+void object::test<16> ()
+{
+    checkOffsetCurve(
+        "LINESTRING (1 1, 9 1, 9 2, 1 2)", 1,
+        "LINESTRING EMPTY"
+        );
+
+    checkOffsetCurve(
+        "LINESTRING (1 1, 9 1, 9 2, 1 2)", -1,
+        "LINESTRING (1 0, 9 0, 9.2 0.02, 9.38 0.08, 9.56 0.17, 9.71 0.29, 9.83 0.44, 9.92 0.62, 9.98 0.8, 10 1, 10 2, 9.98 2.2, 9.92 2.38, 9.83 2.56, 9.71 2.71, 9.56 2.83, 9.38 2.92, 9.2 2.98, 9 3, 1 3)"
+        );
+}
+
+// testEmptyResult
+template<>
+template<>
+void object::test<17> ()
+{
+    checkOffsetCurve(
+        "LINESTRING (3 5, 5 7, 7 5)", -4,
+        "LINESTRING EMPTY"
+        );
+}
+
+// testSelfCross
+template<>
+template<>
+void object::test<18> ()
+{
+    checkOffsetCurve(
+        "LINESTRING (50 90, 50 10, 90 50, 10 50)", 10,
+        "MULTILINESTRING ((60 90, 60 60), (60 40, 60 34.14, 65.85 40, 60 40), (40 40, 10 40))" );
+}
+
+// testSelfCrossNeg
+template<>
+template<>
+void object::test<19> ()
+{
+    checkOffsetCurve(
+        "LINESTRING (50 90, 50 10, 90 50, 10 50)", -10,
+        "MULTILINESTRING ((40 90, 40 60, 10 60), (40 40, 40 10, 40.19 8.05, 40.76 6.17, 41.69 4.44, 42.93 2.93, 44.44 1.69, 46.17 0.76, 48.05 0.19, 50 0, 51.95 0.19, 53.83 0.76, 55.56 1.69, 57.07 2.93, 97.07 42.93, 98.31 44.44, 99.24 46.17, 99.81 48.05, 100 50, 99.81 51.95, 99.24 53.83, 98.31 55.56, 97.07 57.07, 95.56 58.31, 93.83 59.24, 91.95 59.81, 90 60, 60 60))" );
+}
+
+// testSelfCrossCWNeg
+template<>
+template<>
+void object::test<20> ()
+{
+    checkOffsetCurve(
+        "LINESTRING (0 70, 100 70, 40 0, 40 100)", -10,
+        "MULTILINESTRING ((0 60, 30 60), (50 60, 50 27.03, 78.25 60, 50 60), (50 80, 50 100))" );
+}
+
+// testSelfCrossDartInside
+template<>
+template<>
+void object::test<21> ()
+{
+    checkOffsetCurve(
+        "LINESTRING (60 50, 10 80, 50 10, 90 80, 40 50)", 10,
+        "MULTILINESTRING ((54.86 41.43, 50 44.34, 45.14 41.43), (43.9 40.83, 50 30.16, 56.1 40.83))" );
+}
+
+
+// testSelfCrossDartOutside
+template<>
+template<>
+void object::test<22> ()
+{
+    checkOffsetCurve(
+        "LINESTRING (60 50, 10 80, 50 10, 90 80, 40 50)", -10,
+        "LINESTRING (50 67.66, 15.14 88.57, 13.32 89.43, 11.35 89.91, 9.33 89.98, 7.34 89.64, 5.46 88.91, 3.76 87.82, 2.32 86.4, 1.19 84.73, 0.42 82.86, 0.04 80.88, 0.07 78.86, 0.5 76.88, 1.32 75.04, 41.32 5.04, 42.42 3.48, 43.8 2.16, 45.4 1.12, 47.17 0.41, 49.05 0.05, 50.95 0.05, 52.83 0.41, 54.6 1.12, 56.2 2.16, 57.58 3.48, 58.68 5.04, 98.68 75.04, 99.5 76.88, 99.93 78.86, 99.96 80.88, 99.58 82.86, 98.81 84.73, 97.68 86.4, 96.24 87.82, 94.54 88.91, 92.66 89.64, 90.67 89.98, 88.65 89.91, 86.68 89.43, 84.86 88.57, 50 67.66)" );
+}
+
+
+// testSelfCrossDart2Inside
+template<>
+template<>
+void object::test<23> ()
+{
+    checkOffsetCurve(
+        "LINESTRING (64 45, 10 80, 50 10, 90 80, 35 45)", 10,
+        "LINESTRING (55.00 38.91, 49.58 42.42, 44.74 39.34, 50 30.15, 55.00 38.91)" );
+}
+
+// testRing
+template<>
+template<>
+void object::test<24> ()
+{
+    checkOffsetCurve(
+        "LINESTRING (10 10, 50 90, 90 10, 10 10)", -10,
+        "LINESTRING (26.18 20, 50 67.63, 73.81 20, 26.18 20)" );
+}
+
+// testClosedCurve
+template<>
+template<>
+void object::test<25> ()
+{
+    checkOffsetCurve(
+        "LINESTRING (70 80, 10 80, 50 10, 90 80, 40 80)", 10,
+        "LINESTRING (70 70, 40 70, 27.23 70, 50 30.15, 72.76 70, 70 70)"
+        );
+}
+
+
+// testOverlapTriangleInside
+template<>
+template<>
+void object::test<26> ()
+{
+    checkOffsetCurve(
+        "LINESTRING (70 80, 10 80, 50 10, 90 80, 40 80)", 10,
+        "LINESTRING (70 70, 40 70, 27.23 70, 50 30.15, 72.76 70, 70 70)" );
+}
+
+
+// testOverlapTriangleOutside
+template<>
+template<>
+void object::test<27> ()
+{
+    checkOffsetCurve(
+        "LINESTRING (70 80, 10 80, 50 10, 90 80, 40 80)", -10,
+        "LINESTRING (70 90, 40 90, 10 90, 8.11 89.82, 6.29 89.29, 4.6 88.42, 3.11 87.25, 1.87 85.82, 0.91 84.18, 0.29 82.39, 0.01 80.51, 0.1 78.61, 0.54 76.77, 1.32 75.04, 41.32 5.04, 42.42 3.48, 43.8 2.16, 45.4 1.12, 47.17 0.41, 49.05 0.05, 50.95 0.05, 52.83 0.41, 54.6 1.12, 56.2 2.16, 57.58 3.48, 58.68 5.04, 98.68 75.04, 99.46 76.77, 99.9 78.61, 99.99 80.51, 99.71 82.39, 99.09 84.18, 98.13 85.82, 96.89 87.25, 95.4 88.42, 93.71 89.29, 91.89 89.82, 90 90, 70 90)"
+        );
+}
+
+
+// testMultiPoint
+template<>
+template<>
+void object::test<28> ()
+{
+    checkOffsetCurve(
+        "MULTIPOINT ((0 0), (1 1))", 1,
+        "LINESTRING EMPTY"
+    );
+}
+
+
+// testMultiLine
+template<>
+template<>
+void object::test<29> ()
+{
+    checkOffsetCurve(
+        "MULTILINESTRING ((20 30, 60 10, 80 60), (40 50, 80 30))", 10,
+        "MULTILINESTRING ((24.47 38.94, 54.75 23.8, 70.72 63.71), (44.47 58.94, 84.47 38.94))"
+    );
+}
+
+
+// testMixedWithPoint
+template<>
+template<>
+void object::test<30> ()
+{
+    checkOffsetCurve(
+        "GEOMETRYCOLLECTION (LINESTRING (20 30, 60 10, 80 60), POINT (0 0))", 10,
+        "LINESTRING (24.47 38.94, 54.75 23.8, 70.72 63.71)"
+    );
+}
+
+
+// testPolygon1
+template<>
+template<>
+void object::test<31> ()
+{
+    checkOffsetCurve(
+        "POLYGON ((100 200, 200 100, 100 100, 100 200))", 10,
+        "LINESTRING (90 200, 90.19 201.95, 90.76 203.83, 91.69 205.56, 92.93 207.07, 94.44 208.31, 96.17 209.24, 98.05 209.81, 100 210, 101.95 209.81, 103.83 209.24, 105.56 208.31, 107.07 207.07, 207.07 107.07, 208.31 105.56, 209.24 103.83, 209.81 101.95, 210 100, 209.81 98.05, 209.24 96.17, 208.31 94.44, 207.07 92.93, 205.56 91.69, 203.83 90.76, 201.95 90.19, 200 90, 100 90, 98.05 90.19, 96.17 90.76, 94.44 91.69, 92.93 92.93, 91.69 94.44, 90.76 96.17, 90.19 98.05, 90 100, 90 200)"
+    );
+}
+
+// testPolygon2
+template<>
+template<>
+void object::test<32> ()
+{
+    checkOffsetCurve(
+        "POLYGON ((100 200, 200 100, 100 100, 100 200))", -10,
+        "LINESTRING (110 175.86, 175.86 110, 110 110, 110 175.86)"
+    );
+}
+
+
+// testPolygonWithHole1
+template<>
+template<>
+void object::test<33> ()
+{
+    checkOffsetCurve(
+        "POLYGON ((20 80, 80 80, 80 20, 20 20, 20 80), (30 70, 70 70, 70 30, 30 30, 30 70))", 10,
+        "MULTILINESTRING ((10 80, 10.19 81.95, 10.76 83.83, 11.69 85.56, 12.93 87.07, 14.44 88.31, 16.17 89.24, 18.05 89.81, 20 90, 80 90, 81.95 89.81, 83.83 89.24, 85.56 88.31, 87.07 87.07, 88.31 85.56, 89.24 83.83, 89.81 81.95, 90 80, 90 20, 89.81 18.05, 89.24 16.17, 88.31 14.44, 87.07 12.93, 85.56 11.69, 83.83 10.76, 81.95 10.19, 80 10, 20 10, 18.05 10.19, 16.17 10.76, 14.44 11.69, 12.93 12.93, 11.69 14.44, 10.76 16.17, 10.19 18.05, 10 20, 10 80), (40 60, 40 40, 60 40, 60 60, 40 60))"
+    );
+}
+
+// testPolygonWithHole2
+template<>
+template<>
+void object::test<34> ()
+{
+    checkOffsetCurve(
+        "POLYGON ((20 80, 80 80, 80 20, 20 20, 20 80), (30 70, 70 70, 70 30, 30 30, 30 70))", -10,
+        "LINESTRING EMPTY"
+    );
+}
+
+
+
+//-------------------------------------------------
+
+// testJoined1
+template<>
+template<>
+void object::test<35> ()
+{
+    checkOffsetCurveJoined("LINESTRING (0 50, 100 50, 50 100, 50 0)", 10,
+        "LINESTRING (0 60, 75.85 60, 60 75.85, 60 0)"
+    );
+}
+
+// testJoined2
+template<>
+template<>
+void object::test<36> ()
+{
+    checkOffsetCurveJoined("LINESTRING (0 50, 100 50, 50 100, 50 0)", -10,
+        "LINESTRING (0 40, 100 40, 101.95 40.19, 103.83 40.76, 105.56 41.69, 107.07 42.93, 108.31 44.44, 109.24 46.17, 109.81 48.05, 110 50, 109.81 51.95, 109.24 53.83, 108.31 55.56, 107.07 57.07, 57.07 107.07, 55.56 108.31, 53.83 109.24, 51.95 109.81, 50 110, 48.05 109.81, 46.17 109.24, 44.44 108.31, 42.93 107.07, 41.69 105.56, 40.76 103.83, 40.19 101.95, 40 100, 40 0)"
+    );
+}
+
+
+//-------------------------------------------------
+
+// testInfiniteLoop
+template<>
+template<>
+void object::test<37> ()
+{
+    checkOffsetCurve(
+        "LINESTRING (21 101, -1 78, 12 43, 50 112, 73 -5, 19 2, 87 85, -7 38, 105 40)", 4,
+        "MULTILINESTRING ((23.89 98.24, 3.62 77.04, 12.71 52.58, 46.50 113.93, 46.96 114.60, 47.55 115.16, 48.24 115.59, 49.00 115.87, 49.80 116.00, 50.62 115.95, 51.40 115.75, 52.13 115.38, 52.77 114.88, 53.30 114.26, 53.69 113.55, 53.92 112.77, 61.06 76.50), (62.66 68.36, 63.73 62.91, 72.03 73.05, 62.66 68.36), (65.73 52.73, 67.58 43.33, 104.93 44.00), (69.14 35.36, 76.92 -4.23, 77.00 -4.98, 76.93 -5.73, 76.72 -6.46, 76.38 -7.14, 75.92 -7.73, 75.35 -8.24, 74.70 -8.62, 73.99 -8.88, 73.24 -8.99, 72.49 -8.97, 18.49 -1.97, 17.73 -1.79, 17.03 -1.48, 16.39 -1.04, 15.86 -0.48, 15.44 0.17, 15.16 0.89, 15.02 1.65, 15.02 2.42, 15.18 3.18, 15.48 3.89, 15.91 4.53, 40.74 34.85), (47.40 42.97, 57.15 54.88), (55.23 64.64, 18.32 46.19), (16.24 42.42, 47.40 42.97), (57.89 43.16, 59.45 43.19))"
+    );
+}
+
+//---------------------------------------
+
+// testQuadSegs
+template<>
+template<>
+void object::test<38> ()
+{
+    checkOffsetCurve(
+        "LINESTRING (20 20, 50 50, 80 20)",
+        10, 2, -1, -1,
+        "LINESTRING (12.928932188134524 27.071067811865476, 42.928932188134524 57.071067811865476, 44.44429766980398 58.314696123025456, 46.1731656763491 59.23879532511287, 48.04909677983872 59.80785280403231, 50 60, 51.95090322016128 59.80785280403231, 53.8268343236509 59.23879532511287, 55.55570233019602 58.314696123025456, 57.071067811865476 57.071067811865476, 87.07106781186548 27.071067811865476)"
+        );
+}
+
+// testJoinBevel
+template<>
+template<>
+void object::test<39> ()
+{
+    checkOffsetCurve(
+        "LINESTRING (20 20, 50 50, 80 20)",
+        10, -1, BufferParameters::JOIN_BEVEL, -1,
+        "LINESTRING (12.93 27.07, 42.93 57.07, 57.07 57.07, 87.07 27.07)"
+        );
+}
+
+// testJoinMitre
+template<>
+template<>
+void object::test<40> ()
+{
+    checkOffsetCurve(
+        "LINESTRING (20 20, 50 50, 80 20)",
+        10, -1, BufferParameters::JOIN_MITRE, -1,
+        "LINESTRING (12.93 27.07, 50 64.14, 87.07 27.07)"
+        );
+}
+
+
+// testMinQuadrantSegments
+// See https://github.com/qgis/QGIS/issues/53165
+template<>
+template<>
+void object::test<41> ()
+{
+    checkOffsetCurve(
+        "LINESTRING (553772.0645892698 177770.05079236583, 553780.9235869241 177768.99614978794, 553781.8325485934 177768.41771963477)",
+        -11, 0, BufferParameters::JOIN_MITRE, -1,
+        "LINESTRING (553770.76 177759.13, 553777.54 177758.32)"
+    );
+}
+
+// testMinQuadrantSegments_QGIS
+// See https://github.com/qgis/QGIS/issues/53165#issuecomment-1563214857
+template<>
+template<>
+void object::test<42> ()
+{
+    checkOffsetCurve(
+        "LINESTRING (421 622, 446 625, 449 627)",
+        133, 0, BufferParameters::JOIN_MITRE, -1,
+        "LINESTRING (405.15 754.05, 416.3 755.39)"
+    );
+}
+
+
+
+} // namespace tut
diff --git a/web/content/specifications/geojson.md b/web/content/specifications/geojson.md
new file mode 100644
index 000000000..fb3cee7de
--- /dev/null
+++ b/web/content/specifications/geojson.md
@@ -0,0 +1,99 @@
+---
+title: "GeoJSON"
+date: 2021-10-04T14:18:42-07:00
+draft: false
+---
+
+"GeoJSON" is a standard for structuring JSON when encoding geometry and features. By using GeoJSON, rather than some other arbitrary scheme for structuring JSON, you maximize the interoperability of your JSON output. There are dozens of tools and [websites](http://geojson.io) that happily consume and emit GeoJSON.
+
+The GeoJSON standard is formally maintained by the IETF as "[RFC 7946](https://datatracker.ietf.org/doc/html/rfc7946)".
+
+Unlike [WKB]({{< ref "wkb" >}}) and [WKT]({{< ref "wkt" >}}), GeoJSON does not restrict itself to just geometry representation. It also standardizes the transport of attribution. The three key levels of GeoJSON are:
+
+* [Geometry](https://datatracker.ietf.org/doc/html/rfc7946#section-3.1), representation of Points, LineStrings, Polygons, etc.
+* [Feature](https://datatracker.ietf.org/doc/html/rfc7946#section-3.2), representation of an object that has a "geometry" and an arbitrary set of other non-geometric "properties".
+* [FeatureCollection](https://datatracker.ietf.org/doc/html/rfc7946#section-3.3), representation of a list of Features.
+
+Since GEOS is almost 100% concerned with geometry operations, there is no GEOS abstraction to translate the non-geometric properties of GeoJSON Feature into, so handling of Feature properties is only available via the C++ [GeoJSON.h](https://github.com/libgeos/geos/blob/main/include/geos/io/GeoJSON.h) utilities.
+
+
+### Writing GeoJSON
+
+The GeoJSON writer, in both the C and C++ APIs, only supports writing out the Geometry portion of GeoJSON. So for writing features, the writer will end up embedded in some larger JSON emitter.
+
+```c
+/* Read a linestring */
+const char* linestring = "LINESTRING(0 0 1, 1 1 1, 2 1 2)";
+GEOSWKTReader* reader = GEOSWKTReader_create();
+GEOSGeom* geom = GEOSWKTReader_read(reader, linestring);
+
+/* Write it out as GeoJSON */
+GEOSGeoJSONWriter* writer = GEOSGeoJSONWriter_create();
+
+/* Generate the JSON, with an indentation of 2 */
+int indentation = 2;
+unsigned char* json = GEOSGeoJSONWriter_writeGeometry(writer, geom, indentation);
+
+/* do something ... */
+
+/* Free the WKB */
+GEOSFree(json);
+GEOSGeom_destroy(geom);
+GEOSGeoJSONWriter_destroy(writer);
+GEOSWKTReader_destroy(reader);
+```
+
+### Reading GeoJSON
+
+The C++ GeoJSON reader does include the option to read full `Feature` and `FeatureCollection` objects, with a narrow API for reading the resulting objects.
+
+```c++
+#include <geos/io/GeoJSON.h> // GeoJSONFeatureCollection, etc
+#include <geos/io/GeoJSONReader.h>
+#include <geos/io/WKTWriter.h>
+#include <geos/geom/Geometry.h>
+
+using namespace geos::io;
+using namespace geos::geom;
+
+void main(void)
+{
+    // Read file into string
+    std::ifstream ifs("geojson.json");
+    std::string content((std::istreambuf_iterator<char>(ifs) ),
+                        (std::istreambuf_iterator<char>()    ));
+
+    // Parse GeoJSON string into GeoJSON objects
+
+    GeoJSONReader reader;
+    GeoJSONFeatureCollection fc = reader.readFeatures(content);
+
+    // Prepare WKT writer
+    WKTWriter writer;
+    writer.setTrim(true);
+    writer.setRoundingPrecision(2);
+
+    // Print out the features
+    for (auto& feature: fc) {
+
+        // Read the geometry
+        const Geometry* geom = feature.getGeometry();
+
+        // Read the properties
+        std::map<std::string, GeoJSONValue>& props = feature.getProperties();
+
+        // Write all properties
+        std::cout << "----------" << std::endl;
+        for (const auto& prop : props) {
+            std::cout << prop.first << ": " << prop.second << std::endl;
+        }
+
+        // Write WKT feometry
+        std::cout << "geometry: " << writer.write(geom) << std::endl;
+    }
+}
+```
+
+
+
+

-----------------------------------------------------------------------

Summary of changes:
 NEWS                                               |  1 +
 .../intersection/RectangleIntersection.cpp         |  6 +++++-
 tests/unit/capi/GEOSClipByRectTest.cpp             | 25 ++++++++++++++++++++++
 .../intersection/RectangleIntersectionTest.cpp     | 23 ++++++++++++++++++++
 4 files changed, 54 insertions(+), 1 deletion(-)


hooks/post-receive
-- 
GEOS


More information about the geos-commits mailing list