From: Daniel Baston Date: Fri, 17 Mar 2017 00:28:07 +0000 (+0000) Subject: #3589, Orientation checking and forcing fuctions X-Git-Tag: 2.4.0alpha~148 X-Git-Url: https://granicus.if.org/sourcecode?a=commitdiff_plain;h=6c837cda320ab1eff2546e369e81c45214a05bf2;p=postgis #3589, Orientation checking and forcing fuctions git-svn-id: http://svn.osgeo.org/postgis/trunk@15336 b70326c6-7e19-0410-871a-916f4a2858ee --- diff --git a/NEWS b/NEWS index 56ef39370..de2fb277c 100644 --- a/NEWS +++ b/NEWS @@ -5,6 +5,7 @@ PostGIS 2.4.0 - #3599, Geobuf output support via ST_AsGeobuf (Björn Harrtell) - #3661, Mapbox vector tile output support via ST_AsMVT (Björn Harrtell / CartoDB) + - #3689, Add orientation checking and forcing functions (Dan Baston) PostGIS 2.3.0 2016/09/26 diff --git a/doc/reference_accessor.xml b/doc/reference_accessor.xml index 4a7b8a026..66ed9dcdb 100644 --- a/doc/reference_accessor.xml +++ b/doc/reference_accessor.xml @@ -872,6 +872,129 @@ FROM (SELECT ST_BuildArea( + + + + ST_IsPolygonCCW + + + Returns true if all exterior rings are oriented counter-clockwise and all interior rings are oriented clockwise. + + + + + + + + boolean + ST_IsPolygonCCW + + + geometry + geom + + + + + + + Description + + + Returns true for (Multi)Polygons that use a counter-clockwise + orientation for their exterior ring, and a clockwise direction + for all interior rings. + + + + Returns true for all non-polygonal geometries. + + + + + If a polygonal geometry does not use reversed orientation + for interior rings (i.e., if one or more interior rings + are oriented in the same direction as an exterior ring) + then both ST_IsPolygonCW and ST_IsPolygonCCW will return false. + + + + &Z_support; + &M_support; + + + + + See Also + + , + , + + + + + + + + + ST_IsPolygonCW + + + Returns true if all exterior rings are oriented clockwise and all interior rings are oriented counter-clockwise. + + + + + + + + boolean + ST_IsPolygonCW + + + geometry + geom + + + + + + + Description + + + Returns true for (Multi)Polygons that use a clockwise + orientation for their exterior ring, and a counter-clockwise direction + for all interior rings. + + + + Returns true for all non-polygonal geometries. + + + + + If a polygonal geometry does not use reversed orientation + for interior rings (i.e., if one or more interior rings + are oriented in the same direction as an exterior ring) + then both ST_IsPolygonCW and ST_IsPolygonCCW will return false. + + + + &Z_support; + &M_support; + + + + See Also + + , + , + + + + + ST_IsClosed diff --git a/doc/reference_editor.xml b/doc/reference_editor.xml index 741d25266..d097d7087 100644 --- a/doc/reference_editor.xml +++ b/doc/reference_editor.xml @@ -449,6 +449,54 @@ SELECT ST_AsEWKT(ST_Force4D('MULTILINESTRINGM((0 0 1,0 5 2,5 0 3,0 0 4),(1 1 1, + + + + ST_ForcePolygonCCW + + + Orients all exterior rings counter-clockwise and all interior rings clockwise. + + + + + + + + geometry + ST_ForcePolygonCCW + + + geometry + geom + + + + + + + Description + + + Forces (Multi)Polygons to use a counter-clockwise orientation for + their exterior ring, and a clockwise orientation for their interior + rings. Non-polygonal geometries are returned unchanged. + + + &Z_support; + &M_support; + + + + See Also + + , + , + + + + + ST_ForceCollection @@ -531,6 +579,53 @@ GEOMETRYCOLLECTION( + + + + ST_ForcePolygonCW + + + Orients all exterior rings clockwise and all interior rings counter-clockwise. + + + + + + + + geometry + ST_ForcePolygonCW + + + geometry + geom + + + + + + + Description + + + Forces (Multi)Polygons to use a clockwise orientation for + their exterior ring, and a counter-clockwise orientation for their interior + rings. Non-polygonal geometries are returned unchanged. + + + &Z_support; + &M_support; + + + + See Also + + , + , + + + + @@ -585,11 +680,17 @@ GEOMETRYCOLLECTION( Description - Forces the orientation of the vertices in a polygon to follow the - Right-Hand-Rule. In GIS terminology, this means that the area that is bounded by the + Forces the orientation of the vertices in a polygon to follow a + Right-Hand-Rule, in which the area that is bounded by the polygon is to the right of the boundary. In particular, the exterior ring is orientated in a clockwise direction and the interior rings in a counter-clockwise - direction. + direction. This function is a synonym for + + + + The above definition of the Right-Hand-Rule conflicts with definitions used in other contexts. To avoid confusion, it is recommended to use ST_ForcePolygonCW. + + Enhanced: 2.0.0 support for Polyhedral surfaces was introduced. &Z_support; @@ -613,7 +714,12 @@ GEOMETRYCOLLECTION( See Also - , + + , + , + , + , + , , diff --git a/liblwgeom/cunit/cu_libgeom.c b/liblwgeom/cunit/cu_libgeom.c index 7ae07f2a7..b325e2f8c 100644 --- a/liblwgeom/cunit/cu_libgeom.c +++ b/liblwgeom/cunit/cu_libgeom.c @@ -741,6 +741,7 @@ static void test_lwgeom_force_clockwise(void) /* counterclockwise, must be reversed */ geom = lwgeom_from_wkt("POLYGON((0 0, 10 0, 10 10, 0 10, 0 0))", LW_PARSER_CHECK_NONE); + CU_ASSERT_FALSE(lwgeom_is_clockwise(geom)); lwgeom_force_clockwise(geom); in_ewkt = "POLYGON((0 0,0 10,10 10,10 0,0 0))"; out_ewkt = lwgeom_to_ewkt(geom); @@ -752,6 +753,7 @@ static void test_lwgeom_force_clockwise(void) /* clockwise, fine as is */ geom = lwgeom_from_wkt("POLYGON((0 0, 0 10, 10 10, 10 0, 0 0))", LW_PARSER_CHECK_NONE); + CU_ASSERT_TRUE(lwgeom_is_clockwise(geom)); lwgeom_force_clockwise(geom); in_ewkt = "POLYGON((0 0,0 10,10 10,10 0,0 0))"; out_ewkt = lwgeom_to_ewkt(geom); @@ -763,6 +765,7 @@ static void test_lwgeom_force_clockwise(void) /* counterclockwise shell (must be reversed), mixed-wise holes */ geom = lwgeom_from_wkt("POLYGON((0 0,10 0,10 10,0 10,0 0),(2 2,2 4,4 2,2 2),(6 2,8 2,8 4,6 2))", LW_PARSER_CHECK_NONE); + CU_ASSERT_FALSE(lwgeom_is_clockwise(geom)); lwgeom_force_clockwise(geom); in_ewkt = "POLYGON((0 0,0 10,10 10,10 0,0 0),(2 2,4 2,2 4,2 2),(6 2,8 2,8 4,6 2))"; out_ewkt = lwgeom_to_ewkt(geom); @@ -774,6 +777,7 @@ static void test_lwgeom_force_clockwise(void) /* clockwise shell (fine), mixed-wise holes */ geom = lwgeom_from_wkt("POLYGON((0 0,0 10,10 10,10 0,0 0),(2 2,4 2,2 4,2 2),(6 2,8 4,8 2,6 2))", LW_PARSER_CHECK_NONE); + CU_ASSERT_FALSE(lwgeom_is_clockwise(geom)); lwgeom_force_clockwise(geom); in_ewkt = "POLYGON((0 0,0 10,10 10,10 0,0 0),(2 2,4 2,2 4,2 2),(6 2,8 2,8 4,6 2))"; out_ewkt = lwgeom_to_ewkt(geom); diff --git a/liblwgeom/liblwgeom.h.in b/liblwgeom/liblwgeom.h.in index d15afa8b3..07fa591be 100644 --- a/liblwgeom/liblwgeom.h.in +++ b/liblwgeom/liblwgeom.h.in @@ -1218,6 +1218,9 @@ extern void lwgeom_force_clockwise(LWGEOM *lwgeom); extern void lwpoly_force_clockwise(LWPOLY *poly); extern void lwtriangle_force_clockwise(LWTRIANGLE *triangle); +extern int lwgeom_is_clockwise(LWGEOM *lwgeom); +extern int lwpoly_is_clockwise(LWPOLY *poly); +extern int lwtriangle_is_clockwise(LWTRIANGLE *triangle); extern void interpolate_point4d(POINT4D *A, POINT4D *B, POINT4D *I, double F); diff --git a/liblwgeom/lwgeom.c b/liblwgeom/lwgeom.c index 769d00193..4d45ce0b5 100644 --- a/liblwgeom/lwgeom.c +++ b/liblwgeom/lwgeom.c @@ -59,6 +59,35 @@ lwgeom_force_clockwise(LWGEOM *lwgeom) } } +/** Check clockwise orientation on LWGEOM polygons **/ +int +lwgeom_is_clockwise(LWGEOM *lwgeom) +{ + switch (lwgeom->type) + { + case POLYGONTYPE: + return lwpoly_is_clockwise((LWPOLY *)lwgeom); + + case TRIANGLETYPE: + return lwtriangle_is_clockwise((LWTRIANGLE *)lwgeom); + + case MULTIPOLYGONTYPE: + case COLLECTIONTYPE: + { + int i; + LWCOLLECTION* coll = (LWCOLLECTION *)lwgeom; + + for (i=0; i < coll->ngeoms; i++) + if (!lwgeom_is_clockwise(coll->geoms[i])) + return LW_FALSE; + return LW_TRUE; + } + default: + return LW_TRUE; + return LW_FALSE; + } +} + /** Reverse vertex order of LWGEOM **/ void lwgeom_reverse(LWGEOM *lwgeom) diff --git a/liblwgeom/lwpoly.c b/liblwgeom/lwpoly.c index 4d2b8a450..f8fa89cd7 100644 --- a/liblwgeom/lwpoly.c +++ b/liblwgeom/lwpoly.c @@ -284,6 +284,24 @@ lwpoly_force_clockwise(LWPOLY *poly) } +int +lwpoly_is_clockwise(LWPOLY *poly) +{ + int i; + + if ( lwpoly_is_empty(poly) ) + return LW_TRUE; + + if ( ptarray_isccw(poly->rings[0]) ) + return LW_FALSE; + + for ( i = 1; i < poly->nrings; i++) + if ( !ptarray_isccw(poly->rings[i]) ) + return LW_FALSE; + + return LW_TRUE; +} + void lwpoly_release(LWPOLY *lwpoly) { diff --git a/liblwgeom/lwtriangle.c b/liblwgeom/lwtriangle.c index b2896a839..d87b1e3b2 100644 --- a/liblwgeom/lwtriangle.c +++ b/liblwgeom/lwtriangle.c @@ -109,6 +109,12 @@ lwtriangle_force_clockwise(LWTRIANGLE *triangle) ptarray_reverse(triangle->points); } +int +lwtriangle_is_clockwise(LWTRIANGLE *triangle) +{ + return !ptarray_isccw(triangle->points); +} + void lwtriangle_reverse(LWTRIANGLE *triangle) { diff --git a/postgis/lwgeom_functions_analytic.c b/postgis/lwgeom_functions_analytic.c index 922f5d98b..7d4ad951a 100644 --- a/postgis/lwgeom_functions_analytic.c +++ b/postgis/lwgeom_functions_analytic.c @@ -55,6 +55,8 @@ Datum ST_LineCrossingDirection(PG_FUNCTION_ARGS); Datum ST_MinimumBoundingRadius(PG_FUNCTION_ARGS); Datum ST_MinimumBoundingCircle(PG_FUNCTION_ARGS); Datum ST_GeometricMedian(PG_FUNCTION_ARGS); +Datum ST_IsPolygonCCW(PG_FUNCTION_ARGS); +Datum ST_IsPolygonCW(PG_FUNCTION_ARGS); static double determineSide(const POINT2D *seg1, const POINT2D *seg2, const POINT2D *point); @@ -1282,3 +1284,57 @@ Datum ST_GeometricMedian(PG_FUNCTION_ARGS) PG_RETURN_POINTER(result); } +/********************************************************************** + * + * ST_IsPolygonCW + * + **********************************************************************/ + +PG_FUNCTION_INFO_V1(ST_IsPolygonCW); +Datum ST_IsPolygonCW(PG_FUNCTION_ARGS) +{ + GSERIALIZED* geom; + LWGEOM* input; + bool is_clockwise; + + if (PG_ARGISNULL(0)) + PG_RETURN_NULL(); + + geom = PG_GETARG_GSERIALIZED_P(0); + input = lwgeom_from_gserialized(geom); + + is_clockwise = lwgeom_is_clockwise(input); + + lwgeom_free(input); + PG_FREE_IF_COPY(geom, 0); + + PG_RETURN_BOOL(is_clockwise); +} + +/********************************************************************** + * + * ST_IsPolygonCCW + * + **********************************************************************/ + +PG_FUNCTION_INFO_V1(ST_IsPolygonCCW); +Datum ST_IsPolygonCCW(PG_FUNCTION_ARGS) +{ + GSERIALIZED* geom; + LWGEOM* input; + bool is_ccw; + + if (PG_ARGISNULL(0)) + PG_RETURN_NULL(); + + geom = PG_GETARG_GSERIALIZED_P_COPY(0); + input = lwgeom_from_gserialized(geom); + + lwgeom_reverse(input); + is_ccw = lwgeom_is_clockwise(input); + + lwgeom_free(input); + PG_FREE_IF_COPY(geom, 0); + + PG_RETURN_BOOL(is_ccw); +} diff --git a/postgis/postgis.sql.in b/postgis/postgis.sql.in index 3ea1a82bc..4f58b0b8c 100644 --- a/postgis/postgis.sql.in +++ b/postgis/postgis.sql.in @@ -1235,6 +1235,20 @@ CREATE OR REPLACE FUNCTION ST_Area(geometry) LANGUAGE 'c' IMMUTABLE STRICT _PARALLEL COST 10; +-- Availability: 2.4.0 +CREATE OR REPLACE FUNCTION ST_IsPolygonCW(geometry) + RETURNS boolean + AS 'MODULE_PATHNAME','ST_IsPolygonCW' + LANGUAGE 'c' IMMUTABLE STRICT + COST 10; + +-- Availability: 2.4.0 +CREATE OR REPLACE FUNCTION ST_IsPolygonCCW(geometry) + RETURNS boolean + AS 'MODULE_PATHNAME','ST_IsPolygonCCW' + LANGUAGE 'c' IMMUTABLE STRICT + COST 10; + -- Availability: 2.0.0 CREATE OR REPLACE FUNCTION ST_DistanceSpheroid(geom1 geometry, geom2 geometry,spheroid) RETURNS FLOAT8 @@ -1467,6 +1481,20 @@ CREATE OR REPLACE FUNCTION ST_Reverse(geometry) LANGUAGE 'c' IMMUTABLE STRICT _PARALLEL COST 1; -- reset cost, see #3675 +-- Availability: 2.4.0 +CREATE OR REPLACE FUNCTION ST_ForcePolygonCW(geometry) + RETURNS geometry + AS 'MODULE_PATHNAME', 'LWGEOM_force_clockwise_poly' + LANGUAGE 'c' IMMUTABLE STRICT _PARALLEL + COST 15; + +-- Availability: 2.4.0 +CREATE OR REPLACE FUNCTION ST_ForcePolygonCCW(geometry) + RETURNS geometry + AS $$ SELECT @extschema@.ST_Reverse(@extschema@.ST_ForcePolygonCW($1)) $$ + LANGUAGE SQL IMMUTABLE STRICT _PARALLEL + COST 15; + -- Availability: 1.2.2 CREATE OR REPLACE FUNCTION ST_ForceRHR(geometry) RETURNS geometry diff --git a/regress/Makefile.in b/regress/Makefile.in index c68d3f032..3301d4e2d 100644 --- a/regress/Makefile.in +++ b/regress/Makefile.in @@ -102,6 +102,7 @@ TESTS = \ minimum_bounding_circle \ normalize \ operators \ + orientation \ out_geometry \ out_geography \ polygonize \ diff --git a/regress/orientation.sql b/regress/orientation.sql new file mode 100644 index 000000000..92e7b6f8d --- /dev/null +++ b/regress/orientation.sql @@ -0,0 +1,65 @@ +-- pg24 + +-- Non-applicable types +SELECT '1', ST_IsPolygonCCW('POINT (0 0)'); +SELECT '2', ST_IsPolygonCCW('MULTIPOINT ((0 0), (1 1))'); +SELECT '3', ST_IsPolygonCCW('LINESTRING (1 1, 2 2)'); +SELECT '4', ST_IsPolygonCCW('MULTILINESTRING ((1 1, 2 2), (3 3, 0 0))'); +SELECT '5', ST_IsPolygonCW('POINT (0 0)'); +SELECT '6', ST_IsPolygonCW('MULTIPOINT ((0 0), (1 1))'); +SELECT '7', ST_IsPolygonCW('LINESTRING (1 1, 2 2)'); +SELECT '8', ST_IsPolygonCW('MULTILINESTRING ((1 1, 2 2), (3 3, 0 0))'); + +-- NULL handling +SELECT '101', ST_IsPolygonCW(NULL::geometry); +SELECT '102', ST_IsPolygonCCW(NULL::geometry); +SELECT '103', ST_ForcePolygonCW(NULL::geometry); +SELECT '104', ST_ForcePolygonCCW(NULL::geometry); + +-- EMPTY handling +SELECT '201', ST_AsText(ST_ForcePolygonCW('POLYGON EMPTY'::geometry)); +SELECT '202', ST_AsText(ST_ForcePolygonCCW('POLYGON EMPTY'::geometry)); +SELECT '203', ST_IsPolygonCW('POLYGON EMPTY'::geometry); +SELECT '204', ST_IsPolygonCCW('POLYGON EMPTY'::geometry); + +-- Preserves SRID +SELECT '301', ST_SRID(ST_ForcePolygonCW('SRID=4269;POLYGON ((0 0, 1 1, 1 0, 0 0))')); +SELECT '302', ST_SRID(ST_ForcePolygonCCW('SRID=4269;POLYGON ((0 0, 1 0, 1 1, 0 0))')); + +-- Single polygon, ccw exterior ring only +SELECT '401', ST_IsPolygonCW('POLYGON ((0 0, 1 0, 1 1, 0 0))'); +SELECT '402', ST_IsPolygonCCW('POLYGON ((0 0, 1 0, 1 1, 0 0))'); + +-- Single polygon, cw exterior ring only +SELECT '403', ST_IsPolygonCW('POLYGON ((0 0, 1 1, 1 0, 0 0))'); +SELECT '404', ST_IsPolygonCCW('POLYGON ((0 0, 1 1, 1 0, 0 0))'); + +-- Single polygon, ccw exterior ring, cw interior rings +SELECT '405', ST_IsPolygonCW('POLYGON ((0 0, 10 0, 10 10, 0 10, 0 0), (1 1, 1 2, 2 2, 1 1), (5 5, 5 7, 7 7, 5 5))'); +SELECT '406', ST_IsPolygonCCW('POLYGON ((0 0, 10 0, 10 10, 0 10, 0 0), (1 1, 1 2, 2 2, 1 1), (5 5, 5 7, 7 7, 5 5))'); + +-- Single polygon, cw exterior ring, ccw interior rings +SELECT '407', ST_IsPolygonCW( 'POLYGON ((0 0, 0 10, 10 10, 10 0, 0 0), (1 1, 2 2, 1 2, 1 1), (5 5, 7 7, 5 7, 5 5))'); +SELECT '408', ST_IsPolygonCCW('POLYGON ((0 0, 0 10, 10 10, 10 0, 0 0), (1 1, 2 2, 1 2, 1 1), (5 5, 7 7, 5 7, 5 5))'); + +-- Single polygon, ccw exerior ring, mixed interior rings +SELECT '409', ST_IsPolygonCW( 'POLYGON ((0 0, 10 0, 10 10, 0 10, 0 0), (1 1, 1 2, 2 2, 1 1), (5 5, 7 7, 5 7, 5 5))'); +SELECT '410', ST_IsPolygonCCW('POLYGON ((0 0, 10 0, 10 10, 0 10, 0 0), (1 1, 1 2, 2 2, 1 1), (5 5, 7 7, 5 7, 5 5))'); + +-- Single polygon, cw exterior ring, mixed interior rings +SELECT '411', ST_IsPolygonCW( 'POLYGON ((0 0, 0 10, 10 10, 10 0, 0 0), (1 1, 2 2, 1 2, 1 1), (5 5, 5 7, 7 7, 5 5))'); +SELECT '412', ST_IsPolygonCCW('POLYGON ((0 0, 0 10, 10 10, 10 0, 0 0), (1 1, 2 2, 1 2, 1 1), (5 5, 5 7, 7 7, 5 5))'); + +-- MultiPolygon, ccw exterior rings only +SELECT '413', ST_IsPolygonCW( 'MULTIPOLYGON (((0 0, 1 0, 1 1, 0 0)), ((100 0, 101 0, 101 1, 100 0)))'); +SELECT '414', ST_IsPolygonCCW('MULTIPOLYGON (((0 0, 1 0, 1 1, 0 0)), ((100 0, 101 0, 101 1, 100 0)))'); + +-- MultiPolygon, cw exterior rings only +SELECT '415', ST_IsPolygonCW( 'MULTIPOLYGON (((0 0, 1 1, 1 0, 0 0)), ((100 0, 101 1, 101 0, 100 0)))'); +SELECT '416', ST_IsPolygonCCW('MULTIPOLYGON (((0 0, 1 1, 1 0, 0 0)), ((100 0, 101 1, 101 0, 100 0)))'); + +-- MultiPolygon, mixed exterior rings +SELECT '417', ST_IsPolygonCW( 'MULTIPOLYGON (((0 0, 1 0, 1 1, 0 0)), ((100 0, 101 1, 101 0, 100 0)))'); +SELECT '418', ST_IsPolygonCCW('MULTIPOLYGON (((0 0, 1 0, 1 1, 0 0)), ((100 0, 101 1, 101 0, 100 0)))'); + + diff --git a/regress/orientation_expected b/regress/orientation_expected new file mode 100644 index 000000000..cb8789ff0 --- /dev/null +++ b/regress/orientation_expected @@ -0,0 +1,36 @@ +1|t +2|t +3|t +4|t +5|t +6|t +7|t +8|t +101| +102| +103| +104| +201|POLYGON EMPTY +202|POLYGON EMPTY +203|t +204|t +301|4269 +302|4269 +401|f +402|t +403|t +404|f +405|f +406|t +407|t +408|f +409|f +410|f +411|f +412|f +413|f +414|t +415|t +416|f +417|f +418|f