Skip to content

Instantly share code, notes, and snippets.

@pramsey
Last active February 21, 2024 00:25
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pramsey/cbf6db1edb8762854925f6b6ad6d6b35 to your computer and use it in GitHub Desktop.
Save pramsey/cbf6db1edb8762854925f6b6ad6d6b35 to your computer and use it in GitHub Desktop.
PostGIS Curve Decomposition Functions

Current State of CompoundCurve

First question, is a CompoundCurve a unitary geometry, like a LineString, or is it a collection of geometries. Internally in PostGIS it is structured identically to a collection, and that makes some handling easier, because we just delegate calculations to the collection functions.

WITH f(geom) AS (
SELECT 
  'COMPOUNDCURVE(
    LINESTRING(2 2, 2.5 2.5),
    CIRCULARSTRING(2.5 2.5, 4.5 2.5, 3.5 3.5),
    (3.5 3.5, 2.5 4.5, 3 5)
  )'::geometry AS geom
)
SELECT 
  ST_NumGeometries(geom), 
  ST_GeometryType(ST_GeometryN(geom,1)), 
  ST_GeometryN(geom,2) AS geometryn_2
FROM f;
st_numgeometries | 3
st_geometrytype  | ST_CompoundCurve
geometryn_2      | 

Currently, we are treating CompoundCurve as both unitary (as seen in the ST_GeometryN results) and as a collection (as seen in the ST_NumGeometries result).

Second question, given the answer to the above question, is ST_Dump working right?

WITH f(geom) AS (
SELECT 
  'COMPOUNDCURVE(
    LINESTRING(2 2, 2.5 2.5),
    CIRCULARSTRING(2.5 2.5, 4.5 2.5, 3.5 3.5),
    (3.5 3.5, 2.5 4.5, 3 5, 2 2)
  )'::geometry AS geom
)
SELECT s.path, ST_AsText(s.geom)
FROM f, ST_Dump(f.geom) s;

Currently this returns three results, so it is using "collection" semantics.

WITH f(geom) AS (
SELECT 
  'COMPOUNDCURVE(
    LINESTRING(2 2, 2.5 2.5),
    CIRCULARSTRING(2.5 2.5, 4.5 2.5, 3.5 3.5),
    (3.5 3.5, 2.5 4.5, 3 5, 2 2)
  )'::geometry AS geom
)
SELECT s.path, ST_AsText(s.geom)
FROM f, ST_DumpSegments(f.geom) s;

The ST_Segments function does not even handle curved objects it seems. The right answer would probably be a set of two-point LineString for straight segments and three-point CircularStrings for arc segments.

WITH f(geom) AS (
SELECT 
  'COMPOUNDCURVE(
    LINESTRING(2 2, 2.5 2.5),
    CIRCULARSTRING(2.5 2.5, 4.5 2.5, 3.5 3.5),
    (3.5 3.5, 2.5 4.5, 3 5, 2 2)
  )'::geometry AS geom
)
SELECT s.path, ST_AsText(s.geom)
FROM f, ST_DumpPoints(f.geom) s;

Fortunately ST_DumpPoints doesn't care about substructure, and the path parameter is rich enough to reconstruct the original object (sort of! without knowing the type of the rings, a round trip is not possible in the way it is for a Polygon or a LineString).

Proposal for CompoundCurve

Treating the CompoundCurve as unitary is perhaps the answer that "feels" right, although it must be said: it is a lot harder to do than just treating it as a collection. There is a serious practicality advantage to just saying "it's a collection, access it that way".

COMPOUNDCURVE(
    LINESTRING(2 2, 2.5 2.5),
    CIRCULARSTRING(2.5 2.5, 4.5 2.5, 3.5 3.5),
    LINESTRING(3.5 3.5, 2.5 4.5, 3 5, 2 2)
)

As a unitary type, we'd like the following results.

  • ST_NumGeometries(geom) => 1
  • ST_GeometryN(geom,1) => geom
  • ST_GeometryType(ST_GeometryN(geom,1)) => ST_CompoundCurve
  • ST_GeometryN(geom,2) => NULL
  • ST_NumCurves(geom) => 3
  • ST_GeometryType(ST_CurveN(geom,1)) => ST_LineString
  • ST_GeometryType(ST_CurveN(geom,2)) => ST_CircularString
  • ST_GeometryType(ST_CurveN(geom,3)) => ST_LineString
  • ST_GeometryType(ST_CurveN(geom,4)) => NULL
  • ST_GeometryType(ST_CurveN(geom,0)) => NULL
  • ST_CurveN(ST_CurveN(geom,3), 1) => NULL
  • ST_GeometryType(ST_Dump(geom).geom) => ST_CompoundCurve
WITH f(geom) AS (
SELECT 
  'COMPOUNDCURVE(
    LINESTRING(2 2, 2.5 2.5),
    CIRCULARSTRING(2.5 2.5, 4.5 2.5, 3.5 3.5),
    LINESTRING(3.5 3.5, 2.5 4.5, 3 5, 2 2)
  )'::geometry AS geom
)
SELECT 
  ST_NumGeometries(geom) as numgeometries, 
  ST_GeometryType(ST_GeometryN(geom,1)) as geometryn_1, 
  ST_GeometryType(ST_CurveN(geom,1)) as curven_1,
  ST_GeometryN(geom,2) AS geometryn_2,
  ST_NumCurves(geom) AS numcurves,
  ST_GeometryType(ST_CurveN(geom,1)) as curven_1,
  ST_GeometryType(ST_CurveN(geom,2)) as curven_2,
  ST_GeometryType(ST_CurveN(geom,3)) as curven_3,
  ST_GeometryType(ST_CurveN(geom,3)) as curven_4,
  ST_CurveN(ST_CurveN(geom,3), 1) as curven_3_curven_1
FROM f;

We'd also like ST_Dump to return the original object. And ST_DumpSegments should support curves, outputting all the segments, both linear and arc.

ST_AsText(ST_DumpSegments(geom).geom) =>

  LINESTRING(2 2, 2.5 2.5)
  CIRCULARSTRING(2.5 2.5, 4.5 2.5, 3.5 3.5)
  LINESTRING(3.5 3.5, 2.5 4.5)
  LINESTRING(2.5 4.5, 3 5)
  LINESTRING(3 5, 2 2)

Current State of CurvePolygon

The CurvePolygon is, if anything, even more fraught than the CompoundCurve since it consists of rings that themselves can be compound, but can also be plain LineString or CircularString, and must be closed rings.

WITH f(geom) AS (
SELECT 
  'CURVEPOLYGON(COMPOUNDCURVE(
    LINESTRING(2 2, 2.5 2.5),
    CIRCULARSTRING(2.5 2.5, 4.5 2.5, 3.5 3.5),
    LINESTRING(3.5 3.5, 2.5 4.5, 3 5, 2 2)),
    LINESTRING(4 4, 4 5, 5 4, 4 4)
    )'::geometry AS geom
)
SELECT 
  ST_NumGeometries(geom), 
  ST_NumInteriorRings(geom),
  ST_GeometryType(ST_GeometryN(geom,1)) AS geometryn_1,
  ST_GeometryN(geom,2) AS geometryn_2,
  ST_GeometryType(ST_InteriorRingN(geom, 1)) AS iringn_1,
  ST_GeometryType(ST_ExteriorRing(geom)) AS exringn
FROM f;
st_numgeometries    | 2
st_numinteriorrings | 1
st_geometrytype     | ST_CurvePolygon
geometryn_1         | ST_CurvePolygon
geometryn_2         | 
iringn_1            | ST_LineString
exringn             | ST_CompoundCurve

The current state is very odd. Because the structure is, again, internally just a collection, the ST_NumGeometries call returns a count of subgeometries which are, in this case, the rings. ST_GeometryN returns the geometry, so a unitary interpretation.

The ST_NumInteriorRings is correct and ST_InteriorRingN correctly returns ST_LineString for the interior ring.

WITH f(geom) AS (
SELECT 
  'CURVEPOLYGON(COMPOUNDCURVE(
    LINESTRING(2 2, 2.5 2.5),
    CIRCULARSTRING(2.5 2.5, 4.5 2.5, 3.5 3.5),
    LINESTRING(3.5 3.5, 2.5 4.5, 3 5, 2 2)),
    LINESTRING(4 4, 4 5, 5 4, 4 4)
    )'::geometry AS geom
)
SELECT s.path, ST_AsText(s.geom)
FROM f, ST_Dump(f.geom) s;

The ST_Dump currently spits out the "simple parts", because CompoundCurve isn't considered simple. From the point of view of ST_Dump, the CurvePolygon is just a nested collection.

WITH f(geom) AS (
SELECT 
  'CURVEPOLYGON(COMPOUNDCURVE(
    LINESTRING(2 2, 2.5 2.5),
    CIRCULARSTRING(2.5 2.5, 4.5 2.5, 3.5 3.5),
    LINESTRING(3.5 3.5, 2.5 4.5, 3 5, 2 2)),
    LINESTRING(4 4, 4 5, 5 4, 4 4)
    )'::geometry AS geom
)
SELECT s.path, ST_AsText(s.geom)
FROM f, ST_DumpSegments(f.geom) s;

ST_DumpSegments is not implemented (returns 0 results), same as for CompoundCurve.

WITH f(geom) AS (
SELECT 
  'CURVEPOLYGON(COMPOUNDCURVE(
    LINESTRING(2 2, 2.5 2.5),
    CIRCULARSTRING(2.5 2.5, 4.5 2.5, 3.5 3.5),
    LINESTRING(3.5 3.5, 2.5 4.5, 3 5, 2 2)),
    LINESTRING(4 4, 4 5, 5 4, 4 4)
    )'::geometry AS geom
)
SELECT s.path, ST_AsText(s.geom)
FROM f, ST_DumpRings(f.geom) s;

ST_DumpRings is also not implemented, but in a different way (errors out).

WITH f(geom) AS (
SELECT 
  'CURVEPOLYGON(COMPOUNDCURVE(
    LINESTRING(2 2, 2.5 2.5),
    CIRCULARSTRING(2.5 2.5, 4.5 2.5, 3.5 3.5),
    LINESTRING(3.5 3.5, 2.5 4.5, 3 5, 2 2)),
    LINESTRING(4 4, 4 5, 5 4, 4 4)
    )'::geometry AS geom
)
SELECT s.path, ST_AsText(s.geom)
FROM f, ST_DumpPoints(f.geom) s;

ST_DumpPoints outputs the full set of points, and with path information at the correct depth.

Proposal for CurvePolygon

Both CompoundCurve and CurvePolygon are, strictly speaking, unitary objects, though structurally they are easy to represent as collections. Again, just going with collection would be an easier implementation path.

CURVEPOLYGON(COMPOUNDCURVE(
    LINESTRING(2 2, 2.5 2.5),
    CIRCULARSTRING(2.5 2.5, 4.5 2.5, 3.5 3.5),
    LINESTRING(3.5 3.5, 2.5 4.5, 3 5, 2 2)),
    LINESTRING(4 4, 4 5, 5 4, 4 4)
    )
  • ST_NumGeometries(geom) => 1
  • ST_GeometryN(geom,1) => geom
  • ST_GeometryN(geom,2) => NULL
  • ST_NumInteriorRings(geom) => 1
  • ST_NumCurves(geom) => 4
  • ST_NumCurves(ST_ExteriorRing(geom)) => 3
  • ST_NumCurves(ST_InteriorRingN(geom, 1)) => 1
  • ST_GeometryType(ST_Dump(geom).geom) => ST_CurvePolygon
  • ST_GeometryType(ST_DumpRings(geom).geom) => {ST_CompoundCurve, ST_LineString}

The ST_DumpSegments should be upgraded as with CompoundCurve and CircularString to return arc segments where appropriate, although it is hard to imagine the function getting much use.

Proposal for MultiCurve

The MultiCurve is already a collection, so applying collection semantics makes sense and the results seem "OK".

WITH f(geom) AS (
SELECT 
  'MULTICURVE(COMPOUNDCURVE(
    LINESTRING(2 2, 2.5 2.5),
    CIRCULARSTRING(2.5 2.5, 4.5 2.5, 3.5 3.5),
    LINESTRING(3.5 3.5, 2.5 4.5, 3 5, 2 2)),
    LINESTRING(4 4, 4 5, 5 4, 4 4)
    )'::geometry AS geom
)
SELECT 
  ST_NumGeometries(geom), 
  ST_NumInteriorRings(geom),
  ST_GeometryType(ST_GeometryN(geom,1)) AS geometryn_1,
  ST_GeometryType(ST_GeometryN(geom,2)) AS geometryn_2,
  ST_GeometryType(ST_InteriorRingN(geom, 1)) AS iringn_1,
  ST_GeometryType(ST_ExteriorRing(geom)) AS exringn
FROM f;
st_numgeometries    | 2
st_numinteriorrings | 
geometryn_1         | ST_CompoundCurve
geometryn_2         | ST_LineString
iringn_1            | 
exringn             | 

The ST_Dump output still recurses into the CompoundCurve as if it were a collection. If CompoundCurve is considered unitary, that should be changed. Same with CurvePolygon.

WITH f(geom) AS (
SELECT 
  'MULTICURVE(COMPOUNDCURVE(
    LINESTRING(2 2, 2.5 2.5),
    CIRCULARSTRING(2.5 2.5, 4.5 2.5, 3.5 3.5),
    LINESTRING(3.5 3.5, 2.5 4.5, 3 5, 2 2)),
    LINESTRING(4 4, 4 5, 5 4, 4 4)
    )'::geometry AS geom
)
SELECT s.path, ST_AsText(s.geom)
FROM f, ST_Dump(f.geom) s;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment