| | 3874 | /*------------------------------------------------------------- |
| | 3875 | * |
| | 3876 | * ST_MakeValid |
| | 3877 | * |
| | 3878 | * Attempts to make an invalid geometries valid w/out loosing |
| | 3879 | * point sets. |
| | 3880 | * |
| | 3881 | * Polygons may become collection of polygons and lines. |
| | 3882 | * Collapsed rings (or portions of rings) may be dissolved in |
| | 3883 | * polygon area or transformed to linestring if outside any other |
| | 3884 | * ring. |
| | 3885 | * |
| | 3886 | * Author: Sandro Santilli <strk@keybit.net> |
| | 3887 | * |
| | 3888 | * Work done for Regione Toscana - Sistema Informativo per il Governo |
| | 3889 | * del Territorio e dell'Ambiente (RT-SIGTA). |
| | 3890 | * |
| | 3891 | * Thanks to Dr. Horst Duester for previous work on a plpgsql version |
| | 3892 | * of the cleanup logic [1] |
| | 3893 | * |
| | 3894 | * Thanks to Andrea Peri for recommandations on constraints. |
| | 3895 | * |
| | 3896 | * [1] http://www.sogis1.so.ch/sogis/dl/postgis/cleanGeometry.sql |
| | 3897 | * |
| | 3898 | * |
| | 3899 | *------------------------------------------------------------*/ |
| | 3900 | |
| | 3901 | LWGEOM * lwcollection_make_valid(LWCOLLECTION *g); |
| | 3902 | LWGEOM * lwline_make_valid(LWLINE *line); |
| | 3903 | LWGEOM * lwpoly_make_valid(LWPOLY *poly); |
| | 3904 | POINTARRAY* ring_make_valid(POINTARRAY* ring); |
| | 3905 | |
| | 3906 | /* |
| | 3907 | * Ensure the geometry is "valid" |
| | 3908 | * May return the input untouched (if already valid). |
| | 3909 | * May return geometries of lower dimension (on collapses) |
| | 3910 | */ |
| | 3911 | static LWGEOM * |
| | 3912 | lwgeom_make_valid(LWGEOM *geom) |
| | 3913 | { |
| | 3914 | LWDEBUGF(2, "lwgeom_make_valid enter (type %d)", TYPE_GETTYPE(geom->type)); |
| | 3915 | switch (TYPE_GETTYPE(geom->type)) |
| | 3916 | { |
| | 3917 | case POINTTYPE: |
| | 3918 | case MULTIPOINTTYPE: |
| | 3919 | /* a point is always valid */ |
| | 3920 | return geom; |
| | 3921 | break; |
| | 3922 | |
| | 3923 | case LINETYPE: |
| | 3924 | /* lines need at least 2 points */ |
| | 3925 | return lwline_make_valid((LWLINE *)geom); |
| | 3926 | break; |
| | 3927 | |
| | 3928 | case POLYGONTYPE: |
| | 3929 | /* polygons need all rings closed */ |
| | 3930 | return lwpoly_make_valid((LWPOLY *)geom); |
| | 3931 | break; |
| | 3932 | |
| | 3933 | case MULTILINETYPE: |
| | 3934 | case MULTIPOLYGONTYPE: |
| | 3935 | case COLLECTIONTYPE: |
| | 3936 | return lwcollection_make_valid((LWCOLLECTION *)geom); |
| | 3937 | break; |
| | 3938 | |
| | 3939 | case CIRCSTRINGTYPE: |
| | 3940 | case COMPOUNDTYPE: |
| | 3941 | case CURVEPOLYTYPE: |
| | 3942 | case MULTISURFACETYPE: |
| | 3943 | case MULTICURVETYPE: |
| | 3944 | default: |
| | 3945 | lwerror("unsupported input geometry type: %d", TYPE_GETTYPE(geom->type)); |
| | 3946 | break; |
| | 3947 | } |
| | 3948 | return 0; |
| | 3949 | } |
| | 3950 | |
| | 3951 | /* |
| | 3952 | * Close the point array, if not already closed in 2d. |
| | 3953 | * Returns the input if already closed in 2d, or a newly |
| | 3954 | * constructed POINTARRAY. |
| | 3955 | */ |
| | 3956 | POINTARRAY* ptarray_close2d(POINTARRAY* ring); |
| | 3957 | POINTARRAY* |
| | 3958 | ptarray_close2d(POINTARRAY* ring) |
| | 3959 | { |
| | 3960 | POINTARRAY* newring; |
| | 3961 | |
| | 3962 | /* close the ring if not already closed (2d only) */ |
| | 3963 | if ( ! ptarray_isclosed2d(ring) ) |
| | 3964 | { |
| | 3965 | /* close it up */ |
| | 3966 | newring = ptarray_addPoint(ring, |
| | 3967 | getPoint_internal(ring, 0), |
| | 3968 | TYPE_NDIMS(ring->dims), |
| | 3969 | ring->npoints); |
| | 3970 | ring = newring; |
| | 3971 | } |
| | 3972 | return ring; |
| | 3973 | } |
| | 3974 | |
| | 3975 | /* May return the same input or a new one (never zero) */ |
| | 3976 | POINTARRAY* |
| | 3977 | ring_make_valid(POINTARRAY* ring) |
| | 3978 | { |
| | 3979 | POINTARRAY* closedring; |
| | 3980 | |
| | 3981 | /* close the ring if not already closed (2d only) */ |
| | 3982 | closedring = ptarray_close2d(ring); |
| | 3983 | if (closedring != ring ) |
| | 3984 | { |
| | 3985 | ptarray_free(ring); /* should we do this ? */ |
| | 3986 | ring = closedring; |
| | 3987 | } |
| | 3988 | |
| | 3989 | /* return 0 for collapsed ring (after closeup) */ |
| | 3990 | |
| | 3991 | if ( ring->npoints < 4 ) |
| | 3992 | { |
| | 3993 | LWDEBUGF(4, "ring has %d points, adding another", ring->npoints); |
| | 3994 | /* let's add another... */ |
| | 3995 | closedring = ptarray_addPoint(closedring, |
| | 3996 | getPoint_internal(closedring, 0), |
| | 3997 | TYPE_NDIMS(closedring->dims), |
| | 3998 | closedring->npoints); |
| | 3999 | return closedring; |
| | 4000 | } |
| | 4001 | |
| | 4002 | |
| | 4003 | return ring; |
| | 4004 | } |
| | 4005 | |
| | 4006 | /* Return 0 if poly was collapsed, or the input with updated rings */ |
| | 4007 | LWGEOM * |
| | 4008 | lwpoly_make_valid(LWPOLY *poly) |
| | 4009 | { |
| | 4010 | LWGEOM* ret; |
| | 4011 | POINTARRAY **new_rings; |
| | 4012 | int new_nrings=0, i; |
| | 4013 | |
| | 4014 | /* Allocate enough pointers for all rings */ |
| | 4015 | new_rings = lwalloc(sizeof(POINTARRAY*)*poly->nrings); |
| | 4016 | |
| | 4017 | /* All rings must be closed and have > 3 points */ |
| | 4018 | for (i=0; i<poly->nrings; i++) |
| | 4019 | { |
| | 4020 | POINTARRAY* ring_in = poly->rings[i]; |
| | 4021 | POINTARRAY* ring_out = ring_make_valid(ring_in); |
| | 4022 | |
| | 4023 | if ( ring_in != ring_out ) |
| | 4024 | { |
| | 4025 | LWDEBUGF(3, "lwpoly_make_valid: ring %d cleaned", i); |
| | 4026 | /* this may come right from |
| | 4027 | * the binary representation lands |
| | 4028 | */ |
| | 4029 | /*ptarray_free(ring_in); */ |
| | 4030 | } |
| | 4031 | else |
| | 4032 | { |
| | 4033 | LWDEBUGF(3, "lwpoly_make_valid: ring %d untouched", i); |
| | 4034 | } |
| | 4035 | |
| | 4036 | if ( ring_out ) |
| | 4037 | { |
| | 4038 | new_rings[new_nrings++] = ring_out; |
| | 4039 | } |
| | 4040 | } |
| | 4041 | |
| | 4042 | if ( new_nrings ) |
| | 4043 | { |
| | 4044 | lwfree(poly->rings); |
| | 4045 | poly->rings = new_rings; |
| | 4046 | poly->nrings = new_nrings; |
| | 4047 | ret = (LWGEOM*)poly; |
| | 4048 | } |
| | 4049 | else |
| | 4050 | { |
| | 4051 | /* was collapsed, will return zero */ |
| | 4052 | LWDEBUG(3, "lwpoly_make_valid: all polygon rings collapsed"); |
| | 4053 | lwpoly_release(poly); |
| | 4054 | |
| | 4055 | /* Make a POLYGON EMPTY or COLLECTION EMPTY ? */ |
| | 4056 | /* COLLECTION EMPTY */ |
| | 4057 | #if 0 |
| | 4058 | ret = (LWGEOM*)lwcollection_construct_empty(poly->SRID, |
| | 4059 | TYPE_HASZ(poly->type), TYPE_HASM(poly->type)); |
| | 4060 | #else |
| | 4061 | /* POLYGON EMPTY */ |
| | 4062 | lwfree(poly->rings); |
| | 4063 | poly->rings = new_rings; |
| | 4064 | poly->nrings = new_nrings; |
| | 4065 | ret = (LWGEOM*)poly; |
| | 4066 | #endif |
| | 4067 | } |
| | 4068 | |
| | 4069 | return ret; |
| | 4070 | } |
| | 4071 | |
| | 4072 | LWGEOM * |
| | 4073 | lwline_make_valid(LWLINE *line) |
| | 4074 | { |
| | 4075 | LWGEOM *ret; |
| | 4076 | |
| | 4077 | if (line->points->npoints == 1) /* 0 is fine, 2 is fine */ |
| | 4078 | { |
| | 4079 | /* Turn into a point (dropping bbox, as we don't |
| | 4080 | * need it for points) |
| | 4081 | */ |
| | 4082 | ret = (LWGEOM*)lwpoint_construct(line->SRID, 0, line->points); |
| | 4083 | return ret; |
| | 4084 | } |
| | 4085 | else |
| | 4086 | { |
| | 4087 | return (LWGEOM*)line; |
| | 4088 | /* return lwline_clone(line); */ |
| | 4089 | } |
| | 4090 | } |
| | 4091 | |
| | 4092 | LWGEOM * |
| | 4093 | lwcollection_make_valid(LWCOLLECTION *g) |
| | 4094 | { |
| | 4095 | LWGEOM **new_geoms; |
| | 4096 | uint32 i, new_ngeoms=0; |
| | 4097 | LWCOLLECTION *ret; |
| | 4098 | |
| | 4099 | /* enough space for all components */ |
| | 4100 | new_geoms = lwalloc(sizeof(LWGEOM *)*g->ngeoms); |
| | 4101 | |
| | 4102 | ret = lwalloc(sizeof(LWCOLLECTION)); |
| | 4103 | memcpy(ret, g, sizeof(LWCOLLECTION)); |
| | 4104 | |
| | 4105 | for (i=0; i<g->ngeoms; i++) |
| | 4106 | { |
| | 4107 | LWGEOM* newg = lwgeom_make_valid(g->geoms[i]); |
| | 4108 | if ( newg ) new_geoms[new_ngeoms++] = newg; |
| | 4109 | } |
| | 4110 | |
| | 4111 | ret->bbox = 0; /* recompute later... */ |
| | 4112 | |
| | 4113 | ret->ngeoms = new_ngeoms; |
| | 4114 | if ( new_ngeoms ) |
| | 4115 | { |
| | 4116 | ret->geoms = new_geoms; |
| | 4117 | } |
| | 4118 | else |
| | 4119 | { |
| | 4120 | free(new_geoms); |
| | 4121 | ret->geoms = 0; |
| | 4122 | } |
| | 4123 | |
| | 4124 | return (LWGEOM*)ret; |
| | 4125 | } |
| | 4126 | |
| | 4127 | Datum st_makevalid(PG_FUNCTION_ARGS); |
| | 4128 | PG_FUNCTION_INFO_V1(st_makevalid); |
| | 4129 | Datum st_makevalid(PG_FUNCTION_ARGS) |
| | 4130 | { |
| | 4131 | PG_LWGEOM *in, *out; |
| | 4132 | GEOSGeom geosgeom, geos_bound, geos_bound_noded, geos_tmp_point; |
| | 4133 | GEOSGeom geos_cut_edges, geos_area; |
| | 4134 | LWGEOM *lwgeom_in, *lwgeom_out; |
| | 4135 | /* LWGEOM *lwgeom_pointset; */ |
| | 4136 | char ret_char; |
| | 4137 | int is3d; |
| | 4138 | int nargs; |
| | 4139 | int collect_collapses = false; |
| | 4140 | |
| | 4141 | in = (PG_LWGEOM *)PG_DETOAST_DATUM(PG_GETARG_DATUM(0)); |
| | 4142 | lwgeom_in = lwgeom_deserialize(SERIALIZED_FORM(in)); |
| | 4143 | |
| | 4144 | is3d = TYPE_HASZ(lwgeom_in->type); |
| | 4145 | |
| | 4146 | nargs = PG_NARGS(); |
| | 4147 | if (nargs > 1) |
| | 4148 | { |
| | 4149 | collect_collapses = PG_GETARG_BOOL(1); |
| | 4150 | } |
| | 4151 | |
| | 4152 | /* |
| | 4153 | * Step 1 : try to convert to GEOS, if impossible, clean that up first |
| | 4154 | * otherwise (adding only duplicates of existing points) |
| | 4155 | */ |
| | 4156 | |
| | 4157 | initGEOS(errorlogger, errorlogger); |
| | 4158 | |
| | 4159 | |
| | 4160 | lwgeom_out = lwgeom_in; |
| | 4161 | geosgeom = LWGEOM2GEOS(lwgeom_out); |
| | 4162 | if ( ! geosgeom ) |
| | 4163 | { |
| | 4164 | POSTGIS_DEBUGF(4, |
| | 4165 | "Original geom can't be converted to GEOS (%s)" |
| | 4166 | " - will try cleaning that up first", |
| | 4167 | loggederror); |
| | 4168 | |
| | 4169 | |
| | 4170 | lwgeom_out = lwgeom_make_valid(lwgeom_out); |
| | 4171 | if ( ! lwgeom_out ) |
| | 4172 | { |
| | 4173 | lwerror("Could not make a valid geometry out of input"); |
| | 4174 | } |
| | 4175 | |
| | 4176 | /* try again as we did cleanup now */ |
| | 4177 | geosgeom = LWGEOM2GEOS(lwgeom_out); |
| | 4178 | if ( ! geosgeom ) |
| | 4179 | { |
| | 4180 | lwerror("Couldn't convert POSTGIS geom to GEOS: %s", |
| | 4181 | loggederror); |
| | 4182 | PG_RETURN_NULL(); |
| | 4183 | } |
| | 4184 | |
| | 4185 | } |
| | 4186 | else |
| | 4187 | { |
| | 4188 | POSTGIS_DEBUG(4, "original geom converted to GEOS"); |
| | 4189 | lwgeom_out = lwgeom_in; |
| | 4190 | } |
| | 4191 | |
| | 4192 | |
| | 4193 | /* |
| | 4194 | * Step 2 : return the resulting geometry if it's now valid |
| | 4195 | */ |
| | 4196 | |
| | 4197 | ret_char = GEOSisValid(geosgeom); |
| | 4198 | if ( ret_char == 2 ) |
| | 4199 | { |
| | 4200 | lwerror("GEOSisValid() threw an error: %s", loggederror); |
| | 4201 | PG_RETURN_NULL(); /* I don't think should ever happen */ |
| | 4202 | } |
| | 4203 | else if ( ret_char ) |
| | 4204 | { |
| | 4205 | /* It's valid at this step, return what we have */ |
| | 4206 | |
| | 4207 | GEOSGeom_destroy(geosgeom); |
| | 4208 | geosgeom=0; |
| | 4209 | |
| | 4210 | out = pglwgeom_serialize(lwgeom_out); |
| | 4211 | lwgeom_release(lwgeom_out); |
| | 4212 | lwgeom_out=0; |
| | 4213 | |
| | 4214 | PG_FREE_IF_COPY(in, 0); |
| | 4215 | |
| | 4216 | PG_RETURN_POINTER(out); |
| | 4217 | } |
| | 4218 | |
| | 4219 | POSTGIS_DEBUGF(3, |
| | 4220 | "Geometry [%s] is still not valid:", |
| | 4221 | lwgeom_to_ewkt(lwgeom_out, PARSER_CHECK_NONE)); |
| | 4222 | POSTGIS_DEBUGF(3, " %s", loggederror); |
| | 4223 | POSTGIS_DEBUG(3, " will try to clean up further"); |
| | 4224 | |
| | 4225 | /* |
| | 4226 | * Step 3 : make what we got now (geosgeom) valid |
| | 4227 | */ |
| | 4228 | |
| | 4229 | switch (GEOSGeomTypeId(geosgeom)) |
| | 4230 | { |
| | 4231 | case GEOS_MULTIPOINT: |
| | 4232 | case GEOS_POINT: |
| | 4233 | /* points are always valid, but we might have invalid ordinate values */ |
| | 4234 | lwerror("PUNTUAL geometry resulted invalid to GEOS -- dunno how to clean that up"); |
| | 4235 | break; |
| | 4236 | |
| | 4237 | case GEOS_LINESTRING: |
| | 4238 | case GEOS_MULTILINESTRING: |
| | 4239 | lwerror("ST_MakeValid: doesn't support linear types"); |
| | 4240 | break; |
| | 4241 | |
| | 4242 | case GEOS_POLYGON: |
| | 4243 | case GEOS_MULTIPOLYGON: |
| | 4244 | geos_bound = GEOSBoundary(geosgeom); |
| | 4245 | if ( NULL == geos_bound ) |
| | 4246 | { |
| | 4247 | GEOSGeom_destroy(geosgeom); |
| | 4248 | geosgeom=0; |
| | 4249 | lwgeom_release(lwgeom_in); |
| | 4250 | lwgeom_in=0; |
| | 4251 | lwgeom_release(lwgeom_out); |
| | 4252 | lwgeom_out=0; |
| | 4253 | PG_FREE_IF_COPY(in, 0); |
| | 4254 | lwerror("GEOSboundary() threw an error: %s", loggederror); |
| | 4255 | PG_RETURN_NULL(); /* never get here */ |
| | 4256 | } |
| | 4257 | |
| | 4258 | POSTGIS_DEBUGF(3, |
| | 4259 | "Boundaries: %s", |
| | 4260 | lwgeom_to_ewkt(GEOS2LWGEOM(geos_bound, is3d), |
| | 4261 | PARSER_CHECK_NONE)); |
| | 4262 | |
| | 4263 | /* |
| | 4264 | * Union with an empty point, obtaining full noding |
| | 4265 | * and dissolving of duplicated repeated points |
| | 4266 | * |
| | 4267 | * TODO: substitute this with UnaryUnion? |
| | 4268 | * |
| | 4269 | * need a point on the line here rather |
| | 4270 | * than an arbitrary one ? |
| | 4271 | */ |
| | 4272 | geos_tmp_point = GEOSGeom_createPoint(0); |
| | 4273 | // GEOSPointOnSurface(geos_bound); |
| | 4274 | if ( ! geos_tmp_point ) |
| | 4275 | { |
| | 4276 | lwerror("GEOSGeom_createPoint(0): %s", loggederror); |
| | 4277 | PG_RETURN_NULL(); /* never get here */ |
| | 4278 | } |
| | 4279 | |
| | 4280 | geos_bound_noded = GEOSUnion(geos_bound, geos_tmp_point); |
| | 4281 | if ( NULL == geos_bound_noded ) |
| | 4282 | { |
| | 4283 | GEOSGeom_destroy(geosgeom); |
| | 4284 | geosgeom=0; |
| | 4285 | GEOSGeom_destroy(geos_tmp_point); |
| | 4286 | geos_tmp_point=0; |
| | 4287 | geos_tmp_point=0; |
| | 4288 | GEOSGeom_destroy(geos_bound); |
| | 4289 | geos_bound=0; |
| | 4290 | lwgeom_release(lwgeom_in); |
| | 4291 | lwgeom_in=0; |
| | 4292 | lwgeom_release(lwgeom_out); |
| | 4293 | lwgeom_out=0; |
| | 4294 | PG_FREE_IF_COPY(in, 0); |
| | 4295 | lwerror("GEOSUnion() threw an error: %s", loggederror); |
| | 4296 | PG_RETURN_NULL(); /* never get here */ |
| | 4297 | } |
| | 4298 | |
| | 4299 | POSTGIS_DEBUGF(3, |
| | 4300 | "Noded: %s", |
| | 4301 | lwgeom_to_ewkt(GEOS2LWGEOM(geos_bound_noded, is3d), |
| | 4302 | PARSER_CHECK_NONE)); |
| | 4303 | |
| | 4304 | GEOSGeom_destroy(geos_tmp_point); |
| | 4305 | geos_tmp_point=0; |
| | 4306 | GEOSGeom_destroy(geosgeom); |
| | 4307 | geosgeom=0; |
| | 4308 | |
| | 4309 | geos_area = LWGEOM_GEOS_buildArea(geos_bound_noded); |
| | 4310 | if ( ! geos_area ) /* must be an exception ... */ |
| | 4311 | { |
| | 4312 | /* cleanup and throw */ |
| | 4313 | GEOSGeom_destroy(geos_bound); |
| | 4314 | geos_bound=0; |
| | 4315 | GEOSGeom_destroy(geos_bound_noded); |
| | 4316 | geos_bound_noded=0; |
| | 4317 | PG_FREE_IF_COPY(in, 0); |
| | 4318 | lwerror("LWGEOM_GEOS_buildArea() threw an error: %s", |
| | 4319 | loggederror); |
| | 4320 | PG_RETURN_NULL(); /* never get here */ |
| | 4321 | } |
| | 4322 | |
| | 4323 | if ( ! collect_collapses ) |
| | 4324 | { |
| | 4325 | geosgeom = geos_area; |
| | 4326 | } |
| | 4327 | else /* collect_collapses */ |
| | 4328 | { |
| | 4329 | /* Compute what's left out from original boundary |
| | 4330 | * (this would be the so called 'cut lines' */ |
| | 4331 | geos_cut_edges = GEOSDifference(geos_bound_noded, geos_area); |
| | 4332 | if ( ! geos_cut_edges ) /* an exception again */ |
| | 4333 | { |
| | 4334 | /* cleanup and throw */ |
| | 4335 | GEOSGeom_destroy(geos_bound); |
| | 4336 | geos_bound=0; |
| | 4337 | GEOSGeom_destroy(geos_area); |
| | 4338 | geos_area=0; |
| | 4339 | GEOSGeom_destroy(geos_bound_noded); |
| | 4340 | geos_bound_noded=0; |
| | 4341 | PG_FREE_IF_COPY(in, 0); |
| | 4342 | lwerror("GEOSDifference() threw an error: %s", |
| | 4343 | loggederror); |
| | 4344 | PG_RETURN_NULL(); /* never get here */ |
| | 4345 | } |
| | 4346 | |
| | 4347 | GEOSGeom_destroy(geos_bound); |
| | 4348 | geos_bound=0; |
| | 4349 | GEOSGeom_destroy(geos_bound_noded); |
| | 4350 | geos_bound_noded=0; |
| | 4351 | |
| | 4352 | /* Finally put togheter cut edges and area |
| | 4353 | * (could become a collection) */ |
| | 4354 | geosgeom = GEOSUnion(geos_area, geos_cut_edges); |
| | 4355 | if ( ! geosgeom ) /* an exception again */ |
| | 4356 | { |
| | 4357 | /* cleanup and throw */ |
| | 4358 | GEOSGeom_destroy(geos_area); |
| | 4359 | geos_area=0; |
| | 4360 | GEOSGeom_destroy(geos_cut_edges); |
| | 4361 | geos_cut_edges=0; |
| | 4362 | PG_FREE_IF_COPY(in, 0); |
| | 4363 | lwerror("GEOSUnion() threw an error: %s", |
| | 4364 | loggederror); |
| | 4365 | PG_RETURN_NULL(); /* never get here */ |
| | 4366 | } |
| | 4367 | } |
| | 4368 | |
| | 4369 | break; /* we've done */ |
| | 4370 | |
| | 4371 | default: |
| | 4372 | { |
| | 4373 | char* tmp = GEOSGeomType(geosgeom); |
| | 4374 | char* typname = pstrdup(tmp); |
| | 4375 | GEOSFree(tmp); |
| | 4376 | lwerror("ST_MakeValid: doesn't support geometry type: %d", |
| | 4377 | typname); |
| | 4378 | break; |
| | 4379 | } |
| | 4380 | } |
| | 4381 | |
| | 4382 | |
| | 4383 | if ( ! geosgeom ) PG_RETURN_NULL(); |
| | 4384 | |
| | 4385 | /* Now check if every point of input is also found |
| | 4386 | * in output, or abort by returning NULL |
| | 4387 | * |
| | 4388 | * Input geometry was lwgeom_in |
| | 4389 | */ |
| | 4390 | |
| | 4391 | out = GEOS2POSTGIS(geosgeom, is3d); |
| | 4392 | PG_RETURN_POINTER(out); |
| | 4393 | } |