diff --git a/NEWS.md b/NEWS.md index e320dd8ae6..63bb507d53 100644 --- a/NEWS.md +++ b/NEWS.md @@ -51,7 +51,7 @@ xxxx-xx-xx - HausdorffDistance: Fix crash on collection containing empty point (GH-840, Dan Baston) - MaximumInscribedCircle: Fix infinite loop with non-finite coordinates (GH-843, Dan Baston) - DistanceOp: Fix crash on collection containing empty point (GH-842, Dan Baston) - + - Improve OffsetCurve behaviour and add Joined mode (JTS-956, Martin Davis) ## Changes in 3.11.0 2022-07-01 diff --git a/include/geos/operation/buffer/OffsetCurve.h b/include/geos/operation/buffer/OffsetCurve.h index 05dd8c2c79..e1caee1e09 100644 --- a/include/geos/operation/buffer/OffsetCurve.h +++ b/include/geos/operation/buffer/OffsetCurve.h @@ -15,45 +15,33 @@ #pragma once #include -#include -#include -#include - -#ifdef _MSC_VER -#pragma warning(push) -#pragma warning(disable: 4251) // warning C4251: needs to have dll-interface to be used by clients of class -#endif +#include +#include // Forward declarations namespace geos { namespace geom { +class Coordinate; +class CoordinateSequence; class Geometry; class LineString; -class LinearRing; class Polygon; -class CoordinateSequence; -class Coordinate; } namespace operation { namespace buffer { +class OffsetCurveSection; class SegmentMCIndex; } } -namespace index { -namespace chain { -class MonotoneChain; -} -} } +using geos::geom::Coordinate; +using geos::geom::CoordinateSequence; using geos::geom::Geometry; using geos::geom::GeometryFactory; using geos::geom::LineString; -using geos::geom::LinearRing; using geos::geom::Polygon; -using geos::geom::CoordinateSequence; -using geos::geom::Coordinate; namespace geos { namespace operation { @@ -61,210 +49,253 @@ namespace buffer { /** * Computes an offset curve from a geometry. - * The offset curve is a linear geometry which is offset a specified distance + * 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 of a line is a LineString which - * The offset curve of a Point is an empty LineString. - * The offset curve of a Polygon is the boundary of the polygon buffer (which - * may be a MultiLineString. - * For a collection the output is a MultiLineString of the element offset curves. + * 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. * - * The offset curve is computed as a single contiguous section of the geometry buffer boundary. - * In some geometric situations this definition is ill-defined. - * This algorithm provides a "best-effort" interpretation. - * In particular: + * * 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. * - * * For self-intersecting lines, the buffer boundary includes - * offset lines for both left and right sides of the input line. - * Only a single contiguous portion on the specified side is returned. - * * If the offset corresponds to buffer holes, only the largest hole is used. + * 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: - // Constants - static constexpr int NEARNESS_FACTOR = 10000; - // Members const Geometry& inputGeom; double distance; + bool isJoined = false; + BufferParameters bufferParams; double matchDistance; const GeometryFactory* geomFactory; // Methods - std::unique_ptr computeCurve(const LineString& lineGeom, double distance); + std::unique_ptr computeCurve( + const LineString& lineGeom, double distance); + + std::vector> computeSections( + const LineString& lineGeom, double distance); - std::unique_ptr offsetSegment(const CoordinateSequence* pts, double distance); + std::unique_ptr offsetSegment( + const CoordinateSequence* pts, double distance); - static std::unique_ptr getBufferOriented(const LineString& geom, double distance, BufferParameters& bufParms); + static std::unique_ptr 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. + * 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 std::unique_ptr extractMaxAreaPolygon(const Geometry& geom); - - static std::unique_ptr extractLongestHole(const Polygon& poly); + static const Polygon* extractMaxAreaPolygon(const Geometry* geom); - std::unique_ptr computeCurve( - const CoordinateSequence* bufferPts, - std::vector& rawOffsetList); - - int markMatchingSegments(const Coordinate& p0, const Coordinate& p1, - SegmentMCIndex& segIndex, const CoordinateSequence* bufferPts, - std::vector& isInCurve); - - static double subsegmentMatchFrac(const Coordinate& p0, const Coordinate& p1, - const Coordinate& seg0, const Coordinate& seg1, double matchDistance); + void computeCurveSections( + const CoordinateSequence* bufferRingPts, + const CoordinateSequence& rawCurve, + std::vector>& sections); /** - * Extracts a section of a ring of coordinates, starting at a given index, - * and keeping coordinates which are flagged as being required. + * Matches the segments in a buffer ring to the raw offset curve + * to obtain their match positions (if any). * - * @param ring the ring of points - * @param startIndex the index of the start coordinate - * @param isExtracted flag indicating if coordinate is to be extracted - * @return + * @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 */ - static void extractSection(const CoordinateSequence* ring, int iStartIndex, - std::vector& isExtracted, CoordinateSequence& extractedPoints); - - static std::size_t next(std::size_t i, std::size_t size); - - - /* private */ - class MatchCurveSegmentAction : public index::chain::MonotoneChainSelectAction - { - - private: + std::size_t matchSegments( + const Coordinate& raw0, const Coordinate& raw1, + std::size_t rawCurveIndex, + SegmentMCIndex& bufferSegIndex, + const CoordinateSequence* bufferPts, + std::vector& rawCurvePos); - const Coordinate& p0; - const Coordinate& p1; - const CoordinateSequence* bufferPts; - double matchDistance; - std::vector& isInCurve; - double minFrac = -1; - int minCurveIndex = -1; + static double segmentMatchFrac( + const Coordinate& p0, const Coordinate& p1, + const Coordinate& seg0, const Coordinate& seg1, + double matchDistance); - public: + /** + * 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& rawCurveLoc, + std::size_t startIndex, + std::vector>& sections); - MatchCurveSegmentAction( - const Coordinate& p_p0, const Coordinate& p_p1, - const CoordinateSequence* p_bufferPts, double p_matchDistance, - std::vector& p_isInCurve) - : p0(p_p0) - , p1(p_p1) - , bufferPts(p_bufferPts) - , matchDistance(p_matchDistance) - , isInCurve(p_isInCurve) - , minFrac(-1) - , minCurveIndex(-1) - {}; + std::size_t findSectionStart( + const std::vector& loc, + std::size_t end); - void select(const index::chain::MonotoneChain& mc, std::size_t segIndex) override; - void select(const geom::LineSegment& seg) override { (void)seg; return; }; + std::size_t findSectionEnd( + const std::vector& loc, + std::size_t start, + std::size_t firstStartIndex); - int getMinCurveIndex() { return minCurveIndex; } - }; + 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; + static constexpr std::size_t UNKNOWN_INDEX = std::numeric_limits::max(); + /** - * Creates a new instance for computing an offset curve for a geometryat a given distance. - * with default quadrant segments BufferParameters::DEFAULT_QUADRANT_SEGMENTS - * and join style BufferParameters::JOIN_STYLE. + * 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 = left, negative = right) + * @param dist the offset distance (positive for left, negative for right) * - * \see BufferParameters + * @see BufferParameters */ OffsetCurve(const Geometry& geom, double dist) : inputGeom(geom) , distance(dist) - , matchDistance(std::abs(dist)/NEARNESS_FACTOR) + , matchDistance(std::abs(dist)/MATCH_DISTANCE_FACTOR) , geomFactory(geom.getFactory()) {}; /** * Creates a new instance for computing an offset curve for a geometry at a given distance. - * allowing the quadrant segments and join style and mitre limit to be set - * via BufferParameters. + * setting the quadrant segments and join style and mitre limit + * via {@link BufferParameters}. * - * @param geom - * @param dist - * @param bp + * @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) , bufferParams(bp) - , matchDistance(std::abs(dist)/NEARNESS_FACTOR) + , matchDistance(std::abs(dist)/MATCH_DISTANCE_FACTOR) , geomFactory(geom.getFactory()) {}; /** - * Computes the offset curve of a geometry at a given distance, - * and for a specified quadrant segments, join style and mitre limit. + * 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 geom a geometry - * @param dist the offset distance (positive = left, negative = right) - * @param quadSegs the quadrant segments (-1 for default) - * @param joinStyle the join style (-1 for default) - * @param mitreLimit the mitre limit (-1 for default) - * @return the offset curve + * @param pIsJoined true if joined mode should be used. */ + void setJoined(bool pIsJoined); + static std::unique_ptr getCurve( const Geometry& geom, - double dist, int quadSegs, BufferParameters::JoinStyle joinStyle, double mitreLimit); + double dist, + int quadSegs, + BufferParameters::JoinStyle joinStyle, + double mitreLimit); - static std::unique_ptr getCurve(const Geometry& geom, double dist); + static std::unique_ptr 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 getCurveJoined( + const Geometry& geom, double dist); + + /** + * Gets the computed offset curve lines. + * + * @return the offset curve geometry + */ std::unique_ptr getCurve(); /** - * Gets the raw offset line. - * The quadrant segments and join style and mitre limit to be set + * 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. - * The raw offset line is matched to the buffer ring (which is clean) - * to extract the offset curve. * - * @param geom the linestring to offset - * @param dist the offset distance + * @param line the line to offset + * @param distance the offset distance (positive for left, negative for right) * @param bufParams the buffer parameters to use - * @param lineList the vector to populate with the return value + * @return the raw offset curve points */ - static void rawOffset(const LineString& geom, double dist, BufferParameters& bufParams, std::vector& lineList); - static void rawOffset(const LineString& geom, double dist, std::vector& lineList); + static std::unique_ptr 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 rawOffset( + const LineString& line, + double distance); +}; } // namespace geos::operation::buffer } // namespace geos::operation } // namespace geos -#ifdef _MSC_VER -#pragma warning(pop) -#endif diff --git a/include/geos/operation/buffer/OffsetCurveBuilder.h b/include/geos/operation/buffer/OffsetCurveBuilder.h index 8411c67926..f73672b1cc 100644 --- a/include/geos/operation/buffer/OffsetCurveBuilder.h +++ b/include/geos/operation/buffer/OffsetCurveBuilder.h @@ -40,6 +40,9 @@ class PrecisionModel; } } +using geos::geom::CoordinateSequence; +using geos::geom::PrecisionModel; + namespace geos { namespace operation { // geos.operation namespace buffer { // geos.operation.buffer @@ -55,8 +58,12 @@ namespace buffer { // geos.operation.buffer * it may contain self-intersections (and usually will). * The final buffer polygon is computed by forming a topological graph * of all the noded raw curves and tracing outside contours. - * The points in the raw curve are rounded to a given geom::PrecisionModel. + * The points in the raw curve are rounded to a given PrecisionModel. * + * Note: this may not produce correct results if the input + * contains repeated or invalid points. + * Repeated points should be removed before calling. + * See removeRepeatedAndInvalidPoints. */ class GEOS_DLL OffsetCurveBuilder { public: @@ -68,13 +75,13 @@ class GEOS_DLL OffsetCurveBuilder { * kept alive for the whole lifetime of * the buffer builder. */ - OffsetCurveBuilder(const geom::PrecisionModel* newPrecisionModel, - const BufferParameters& nBufParams) - : - distance(0.0), - precisionModel(newPrecisionModel), - bufParams(nBufParams) - {} + OffsetCurveBuilder( + const PrecisionModel* newPrecisionModel, + const BufferParameters& nBufParams) + : distance(0.0) + , precisionModel(newPrecisionModel) + , bufParams(nBufParams) + {} /** \brief * Gets the buffer parameters being used to generate the curve. @@ -91,10 +98,9 @@ class GEOS_DLL OffsetCurveBuilder { * Tests whether the offset curve for line or point geometries * at the given offset distance is empty (does not exist). * This is the case if: - *
    - *
  • the distance is zero, - *
  • the distance is negative, except for the case of singled-sided buffers - *
+ * + * * the distance is zero, + * * the distance is negative, except for the case of singled-sided buffers * * @param distance the offset curve distance * @return true if the offset curve is empty @@ -113,9 +119,23 @@ class GEOS_DLL OffsetCurveBuilder { * CoordinateSequences will be pushed_back. * Caller is responsible to delete these new elements. */ - void getLineCurve(const geom::CoordinateSequence* inputPts, - double distance, - std::vector& lineList); + void getLineCurve(const CoordinateSequence* inputPts, + double distance, + std::vector& lineList); + + /** + * This method handles single points as well as LineStrings. + * LineStrings are assumed not to be closed (the function will not + * fail for closed lines, but will generate superfluous line caps). + * + * @param inputPts the vertices of the line to offset + * @param pDistance the offset distance + * + * @return a Coordinate array representing the curve + * or null if the curve is empty + */ + std::unique_ptr getLineCurve( + const CoordinateSequence* inputPts, double pDistance); /** \brief * This method handles single points as well as lines. @@ -135,8 +155,8 @@ class GEOS_DLL OffsetCurveBuilder { * * @note This is a GEOS extension. */ - void getSingleSidedLineCurve(const geom::CoordinateSequence* inputPts, - double distance, std::vector& lineList, + void getSingleSidedLineCurve(const CoordinateSequence* inputPts, + double distance, std::vector& lineList, bool leftSide, bool rightSide) ; /** \brief @@ -149,19 +169,38 @@ class GEOS_DLL OffsetCurveBuilder { * @param lineList the std::vector to which CoordinateSequences will * be pushed_back */ - void getRingCurve(const geom::CoordinateSequence* inputPts, int side, + void getRingCurve(const CoordinateSequence* inputPts, int side, double distance, - std::vector& lineList); + std::vector& lineList); - void getOffsetCurve(const geom::CoordinateSequence* inputPts, + /** + * This method handles the degenerate cases of single points and lines, + * as well as valid rings. + * + * @param inputPts the coordinates of the ring (must not contain repeated points) + * @param side side the side Position of the ring on which to construct the buffer line + * @param pDistance the positive distance at which to create the offset + * @return a Coordinate array representing the curve, + * or null if the curve is empty + */ + std::unique_ptr getRingCurve( + const CoordinateSequence* inputPts, + int side, double pDistance); + + void getOffsetCurve(const CoordinateSequence* inputPts, double p_distance, - std::vector& lineList); + std::vector& lineList); + + std::unique_ptr getOffsetCurve( + const CoordinateSequence* inputPts, + double pDistance); + private: double distance; - const geom::PrecisionModel* precisionModel; + const PrecisionModel* precisionModel; const BufferParameters& bufParams; @@ -183,21 +222,24 @@ class GEOS_DLL OffsetCurveBuilder { */ double simplifyTolerance(double bufDistance); - void computeLineBufferCurve(const geom::CoordinateSequence& inputPts, + void computeLineBufferCurve(const CoordinateSequence& inputPts, OffsetSegmentGenerator& segGen); - void computeSingleSidedBufferCurve(const geom::CoordinateSequence& inputPts, + void computeSingleSidedBufferCurve(const CoordinateSequence& inputPts, bool isRightSide, OffsetSegmentGenerator& segGen); - void computeRingBufferCurve(const geom::CoordinateSequence& inputPts, + void computeRingBufferCurve(const CoordinateSequence& inputPts, int side, OffsetSegmentGenerator& segGen); - std::unique_ptr getSegGen(double dist); - void computePointCurve(const geom::Coordinate& pt, OffsetSegmentGenerator& segGen); + void computeOffsetCurve( + const CoordinateSequence* inputPts, + bool isRightSide, + OffsetSegmentGenerator& segGen); + // Declare type as noncopyable diff --git a/include/geos/operation/buffer/OffsetCurveSection.h b/include/geos/operation/buffer/OffsetCurveSection.h new file mode 100644 index 0000000000..fe8d9035ee --- /dev/null +++ b/include/geos/operation/buffer/OffsetCurveSection.h @@ -0,0 +1,110 @@ +/********************************************************************** + * + * GEOS - Geometry Engine Open Source + * http://geos.osgeo.org + * + * Copyright (c) 2021 Martin Davis + * Copyright (C) 2021 Paul Ramsey + * + * 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 + +// Forward declarations +namespace geos { +namespace geom { +class Coordinate; +class CoordinateSequence; +class Geometry; +class GeometryFactory; +class LineString; +} +} + +using geos::geom::Coordinate; +using geos::geom::CoordinateSequence; +using geos::geom::Geometry; +using geos::geom::GeometryFactory; +using geos::geom::LineString; + +namespace geos { // geos. +namespace operation { // geos.operation +namespace buffer { // geos.operation.buffer + +/** + * Models a section of a raw offset curve, + * starting at a given location along the raw curve. + * The location is a decimal number, with the integer part + * containing the segment index and the fractional part + * giving the fractional distance along the segment. + * The location of the last section segment + * is also kept, to allow optimizing joining sections together. + * + * @author mdavis + */ +class GEOS_DLL OffsetCurveSection { + +private: + + std::unique_ptr sectionPts; + double location; + double locLast; + + bool isEndInSameSegment(double nextLoc) const; + + +public: + + OffsetCurveSection(std::unique_ptr && secPts, double pLoc, double pLocLast) + : sectionPts(std::move(secPts)) + , location(pLoc) + , locLast(pLocLast) + {}; + + const CoordinateSequence* getCoordinates() const; + std::unique_ptr releaseCoordinates(); + + double getLocation() const { return location; }; + + /** + * Joins section coordinates into a LineString. + * Join vertices which lie in the same raw curve segment + * are removed, to simplify the result linework. + * + * @param sections the sections to join + * @param geomFactory the geometry factory to use + * @return the simplified linestring for the joined sections + */ + static std::unique_ptr toLine( + std::vector>& sections, + const GeometryFactory* geomFactory); + + static std::unique_ptr toGeometry( + std::vector>& sections, + const GeometryFactory* geomFactory); + + static std::unique_ptr create( + const CoordinateSequence* srcPts, + std::size_t start, std::size_t end, + double loc, double locLast); + + static bool OffsetCurveSectionComparator( + const std::unique_ptr& a, + const std::unique_ptr& b); + +}; + + +} // namespace geos.operation.buffer +} // namespace geos.operation +} // namespace geos + diff --git a/include/geos/operation/buffer/OffsetSegmentGenerator.h b/include/geos/operation/buffer/OffsetSegmentGenerator.h index 0ae9258204..2f3a369c69 100644 --- a/include/geos/operation/buffer/OffsetSegmentGenerator.h +++ b/include/geos/operation/buffer/OffsetSegmentGenerator.h @@ -106,6 +106,12 @@ class GEOS_DLL OffsetSegmentGenerator { to.push_back(segList.getCoordinates()); } + std::unique_ptr + getCoordinates() + { + return std::unique_ptr(segList.getCoordinates()); + } + void closeRing() { diff --git a/src/operation/buffer/OffsetCurve.cpp b/src/operation/buffer/OffsetCurve.cpp index c49d654244..9fc3c616ff 100644 --- a/src/operation/buffer/OffsetCurve.cpp +++ b/src/operation/buffer/OffsetCurve.cpp @@ -12,59 +12,49 @@ * **********************************************************************/ -#include -#include -#include -#include -#include #include #include -#include -#include #include +#include +#include #include #include #include -#include +#include #include +#include #include #include #include +#include +#include + +#include +#include +#include +#include +#include -#include +using geos::algorithm::Distance; +using geos::geom::util::GeometryMapper; +using geos::index::chain::MonotoneChain; +using geos::index::chain::MonotoneChainSelectAction; +using geos::operation::valid::RepeatedPointRemover; -using namespace geos::index::chain; using namespace geos::geom; -using geos::geom::util::GeometryMapper; namespace geos { namespace operation { namespace buffer { -/* public static */ -std::unique_ptr -OffsetCurve::getCurve(const Geometry& geom, double distance) -{ - OffsetCurve oc(geom, distance); - return oc.getCurve(); -} - +static constexpr double NOT_IN_CURVE = -1.0; -/* public static */ -std::unique_ptr -OffsetCurve::getCurve(const Geometry& geom, - double dist, - int quadSegs, - BufferParameters::JoinStyle joinStyle, - double mitreLimit) +/* public */ +void +OffsetCurve::setJoined(bool pIsJoined) { - BufferParameters bufParms; - if (quadSegs >= 0) bufParms.setQuadrantSegments(quadSegs); - if (joinStyle >= 0) bufParms.setJoinStyle(joinStyle); - if (mitreLimit >= 0) bufParms.setMitreLimit(mitreLimit); - OffsetCurve oc(geom, dist, bufParms); - return oc.getCurve(); + isJoined = pIsJoined; } @@ -92,48 +82,99 @@ OffsetCurve::getCurve() return GeometryMapper::flatMap(inputGeom, 1, GetCurveMapOp); } +/* public static */ +std::unique_ptr +OffsetCurve::getCurve(const Geometry& geom, double distance) +{ + OffsetCurve oc(geom, distance); + return oc.getCurve(); +} + /* public static */ -void -OffsetCurve::rawOffset(const LineString& geom, double dist, - std::vector& lineList) +std::unique_ptr +OffsetCurve::getCurve(const Geometry& geom, + double dist, + int quadSegs, + BufferParameters::JoinStyle joinStyle, + double mitreLimit) +{ + BufferParameters bufParms; + if (quadSegs >= 0) bufParms.setQuadrantSegments(quadSegs); + if (joinStyle >= 0) bufParms.setJoinStyle(joinStyle); + if (mitreLimit >= 0) bufParms.setMitreLimit(mitreLimit); + OffsetCurve oc(geom, dist, bufParms); + return oc.getCurve(); +} + + +/* public static */ +std::unique_ptr +OffsetCurve::getCurveJoined(const Geometry& geom, double dist) { - BufferParameters bp; - rawOffset(geom, dist, bp, lineList); - return; + OffsetCurve oc(geom, dist); + oc.setJoined(true); + return oc.getCurve(); } /* public static */ -void -OffsetCurve::rawOffset(const LineString& geom, double distance, BufferParameters& bufParams, - std::vector& lineList) +std::unique_ptr +OffsetCurve::rawOffsetCurve( + const LineString& line, + double dist, + BufferParameters& bufParams) { - OffsetCurveBuilder ocb(geom.getFactory()->getPrecisionModel(), bufParams); - ocb.getOffsetCurve(geom.getCoordinatesRO(), distance, lineList); - return; + const CoordinateSequence* pts = line.getCoordinatesRO(); + std::unique_ptr cleanPts = RepeatedPointRemover::removeRepeatedAndInvalidPoints(pts); + + OffsetCurveBuilder ocb(line.getFactory()->getPrecisionModel(), bufParams); + return ocb.getOffsetCurve(cleanPts.get(), dist); } +/* public static */ +std::unique_ptr +OffsetCurve::rawOffset(const LineString& line, double dist) +{ + BufferParameters bufParams; + return rawOffsetCurve(line, dist, bufParams); +} + /* private */ -std::unique_ptr -OffsetCurve::computeCurve(const LineString& lineGeom, double p_distance) +std::unique_ptr +OffsetCurve::computeCurve(const LineString& lineGeom, double dist) { - //-- first handle special/simple cases + //-- first handle simple cases + //-- empty or single-point line if (lineGeom.getNumPoints() < 2 || lineGeom.getLength() == 0.0) { return geomFactory->createLineString(); } + //-- two-point line if (lineGeom.getNumPoints() == 2) { - return offsetSegment(lineGeom.getCoordinatesRO(), p_distance); + return offsetSegment(lineGeom.getCoordinatesRO(), dist); } - std::vector rawOffsetLines; - rawOffset(lineGeom, p_distance, bufferParams, rawOffsetLines); - if (rawOffsetLines.empty() || rawOffsetLines[0]->size() == 0) { - for (auto* cs: rawOffsetLines) - delete cs; - return geomFactory->createLineString(); + auto sections = computeSections(lineGeom, dist); + + if (isJoined) { + return OffsetCurveSection::toLine(sections, geomFactory); + } + else { + return OffsetCurveSection::toGeometry(sections, geomFactory); + } +} + +/* private */ +std::vector> +OffsetCurve::computeSections(const LineString& lineGeom, double dist) +{ + std::unique_ptr rawCurve = rawOffsetCurve(lineGeom, dist, bufferParams); + std::vector> sections; + if (rawCurve->size() == 0) { + return sections; } + /** * Note: If the raw offset curve has no * narrow concave angles or self-intersections it could be returned as is. @@ -141,213 +182,316 @@ OffsetCurve::computeCurve(const LineString& lineGeom, double p_distance) * and testing indicates little performance advantage, * so not doing this. */ + std::unique_ptr bufferPoly = getBufferOriented(lineGeom, dist, bufferParams); - std::unique_ptr bufferPoly = getBufferOriented(lineGeom, p_distance, bufferParams); + //-- first extract offset curve sections from shell + auto shell = bufferPoly->getExteriorRing()->getCoordinatesRO(); + computeCurveSections(shell, *rawCurve, sections); - //-- first try matching shell to raw curve - const CoordinateSequence* shell = bufferPoly->getExteriorRing()->getCoordinatesRO(); - std::unique_ptr offsetCurve = computeCurve(shell, rawOffsetLines); - if (! offsetCurve->isEmpty() || bufferPoly->getNumInteriorRing() == 0) { - for (auto* cs: rawOffsetLines) - delete cs; - return offsetCurve; + //-- extract offset curve sections from holes + for (std::size_t i = 0; i < bufferPoly->getNumInteriorRing(); i++) { + auto hole = bufferPoly->getInteriorRingN(i)->getCoordinatesRO(); + computeCurveSections(hole, *rawCurve, sections); } - - //-- if shell didn't work, try matching to largest hole - auto longestHole = extractLongestHole(*bufferPoly); - const CoordinateSequence* holePts = longestHole ? longestHole->getCoordinatesRO() : nullptr; - offsetCurve = computeCurve(holePts, rawOffsetLines); - for (auto* cs: rawOffsetLines) - delete cs; - return offsetCurve; + return sections; } - /* private */ +/* private */ std::unique_ptr -OffsetCurve::offsetSegment(const CoordinateSequence* pts, double p_distance) +OffsetCurve::offsetSegment(const CoordinateSequence* pts, double dist) { - LineSegment ls(pts->getAt(0), pts->getAt(1)); - LineSegment offsetSeg = ls.offset(p_distance); - auto coords = detail::make_unique(2u); - coords->setAt(offsetSeg.p0, 0); - coords->setAt(offsetSeg.p1, 1); - return geomFactory->createLineString(std::move(coords)); + LineSegment offsetSeg(pts->getAt(0), pts->getAt(1)); + offsetSeg = offsetSeg.offset(dist); + CoordinateSequence cs; + cs.add(offsetSeg.p0); + cs.add(offsetSeg.p1); + return geomFactory->createLineString(std::move(cs)); } /* private static */ std::unique_ptr -OffsetCurve::getBufferOriented(const LineString& geom, double p_distance, BufferParameters& bufParms) +OffsetCurve::getBufferOriented(const LineString& geom, double dist, BufferParameters& bufParams) { - std::unique_ptr buffer = BufferOp::bufferOp(&geom, std::abs(p_distance), bufParms); - std::unique_ptr bufferPoly = extractMaxAreaPolygon(*buffer); + std::unique_ptr buffer = BufferOp::bufferOp(&geom, std::abs(dist), bufParams); + const Polygon* bufferPoly = extractMaxAreaPolygon(buffer.get()); //-- for negative distances (Right of input) reverse buffer direction to match offset curve - if (p_distance < 0) { - bufferPoly = bufferPoly->reverse(); - } - return bufferPoly; + return dist < 0 + ? bufferPoly->reverse() + : bufferPoly->clone(); } /* private static */ -std::unique_ptr -OffsetCurve::extractMaxAreaPolygon(const Geometry& geom) +const Polygon* +OffsetCurve::extractMaxAreaPolygon(const Geometry* geom) { - const std::size_t numGeometries = geom.getNumGeometries(); - if (numGeometries == 1) { - const Polygon& poly = static_cast(geom); - return poly.clone(); - } - - assert(numGeometries > 1); - const Polygon* maxPoly = static_cast(geom.getGeometryN(0)); - double maxArea = maxPoly->getArea(); - for (std::size_t i = 1; i < numGeometries; i++) { - const Polygon* poly = static_cast(geom.getGeometryN(i)); + if (geom->getGeometryTypeId() == GEOS_POLYGON) + return static_cast(geom); + + double maxArea = 0.0; + const Polygon* maxPoly = nullptr; + for (std::size_t i = 0; i < geom->getNumGeometries(); i++) { + const Geometry* subgeom = geom->getGeometryN(i); + if (subgeom->getGeometryTypeId() != GEOS_POLYGON) continue; + const Polygon* poly = static_cast(subgeom); double area = poly->getArea(); - if (area > maxArea) { + if (maxPoly == nullptr || area > maxArea) { maxPoly = poly; maxArea = area; } } - return maxPoly->clone(); + return maxPoly; } -/* private static */ -std::unique_ptr -OffsetCurve::extractLongestHole(const Polygon& poly) -{ - const LinearRing* largestHole = nullptr; - double maxLen = -1; - for (std::size_t i = 0; i < poly.getNumInteriorRing(); i++) { - const LinearRing* hole = poly.getInteriorRingN(i); - double len = hole->getLength(); - if (len > maxLen) { - largestHole = hole; - maxLen = len; - } - } - return largestHole ? largestHole->clone() : nullptr; -} /* private */ -std::unique_ptr -OffsetCurve::computeCurve(const CoordinateSequence* bufferPts, std::vector& rawOffsetList) +void +OffsetCurve::computeCurveSections( + const CoordinateSequence* bufferRingPts, + const CoordinateSequence& rawCurve, + std::vector>& sections) { - std::vector isInCurve; - isInCurve.resize(bufferPts->size() - 1, false); - - SegmentMCIndex segIndex(bufferPts); - - int curveStart = -1; - CoordinateSequence* cs = rawOffsetList[0]; - for (std::size_t i = 0; i < cs->size() - 1; i++) { - int index = markMatchingSegments( - cs->getAt(i), cs->getAt(i+1), - segIndex, bufferPts, isInCurve); - if (curveStart < 0) { - curveStart = index; + std::vector rawPosition(bufferRingPts->size()-1, NOT_IN_CURVE); + + SegmentMCIndex bufferSegIndex(bufferRingPts); + std::size_t bufferFirstIndex = UNKNOWN_INDEX; + double minRawPosition = -1; + for (std::size_t i = 0; i < rawCurve.size() - 1; i++) { + std::size_t minBufferIndexForSeg = matchSegments(rawCurve[i], rawCurve[i+1], i, bufferSegIndex, bufferRingPts, rawPosition); + if (minBufferIndexForSeg != UNKNOWN_INDEX) { + double pos = rawPosition[minBufferIndexForSeg]; + if (bufferFirstIndex == UNKNOWN_INDEX || pos < minRawPosition) { + minRawPosition = pos; + bufferFirstIndex = minBufferIndexForSeg; + } } } - auto curvePts = detail::make_unique(); - extractSection(bufferPts, curveStart, isInCurve, *curvePts); - return geomFactory->createLineString(std::move(curvePts)); + //-- no matching sections found in this buffer ring + if (bufferFirstIndex == UNKNOWN_INDEX) + return; + + extractSections(bufferRingPts, rawPosition, bufferFirstIndex, sections); } -void -OffsetCurve::MatchCurveSegmentAction::select(const MonotoneChain& mc, std::size_t segIndex) + +/* private */ +std::size_t +OffsetCurve::matchSegments( + const Coordinate& raw0, const Coordinate& raw1, + std::size_t rawCurveIndex, + SegmentMCIndex& bufferSegIndex, + const CoordinateSequence* bufferPts, + std::vector& rawCurvePos) { - (void)mc; // Quiet unused variable warning /** - * A curveRingPt segment may match all or only a portion of a single raw segment. - * There may be multiple curve ring segs that match along the raw segment. - * The one closest to the segment start is recorded as the offset curve start. + * An action to match a raw offset curve segment + * to segments in a buffer ring + * and record the matched segment locations(s) along the raw curve. + * + * @author Martin Davis */ - double frac = subsegmentMatchFrac(bufferPts->getAt(segIndex), bufferPts->getAt(segIndex+1), p0, p1, matchDistance); - //-- no match - if (frac < 0) return; + /* private static */ + class MatchCurveSegmentAction : public MonotoneChainSelectAction + { + + public: + + const Coordinate& p0; + const Coordinate& p1; + std::size_t rawCurveIndex; + double matchDistance; + const CoordinateSequence* bufferRingPts; + std::vector& rawCurveLoc; + double minRawLocation; + std::size_t bufferRingMinIndex; + + MatchCurveSegmentAction( + const Coordinate& p_p0, + const Coordinate& p_p1, + std::size_t p_rawCurveIndex, + double p_matchDistance, + const CoordinateSequence* p_bufferRingPts, + std::vector& p_rawCurveLoc) + : p0(p_p0) + , p1(p_p1) + , rawCurveIndex(p_rawCurveIndex) + , matchDistance(p_matchDistance) + , bufferRingPts(p_bufferRingPts) + , rawCurveLoc(p_rawCurveLoc) + , minRawLocation(-1.0) + , bufferRingMinIndex(UNKNOWN_INDEX) + {}; + + std::size_t getBufferMinIndex() { + return bufferRingMinIndex; + } - isInCurve[segIndex] = true; + void select(const geom::LineSegment& seg) override { + (void)seg; // quiet ununsed variable warning + return; + } - //-- record lowest index - if (minFrac < 0 || frac < minFrac) { - minFrac = frac; - minCurveIndex = static_cast(segIndex); - } -} + void select(const MonotoneChain& mc, std::size_t segIndex) override + { + (void)mc; // quiet ununsed variable warning + /** + * A curveRingPt segment may match all or only a portion of a single raw segment. + * There may be multiple curve ring segs that match along the raw segment. + */ + double frac = segmentMatchFrac( + bufferRingPts->getAt(segIndex), + bufferRingPts->getAt(segIndex+1), + p0, p1, matchDistance); + + //-- no match + if (frac < 0) return; + + //-- location is used to sort segments along raw curve + double location = static_cast(rawCurveIndex) + frac; + rawCurveLoc[segIndex] = location; + //-- record lowest index + if (minRawLocation < 0 || location < minRawLocation) { + minRawLocation = location; + bufferRingMinIndex = segIndex; + } + } + }; -/* private */ -int -OffsetCurve::markMatchingSegments( - const Coordinate& p0, const Coordinate& p1, - SegmentMCIndex& segIndex, const CoordinateSequence* bufferPts, - std::vector& isInCurve) -{ - Envelope matchEnv(p0, p1); + Envelope matchEnv(raw0, raw1); matchEnv.expandBy(matchDistance); - MatchCurveSegmentAction action(p0, p1, bufferPts, matchDistance, isInCurve); - segIndex.query(&matchEnv, action); - return action.getMinCurveIndex(); + MatchCurveSegmentAction matchAction(raw0, raw1, rawCurveIndex, matchDistance, bufferPts, rawCurvePos); + bufferSegIndex.query(&matchEnv, matchAction); + return matchAction.getBufferMinIndex(); } - /* private static */ double -OffsetCurve::subsegmentMatchFrac(const Coordinate& p0, const Coordinate& p1, - const Coordinate& seg0, const Coordinate& seg1, double matchDistance) +OffsetCurve::segmentMatchFrac( + const Coordinate& p0, const Coordinate& p1, + const Coordinate& seg0, const Coordinate& seg1, + double matchDistance) { - if (matchDistance < algorithm::Distance::pointToSegment(p0, seg0, seg1)) - return -1; - if (matchDistance < algorithm::Distance::pointToSegment(p1, seg0, seg1)) - return -1; - //-- matched - determine position as fraction + if (matchDistance < Distance::pointToSegment(p0, seg0, seg1)) + return -1.0; + if (matchDistance < Distance::pointToSegment(p1, seg0, seg1)) + return -1.0; + //-- matched - determine position as fraction along segment LineSegment seg(seg0, seg1); return seg.segmentFraction(p0); } -/* private static */ +/* private */ void -OffsetCurve::extractSection(const CoordinateSequence* ring, int iStartIndex, - std::vector& isExtracted, CoordinateSequence& extractedPoints) +OffsetCurve::extractSections( + const CoordinateSequence* ringPts, + std::vector& rawCurveLoc, + std::size_t startIndex, + std::vector>& sections) { - if (iStartIndex < 0) - return; + std::size_t sectionStart = startIndex; + std::size_t sectionCount = 0; + std::size_t sectionEnd; + do { + sectionEnd = findSectionEnd(rawCurveLoc, sectionStart, startIndex); + double location = rawCurveLoc[sectionStart]; + std::size_t lastIndex = prevIndex(sectionEnd, rawCurveLoc.size()); + double lastLoc = rawCurveLoc[lastIndex]; + std::unique_ptr section = OffsetCurveSection::create(ringPts, sectionStart, sectionEnd, location, lastLoc); + sections.emplace_back(section.release()); + sectionStart = findSectionStart(rawCurveLoc, sectionEnd); + + //-- check for an abnormal state + if (sectionCount++ > ringPts->size()) { + util::Assert::shouldNeverReachHere("Too many sections for ring - probable bug"); + } + } while (sectionStart != startIndex && sectionEnd != startIndex); +} + - CoordinateList coordList; - std::size_t startIndex = static_cast(iStartIndex); - std::size_t i = startIndex; +/* private */ +std::size_t +OffsetCurve::findSectionStart( + const std::vector& loc, + std::size_t end) +{ + std::size_t start = end; do { - coordList.insert(coordList.end(), ring->getAt(i), false); - if (! isExtracted[i]) { - break; + std::size_t next = nextIndex(start, loc.size()); + //-- skip ahead if segment is not in raw curve + if (loc[start] == NOT_IN_CURVE) { + start = next; + continue; } - i = next(i, ring->size() - 1); - } while (i != startIndex); - //-- handle case where every segment is extracted - if (isExtracted[i]) { - coordList.insert(coordList.end(), ring->getAt(i), false); - } + std::size_t prev = prevIndex(start, loc.size()); + //-- if prev segment is not in raw curve then have found a start + if (loc[prev] == NOT_IN_CURVE) { + return start; + } + if (isJoined) { + /** + * Start section at next gap in raw curve. + * Only needed for joined curve, since otherwise + * contiguous buffer segments can be in same curve section. + */ + double locDelta = std::abs(loc[start] - loc[prev]); + if (locDelta > 1.0) + return start; + } + start = next; + } while (start != end); + return start; +} - //-- if only one point found return empty LineString - if (coordList.size() == 1) - return; - extractedPoints.add(coordList.begin(), coordList.end()); +/* private */ +std::size_t +OffsetCurve::findSectionEnd( + const std::vector& loc, + std::size_t start, + std::size_t firstStartIndex) +{ + // assert: pos[start] is IN CURVE + std::size_t end = start; + std::size_t next; + do { + next = nextIndex(end, loc.size()); + if (loc[next] == NOT_IN_CURVE) + return next; + if (isJoined) { + /** + * End section at gap in raw curve. + * Only needed for joined curve, since otherwise + * contigous buffer segments can be in same section + */ + double locDelta = std::abs(loc[next] - loc[end]); + if (locDelta > 1) + return next; + } + end = next; + } while (end != start && end != firstStartIndex); + return end; +} - return; +/* private static */ +std::size_t +OffsetCurve::nextIndex(std::size_t i, std::size_t size) +{ + return i >= size - 1 ? 0 : i + 1; } /* private static */ std::size_t -OffsetCurve::next(std::size_t i, std::size_t size) { - i += 1; - return (i < size) ? i : 0; +OffsetCurve::prevIndex(std::size_t i, std::size_t size) +{ + return i == 0 ? size - 1 : i - 1; } + } // namespace geos.operation.buffer } // namespace geos.operation } // namespace geos diff --git a/src/operation/buffer/OffsetCurveBuilder.cpp b/src/operation/buffer/OffsetCurveBuilder.cpp index 227a646967..1628ef9bb2 100644 --- a/src/operation/buffer/OffsetCurveBuilder.cpp +++ b/src/operation/buffer/OffsetCurveBuilder.cpp @@ -63,23 +63,50 @@ OffsetCurveBuilder::getLineCurve(const CoordinateSequence* inputPts, double posDistance = std::abs(distance); - std::unique_ptr segGen = getSegGen(posDistance); + OffsetSegmentGenerator segGen(precisionModel, bufParams, posDistance); if(inputPts->getSize() <= 1) { - computePointCurve(inputPts->getAt(0), *segGen); + computePointCurve(inputPts->getAt(0), segGen); } else { if(bufParams.isSingleSided()) { bool isRightSide = distance < 0.0; - computeSingleSidedBufferCurve(*inputPts, isRightSide, *segGen); + computeSingleSidedBufferCurve(*inputPts, isRightSide, segGen); } else { - computeLineBufferCurve(*inputPts, *segGen); + computeLineBufferCurve(*inputPts, segGen); } } - segGen->getCoordinates(lineList); + segGen.getCoordinates(lineList); } + +/* public */ +std::unique_ptr +OffsetCurveBuilder::getLineCurve(const CoordinateSequence* inputPts, double pDistance) +{ + distance = pDistance; + + if (isLineOffsetEmpty(distance)) return nullptr; + + double posDistance = std::abs(distance); + OffsetSegmentGenerator segGen(precisionModel, bufParams, posDistance); + if (inputPts->size() <= 1) { + computePointCurve(inputPts->getAt(0), segGen); + } + else { + if (bufParams.isSingleSided()) { + bool isRightSide = distance < 0.0; + computeSingleSidedBufferCurve(*inputPts, isRightSide, segGen); + } + else + computeLineBufferCurve(*inputPts, segGen); + } + + return segGen.getCoordinates(); +} + + /* public */ void OffsetCurveBuilder::getOffsetCurve( @@ -94,15 +121,15 @@ OffsetCurveBuilder::getOffsetCurve( bool isRightSide = p_distance < 0.0; double posDistance = std::abs(p_distance); - std::unique_ptr segGen = getSegGen(posDistance); + OffsetSegmentGenerator segGen(precisionModel, bufParams, posDistance); if (inputPts->size() <= 1) { - computePointCurve(inputPts->getAt(0), *segGen); + computePointCurve(inputPts->getAt(0), segGen); } else { - computeSingleSidedBufferCurve(*inputPts, isRightSide, *segGen); + computeSingleSidedBufferCurve(*inputPts, isRightSide, segGen); } - segGen->getCoordinates(lineList); + segGen.getCoordinates(lineList); // for right side line is traversed in reverse direction, so have to reverse generated line if (isRightSide) { @@ -114,6 +141,78 @@ OffsetCurveBuilder::getOffsetCurve( } +/* public */ +std::unique_ptr +OffsetCurveBuilder::getOffsetCurve( + const CoordinateSequence* inputPts, + double pDistance) +{ + distance = pDistance; + + // a zero width offset curve is empty + if (distance == 0.0) return nullptr; + + bool isRightSide = distance < 0.0; + double posDistance = std::abs(distance); + OffsetSegmentGenerator segGen(precisionModel, bufParams, posDistance); + if (inputPts->size() <= 1) { + computePointCurve(inputPts->getAt(0), segGen); + } + else { + computeOffsetCurve(inputPts, isRightSide, segGen); + } + std::unique_ptr curvePts = segGen.getCoordinates(); + // for right side line is traversed in reverse direction, so have to reverse generated line + if (isRightSide) + curvePts->reverse(); + + return curvePts; +} + + +/* private */ +void +OffsetCurveBuilder::computeOffsetCurve( + const CoordinateSequence* inputPts, + bool isRightSide, + OffsetSegmentGenerator& segGen) +{ + double distTol = simplifyTolerance(std::abs(distance)); + + if (isRightSide) { + //---------- compute points for right side of line + // Simplify the appropriate side of the line before generating + auto simp2 = BufferInputLineSimplifier::simplify(*inputPts, -distTol); + std::size_t n2 = simp2->size() - 1; + if (!n2) + throw util::IllegalArgumentException("Cannot get offset of single-vertex line"); + + // since we are traversing line in opposite order, offset position is still LEFT + segGen.initSideSegments(simp2->getAt(n2), simp2->getAt(n2-1), Position::LEFT); + segGen.addFirstSegment(); + for (std::size_t i = n2 - 1; i > 0; --i) { + segGen.addNextSegment(simp2->getAt(i - 1), true); + } + } + else { + //--------- compute points for left side of line + // Simplify the appropriate side of the line before generating + auto simp1 = BufferInputLineSimplifier::simplify(*inputPts, distTol); + std::size_t n1 = simp1->size() - 1; + if (!n1) + throw util::IllegalArgumentException("Cannot get offset of single-vertex line"); + + segGen.initSideSegments(simp1->getAt(0), simp1->getAt(1), Position::LEFT); + segGen.addFirstSegment(); + for (std::size_t i = 2; i <= n1; i++) { + segGen.addNextSegment(simp1->getAt(i), true); + } + } + segGen.addLastSegment(); +} + + + /* private */ void OffsetCurveBuilder::computePointCurve(const Coordinate& pt, @@ -150,7 +249,7 @@ OffsetCurveBuilder::getSingleSidedLineCurve(const CoordinateSequence* inputPts, double distTol = simplifyTolerance(p_distance); - std::unique_ptr segGen = getSegGen(p_distance); + OffsetSegmentGenerator segGen(precisionModel, bufParams, p_distance); if(leftSide) { //--------- compute points for left side of line @@ -164,12 +263,12 @@ OffsetCurveBuilder::getSingleSidedLineCurve(const CoordinateSequence* inputPts, if(! n1) { throw util::IllegalArgumentException("Cannot get offset of single-vertex line"); } - segGen->initSideSegments(simp1[0], simp1[1], Position::LEFT); - segGen->addFirstSegment(); + segGen.initSideSegments(simp1[0], simp1[1], Position::LEFT); + segGen.addFirstSegment(); for(std::size_t i = 2; i <= n1; ++i) { - segGen->addNextSegment(simp1[i], true); + segGen.addNextSegment(simp1[i], true); } - segGen->addLastSegment(); + segGen.addLastSegment(); } if(rightSide) { @@ -184,15 +283,15 @@ OffsetCurveBuilder::getSingleSidedLineCurve(const CoordinateSequence* inputPts, if(! n2) { throw util::IllegalArgumentException("Cannot get offset of single-vertex line"); } - segGen->initSideSegments(simp2[n2], simp2[n2 - 1], Position::LEFT); - segGen->addFirstSegment(); + segGen.initSideSegments(simp2[n2], simp2[n2 - 1], Position::LEFT); + segGen.addFirstSegment(); for(std::size_t i = n2 - 1; i > 0; --i) { - segGen->addNextSegment(simp2[i - 1], true); + segGen.addNextSegment(simp2[i - 1], true); } - segGen->addLastSegment(); + segGen.addLastSegment(); } - segGen->getCoordinates(lineList); + segGen.getCoordinates(lineList); } /*public*/ @@ -227,11 +326,30 @@ OffsetCurveBuilder::getRingCurve(const CoordinateSequence* inputPts, return; } - std::unique_ptr segGen = getSegGen(std::abs(distance)); - computeRingBufferCurve(*inputPts, side, *segGen); - segGen->getCoordinates(lineList); + OffsetSegmentGenerator segGen(precisionModel, bufParams, std::abs(distance)); + computeRingBufferCurve(*inputPts, side, segGen); + segGen.getCoordinates(lineList); } + +/* public */ +std::unique_ptr +OffsetCurveBuilder::getRingCurve(const CoordinateSequence* inputPts, int side, double pDistance) +{ + distance = pDistance; + if (inputPts->size() <= 2) + return getLineCurve(inputPts, distance); + + // optimize creating ring for for zero distance + if (distance == 0.0) { + return inputPts->clone(); + } + OffsetSegmentGenerator segGen(precisionModel, bufParams, distance); + computeRingBufferCurve(*inputPts, side, segGen); + return segGen.getCoordinates(); +} + + /* private */ double OffsetCurveBuilder::simplifyTolerance(double bufDistance) @@ -354,15 +472,6 @@ OffsetCurveBuilder::computeSingleSidedBufferCurve( segGen.closeRing(); } -/*private*/ -std::unique_ptr -OffsetCurveBuilder::getSegGen(double dist) -{ - std::unique_ptr osg( - new OffsetSegmentGenerator(precisionModel, bufParams, dist) - ); - return osg; -} } // namespace geos.operation.buffer } // namespace geos.operation diff --git a/src/operation/buffer/OffsetCurveSection.cpp b/src/operation/buffer/OffsetCurveSection.cpp new file mode 100644 index 0000000000..b7051a7eec --- /dev/null +++ b/src/operation/buffer/OffsetCurveSection.cpp @@ -0,0 +1,168 @@ +/********************************************************************** + * + * GEOS - Geometry Engine Open Source + * http://geos.osgeo.org + * + * Copyright (c) 2021 Martin Davis + * Copyright (C) 2021 Paul Ramsey + * + * 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 +#include +#include +#include + +using geos::geom::Geometry; +using geos::geom::Coordinate; +using geos::geom::CoordinateSequence; +using geos::geom::LineString; + + +namespace geos { // geos +namespace operation { // geos.operation +namespace buffer { // geos.operation.buffer + + +/*public*/ +const CoordinateSequence* +OffsetCurveSection::getCoordinates() const +{ + return sectionPts.get(); +} + +std::unique_ptr +OffsetCurveSection::releaseCoordinates() +{ + return std::move(sectionPts); +} + + +/* private */ +bool +OffsetCurveSection::isEndInSameSegment(double nextLoc) const +{ + long segIndex = std::lround(std::floor(locLast)); + long nextIndex = std::lround(std::floor(nextLoc)); + // long segIndex = static_cast(locLast); + // long nextIndex = static_cast(nextLoc); + return segIndex == nextIndex; +} + +bool +OffsetCurveSection::OffsetCurveSectionComparator( + const std::unique_ptr& a, + const std::unique_ptr& b) +{ + if (a->getLocation() < b->getLocation()) + return true; + else + return false; +} + +/* public static */ +std::unique_ptr +OffsetCurveSection::toGeometry( + std::vector>& sections, + const GeometryFactory* geomFactory) +{ + if (sections.size() == 0) + return geomFactory->createLineString(); + if (sections.size() == 1) { + auto cs = sections[0]->releaseCoordinates(); + return geomFactory->createLineString(std::move(cs)); + } + + //-- sort sections in order along the offset curve + + std::sort(sections.begin(), sections.end(), OffsetCurveSectionComparator); + std::vector> lines; + for (auto& section : sections) { + auto cs = section->releaseCoordinates(); + auto ls = geomFactory->createLineString(std::move(cs)); + lines.emplace_back(ls.release()); + } + return geomFactory->createMultiLineString(std::move(lines)); +} + +/** +* Joins section coordinates into a LineString. +* Join vertices which lie in the same raw curve segment +* are removed, to simplify the result linework. +* +* @param sections the sections to join +* @param geomFactory the geometry factory to use +* @return the simplified linestring for the joined sections +*/ +/* public static */ +std::unique_ptr +OffsetCurveSection::toLine( + std::vector>& sections, + const GeometryFactory* geomFactory) +{ + if (sections.size() == 0) + return geomFactory->createLineString(); + if (sections.size() == 1) { + auto cs = sections[0]->releaseCoordinates(); + return geomFactory->createLineString(std::move(cs)); + } + + //-- sort sections in order along the offset curve + std::sort(sections.begin(), sections.end(), OffsetCurveSectionComparator); + + std::unique_ptr pts(new CoordinateSequence()); + + bool removeStartPt = false; + for (std::size_t i = 0; i < sections.size(); i++) { + auto& section = sections[i]; + bool removeEndPt = false; + if (i < sections.size() - 1) { + double nextStartLoc = sections[i+1]->getLocation(); + removeEndPt = section->isEndInSameSegment(nextStartLoc); + } + const CoordinateSequence* secPts = section->getCoordinates(); + for (std::size_t j = 0; j < secPts->size(); j++) { + if ((removeStartPt && j == 0) || (removeEndPt && j == secPts->size()-1)) + continue; + pts->add(secPts->getAt(j), false); + } + removeStartPt = removeEndPt; + } + return geomFactory->createLineString(std::move(pts)); +} + +/* public static */ +std::unique_ptr +OffsetCurveSection::create( + const CoordinateSequence* srcPts, + std::size_t start, std::size_t end, + double loc, double locLast) +{ + std::size_t len; + if (end <= start) + len = srcPts->size() - start + end; + else + len = end - start + 1; + + std::unique_ptr secPts(new CoordinateSequence()); + for (std::size_t i = 0; i < len; i++) { + std::size_t index = (start + i) % (srcPts->size() - 1); + secPts->add(srcPts->getAt(index)); + } + std::unique_ptr ocs(new OffsetCurveSection(std::move(secPts), loc, locLast)); + return ocs; +} + + + +} // namespace geos.operation.buffer +} // namespace geos.operation +} // namespace geos diff --git a/tests/unit/operation/buffer/OffsetCurveTest.cpp b/tests/unit/operation/buffer/OffsetCurveTest.cpp index 8a6ff0217c..d23b4506cd 100644 --- a/tests/unit/operation/buffer/OffsetCurveTest.cpp +++ b/tests/unit/operation/buffer/OffsetCurveTest.cpp @@ -67,6 +67,15 @@ struct test_offsetcurve_data { ensure_equals_geometry(result.get(), expected.get(), tolerance); } + void + checkOffsetCurveJoined(const std::string& wkt, double distance, const std::string& wktExpected) + { + std::unique_ptr geom = wktreader.read(wkt); + std::unique_ptr result = OffsetCurve::getCurveJoined(*geom, distance); + std::unique_ptr expected = wktreader.read(wktExpected); + ensure_equals_geometry(result.get(), expected.get(), 0.05); + } + }; typedef test_group group; @@ -108,33 +117,72 @@ void object::test<3> () ); } -// testSegment1Short +/** +* 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<4> () +{ + 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<5> () { checkOffsetCurve( "LINESTRING (2 2, 2 2.0000001)", 1, "LINESTRING (1 2, 1 2.0000001)", 0.00000001 - ); + ); } // testSegment1 template<> template<> -void object::test<5> () +void object::test<6> () { checkOffsetCurve( "LINESTRING (0 0, 9 9)", 1, "LINESTRING (-0.71 0.71, 8.29 9.71)" - ); + ); +} + + +// testSegment1Neg +template<> +template<> +void object::test<7> () +{ + checkOffsetCurve( + "LINESTRING (0 0, 9 9)", -1, + "LINESTRING (0.71 -0.71, 9.71 8.29)" + ); } // testSegments2 template<> template<> -void object::test<6> () +void object::test<8> () +{ + 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<9> () { checkOffsetCurve( "LINESTRING (0 0, 9 9, 25 0, 30 15)", 1, @@ -142,10 +190,32 @@ void object::test<6> () ); } +// testRightAngle +template<> +template<> +void object::test<10> () +{ + 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<7> () +void object::test<11> () +{ + 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<12> () { checkOffsetCurve( "LINESTRING (1 3, 6 3, 4 5, 9 5)", 1, @@ -153,10 +223,43 @@ void object::test<7> () ); } + +// testAsymmetricU +template<> +template<> +void object::test<13> () +{ + 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<14> () +{ + 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<8> () +void object::test<15> () { checkOffsetCurve( "LINESTRING (3 5, 5 7, 7 5)", -4, @@ -167,27 +270,69 @@ void object::test<8> () // testSelfCross template<> template<> -void object::test<9> () +void object::test<16> () { checkOffsetCurve( "LINESTRING (50 90, 50 10, 90 50, 10 50)", 10, - "LINESTRING (60 90, 60 60)" ); + "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<10> () +void object::test<17> () { checkOffsetCurve( "LINESTRING (50 90, 50 10, 90 50, 10 50)", -10, - "LINESTRING (40 90, 40 60, 10 60)" ); + "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<18> () +{ + 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<19> () +{ + 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<20> () +{ + 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<21> () +{ + 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<11> () +void object::test<22> () { checkOffsetCurve( "LINESTRING (10 10, 50 90, 90 10, 10 10)", -10, @@ -197,18 +342,54 @@ void object::test<11> () // testClosedCurve template<> template<> -void object::test<12> () +void object::test<23> () +{ + 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<24> () +{ + 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<25> () +{ + 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<26> () { checkOffsetCurve( - "LINESTRING (30 70, 80 80, 50 10, 10 80, 60 70)", 10, - "LINESTRING (45 83.2, 78.04 89.81, 80 90, 81.96 89.81, 83.85 89.23, 85.59 88.29, 87.11 87.04, 88.35 85.5, 89.27 83.76, 89.82 81.87, 90 79.9, 89.79 77.94, 89.19 76.06, 59.19 6.06, 58.22 4.3, 56.91 2.77, 55.32 1.53, 53.52 0.64, 51.57 0.12, 49.56 0.01, 47.57 0.3, 45.68 0.98, 43.96 2.03, 42.49 3.4, 41.32 5.04, 1.32 75.04, 0.53 76.77, 0.09 78.63, 0.01 80.53, 0.29 82.41, 0.93 84.2, 1.89 85.85, 3.14 87.28, 4.65 88.45, 6.34 89.31, 8.17 89.83, 10.07 90, 11.96 89.81, 45 83.2)" + "MULTIPOINT ((0 0), (1 1))", 1, + "LINESTRING EMPTY" ); } + // testMultiLine template<> template<> -void object::test<13> () +void object::test<27> () { checkOffsetCurve( "MULTILINESTRING ((20 30, 60 10, 80 60), (40 50, 80 30))", 10, @@ -216,10 +397,23 @@ void object::test<13> () ); } -// testPolygon + +// testMixedWithPoint template<> template<> -void object::test<14> () +void object::test<28> () +{ + 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<29> () { checkOffsetCurve( "POLYGON ((100 200, 200 100, 100 100, 100 200))", 10, @@ -227,10 +421,10 @@ void object::test<14> () ); } -// testPolygon +// testPolygon2 template<> template<> -void object::test<15> () +void object::test<30> () { checkOffsetCurve( "POLYGON ((100 200, 200 100, 100 100, 100 200))", -10, @@ -238,10 +432,11 @@ void object::test<15> () ); } -// testPolygonWithHole + +// testPolygonWithHole1 template<> template<> -void object::test<16> () +void object::test<31> () { checkOffsetCurve( "POLYGON ((20 80, 80 80, 80 20, 20 20, 20 80), (30 70, 70 70, 70 30, 30 30, 30 70))", 10, @@ -249,10 +444,10 @@ void object::test<16> () ); } -// testPolygonWithHole +// testPolygonWithHole2 template<> template<> -void object::test<17> () +void object::test<32> () { checkOffsetCurve( "POLYGON ((20 80, 80 80, 80 20, 20 20, 20 80), (30 70, 70 70, 70 30, 30 30, 30 70))", -10, @@ -260,12 +455,50 @@ void object::test<17> () ); } - //--------------------------------------- + + +//------------------------------------------------- + +// testJoined1 +template<> +template<> +void object::test<33> () +{ + 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<34> () +{ + 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<35> () +{ + 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<18> () +void object::test<36> () { checkOffsetCurve( "LINESTRING (20 20, 50 50, 80 20)", @@ -277,7 +510,7 @@ void object::test<18> () // testJoinBevel template<> template<> -void object::test<19> () +void object::test<37> () { checkOffsetCurve( "LINESTRING (20 20, 50 50, 80 20)", @@ -289,7 +522,7 @@ void object::test<19> () // testJoinMitre template<> template<> -void object::test<20> () +void object::test<38> () { checkOffsetCurve( "LINESTRING (20 20, 50 50, 80 20)",