From b5f9bd6c9ad90a8bea7a40379f6ed8adf1b94486 Mon Sep 17 00:00:00 2001 From: Sandro Santilli Date: Tue, 18 Aug 2015 18:19:51 +0000 Subject: [PATCH] Implement TopoGeo_addPoint in C Funded by Tuscany Region (Italy) - SITA (CIG: 60351023B8) git-svn-id: http://svn.osgeo.org/postgis/trunk@13936 b70326c6-7e19-0410-871a-916f4a2858ee --- liblwgeom/TODO | 2 +- liblwgeom/liblwgeom_topo.h | 47 ++---- liblwgeom/lwgeom_topo.c | 281 +++++++++++++++++++++++++++++++++++ topology/postgis_topology.c | 94 +++++++++++- topology/sql/populate.sql.in | 106 +------------ 5 files changed, 388 insertions(+), 142 deletions(-) diff --git a/liblwgeom/TODO b/liblwgeom/TODO index 40dabd61c..ceca0b24c 100644 --- a/liblwgeom/TODO +++ b/liblwgeom/TODO @@ -25,6 +25,6 @@ lwt_NewEdgeHeal X lwt_GetFaceByPoint X lwt_GetNodeByPoint X lwt_GetEdgeByPoint X -lwt_TopoGeo_AddPoint +lwt_TopoGeo_AddPoint X lwt_TopoGeo_AddLineString lwt_TopoGeo_AddPolygon diff --git a/liblwgeom/liblwgeom_topo.h b/liblwgeom/liblwgeom_topo.h index 60548788e..98399ddcf 100644 --- a/liblwgeom/liblwgeom_topo.h +++ b/liblwgeom/liblwgeom_topo.h @@ -846,14 +846,6 @@ void lwt_FreeBackendIface(LWT_BE_IFACE* iface); * *******************************************************************/ - -/** Element of a TopoGeometry */ -typedef struct LWT_TOPOELEMENT_T { - LWT_ELEMID id; /* primitive or topogeometry id */ - /** primitive type (0:node, 1:edge, 2:face) or layer id */ - int type; -} LWT_TOPOELEMENT; - /** * Topology errors type */ @@ -975,6 +967,7 @@ LWT_ELEMID lwt_GetEdgeByPoint(LWT_TOPOLOGY *topo, LWPOINT *pt, double tol); */ LWT_ELEMID lwt_GetFaceByPoint(LWT_TOPOLOGY *topo, LWPOINT *pt, double tol); + /******************************************************************* * * Topology population (non-ISO) @@ -991,7 +984,8 @@ LWT_ELEMID lwt_GetFaceByPoint(LWT_TOPOLOGY *topo, LWPOINT *pt, double tol); * @param point the point to add * @param tol snap tolerance, the topology tolerance will be used if 0 * - * @return identifier of added (or pre-existing) node + * @return identifier of added (or pre-existing) node or -1 on error + * (liblwgeom error handler will be invoked with error message) */ LWT_ELEMID lwt_AddPoint(LWT_TOPOLOGY* topo, LWPOINT* point, double tol); @@ -1005,11 +999,12 @@ LWT_ELEMID lwt_AddPoint(LWT_TOPOLOGY* topo, LWPOINT* point, double tol); * @param line the line to add * @param tol snap tolerance, the topology tolerance will be used if 0 * @param nedges output parameter, will be set to number of edges the - * line was split into + * line was split into, or -1 on error + * (liblwgeom error handler will be invoked with error message) * * @return an array of edge identifiers that sewed togheter * will build up the input linestring (after snapping). Caller - * will need to free the array using lwfree() + * will need to free the array using lwfree(), if not null. */ LWT_ELEMID* lwt_AddLine(LWT_TOPOLOGY* topo, LWLINE* line, double tol, int* nedges); @@ -1025,37 +1020,16 @@ LWT_ELEMID* lwt_AddLine(LWT_TOPOLOGY* topo, LWLINE* line, double tol, * @param poly the polygon to add * @param tol snap tolerance, the topology tolerance will be used if 0 * @param nfaces output parameter, will be set to number of faces the - * polygon was split into + * polygon was split into, or -1 on error + * (liblwgeom error handler will be invoked with error message) * * @return an array of face identifiers that sewed togheter * will build up the input polygon (after snapping). Caller - * will need to free the array using lwfree() + * will need to free the array using lwfree(), if not null. */ LWT_ELEMID* lwt_AddPolygon(LWT_TOPOLOGY* topo, LWPOLY* point, double tol, int* nfaces); -/** - * Adds a geometry to the topology - * - * The given geometry will snap to existing nodes or - * edges within given tolerance. - * Existing edges or faces may be split by the operation. - * - * @param topo the topology to operate on - * @param geom the geometry to add - * @param tol snap tolerance, the topology tolerance will be used if 0 - * @param nelems output parameter, will be set to number of primitive - * elements the geometry was split into. - * - * @return an array of topoelements that taken togheter - * will build up the input geometry (after snapping). Caller - * will need to free the array using lwfree() - * - * @see lwt_CreateTopoGeom - */ -LWT_TOPOELEMENT* lwt_AddGeometry(LWT_TOPOLOGY* topo, LWGEOM* geom, - double tol, int* nelems); - /******************************************************************* * * ISO signatures here @@ -1085,7 +1059,8 @@ void lwt_CreateTopoGeo(LWT_TOPOLOGY* topo, LWGEOM *geom); * (coincident nodes, crossing edges, * actual face containement) * - * @return ID of the newly added node + * @return ID of the newly added node, or -1 on error + * (liblwgeom error handler will be invoked with error message) * */ LWT_ELEMID lwt_AddIsoNode(LWT_TOPOLOGY* topo, LWT_ELEMID face, diff --git a/liblwgeom/lwgeom_topo.c b/liblwgeom/lwgeom_topo.c index 14894abd3..673c6ea19 100644 --- a/liblwgeom/lwgeom_topo.c +++ b/liblwgeom/lwgeom_topo.c @@ -28,6 +28,7 @@ #include #include /* for PRId64 */ #include +#include #ifdef WIN32 # define LWTFMT_ELEMID "lld" @@ -4583,3 +4584,283 @@ lwt_GetFaceByPoint(LWT_TOPOLOGY *topo, LWPOINT *pt, double tol) return id; } + +/* Return the smallest delta that can perturbate + * the maximum absolute value of a geometry ordinate + */ +static double +_lwt_minTolerance( LWGEOM *g ) +{ + const GBOX* gbox; + double max; + + gbox = lwgeom_get_bbox(g); + if ( ! gbox ) return 0; /* empty */ + max = FP_ABS(gbox->xmin); + if ( max < FP_ABS(gbox->xmax) ) max = FP_ABS(gbox->xmax); + if ( max < FP_ABS(gbox->ymin) ) max = FP_ABS(gbox->ymin); + if ( max < FP_ABS(gbox->ymax) ) max = FP_ABS(gbox->ymax); + + return 3.6 * pow(10, - ( 15 - log(max?max:1.0) ) ); +} + +#define _LWT_MINTOLERANCE( topo, geom ) ( \ + topo->precision ? topo->precision : _lwt_minTolerance(geom) ) + +LWT_ELEMID +lwt_AddPoint(LWT_TOPOLOGY* topo, LWPOINT* point, double tol) +{ + int num, i; + double mindist; + LWT_ISO_NODE *nodes; + LWT_ISO_EDGE *edges; + LWGEOM *pt = lwpoint_as_lwgeom(point); + int flds; + LWT_ELEMID id = 0; + + if ( ! tol ) tol = _LWT_MINTOLERANCE( topo, pt ); + + /* + -- 1. Check if any existing node is closer than the given precision + -- and if so pick the closest + TODO: use WithinBox2D + */ + flds = LWT_COL_NODE_NODE_ID|LWT_COL_NODE_GEOM; + nodes = lwt_be_getNodeWithinDistance2D(topo, point, tol, &num, flds, 0); + if ( num == -1 ) + { + lwerror("Backend error: %s", lwt_be_lastErrorMessage(topo->be_iface)); + return -1; + } + for ( i=0; igeom); + double dist = lwgeom_mindistance2d(g, pt); + if ( dist >= tol ) continue; /* must be closer than tolerated */ + if ( ! id || dist < mindist ) + { + id = n->node_id; + mindist = dist; + } + } + if ( id ) + { + /* found an existing node */ + if ( nodes ) _lwt_release_nodes(nodes, num); + return id; + } + + initGEOS(lwnotice, lwgeom_geos_error); + + /* + -- 2. Check if any existing edge falls within tolerance + -- and if so split it by a point projected on it + TODO: use WithinBox2D + */ + flds = LWT_COL_EDGE_EDGE_ID|LWT_COL_EDGE_GEOM; + edges = lwt_be_getEdgeWithinDistance2D(topo, point, tol, &num, flds, 1); + if ( num == -1 ) + { + lwerror("Backend error: %s", lwt_be_lastErrorMessage(topo->be_iface)); + return -1; + } + if ( num ) + {{ + /* The point is on or near an edge, split the edge */ + + LWT_ISO_EDGE *e = &(edges[0]); + LWGEOM *g = lwline_as_lwgeom(e->geom); + LWGEOM *prj; + int contains; + GEOSGeometry *prjg, *gg; + + LWDEBUGF(1, "Splitting edge %" LWTFMT_ELEMID, e->edge_id); + + /* project point to line, split edge by point */ + prj = lwgeom_closest_point(g, pt); + if ( lwgeom_has_z(pt) ) + {{ + /* + -- This is a workaround for ClosestPoint lack of Z support: + -- http://trac.osgeo.org/postgis/ticket/2033 + */ + LWGEOM *tmp; + double z; + POINT4D p4d; + LWPOINT *prjpt; + /* add Z to "prj" */ + tmp = lwgeom_force_3dz(prj); + prjpt = lwgeom_as_lwpoint(tmp); + getPoint4d_p(point->point, 0, &p4d); + z = p4d.z; + getPoint4d_p(prjpt->point, 0, &p4d); + p4d.z = z; + ptarray_set_point4d(prjpt->point, 0, &p4d); + lwgeom_free(prj); + prj = tmp; + }} + prjg = LWGEOM2GEOS(prj, 0); + if ( ! prjg ) { + lwgeom_free(prj); + _lwt_release_edges(edges, num); + lwerror("Could not convert edge geometry to GEOS: %s", lwgeom_geos_errmsg); + return -1; + } + gg = LWGEOM2GEOS(g, 0); + if ( ! gg ) { + lwgeom_free(prj); + _lwt_release_edges(edges, num); + GEOSGeom_destroy(prjg); + lwerror("Could not convert edge geometry to GEOS: %s", lwgeom_geos_errmsg); + return -1; + } + contains = GEOSContains(gg, prjg); + GEOSGeom_destroy(prjg); + GEOSGeom_destroy(gg); + if ( contains == 2 ) + { + lwgeom_free(prj); + _lwt_release_edges(edges, num); + lwerror("GEOS exception on Contains: %s", lwgeom_geos_errmsg); + return -1; + } + if ( ! contains ) + {{ + double snaptol; + LWGEOM *snapedge; + LWLINE *snapline; + POINT4D p1, p2; + + LWDEBUGF(1, "Edge %" LWTFMT_ELEMID + " does not contain projected point to it", + e->edge_id); + + /* + -- The tolerance must be big enough for snapping to happen + -- and small enough to snap only to the projected point. + -- Unfortunately ST_Distance returns 0 because it also uses + -- a projected point internally, so we need another way. + */ + snaptol = _lwt_minTolerance(prj); + snapedge = lwgeom_snap(g, prj, snaptol); + snapline = lwgeom_as_lwline(snapedge); + + LWDEBUGF(1, "Edge snapped with tolerance %g", snaptol); + + /* TODO: check if snapping did anything ? */ +#if POSTGIS_DEBUG_LEVEL > 0 + { + size_t sz; + char *wkt1 = lwgeom_to_wkt(g, WKT_ISO, 15, &sz); + char *wkt2 = lwgeom_to_wkt(snapedge, WKT_ISO, 15, &sz); + LWDEBUGF(1, "Edge %s snapped became %s", wkt1, wkt2); + lwfree(wkt1); + lwfree(wkt2); + } +#endif + + + /* + -- Snapping currently snaps the first point below tolerance + -- so may possibly move first point. See ticket #1631 + */ + getPoint4d_p(e->geom->points, 0, &p1); + getPoint4d_p(snapline->points, 0, &p2); + LWDEBUGF(1, "Edge first point is %g %g, " + "snapline first point is %g %g", + p1.x, p1.y, p2.x, p2.y); + if ( p1.x != p2.x || p1.y != p2.y ) + { + LWDEBUG(1, "Snapping moved first point, re-adding it"); + if ( LW_SUCCESS != ptarray_insert_point(snapline->points, &p1, 0) ) + { + lwgeom_free(prj); + lwgeom_free(snapedge); + _lwt_release_edges(edges, num); + lwerror("GEOS exception on Contains: %s", lwgeom_geos_errmsg); + return -1; + } +#if POSTGIS_DEBUG_LEVEL > 0 + { + size_t sz; + char *wkt1 = lwgeom_to_wkt(g, WKT_ISO, 15, &sz); + LWDEBUGF(1, "Tweaked snapline became %s", wkt1); + lwfree(wkt1); + } +#endif + } +#if POSTGIS_DEBUG_LEVEL > 0 + else { + LWDEBUG(1, "Snapping did not move first point"); + } +#endif + + if ( -1 == lwt_ChangeEdgeGeom( topo, e->edge_id, snapline ) ) + { + /* TODO: should have invoked lwerror already, leaking memory */ + lwgeom_free(prj); + lwgeom_free(snapedge); + _lwt_release_edges(edges, num); + lwerror("lwt_ChangeEdgeGeom failed"); + return -1; + } + lwgeom_free(snapedge); + }} +#if POSTGIS_DEBUG_LEVEL > 0 + else + {{ + size_t sz; + char *wkt1 = lwgeom_to_wkt(g, WKT_ISO, 15, &sz); + char *wkt2 = lwgeom_to_wkt(prj, WKT_ISO, 15, &sz); + LWDEBUGF(1, "Edge %s contains projected point %s", wkt1, wkt2); + lwfree(wkt1); + lwfree(wkt2); + }} +#endif + + /* TODO: pass 1 as last argument (skipChecks) ? */ + id = lwt_ModEdgeSplit( topo, e->edge_id, lwgeom_as_lwpoint(prj), 0 ); + if ( -1 == id ) + { + /* TODO: should have invoked lwerror already, leaking memory */ + lwgeom_free(prj); + _lwt_release_edges(edges, num); + lwerror("lwt_ModEdgeSplit failed"); + return -1; + } + + lwgeom_free(prj); + _lwt_release_edges(edges, num); + }} + else + { + /* The point is isolated, add it as such */ + /* TODO: pass 1 as last argument (skipChecks) ? */ + id = lwt_AddIsoNode(topo, -1, point, 0); + if ( -1 == id ) + { + /* should have invoked lwerror already, leaking memory */ + lwerror("lwt_AddIsoNode failed"); + return -1; + } + } + + return id; +} + +LWT_ELEMID* +lwt_AddLine(LWT_TOPOLOGY* topo, LWLINE* line, double tol, int* nedges) +{ + *nedges = -1; + lwerror("Not implemented yet"); + return NULL; +} + +LWT_ELEMID* +lwt_AddPolygon(LWT_TOPOLOGY* topo, LWPOLY* point, double tol, int* nfaces) +{ + *nfaces = -1; + lwerror("Not implemented yet"); + return NULL; +} diff --git a/topology/postgis_topology.c b/topology/postgis_topology.c index 6aab4981e..ae55fe01a 100644 --- a/topology/postgis_topology.c +++ b/topology/postgis_topology.c @@ -61,6 +61,8 @@ struct LWT_BE_DATA_T { * doring operations. */ bool data_changed; + + int topoLoadFailMessageFlavor; /* 0:sql, 1:AddPoint */ }; LWT_BE_DATA be_data; @@ -125,7 +127,11 @@ cb_loadTopologyByName(const LWT_BE_DATA* be, const char *name) { pfree(sqldata.data); //cberror(be, "no topology named '%s' was found", name); - cberror(be, "SQL/MM Spatial exception - invalid topology name"); + if ( be->topoLoadFailMessageFlavor == 1 ) { + cberror(be, "No topology with name \"%s\" in topology.topology", name); + } else { + cberror(be, "SQL/MM Spatial exception - invalid topology name"); + } return NULL; } if ( SPI_processed > 1 ) @@ -2656,6 +2662,10 @@ _PG_init(void) * is valid for the whole backend lifetime */ old_context = MemoryContextSwitchTo( TopMemoryContext ); + /* initialize backend data */ + be_data.data_changed = false; + be_data.topoLoadFailMessageFlavor = 0; + /* register callbacks against liblwgeom-topo */ be_iface = lwt_CreateBackendIface(&be_data); lwt_BackendIfaceRegisterCallbacks(be_iface, &be_callbacks); @@ -3912,3 +3922,85 @@ Datum GetFaceByPoint(PG_FUNCTION_ARGS) SPI_finish(); PG_RETURN_INT32(node_id); } + +/* TopoGeo_AddPoint(atopology, point, tolerance) */ +Datum TopoGeo_AddPoint(PG_FUNCTION_ARGS); +PG_FUNCTION_INFO_V1(TopoGeo_AddPoint); +Datum TopoGeo_AddPoint(PG_FUNCTION_ARGS) +{ + text* toponame_text; + char* toponame; + double tol; + LWT_ELEMID node_id; + GSERIALIZED *geom; + LWGEOM *lwgeom; + LWPOINT *pt; + LWT_TOPOLOGY *topo; + + toponame_text = PG_GETARG_TEXT_P(0); + toponame = text2cstring(toponame_text); + PG_FREE_IF_COPY(toponame_text, 0); + + geom = PG_GETARG_GSERIALIZED_P(1); + lwgeom = lwgeom_from_gserialized(geom); + pt = lwgeom_as_lwpoint(lwgeom); + if ( ! pt ) {{ + char buf[32]; + char *ptr; + snprintf(buf, 32, "%s", lwtype_name(lwgeom_get_type(lwgeom))); + buf[31] = '\0'; + ptr = buf; + while (*ptr) { + *ptr = toupper(*ptr); + ++ptr; + } + lwgeom_free(lwgeom); + PG_FREE_IF_COPY(geom, 1); + lwpgerror("Invalid geometry type (%s) passed to TopoGeo_AddPoint" + ", expected POINT", buf ); + PG_RETURN_NULL(); + }} + + tol = PG_GETARG_FLOAT8(2); + if ( tol < 0 ) + { + PG_FREE_IF_COPY(geom, 1); + lwpgerror("Tolerance must be >=0"); + PG_RETURN_NULL(); + } + + if ( SPI_OK_CONNECT != SPI_connect() ) { + lwpgerror("Could not connect to SPI"); + PG_RETURN_NULL(); + } + be_data.data_changed = false; + + { + int pre = be_data.topoLoadFailMessageFlavor; + be_data.topoLoadFailMessageFlavor = 1; + topo = lwt_LoadTopology(be_iface, toponame); + be_data.topoLoadFailMessageFlavor = pre; + } + pfree(toponame); + if ( ! topo ) { + /* should never reach this point, as lwerror would raise an exception */ + SPI_finish(); + PG_RETURN_NULL(); + } + + POSTGIS_DEBUG(1, "Calling lwt_AddPoint"); + node_id = lwt_AddPoint(topo, pt, tol); + POSTGIS_DEBUG(1, "lwt_AddPoint returned"); + lwgeom_free(lwgeom); + PG_FREE_IF_COPY(geom, 1); + lwt_FreeTopology(topo); + + if ( node_id == -1 ) { + /* should never reach this point, as lwerror would raise an exception */ + SPI_finish(); + PG_RETURN_NULL(); + } + + SPI_finish(); + PG_RETURN_INT32(node_id); +} diff --git a/topology/sql/populate.sql.in b/topology/sql/populate.sql.in index 9eae6bb89..0619960f6 100644 --- a/topology/sql/populate.sql.in +++ b/topology/sql/populate.sql.in @@ -667,110 +667,8 @@ LANGUAGE 'plpgsql' VOLATILE; -- CREATE OR REPLACE FUNCTION topology.TopoGeo_AddPoint(atopology varchar, apoint geometry, tolerance float8 DEFAULT 0) RETURNS int AS -$$ -DECLARE - id integer; - rec RECORD; - sql text; - prj GEOMETRY; - snapedge GEOMETRY; - snaptol FLOAT8; - tol FLOAT8; - z FLOAT8; -BEGIN - - -- 0. Check arguments - IF geometrytype(apoint) != 'POINT' THEN - RAISE EXCEPTION 'Invalid geometry type (%) passed to TopoGeo_AddPoint, expected POINT', geometrytype(apoint); - END IF; - - -- Get tolerance, if 0 was given - tol := COALESCE( NULLIF(tolerance, 0), topology._st_mintolerance(atopology, apoint) ); - - -- 1. Check if any existing node is closer than the given precision - -- and if so pick the closest - sql := 'SELECT a.node_id FROM ' - || quote_ident(atopology) - || '.node as a WHERE ST_DWithin(a.geom,$1,' - || tol || ') AND ST_Distance($1, a.geom) < ' || tol - || ' ORDER BY ST_Distance($1, a.geom) LIMIT 1'; -#ifdef POSTGIS_TOPOLOGY_DEBUG - RAISE DEBUG '%', sql; -#endif - EXECUTE sql INTO id USING apoint; - IF id IS NOT NULL THEN - RETURN id; - END IF; - -#ifdef POSTGIS_TOPOLOGY_DEBUG - RAISE DEBUG 'No existing node within tolerance distance'; -#endif - - -- 2. Check if any existing edge falls within tolerance - -- and if so split it by a point projected on it - sql := 'SELECT a.edge_id, a.geom FROM ' - || quote_ident(atopology) - || '.edge as a WHERE ST_DWithin(a.geom,$1,' - || tol || ') ORDER BY ST_Distance($1, a.geom) LIMIT 1'; -#ifdef POSTGIS_TOPOLOGY_DEBUG - RAISE DEBUG '%', sql; -#endif - EXECUTE sql INTO rec USING apoint; - IF rec IS NOT NULL THEN - -- project point to line, split edge by point - prj := ST_ClosestPoint(rec.geom, apoint); - -- This is a workaround for ClosestPoint lack of Z support: - -- http://trac.osgeo.org/postgis/ticket/2033 - z := ST_Z(apoint); - IF z IS NOT NULL THEN - prj := ST_Translate(ST_Force_3DZ(prj), 0, 0, z); -- no ST_SetZ ... - END IF; -#ifdef POSTGIS_TOPOLOGY_DEBUG - RAISE DEBUG 'Splitting edge % with closest point %', rec.edge_id, ST_AsText(prj); -#endif - IF NOT ST_Contains(rec.geom, prj) THEN -#ifdef POSTGIS_TOPOLOGY_DEBUG - RAISE DEBUG ' Snapping edge to contain closest point'; -#endif - -- The tolerance must be big enough for snapping to happen - -- and small enough to snap only to the projected point. - -- Unfortunately ST_Distance returns 0 because it also uses - -- a projected point internally, so we need another way. - snaptol := topology._st_mintolerance(prj); -#ifdef POSTGIS_TOPOLOGY_DEBUG - RAISE DEBUG 'Tolerance for snapping to point % = %', ST_AsText(prj), snaptol; -#endif - snapedge := ST_Snap(rec.geom, prj, snaptol); - - -- Snapping currently snaps the first point below tolerance - -- so may possibly move first point. See ticket #1631 - IF NOT ST_Equals(ST_StartPoint(rec.geom), ST_StartPoint(snapedge)) - THEN -#ifdef POSTGIS_TOPOLOGY_DEBUG - RAISE WARNING 'Snapping moved first edge vertex, fixing'; -#endif - snapedge := ST_MakeLine(ST_StartPoint(rec.geom), snapedge); - END IF; - -#ifdef POSTGIS_TOPOLOGY_DEBUG - IF NOT ST_Contains(snapedge, prj) THEN -- or if equal ? - RAISE WARNING 'Edge within % distance from node still does not contain the node after snapping to it with tolerance %', tol, snaptol; - END IF; -#endif - PERFORM topology.ST_ChangeEdgeGeom(atopology, rec.edge_id, snapedge); - END IF; - id := topology.ST_ModEdgeSplit(atopology, rec.edge_id, prj); - ELSE -#ifdef POSTGIS_TOPOLOGY_DEBUG - RAISE DEBUG 'No existing edge within tolerance distance'; -#endif - id := topology.ST_AddIsoNode(atopology, NULL, apoint); - END IF; - - RETURN id; -END -$$ -LANGUAGE 'plpgsql' VOLATILE; + 'MODULE_PATHNAME', 'TopoGeo_AddPoint' + LANGUAGE 'c' VOLATILE; --} TopoGeo_AddPoint --{ -- 2.50.0