From 7edd2a57f1c8b34032f7d3e480cbe1a05cc35886 Mon Sep 17 00:00:00 2001 From: Sandro Santilli Date: Fri, 1 Jul 2011 09:56:11 +0000 Subject: [PATCH] Add ST_OffsetCurve function supporting both GEOS-3.2 and GEOS-3.3+. Uses distance parameter sign to derive left/right side. Includes regress testing and documentation. Based on patch by Rafal Magda. git-svn-id: http://svn.osgeo.org/postgis/trunk@7534 b70326c6-7e19-0410-871a-916f4a2858ee --- doc/reference_processing.xml | 95 +++++++++++++++++ postgis/lwgeom_geos.c | 195 +++++++++++++++++++++++++++++++++++ postgis/postgis.sql.in.c | 7 ++ regress/Makefile.in | 1 + regress/offsetcurve.sql | 43 ++++++++ regress/offsetcurve_expected | 15 +++ 6 files changed, 356 insertions(+) create mode 100644 regress/offsetcurve.sql create mode 100644 regress/offsetcurve_expected diff --git a/doc/reference_processing.xml b/doc/reference_processing.xml index b48d56577..f0cfd4022 100644 --- a/doc/reference_processing.xml +++ b/doc/reference_processing.xml @@ -283,6 +283,101 @@ MULTIPOLYGON(((-1 2,3 4,5 6,-1 2)),((-1 2,2 3,5 6,-1 2))) | POLYGON((-1 2,5 6,3 + + + ST_OffsetCurve + + +Return an offset line at a given distance and side from an input line. + + + + + + + geometry ST_OffsetCurve + geometry line + float signed_distance + + + + geometry ST_OffsetCurve + geometry line + float signed_distance + text style_parameters + + + + + + + Description + + +Return an offset line at a given distance and side from an input line. +All points of the returned geometries are not further than the given +distance from the input geometry. + + + +For positive distance the offset will be at the left side of the input line +and retain the same direction. For a negative distance it'll be at the right +side and in the opposite direction. + + + +Availability: 2.0 - requires GEOS >= 3.2, improved with GEOS >= 3.3 + + + +The optional third parameter allows specifying a list of blank-separated +key=value pairs to tweak operations as follows: + + +'quad_segs=#' : number of segments used to approximate a quarter circle (defaults to 8). + + +'join=round|mitre|bevel' : join style (defaults to "round"). 'miter' is also accepted as a synonym for 'mitre'. + + +'mitre_limit=#.#' : mitre ratio limit (only affects mitred join style). 'miter_limit' is also accepted as a synonym for 'mitre_limit'. + + + + + +Units of distance are measured in units of the spatial reference system. + + + The inputs can only be LINESTRINGS. + + Performed by the GEOS module. + + +This function ignores the third dimension (z) and will always give a +2-d result even when presented with a 3d-geometry. + + + + + Examples +Compute an open buffer around roads + +SELECT ST_Union( + ST_OffsetCurve(f.the_geom, f.width/2, "quad_segs=4 join=round"), + ST_OffsetCurve(f.the_geom, -f.width/2, "quad_segs=4 join=round") +) as track +FROM someroadstable; + + + + + + See Also + + + + ST_BuildArea diff --git a/postgis/lwgeom_geos.c b/postgis/lwgeom_geos.c index ca8a46468..7afcd4407 100644 --- a/postgis/lwgeom_geos.c +++ b/postgis/lwgeom_geos.c @@ -46,6 +46,7 @@ Datum isvalid(PG_FUNCTION_ARGS); Datum isvalidreason(PG_FUNCTION_ARGS); Datum isvaliddetail(PG_FUNCTION_ARGS); Datum buffer(PG_FUNCTION_ARGS); +Datum offsetcurve(PG_FUNCTION_ARGS); Datum intersection(PG_FUNCTION_ARGS); Datum convexhull(PG_FUNCTION_ARGS); Datum topologypreservesimplify(PG_FUNCTION_ARGS); @@ -1376,6 +1377,200 @@ Datum buffer(PG_FUNCTION_ARGS) PG_RETURN_POINTER(result); } +PG_FUNCTION_INFO_V1(offsetcurve); +Datum offsetcurve(PG_FUNCTION_ARGS) +{ +#if POSTGIS_GEOS_VERSION >= 32 + PG_LWGEOM *geom1; + double size; + GEOSGeometry *g1, *g3; + PG_LWGEOM *result; + int quadsegs = 8; /* the default */ + int nargs; + + enum + { + JOIN_ROUND = 1, + JOIN_MITRE = 2, + JOIN_BEVEL = 3 + }; + static const double DEFAULT_MITRE_LIMIT = 5.0; + static const int DEFAULT_JOIN_STYLE = JOIN_ROUND; + + double mitreLimit = DEFAULT_MITRE_LIMIT; + int joinStyle = DEFAULT_JOIN_STYLE; + char *param; + char *params = NULL; + + + PROFSTART(PROF_QRUN); + // geom arg + geom1 = (PG_LWGEOM *) PG_DETOAST_DATUM(PG_GETARG_DATUM(0)); + // distance/size/direction arg + size = PG_GETARG_FLOAT8(1); + + /* + * For distance = 0 we just return the input. + * Note that due to a bug, GEOS 3.3.0 would return EMPTY. + * See http://trac.osgeo.org/geos/ticket/454 + */ + if ( size == 0 ) { + PG_RETURN_POINTER(geom1); + } + + + + nargs = PG_NARGS(); + + initGEOS(lwnotice, lwnotice); + initGEOS(lwnotice, lwgeom_geos_error); + + PROFSTART(PROF_P2G1); + g1 = (GEOSGeometry *)POSTGIS2GEOS(geom1); + if ( ! g1 ) { + lwerror("Geometry could not be converted to GEOS: %s", + lwgeom_geos_errmsg); + PG_RETURN_NULL(); + } + PROFSTOP(PROF_P2G1); + + // options arg (optional) + if (nargs > 2) + { + /* We strdup `cause we're going to modify it */ + params = pstrdup(PG_GETARG_CSTRING(2)); + + POSTGIS_DEBUGF(3, "Params: %s", params); + + for (param=params; ; param=NULL) + { + char *key, *val; + param = strtok(param, " "); + if ( param == NULL ) break; + POSTGIS_DEBUGF(3, "Param: %s", param); + + key = param; + val = strchr(key, '='); + if ( val == NULL || *(val+1) == '\0' ) + { + lwerror("Missing value for buffer " + "parameter %s", key); + break; + } + *val = '\0'; + ++val; + + POSTGIS_DEBUGF(3, "Param: %s : %s", key, val); + + if ( !strcmp(key, "join") ) + { + if ( !strcmp(val, "round") ) + { + joinStyle = JOIN_ROUND; + } + else if ( !strcmp(val, "mitre") || + !strcmp(val, "miter") ) + { + joinStyle = JOIN_MITRE; + } + else if ( !strcmp(val, "bevel") ) + { + joinStyle = JOIN_BEVEL; + } + else + { + lwerror("Invalid buffer end cap " + "style: %s (accept: " + "'round', 'mitre', 'miter' " + " or 'bevel'" + ")", val); + break; + } + } + else if ( !strcmp(key, "mitre_limit") || + !strcmp(key, "miter_limit") ) + { + /* mitreLimit is a float */ + mitreLimit = atof(val); + } + else if ( !strcmp(key, "quad_segs") ) + { + /* quadrant segments is an int */ + quadsegs = atoi(val); + } + else + { + lwerror("Invalid buffer parameter: %s (accept: " + "'join', 'mitre_limit', " + "'miter_limit and " + "'quad_segs')", key); + break; + } + } + + pfree(params); /* was pstrduped */ + + POSTGIS_DEBUGF(3, "joinStyle:%d mitreLimit:%g", + joinStyle, mitreLimit); + + } + + PROFSTART(PROF_GRUN); + +#if POSTGIS_GEOS_VERSION < 33 + g3 = GEOSSingleSidedBuffer(g1, size < 0 ? -size : size, + quadsegs, joinStyle, mitreLimit, + size < 0 ? 0 : 1); +#else + g3 = GEOSOffsetCurve(g1, size, quadsegs, joinStyle, mitreLimit); +#endif + PROFSTOP(PROF_GRUN); + + if (g3 == NULL) + { + lwerror("GEOSOffsetCurve: %s", lwgeom_geos_errmsg); + GEOSGeom_destroy(g1); + PG_RETURN_NULL(); /* never get here */ + } + + POSTGIS_DEBUGF(3, "result: %s", GEOSGeomToWKT(g3)); + + GEOSSetSRID(g3, pglwgeom_get_srid(geom1)); + + PROFSTART(PROF_G2P); + result = GEOS2POSTGIS(g3, pglwgeom_has_z(geom1)); + PROFSTOP(PROF_G2P); + + if (result == NULL) + { + GEOSGeom_destroy(g1); + GEOSGeom_destroy(g3); + lwerror("ST_OffsetCurve() threw an error (result postgis geometry formation)!"); + PG_RETURN_NULL(); /* never get here */ + } + GEOSGeom_destroy(g1); + GEOSGeom_destroy(g3); + + + /* compressType(result); */ + + PROFSTOP(PROF_QRUN); + PROFREPORT("geos",geom1, NULL, result); + + PG_FREE_IF_COPY(geom1, 0); + + PG_RETURN_POINTER(result); +#else /* POSTGIS_GEOS_VERSION < 32 */ + lwerror("The GEOS version this postgis binary " + "was compiled against (%d) doesn't support " + "ST_OffsetCurve function " + "(needs 3.2 or higher)", + POSTGIS_GEOS_VERSION); + PG_RETURN_NULL(); /* never get here */ +#endif /* POSTGIS_GEOS_VERSION < 32 */ +} + + PG_FUNCTION_INFO_V1(intersection); Datum intersection(PG_FUNCTION_ARGS) { diff --git a/postgis/postgis.sql.in.c b/postgis/postgis.sql.in.c index d0df5c4e2..f820bddd5 100644 --- a/postgis/postgis.sql.in.c +++ b/postgis/postgis.sql.in.c @@ -3086,6 +3086,13 @@ CREATE OR REPLACE FUNCTION ST_Buffer(geometry,float8,text) $$ LANGUAGE 'SQL' IMMUTABLE STRICT; +-- Availability: 2.0.0 - requires GEOS-3.2 or higher +CREATE OR REPLACE FUNCTION ST_OffsetCurve(line geometry, distance float8, params cstring DEFAULT '') + RETURNS geometry + AS 'MODULE_PATHNAME','offsetcurve' + LANGUAGE 'C' IMMUTABLE STRICT + COST 100; + -- PostGIS equivalent function: convexhull(geometry) CREATE OR REPLACE FUNCTION ST_ConvexHull(geometry) RETURNS geometry diff --git a/regress/Makefile.in b/regress/Makefile.in index aaf055a95..bb32890aa 100644 --- a/regress/Makefile.in +++ b/regress/Makefile.in @@ -96,6 +96,7 @@ ifeq ($(shell expr $(POSTGIS_GEOS_VERSION) ">=" 32),1) # ST_HausdorffDistance, ST_Buffer(params) TESTS += \ hausdorff \ + offsetcurve \ regress_buffer_params endif diff --git a/regress/offsetcurve.sql b/regress/offsetcurve.sql new file mode 100644 index 000000000..d9df12a38 --- /dev/null +++ b/regress/offsetcurve.sql @@ -0,0 +1,43 @@ +\set VERBOSITY terse +set client_min_messages to NOTICE; +SELECT 't0', ST_OffsetCurve('POINT(0 0)', 10); +SELECT 't0', ST_AsEWKT(ST_OffsetCurve('SRID=42;LINESTRING(0 0, 10 0)', 0)); +SELECT 't1', ST_AsEWKT(ST_OffsetCurve('SRID=42;LINESTRING(0 0, 10 0)', 10)); +SELECT 't2', ST_AsEWKT(ST_OffsetCurve('SRID=42;LINESTRING(0 0, 10 0)', -10)); +SELECT 't3', ST_AsEWKT(ST_OffsetCurve('SRID=42;LINESTRING(10 0, 0 0)', 10)); +SELECT 't4', ST_AsEWKT(ST_OffsetCurve('SRID=42;LINESTRING(10 0, 0 0)', -10)); +SELECT 't5', ST_AsEWKT(ST_SnapToGrid(ST_OffsetCurve( + 'SRID=42;LINESTRING(0 0, 10 0, 10 10)', -10), +1)); +SELECT 't6', ST_AsEWKT(ST_SnapToGrid(ST_OffsetCurve( + 'SRID=42;LINESTRING(0 0, 10 0, 10 10)', -10, + 'quad_segs=2'), +1)); +SELECT 't7', ST_AsEWKT(ST_OffsetCurve( + 'SRID=42;LINESTRING(0 0, 10 0, 10 10)', -10, + 'join=bevel') +); +SELECT 't8', ST_AsEWKT(ST_SnapToGrid(ST_OffsetCurve( + 'SRID=42;LINESTRING(0 0, 10 0, 10 10)', -10, + 'quad_segs=2 join=mitre'), +1)); +SELECT 't9', ST_AsEWKT(ST_SnapToGrid(ST_OffsetCurve( + 'SRID=42;LINESTRING(0 0, 10 0, 5 10)', -10, + 'quad_segs=2 join=mitre mitre_limit=1'), +1)); +SELECT 't10', ST_AsEWKT(ST_SnapToGrid(ST_OffsetCurve( + 'SRID=42;LINESTRING(0 0, 10 0, 5 10)', 2, + 'quad_segs=2 join=mitre mitre_limit=1'), +1)); +SELECT 't10b', ST_AsEWKT(ST_SnapToGrid(ST_OffsetCurve( + 'SRID=42;LINESTRING(0 0, 10 0, 5 10)', 2, + 'quad_segs=2 join=miter miter_limit=1'), +1)); +SELECT 't11', ST_AsEWKT(ST_SnapToGrid(ST_OffsetCurve( + 'LINESTRING(36 38,38 35,41 34,42 33,45 32,47 28,50 28,52 32,57 33)', 2, + 'join=mitre'), +0.2)); +SELECT 't12', ST_AsEWKT(ST_SnapToGrid(ST_OffsetCurve( + 'LINESTRING(36 38,38 35,41 34,42 33,45 32,47 28,50 28,52 32,57 33)', -2, + 'join=mitre'), +0.2)); diff --git a/regress/offsetcurve_expected b/regress/offsetcurve_expected new file mode 100644 index 000000000..878bc3cb6 --- /dev/null +++ b/regress/offsetcurve_expected @@ -0,0 +1,15 @@ +ERROR: GEOSOffsetCurve: IllegalArgumentException: BufferBuilder::bufferLineSingleSided only accept linestrings +t0|SRID=42;LINESTRING(0 0,10 0) +t1|SRID=42;LINESTRING(0 10,10 10) +t2|SRID=42;LINESTRING(10 -10,0 -10) +t3|SRID=42;LINESTRING(10 -10,0 -10) +t4|SRID=42;LINESTRING(0 10,10 10) +t5|SRID=42;LINESTRING(20 10,20 0,20 -2,19 -4,18 -6,17 -7,16 -8,14 -9,12 -10,10 -10,0 -10) +t6|SRID=42;LINESTRING(20 10,20 0,17 -7,10 -10,0 -10) +t7|SRID=42;LINESTRING(20 10,20 0,10 -10,0 -10) +t8|SRID=42;LINESTRING(20 10,20 -10,0 -10) +t9|SRID=42;LINESTRING(14 14,21 -1,16 -9,0 -10) +t10|SRID=42;LINESTRING(0 2,7 2,3 9) +t10b|SRID=42;LINESTRING(0 2,7 2,3 9) +t11|LINESTRING(37.6 39.2,39.2 36.6,42 35.8,43 34.8,46.4 33.6,48.2 30,48.8 30,50.6 33.8,56.6 35) +t12|LINESTRING(57.4 31,53.4 30.2,51.2 26,45.8 26,43.6 30.4,41 31.2,40 32.2,36.8 33.4,34.4 36.8) -- 2.50.1