/********************************************************************** * * PostGIS - Spatial Types for PostgreSQL * http://postgis.net * * PostGIS is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * PostGIS is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with PostGIS. If not, see . * ********************************************************************** * * Copyright (C) 2016-2017 Björn Harrtell * **********************************************************************/ #include #include #include "mvt.h" #include "lwgeom_wagyu.h" #define Max(x, y) ((x) > (y) ? (x) : (y)) /* For a given geometry, look for the highest dimensional basic type, that is, * point, line or polygon */ static uint8_t lwgeom_get_basic_type(LWGEOM *geom) { switch(geom->type) { case POINTTYPE: case LINETYPE: case POLYGONTYPE: return geom->type; case TRIANGLETYPE: return POLYGONTYPE; case MULTIPOINTTYPE: case MULTILINETYPE: case MULTIPOLYGONTYPE: return geom->type - 3; /* Based on LWTYPE positions */ case COLLECTIONTYPE: case TINTYPE: { uint32_t i; uint8_t type = 0; LWCOLLECTION *g = (LWCOLLECTION*)geom; for (i = 0; i < g->ngeoms; i++) { LWGEOM *sg = g->geoms[i]; type = Max(type, lwgeom_get_basic_type(sg)); } return type; } default: exit(1); } } /** * In place process a collection to find a concrete geometry * object and expose that as the actual object. Will some * geom be lost? Sure, but your MVT renderer couldn't * draw it anyways. */ static inline LWGEOM * lwgeom_to_basic_type(LWGEOM *geom, uint8_t original_type) { LWGEOM *geom_out = geom; if (lwgeom_get_type(geom) == COLLECTIONTYPE) { LWCOLLECTION *g = (LWCOLLECTION*)geom; geom_out = (LWGEOM *)lwcollection_extract(g, original_type); } /* If a collection only contains 1 geometry return than instead */ if (lwgeom_is_collection(geom_out)) { LWCOLLECTION *g = (LWCOLLECTION *)geom_out; if (g->ngeoms == 1) { geom_out = g->geoms[0]; } } geom_out->srid = geom->srid; return geom_out; } /* Clips a geometry using lwgeom_clip_by_rect. Might return NULL */ static LWGEOM * mvt_unsafe_clip_by_box(LWGEOM *lwg_in, GBOX *clip_box) { LWGEOM *geom_clipped; GBOX geom_box; 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)) { return NULL; } if (gbox_contains_2d(clip_box, &geom_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 a geometry for MVT using GEOS. * Does NOT work for polygons * Might return NULL */ 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; assert(lwgeom->type != POLYGONTYPE); assert(lwgeom->type != MULTIPOLYGONTYPE); if (clip_geom) { gridspec grid = {0, 0, 0, 0, 1, 1, 0, 0}; GBOX bgbox; bgbox.flags = 0; bgbox.xmax = bgbox.ymax = (double)extent + (double)buffer; bgbox.xmin = bgbox.ymin = -(double)buffer; FLAGS_SET_GEODETIC(bgbox.flags, 0); ng = mvt_unsafe_clip_by_box(ng, &bgbox); /* Make sure there is no pending float values (clipping can do that) */ lwgeom_grid_in_place(ng, &grid); } return ng; } static LWGEOM * mvt_clip_and_validate(LWGEOM *lwgeom, uint8_t basic_type, uint32_t extent, uint32_t buffer, bool clip_geom) { GBOX clip_box = {0}; LWGEOM *clipped_lwgeom; /* Wagyu only supports polygons. Default to geos for other types */ lwgeom = lwgeom_to_basic_type(lwgeom, POLYGONTYPE); if (lwgeom->type != POLYGONTYPE && lwgeom->type != MULTIPOLYGONTYPE) { return mvt_clip_and_validate_geos(lwgeom, basic_type, extent, buffer, clip_geom); } if (!clip_geom) { /* With clipping disabled, we request a clip with the geometry bbox to force validation */ lwgeom_calculate_gbox(lwgeom, &clip_box); } else { clip_box.xmax = clip_box.ymax = (double)extent + (double)buffer; clip_box.xmin = clip_box.ymin = -(double)buffer; } clipped_lwgeom = lwgeom_wagyu_clip_by_box(lwgeom, &clip_box); return clipped_lwgeom; } /** * Transform a geometry into vector tile coordinate space. * * Makes best effort to keep validity. Might collapse geometry into lower * dimension. * * NOTE: modifies in place if possible (not currently possible for polygons) */ LWGEOM *mvt_geom(LWGEOM *lwgeom, const GBOX *gbox, uint32_t extent, uint32_t buffer, bool clip_geom) { AFFINE affine = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; gridspec grid = {0, 0, 0, 0, 1, 1, 0, 0}; double width = gbox->xmax - gbox->xmin; double height = gbox->ymax - gbox->ymin; double fx, fy; const uint8_t basic_type = lwgeom_get_basic_type(lwgeom); int preserve_collapsed = LW_FALSE; /* Simplify it as soon as possible */ lwgeom = lwgeom_to_basic_type(lwgeom, basic_type); /* Short circuit out on EMPTY */ if (lwgeom_is_empty(lwgeom)) return NULL; fx = extent / width; fy = -(extent / height); /* If geometry has disappeared, you're done */ if (lwgeom_is_empty(lwgeom)) return NULL; /* transform to tile coordinate space */ affine.afac = fx; affine.efac = fy; affine.ifac = 1; affine.xoff = -gbox->xmin * fx; affine.yoff = -gbox->ymax * fy; lwgeom_affine(lwgeom, &affine); /* Snap to integer precision, removing duplicate points */ lwgeom_grid_in_place(lwgeom, &grid); /* Remove points on straight lines */ lwgeom_simplify_in_place(lwgeom, 0, preserve_collapsed); /* Remove duplicates in multipoints */ if (lwgeom->type == MULTIPOINTTYPE) lwgeom_remove_repeated_points_in_place(lwgeom, 0); if (!lwgeom || lwgeom_is_empty(lwgeom)) return NULL; lwgeom = mvt_clip_and_validate(lwgeom, basic_type, extent, buffer, clip_geom); if (!lwgeom || lwgeom_is_empty(lwgeom)) return NULL; return lwgeom; }