Commit 3e56be4a authored by Raúl Marín's avatar Raúl Marín

ST_AsMVTGeom (GEOS): Enforce validation at all times

Closes #4348


git-svn-id: http://svn.osgeo.org/postgis/branches/2.5@17352 b70326c6-7e19-0410-871a-916f4a2858ee
parent 36daea0e
Pipeline #52944341 passed with stage
in 23 minutes and 3 seconds
PostGIS 2.5.3
2019/XX/XX
* Bug fixes *
- #4348, ST_AsMVTGeom (GEOS): Enforce validation at all times (Raúl Marín)
PostGIS 2.5.2
2019/03/11
......
......@@ -23,6 +23,7 @@
**********************************************************************/
#include "mvt.h"
#include "lwgeom_geos.h"
#ifdef HAVE_LIBPROTOBUF
......@@ -791,80 +792,269 @@ lwgeom_to_basic_type(LWGEOM *geom, uint8 original_type)
}
static LWGEOM *
mvt_clip_and_validate_geos(LWGEOM *lwgeom, uint8_t basic_type, uint32_t extent, uint32_t buffer, bool clip_geom)
mvt_unsafe_clip_by_box(LWGEOM *lwg_in, GBOX *clip_box)
{
LWGEOM *ng = lwgeom;
LWGEOM *geom_clipped;
GBOX geom_box;
if (clip_geom)
gbox_init(&geom_box);
FLAGS_SET_GEODETIC(geom_box.flags, 0);
lwgeom_calculate_gbox(lwg_in, &geom_box);
if (!gbox_overlaps_2d(&geom_box, clip_box))
{
GBOX bgbox, lwgeom_gbox;
gbox_init(&bgbox);
gbox_init(&lwgeom_gbox);
bgbox.xmax = bgbox.ymax = (double)extent + (double)buffer;
bgbox.xmin = bgbox.ymin = -(double)buffer;
FLAGS_SET_GEODETIC(lwgeom_gbox.flags, 0);
FLAGS_SET_GEODETIC(bgbox.flags, 0);
lwgeom_calculate_gbox(lwgeom, &lwgeom_gbox);
POSTGIS_DEBUG(3, "mvt_geom: geometry outside clip box");
return NULL;
}
if (gbox_contains_2d(clip_box, &geom_box))
{
POSTGIS_DEBUG(3, "mvt_geom: geometry contained fully inside the box");
return lwg_in;
}
geom_clipped = lwgeom_clip_by_rect(lwg_in, clip_box->xmin, clip_box->ymin, clip_box->xmax, clip_box->ymax);
if (!geom_clipped || lwgeom_is_empty(geom_clipped))
return NULL;
return geom_clipped;
}
/**
* Clips an input geometry using GEOSIntersection
* It used to try to use GEOSClipByRect (as mvt_unsafe_clip_by_box) but since that produces
* invalid output when an invalid geometry is given and detecting it resulted to be impossible,
* we use intersection instead and, upon error, force validation of the input and retry.
*/
static LWGEOM *
mvt_safe_clip_polygon_by_box(LWGEOM *lwg_in, GBOX *clip_box)
{
LWGEOM *geom_clipped, *envelope;
GBOX geom_box;
GEOSGeometry *geos_input, *geos_box, *geos_result;
gbox_init(&geom_box);
FLAGS_SET_GEODETIC(geom_box.flags, 0);
lwgeom_calculate_gbox(lwg_in, &geom_box);
if (!gbox_overlaps_2d(&geom_box, clip_box))
{
POSTGIS_DEBUG(3, "mvt_geom: geometry outside clip box");
return NULL;
}
if (gbox_contains_2d(clip_box, &geom_box))
{
POSTGIS_DEBUG(3, "mvt_geom: geometry contained fully inside the box");
return lwg_in;
}
initGEOS(lwnotice, lwgeom_geos_error);
if (!(geos_input = LWGEOM2GEOS(lwg_in, 1)))
return NULL;
envelope = (LWGEOM *)lwpoly_construct_envelope(
lwg_in->srid, clip_box->xmin, clip_box->ymin, clip_box->xmax, clip_box->ymax);
geos_box = LWGEOM2GEOS(envelope, 1);
lwgeom_free(envelope);
if (!geos_box)
{
GEOSGeom_destroy(geos_input);
return NULL;
}
if (!gbox_overlaps_2d(&lwgeom_gbox, &bgbox))
geos_result = GEOSIntersection(geos_input, geos_box);
if (!geos_result)
{
POSTGIS_DEBUG(3, "mvt_geom: no geometry after intersection. Retrying after validation");
GEOSGeom_destroy(geos_input);
lwg_in = lwgeom_make_valid(lwg_in);
if (!(geos_input = LWGEOM2GEOS(lwg_in, 1)))
{
POSTGIS_DEBUG(3, "mvt_geom: geometry outside clip box");
GEOSGeom_destroy(geos_box);
return NULL;
}
geos_result = GEOSIntersection(geos_input, geos_box);
if (!geos_result)
{
GEOSGeom_destroy(geos_box);
GEOSGeom_destroy(geos_input);
return NULL;
}
}
GEOSSetSRID(geos_result, lwg_in->srid);
geom_clipped = GEOS2LWGEOM(geos_result, 0);
GEOSGeom_destroy(geos_box);
GEOSGeom_destroy(geos_input);
GEOSGeom_destroy(geos_result);
if (!geom_clipped || lwgeom_is_empty(geom_clipped))
{
POSTGIS_DEBUG(3, "mvt_geom: no geometry after clipping");
return NULL;
}
return geom_clipped;
}
if (!gbox_contains_2d(&bgbox, &lwgeom_gbox))
/**
* Clips the geometry using GEOSIntersection in a "safe way", cleaning the input
* if necessary and clipping MULTIPOLYGONs separately to reduce the impact
* of using invalid input in GEOS
*/
static LWGEOM *
mvt_iterate_clip_by_box_geos(LWGEOM *lwgeom, GBOX *clip_gbox, uint8_t basic_type)
{
if (basic_type != POLYGONTYPE)
{
return mvt_unsafe_clip_by_box(lwgeom, clip_gbox);
}
if (lwgeom->type != MULTIPOLYGONTYPE || ((LWMPOLY *)lwgeom)->ngeoms == 1)
{
return mvt_safe_clip_polygon_by_box(lwgeom, clip_gbox);
}
else
{
GBOX geom_box;
uint32_t i;
LWCOLLECTION *lwmg;
LWCOLLECTION *res;
gbox_init(&geom_box);
FLAGS_SET_GEODETIC(geom_box.flags, 0);
lwgeom_calculate_gbox(lwgeom, &geom_box);
lwmg = ((LWCOLLECTION *)lwgeom);
res = lwcollection_construct_empty(
MULTIPOLYGONTYPE, lwgeom->srid, FLAGS_GET_Z(lwgeom->flags), FLAGS_GET_M(lwgeom->flags));
for (i = 0; i < lwmg->ngeoms; i++)
{
LWGEOM *clipped_geom =
lwgeom_clip_by_rect(lwgeom, bgbox.xmin, bgbox.ymin, bgbox.xmax, bgbox.ymax);
if (clipped_geom == NULL || lwgeom_is_empty(clipped_geom))
LWGEOM *clipped = mvt_safe_clip_polygon_by_box(lwcollection_getsubgeom(lwmg, i), clip_gbox);
if (clipped)
{
POSTGIS_DEBUG(3, "mvt_geom: no geometry after clip");
return NULL;
clipped = lwgeom_to_basic_type(clipped, POLYGONTYPE);
if (!lwgeom_is_empty(clipped) &&
(clipped->type == POLYGONTYPE || clipped->type == MULTIPOLYGONTYPE))
{
if (!lwgeom_is_collection(clipped))
{
lwcollection_add_lwgeom(res, clipped);
}
else
{
uint32_t j;
for (j = 0; j < ((LWCOLLECTION *)clipped)->ngeoms; j++)
lwcollection_add_lwgeom(
res, lwcollection_getsubgeom((LWCOLLECTION *)clipped, j));
}
}
}
}
return lwcollection_as_lwgeom(res);
}
}
/* For some polygons, the simplify step might have left them
* as invalid, which can cause clipping to return the complementary
* geometry of what it should */
if ((basic_type == POLYGONTYPE) &&
!gbox_contains_2d(&lwgeom_gbox, lwgeom_get_bbox(clipped_geom)))
{
/* TODO: Adapt this when and if Exception Policies are introduced.
* Other options would be to fix the geometry and retry
* or to calculate the difference between the 2 boxes.
*/
POSTGIS_DEBUG(3, "mvt_geom: Invalid geometry after clipping");
lwgeom_free(clipped_geom);
/**
* Given a geometry, it uses GEOS operations to make sure that it's valid according
* to the MVT spec and that all points are snapped into int coordinates
* It iterates several times if needed, if it fails, returns NULL
*/
static LWGEOM *
mvt_grid_and_validate_geos(LWGEOM *ng, uint8_t basic_type)
{
gridspec grid = {0, 0, 0, 0, 1, 1, 0, 0};
ng = lwgeom_to_basic_type(ng, basic_type);
if (basic_type != POLYGONTYPE)
{
/* Make sure there is no pending float values (clipping can do that) */
lwgeom_grid_in_place(ng, &grid);
}
else
{
/* For polygons we have to both snap to the integer grid and force validation.
* The problem with this procedure is that snapping to the grid can create
* an invalid geometry and making it valid can create float values; so
* we iterate several times (up to 3) to generate a valid geom with int coordinates
*/
GEOSGeometry *geo;
uint32_t iterations = 0;
static const uint32_t max_iterations = 3;
bool valid = false;
/* Grid to int */
lwgeom_grid_in_place(ng, &grid);
initGEOS(lwgeom_geos_error, lwgeom_geos_error);
geo = LWGEOM2GEOS(ng, 0);
if (!geo)
return NULL;
valid = GEOSisValid(geo) == 1;
while (!valid && iterations < max_iterations)
{
GEOSGeometry *geo_valid = LWGEOM_GEOS_makeValid(geo);
GEOSGeom_destroy(geo);
if (!geo_valid)
return NULL;
}
ng = clipped_geom;
ng = GEOS2LWGEOM(geo_valid, 0);
GEOSGeom_destroy(geo_valid);
if (!ng)
return NULL;
lwgeom_grid_in_place(ng, &grid);
ng = lwgeom_to_basic_type(ng, basic_type);
geo = LWGEOM2GEOS(ng, 0);
valid = GEOSisValid(geo) == 1;
iterations++;
}
}
GEOSGeom_destroy(geo);
if (basic_type == POLYGONTYPE)
{
/* Force validation as per MVT spec */
ng = lwgeom_make_valid(ng);
if (!valid)
{
POSTGIS_DEBUG(1, "mvt_geom: Could not transform into a valid MVT geometry");
return NULL;
}
/* In image coordinates CW actually comes out a CCW, so we reverse */
lwgeom_force_clockwise(ng);
lwgeom_reverse_in_place(ng);
}
return ng;
}
/* Make sure we return the most basic type after simplification and validation */
ng = lwgeom_to_basic_type(ng, basic_type);
if (basic_type != lwgeom_get_basic_type(ng))
static LWGEOM *
mvt_clip_and_validate_geos(LWGEOM *lwgeom, uint8_t basic_type, uint32_t extent, uint32_t buffer, bool clip_geom)
{
LWGEOM *ng = lwgeom;
if (clip_geom)
{
/* Drop type changes to play nice with MVT renderers */
POSTGIS_DEBUG(3, "mvt_geom: Dropping geometry after type change");
return NULL;
GBOX bgbox;
gbox_init(&bgbox);
bgbox.xmax = bgbox.ymax = (double)extent + (double)buffer;
bgbox.xmin = bgbox.ymin = -(double)buffer;
FLAGS_SET_GEODETIC(bgbox.flags, 0);
ng = mvt_iterate_clip_by_box_geos(lwgeom, &bgbox, basic_type);
if (!ng || lwgeom_is_empty(ng))
{
POSTGIS_DEBUG(3, "mvt_geom: no geometry after clip");
return NULL;
}
}
/* Clipping and validation might produce float values. Grid again into int
* and pray that the output is still valid */
ng = mvt_grid_and_validate_geos(ng, basic_type);
/* Make sure we return the expected type */
if (basic_type != lwgeom_get_basic_type(ng))
{
gridspec grid = {0, 0, 0, 0, 1, 1, 0, 0};
lwgeom_grid_in_place(ng, &grid);
/* Drop type changes to play nice with MVT renderers */
POSTGIS_DEBUG(1, "mvt_geom: Dropping geometry after type change");
return NULL;
}
return ng;
......@@ -886,8 +1076,8 @@ LWGEOM *mvt_geom(LWGEOM *lwgeom, const GBOX *gbox, uint32_t extent, uint32_t buf
double width = gbox->xmax - gbox->xmin;
double height = gbox->ymax - gbox->ymin;
double resx, resy, res, fx, fy;
int preserve_collapsed = LW_TRUE;
const uint8_t basic_type = lwgeom_get_basic_type(lwgeom);
int preserve_collapsed = LW_FALSE;
POSTGIS_DEBUG(2, "mvt_geom called");
/* Simplify it as soon as possible */
......
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -4,7 +4,7 @@ PG3|POINT(2 4092)
PG4|MULTIPOLYGON(((5 4096,10 4091,10 4096,5 4096)),((5 4096,0 4101,0 4096,5 4096)))
PG5|
PG6|POLYGON((2791 594,894 2704,600 594,2791 594))
PG7|POLYGON((1252 1904,1253 1905,1253 1906,1251 1904,1252 1904))
PG7|t
PG8|MULTIPOLYGON(((5 4096,10 4091,10 4096,5 4096)),((5 4096,0 4101,0 4096,5 4096)))
PG9|16777216
PG9.1|2|12.5|f
......@@ -37,8 +37,7 @@ PG35|POINT(4352 4352)
PG36|
PG37|
PG38|
PG39 - ON |POLYGON((10 0,9 2,10 2,10 10,0 10,0 0,10 0))
PG39 - OFF|POLYGON((10 0,9 2,10 2,10 10,0 10,0 0,10 0))
PG39|t
PG40 - ON |LINESTRING(0 10,0 0)
PG40 - OFF|LINESTRING(0 10,0 0)
PG41 - ON |LINESTRING(0 10,0 4,0 2,0 0,1 0)
......@@ -60,13 +59,13 @@ PG53|600
PG54|
PG55|
PG56|
PG57|POLYGON((0 1,0 0,100 0,100 100,0 100,0 1))
PG57|t
PG58|POLYGON((0 100,0 90,10 90,10 100,0 100))
PG59|POLYGON((0 100,0 90,10 90,10 100,0 100))
PG60|POLYGON((0 100,0 90,10 90,10 100,0 100))
PG61|
PG62|POLYGON((0 100,0 90,10 90,10 100,0 100))
PG63|POLYGON((90 0,100 0,100 10,90 10,90 0))
PG63|100|t
PG64|
TG1|GiEKBHRlc3QSDBICAAAYASIECTLePxoCYzEiAigBKIAgeAI=
TG2|GiMKBHRlc3QSDhICAAAYASIGETLePwIBGgJjMSICKAEogCB4Ag==
......@@ -117,5 +116,13 @@ D7|POINT(1 4094)
TU2
ERROR: pgis_asmvt_transfn: parameter row cannot be other than a rowtype
TU3|
#3922|6.5
#3922|t
#4294_Horizontal|LINESTRING(0 10,0 5)
#4294_Vertical|LINESTRING(0 10,5 10)
#4314|
#4348Clip|t
#4348Invalid|t
#4348Dropped|t
#4348ReversedSmall|t
#4348Reversed2|t
#4348Point|t
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment