From: Darafei Praliaskouski Date: Mon, 5 Mar 2018 16:54:13 +0000 (+0000) Subject: ST_OffsetCurve: support for MULTILINESTRING, GEOMETRYCOLLECTION and non-simple inputs X-Git-Tag: 2.5.0alpha~82 X-Git-Url: https://granicus.if.org/sourcecode?a=commitdiff_plain;h=bd44549184efbb56bf68015fcf7fe67a84194bee;p=postgis ST_OffsetCurve: support for MULTILINESTRING, GEOMETRYCOLLECTION and non-simple inputs Closes #2508 Closes https://github.com/postgis/postgis/pull/224 git-svn-id: http://svn.osgeo.org/postgis/trunk@16444 b70326c6-7e19-0410-871a-916f4a2858ee --- diff --git a/NEWS b/NEWS index 5e54a46c9..9d3683e80 100644 --- a/NEWS +++ b/NEWS @@ -37,10 +37,11 @@ PostGIS 2.5.0 - #3977, ST_ClusterKMeans is now faster and simpler (Darafei Praliaskouski) - #3982, ST_AsEncodedPolyline supports LINESTRING EMPTY and MULTIPOINT EMPTY (Darafei Praliaskouski) - - #3986, ST_AsText now has second argument to limit decimal digits + - #3986, ST_AsText now has second argument to limit decimal digits (Marc Ducobu, Darafei Praliaskouski) - #4020, Casting from box3d to geometry now returns correctly connected PolyhedralSurface (Matthias Bay) + - #2508, ST_OffsetCurve now works with collections (Darafei Praliaskouski) PostGIS 2.4.0 2017/09/30 diff --git a/liblwgeom/cunit/cu_clean.c b/liblwgeom/cunit/cu_clean.c index 5fb32c277..e3652c519 100644 --- a/liblwgeom/cunit/cu_clean.c +++ b/liblwgeom/cunit/cu_clean.c @@ -101,7 +101,7 @@ static void test_lwgeom_make_valid(void) /* CU_ASSERT_STRING_EQUAL(ewkt, "GEOMETRYCOLLECTION(POINT(0 0),MULTIPOLYGON(((5 5,0 0,0 10,5 5)),((5 5,10 10,10 0,5 5))),LINESTRING(10 0,10 10))");*/ gexp = lwgeom_from_wkt( -"GEOMETRYCOLLECTION(POINT(0 0),MULTIPOLYGON(((5 5,0 0,0 10,5 5)),((5 5,10 10,10 0,5 5))),LINESTRING(10 0,10 10))", +"GEOMETRYCOLLECTION(MULTIPOLYGON(((5 5,10 10,10 0,5 5)),((0 0,0 10,5 5,0 0))),LINESTRING(10 0,10 10),POINT(0 0))", LW_PARSER_CHECK_NONE); check_geom_equal(gout, gexp); lwfree(ewkt); diff --git a/liblwgeom/cunit/cu_geos.c b/liblwgeom/cunit/cu_geos.c index 3b3f5ec4d..6c56064ff 100644 --- a/liblwgeom/cunit/cu_geos.c +++ b/liblwgeom/cunit/cu_geos.c @@ -63,8 +63,6 @@ static void test_geos_noop(void) lwgeom_free(geom_out); lwgeom_free(geom_in); } - - } static void test_geos_linemerge(void) @@ -93,6 +91,23 @@ static void test_geos_linemerge(void) lwgeom_free(geom2); } +static void +test_geos_offsetcurve(void) +{ + char* ewkt; + char* out_ewkt; + LWGEOM* geom1; + LWGEOM* geom2; + + ewkt = "MULTILINESTRING((-10 0, -10 100), (0 -5, 0 0))"; + geom1 = lwgeom_from_wkt(ewkt, LW_PARSER_CHECK_NONE); + geom2 = lwgeom_offsetcurve(geom1, 2, 10, 1, 1); + out_ewkt = lwgeom_to_ewkt((LWGEOM*)geom2); + ASSERT_STRING_EQUAL(out_ewkt, "MULTILINESTRING((-12 0,-12 100),(-2 -5,-2 0))"); + lwfree(out_ewkt); + lwgeom_free(geom1); + lwgeom_free(geom2); +} static void test_geos_subdivide(void) { @@ -134,4 +149,5 @@ void geos_suite_setup(void) PG_ADD_TEST(suite, test_geos_noop); PG_ADD_TEST(suite, test_geos_subdivide); PG_ADD_TEST(suite, test_geos_linemerge); + PG_ADD_TEST(suite, test_geos_offsetcurve); } diff --git a/liblwgeom/liblwgeom.h.in b/liblwgeom/liblwgeom.h.in index dead6b6ee..5455a57ff 100644 --- a/liblwgeom/liblwgeom.h.in +++ b/liblwgeom/liblwgeom.h.in @@ -630,6 +630,7 @@ extern LWMPOLY* lwmpoly_add_lwpoly(LWMPOLY *mobj, const LWPOLY *obj); extern LWPSURFACE* lwpsurface_add_lwpoly(LWPSURFACE *mobj, const LWPOLY *obj); extern LWTIN* lwtin_add_lwtriangle(LWTIN *mobj, const LWTRIANGLE *obj); +extern LWCOLLECTION* lwcollection_concat_in_place(LWCOLLECTION* col1, const LWCOLLECTION* col2); /*********************************************************************** @@ -2290,16 +2291,15 @@ LWGEOM* lwgeom_sharedpaths(const LWGEOM* geom1, const LWGEOM* geom2); /* * An offset curve against the input line. * - * @param lwline a lineal geometry + * @param geom a lineal geometry or collection of them * @param size offset distance. Offset left if negative and right if positive * @param quadsegs number of quadrature segments in curves (try 8) * @param joinStyle (1 = round, 2 = mitre, 3 = bevel) * @param mitreLimit (try 5.0) * @return derived geometry (linestring or multilinestring) * - * Requires GEOS-3.2.0+ */ -LWGEOM* lwgeom_offsetcurve(const LWLINE *lwline, double size, int quadsegs, int joinStyle, double mitreLimit); +LWGEOM* lwgeom_offsetcurve(const LWGEOM *geom, double size, int quadsegs, int joinStyle, double mitreLimit); /* * Return true if the input geometry is "simple" as per OGC defn. diff --git a/liblwgeom/lwcollection.c b/liblwgeom/lwcollection.c index 6b59b3719..892bbb213 100644 --- a/liblwgeom/lwcollection.c +++ b/liblwgeom/lwcollection.c @@ -186,9 +186,10 @@ void lwcollection_reserve(LWCOLLECTION *col, uint32_t ngeoms) */ LWCOLLECTION* lwcollection_add_lwgeom(LWCOLLECTION *col, const LWGEOM *geom) { - if ( col == NULL || geom == NULL ) return NULL; + if (!col || !geom) return NULL; - if ( col->geoms == NULL && (col->ngeoms || col->maxgeoms) ) { + if (!col->geoms && (col->ngeoms || col->maxgeoms)) + { lwerror("Collection is in inconsistent state. Null memory but non-zero collection counts."); return NULL; } @@ -200,7 +201,7 @@ LWCOLLECTION* lwcollection_add_lwgeom(LWCOLLECTION *col, const LWGEOM *geom) } /* In case this is a truly empty, make some initial space */ - if ( col->geoms == NULL ) + if (!col->geoms) { col->maxgeoms = 2; col->ngeoms = 0; @@ -214,17 +215,16 @@ LWCOLLECTION* lwcollection_add_lwgeom(LWCOLLECTION *col, const LWGEOM *geom) /* See http://trac.osgeo.org/postgis/ticket/2933 */ /* Make sure we don't already have a reference to this geom */ { - int i = 0; - for ( i = 0; i < col->ngeoms; i++ ) - { - if ( col->geoms[i] == geom ) + uint32_t i = 0; + for (i = 0; i < col->ngeoms; i++) { - lwerror("%s [%d] found duplicate geometry in collection %p == %p", __FILE__, __LINE__, col->geoms[i], geom); - LWDEBUGF(4, "Found duplicate geometry in collection %p == %p", col->geoms[i], geom); - return col; + if (col->geoms[i] == geom) + { + lwerror("%s [%d] found duplicate geometry in collection %p == %p", __FILE__, __LINE__, col->geoms[i], geom); + return col; + } } } - } #endif col->geoms[col->ngeoms] = (LWGEOM*)geom; @@ -232,6 +232,20 @@ LWCOLLECTION* lwcollection_add_lwgeom(LWCOLLECTION *col, const LWGEOM *geom) return col; } +/** + * Appends all geometries from col2 to col1 in place. + * Caller is responsible to release col2. + */ +LWCOLLECTION * +lwcollection_concat_in_place(LWCOLLECTION *col1, const LWCOLLECTION *col2) +{ + uint32_t i; + if (!col1 || !col2) return NULL; + for (i = 0; i < col2->ngeoms; i++) + col1 = lwcollection_add_lwgeom(col1, col2->geoms[i]); + return col1; +} + LWCOLLECTION* lwcollection_segmentize2d(const LWCOLLECTION* col, double dist) { diff --git a/liblwgeom/lwgeom_geos.c b/liblwgeom/lwgeom_geos.c index daa925d30..254be6c4a 100644 --- a/liblwgeom/lwgeom_geos.c +++ b/liblwgeom/lwgeom_geos.c @@ -491,7 +491,11 @@ lwgeom_geos_version() inline static int32_t get_result_srid(const LWGEOM* geom1, const LWGEOM* geom2, const char* funcname) { - if (!geom1) lwerror("%s: First argument is null pointer", funcname); + if (!geom1) + { + lwerror("%s: First argument is null pointer", funcname); + return SRID_INVALID; + } if (geom2 && (geom1->srid != geom2->srid)) { lwerror("%s: Operation on mixed SRID geometries (%d != %d)", funcname, geom1->srid, geom2->srid); @@ -1206,8 +1210,8 @@ lwgeom_sharedpaths(const LWGEOM* geom1, const LWGEOM* geom2) return result; } -LWGEOM* -lwgeom_offsetcurve(const LWLINE* lwline, double size, int quadsegs, int joinStyle, double mitreLimit) +static LWGEOM * +lwline_offsetcurve(const LWLINE *lwline, double size, int quadsegs, int joinStyle, double mitreLimit) { LWGEOM* result; LWGEOM* geom = lwline_as_lwgeom(lwline); @@ -1223,13 +1227,116 @@ lwgeom_offsetcurve(const LWLINE* lwline, double size, int quadsegs, int joinStyl g3 = GEOSOffsetCurve(g1, size, quadsegs, joinStyle, mitreLimit); - if (!g3) return geos_clean_and_fail(g1, NULL, NULL, __func__); + if (!g3) + { + geos_clean(g1, NULL, NULL); + return NULL; + } if (!output_geos_as_lwgeom(&g3, &result, srid, is3d, __func__)) return geos_clean_and_fail(g1, NULL, g3, __func__); geos_clean(g1, NULL, g3); + return result; +} + +static LWGEOM * +lwcollection_offsetcurve(const LWCOLLECTION *col, double size, int quadsegs, int joinStyle, double mitreLimit) +{ + const LWGEOM *geom = lwcollection_as_lwgeom(col); + int32_t srid = get_result_srid(geom, NULL, __func__); + uint8_t is3d = FLAGS_GET_Z(col->flags); + LWCOLLECTION *result; + LWGEOM *tmp; + uint32_t i; + if (srid == SRID_INVALID) return NULL; + + result = lwcollection_construct_empty(MULTILINETYPE, srid, is3d, LW_FALSE); + + for (i = 0; i < col->ngeoms; i++) + { + tmp = lwgeom_offsetcurve(col->geoms[i], size, quadsegs, joinStyle, mitreLimit); + + if (!tmp) + { + lwcollection_free(result); + return NULL; + } + + if (!lwgeom_is_empty(tmp)) + { + if (lwgeom_is_collection(tmp)) + result = lwcollection_concat_in_place(result, lwgeom_as_lwcollection(tmp)); + else + result = lwcollection_add_lwgeom(result, tmp); + + if (!result) + { + lwgeom_free(tmp); + return NULL; + } + } + } + if (result->ngeoms == 1) + { + tmp = result->geoms[0]; + lwcollection_release(result); + return tmp; + } + else + return lwcollection_as_lwgeom(result); +} + +LWGEOM* +lwgeom_offsetcurve(const LWGEOM* geom, double size, int quadsegs, int joinStyle, double mitreLimit) +{ + int32_t srid = get_result_srid(geom, NULL, __func__); + LWGEOM *result = NULL; + LWGEOM *noded = NULL; + if (srid == SRID_INVALID) return NULL; + + if (lwgeom_dimension(geom) != 1) + { + lwerror("%s: input is not linear", __func__, lwtype_name(geom->type)); + return NULL; + } + + while (!result) + { + switch (geom->type) + { + case LINETYPE: + result = lwline_offsetcurve(lwgeom_as_lwline(geom), size, quadsegs, joinStyle, mitreLimit); + break; + case COLLECTIONTYPE: + case MULTILINETYPE: + result = lwcollection_offsetcurve(lwgeom_as_lwcollection(geom), size, quadsegs, joinStyle, mitreLimit); + break; + default: + lwerror("%s: unsupported geometry type: %s", __func__, lwtype_name(geom->type)); + return NULL; + } + + if (result) + return result; + else if (!noded) + { + noded = lwgeom_node(geom); + if (!noded) + { + lwfree(noded); + lwerror("lwgeom_offsetcurve: cannot node input"); + return NULL; + } + geom = noded; + } + else + { + lwerror("lwgeom_offsetcurve: noded geometry cannot be offset"); + return NULL; + } + } return result; } @@ -1334,19 +1441,17 @@ lwpoly_to_points(const LWPOLY* lwpoly, uint32_t npoints) } /* shuffle */ + n = sample_height * sample_width; + if (n > 1) { - n = sample_height * sample_width; - if (n > 1) + for (i = 0; i < n - 1; ++i) { - for (i = 0; i < n - 1; ++i) - { - size_t rnd = (size_t)rand(); - size_t j = i + rnd / (RAND_MAX / (n - i) + 1); + size_t rnd = (size_t)rand(); + size_t j = i + rnd / (RAND_MAX / (n - i) + 1); - memcpy(tmp, (char*)cells + j * stride, size); - memcpy((char*)cells + j * stride, (char*)cells + i * stride, size); - memcpy((char*)cells + i * stride, tmp, size); - } + memcpy(tmp, (char *)cells + j * stride, size); + memcpy((char *)cells + j * stride, (char *)cells + i * stride, size); + memcpy((char *)cells + i * stride, tmp, size); } } diff --git a/liblwgeom/lwgeom_geos.h b/liblwgeom/lwgeom_geos.h index a2d28e558..9845cff11 100644 --- a/liblwgeom/lwgeom_geos.h +++ b/liblwgeom/lwgeom_geos.h @@ -35,13 +35,14 @@ LWGEOM* GEOS2LWGEOM(const GEOSGeometry* geom, uint8_t want3d); GEOSGeometry* LWGEOM2GEOS(const LWGEOM* g, uint8_t autofix); GEOSGeometry* GBOX2GEOS(const GBOX* g); GEOSGeometry* LWGEOM_GEOS_buildArea(const GEOSGeometry* geom_in); +GEOSGeometry* LWGEOM_GEOS_makeValid(const GEOSGeometry*); GEOSGeometry* make_geos_point(double x, double y); GEOSGeometry* make_geos_segment(double x1, double y1, double x2, double y2); -int cluster_intersecting(GEOSGeometry** geoms, uint32_t num_geoms, GEOSGeometry*** clusterGeoms, uint32_t* num_clusters); -int cluster_within_distance(LWGEOM** geoms, uint32_t num_geoms, double tolerance, LWGEOM*** clusterGeoms, uint32_t* num_clusters); -int union_dbscan(LWGEOM** geoms, uint32_t num_geoms, UNIONFIND* uf, double eps, uint32_t min_points, char** is_in_cluster_ret); +int cluster_intersecting(GEOSGeometry **geoms, uint32_t num_geoms, GEOSGeometry ***clusterGeoms, uint32_t *num_clusters); +int cluster_within_distance(LWGEOM **geoms, uint32_t num_geoms, double tolerance, LWGEOM ***clusterGeoms, uint32_t *num_clusters); +int union_dbscan(LWGEOM **geoms, uint32_t num_geoms, UNIONFIND *uf, double eps, uint32_t min_points, char **is_in_cluster_ret); POINTARRAY* ptarray_from_GEOSCoordSeq(const GEOSCoordSequence* cs, uint8_t want3d); diff --git a/liblwgeom/lwgeom_geos_clean.c b/liblwgeom/lwgeom_geos_clean.c index 53bc6b0d7..30a52b12d 100644 --- a/liblwgeom/lwgeom_geos_clean.c +++ b/liblwgeom/lwgeom_geos_clean.c @@ -321,34 +321,28 @@ static GEOSGeometry* LWGEOM_GEOS_nodeLines(const GEOSGeometry* lines) { GEOSGeometry* noded; - GEOSGeometry* point; - /* - * Union with first geometry point, obtaining full noding - * and dissolving of duplicated repeated points - * - * TODO: substitute this with UnaryUnion? - */ - - point = LWGEOM_GEOS_getPointN(lines, 0); - if (!point) return NULL; + /* first, try just to node the line */ + noded = GEOSNode(lines); + if (!noded) noded = (GEOSGeometry *)lines; - LWDEBUGF(3, "Boundary point: %s", lwgeom_to_ewkt(GEOS2LWGEOM(point, 0))); + /* GEOS3.7 UnaryUnion fails on regression tests, cannot be used here */ - noded = GEOSUnion(lines, point); - if (NULL == noded) + /* fall back to union of first point with geometry */ + if (!GEOSisValid(noded)) { - GEOSGeom_destroy(point); - return NULL; + GEOSGeometry *unioned, *point; + point = LWGEOM_GEOS_getPointN(lines, 0); + if (!point) return NULL; + unioned = GEOSUnion(noded, point); + if (!unioned) + return NULL; + else + { + GEOSGeom_destroy(noded); + return unioned; + } } - - GEOSGeom_destroy(point); - - LWDEBUGF(3, - "LWGEOM_GEOS_nodeLines: in[%s] out[%s]", - lwgeom_to_ewkt(GEOS2LWGEOM(lines, 0)), - lwgeom_to_ewkt(GEOS2LWGEOM(noded, 0))); - return noded; } @@ -712,8 +706,6 @@ LWGEOM_GEOS_makeValidMultiLine(const GEOSGeometry* gin) return gout; } -static GEOSGeometry* LWGEOM_GEOS_makeValid(const GEOSGeometry*); - /* * We expect initGEOS being called already. * Will return NULL on error (expect error handler being called by then) @@ -770,7 +762,7 @@ LWGEOM_GEOS_makeValidCollection(const GEOSGeometry* gin) return gout; } -static GEOSGeometry* +GEOSGeometry* LWGEOM_GEOS_makeValid(const GEOSGeometry* gin) { GEOSGeometry* gout; @@ -919,7 +911,7 @@ lwgeom_make_valid(LWGEOM* lwgeom_in) initGEOS(lwgeom_geos_error, lwgeom_geos_error); lwgeom_out = lwgeom_in; - geosgeom = LWGEOM2GEOS(lwgeom_out, 0); + geosgeom = LWGEOM2GEOS(lwgeom_out, 1); if (!geosgeom) { LWDEBUGF(4, diff --git a/liblwgeom/lwgeom_geos_node.c b/liblwgeom/lwgeom_geos_node.c index c94fae72e..8e5e8c3bc 100644 --- a/liblwgeom/lwgeom_geos_node.c +++ b/liblwgeom/lwgeom_geos_node.c @@ -244,7 +244,7 @@ lwgeom_node(const LWGEOM* lwgeom_in) lwgeom_free(ep); lwcollection_free(col); - lines->srid = lwgeom_in->srid; + lwgeom_set_srid(lines, lwgeom_in->srid); return (LWGEOM*)lines; } diff --git a/liblwgeom/lwlinearreferencing.c b/liblwgeom/lwlinearreferencing.c index e6eead381..4306eba3a 100644 --- a/liblwgeom/lwlinearreferencing.c +++ b/liblwgeom/lwlinearreferencing.c @@ -822,7 +822,7 @@ lwgeom_clip_to_ordinate_range(const LWGEOM *lwin, char ordinate, double from, do else if ( type == LINETYPE ) { /* lwgeom_offsetcurve(line, offset, quadsegs, joinstyle (round), mitrelimit) */ - LWGEOM *lwoff = lwgeom_offsetcurve(lwgeom_as_lwline(out_col->geoms[i]), offset, 8, 1, 5.0); + LWGEOM *lwoff = lwgeom_offsetcurve(out_col->geoms[i], offset, 8, 1, 5.0); if ( ! lwoff ) { lwerror("lwgeom_offsetcurve returned null"); diff --git a/postgis/lwgeom_geos.c b/postgis/lwgeom_geos.c index 43d091c42..82a3a1832 100644 --- a/postgis/lwgeom_geos.c +++ b/postgis/lwgeom_geos.c @@ -1112,13 +1112,6 @@ Datum ST_OffsetCurve(PG_FUNCTION_ARGS) gser_input = PG_GETARG_GSERIALIZED_P(0); size = PG_GETARG_FLOAT8(1); - /* Check for a useable type */ - if ( gserialized_get_type(gser_input) != LINETYPE ) - { - lwpgerror("ST_OffsetCurve only works with LineStrings"); - PG_RETURN_NULL(); - } - /* * For distance == 0, just return the input. * Note that due to a bug, GEOS 3.3.0 would return EMPTY. @@ -1207,7 +1200,7 @@ Datum ST_OffsetCurve(PG_FUNCTION_ARGS) pfree(paramstr); /* alloc'ed in text_to_cstring */ } - lwgeom_result = lwgeom_offsetcurve(lwgeom_as_lwline(lwgeom_input), size, quadsegs, joinStyle, mitreLimit); + lwgeom_result = lwgeom_offsetcurve(lwgeom_input, size, quadsegs, joinStyle, mitreLimit); if (!lwgeom_result) lwpgerror("ST_OffsetCurve: lwgeom_offsetcurve returned NULL"); diff --git a/regress/offsetcurve.sql b/regress/offsetcurve.sql index f8acbdd32..723391856 100644 --- a/regress/offsetcurve.sql +++ b/regress/offsetcurve.sql @@ -51,3 +51,11 @@ SELECT 't14', ST_AsEWKT(ST_OffsetCurve( 'LINESTRING(0 0,0 20, 10 20, 10 10, 0 10)', -2, '' )); +SELECT 't15', ST_AsEWKT(ST_OffsetCurve( + 'GEOMETRYCOLLECTION(LINESTRING(0 0,0 20, 10 20, 10 10, 0 10),MULTILINESTRING((2 0,2 20, 12 20, 12 10, 2 10),(3 0,3 20, 13 20, 13 10, 3 10)))', -2, + '' +)); +select '#2508', ST_IsValid(ST_OffsetCurve( + '0102000020BB0B000010000000FBB019D1AD1537414A733C4E5333534167CE8F06B815374151F4926C4D335341C4899405B61537413DB009254A335341513EE234AD1537413689A27947335341E38CCA31AB1537415D00E28E44335341951F7F0BB315374104E4CA2441335341A581F041BF153741D46F9F8A3F33534100C27968CD153741C6CAAFE83F335341493DB10CDA1537418919897142335341FCA312FCE01537415D1A1F8045335341C62D3822DD153741554B118E483353411B98FE61D1153741FC35CEE14A33534106DCFDA5C5153741573BD3584B33534167CE8F06B815374151F4926C4D335341FBB019D1AD1537414A733C4E533353414AEB33644E153741595A854786335341', + 10 +)); diff --git a/regress/offsetcurve_expected b/regress/offsetcurve_expected index f70e5db90..cc2d0afbc 100644 --- a/regress/offsetcurve_expected +++ b/regress/offsetcurve_expected @@ -1,4 +1,4 @@ -ERROR: ST_OffsetCurve only works with LineStrings +ERROR: lwgeom_offsetcurve: input is not linear t0|SRID=42;LINESTRING(0 0,10 0) t1|SRID=42;LINESTRING(0 10,10 10) t2|SRID=42;LINESTRING(10 -10,0 -10) @@ -16,3 +16,5 @@ 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. 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) t13|LINESTRING(-2 0,-2 22,12 22,12 8,2 8) t14|MULTILINESTRING((2 12,8 12,8 18,2 18,2 12),(2 8,2 0)) +t15|MULTILINESTRING((2 12,8 12,8 18,2 18,2 12),(2 8,2 0),(4 12,10 12,10 18,4 18,4 12),(4 8,4 0),(5 12,11 12,11 18,5 18,5 12),(5 8,5 0)) +#2508|t