[postgis-tickets] r15323 - Reworked ST_AsMVT and new ST_AsMVTGeom implementation

bjorn at wololo.org bjorn at wololo.org
Tue Mar 7 12:32:18 PST 2017


Author: bjornharrtell
Date: 2017-03-07 12:32:18 -0800 (Tue, 07 Mar 2017)
New Revision: 15323

Modified:
   trunk/doc/reference_output.xml
   trunk/postgis/lwgeom_out_mvt.c
   trunk/postgis/mvt.c
   trunk/postgis/mvt.h
   trunk/postgis/postgis.sql.in
   trunk/regress/mvt.sql
   trunk/regress/mvt_expected
Log:
Reworked ST_AsMVT and new ST_AsMVTGeom implementation
References #3712

Modified: trunk/doc/reference_output.xml
===================================================================
--- trunk/doc/reference_output.xml	2017-03-06 20:40:10 UTC (rev 15322)
+++ trunk/doc/reference_output.xml	2017-03-07 20:32:18 UTC (rev 15323)
@@ -1344,21 +1344,68 @@
 	  </refsection>
 	</refentry>
 
+	<refentry id="ST_AsMVTGeom">
+	  <refnamediv>
+		<refname>ST_AsMVTGeom</refname>
+
+		<refpurpose>Transform a geometry into the coordinate space of a <ulink url="https://www.mapbox.com/vector-tiles/">Mapbox Vector Tile</ulink>.</refpurpose>
+	  </refnamediv>
+	  <refsynopsisdiv>
+		<funcsynopsis>
+			<funcprototype>
+				<funcdef>geometry <function>ST_AsMVTGeom</function></funcdef>
+				<paramdef><type>geometry </type> <parameter>geom</parameter></paramdef>
+				<paramdef><type>box2d </type> <parameter>bounds</parameter></paramdef>
+				<paramdef><type>int4 </type> <parameter>extent</parameter></paramdef>
+				<paramdef><type>int4 </type> <parameter>buffer</parameter></paramdef>
+				<paramdef><type>bool </type> <parameter>clip_geom</parameter></paramdef>
+			</funcprototype>
+		</funcsynopsis>
+	  </refsynopsisdiv>
+
+	  <refsection>
+		<title>Description</title>
+
+		<para>Transform a geometry into the coordinate space of a <ulink url="https://www.mapbox.com/vector-tiles/">Mapbox Vector Tile</ulink> of a set of rows corresponding to a Layer.
+		Makes best effort to keep and even correct validity and might collapse geometry into a lower dimension in the process.
+		</para>
+
+		<para><varname>geom</varname> is the geometry to transform.</para>
+		<para><varname>bounds</varname> is the geometric bounds of the tile contents without buffer.</para>
+		<para><varname>extent</varname> is the tile extent in tile coordinate space as defined by the <ulink url="https://www.mapbox.com/vector-tiles/specification/">specification</ulink>. If NULL it will default to 4096.</para>
+		<para><varname>buffer</varname> is the buffer distance in tile coordinate space to optionally clip geometries. If NULL it will default to 0.</para>
+		<para><varname>clip_geom</varname> is a boolean to control if geometries should be clipped or encoded as is. If NULL it will default to true.</para>
+
+		<para>Availability: 2.4.0</para>
+	  </refsection>
+
+	  <refsection>
+		<title>Examples</title>
+		<programlisting><![CDATA[SELECT ST_AsText(ST_AsMVTGeom(
+	ST_GeomFromText('POLYGON ((0 0, 10 0, 10 5, 0 -5, 0 0))'),
+	ST_MakeBox2D(ST_Point(0, 0), ST_Point(4096, 4096)),
+	4096, 0, false));
+                              st_astext
+--------------------------------------------------------------------
+ MULTIPOLYGON(((5 4096,10 4096,10 4091,5 4096)),((5 4096,0 4096,0 4101,5 4096)))
+
+		]]>
+		</programlisting>
+	  </refsection>
+	</refentry>
+
 	<refentry id="ST_AsMVT">
 	  <refnamediv>
 		<refname>ST_AsMVT</refname>
 
-		<refpurpose>Return a Mapbox Vector Tile representation of a set of rows.</refpurpose>
+		<refpurpose>Return a <ulink url="https://www.mapbox.com/vector-tiles/">Mapbox Vector Tile</ulink> representation of a set of rows.</refpurpose>
 	  </refnamediv>
 	  <refsynopsisdiv>
 		<funcsynopsis>
 			<funcprototype>
 				<funcdef>bytea <function>ST_AsMVT</function></funcdef>
 				<paramdef><type>text </type> <parameter>name</parameter></paramdef>
-				<paramdef><type>box2d </type> <parameter>bounds</parameter></paramdef>
 				<paramdef><type>int4 </type> <parameter>extent</parameter></paramdef>
-				<paramdef><type>int4 </type> <parameter>buffer</parameter></paramdef>
-				<paramdef><type>bool </type> <parameter>clip_geom</parameter></paramdef>
 				<paramdef><type>text </type> <parameter>geom_name</parameter></paramdef>
 				<paramdef><type>anyelement </type> <parameter>row</parameter></paramdef>
 			</funcprototype>
@@ -1368,17 +1415,15 @@
 	  <refsection>
 		<title>Description</title>
 
-		<para>Return a Mapbox Vector Tile representation (<ulink url="https://www.mapbox.com/vector-tiles/specification/">https://www.mapbox.com/vector-tiles/specification/</ulink>) of a set of rows corresponding to a Layer.
+		<para>Return a <ulink url="https://www.mapbox.com/vector-tiles/">Mapbox Vector Tile</ulink> representation of a set of rows corresponding to a Layer.
 		Multiple calls can be concatenated to a tile with multiple Layers.
-		Geometry will be automatically scaled from the geometric bounds to tile screen space and repeated coordinates thrown away.
-		Other row data will be encoded as attributes without redundancy as described by the specification.
+		Geometry is assumed to be in tile coordinate space and valid as per <ulink url="https://www.mapbox.com/vector-tiles/specification/">specification</ulink>.
+		Typically ST_AsMVTGeom can be used to transform geometry into tile coordinate space.
+		Other row data will be encoded as attributes.
 		</para>
 
 		<para><varname>name</varname> is the name of the Layer</para>
-		<para><varname>bounds</varname> is the bounds of the tile in the coordinate system of the geometry field in the row data.</para>
 		<para><varname>extent</varname> is the tile extent in screen space as defined by the specification. If NULL it will default to 4096.</para>
-		<para><varname>buffer</varname> is the buffer distance in screen space to optionally clip geometries. If NULL it will default to 0.</para>
-		<para><varname>clip_geom</varname> is a boolean to control if geometries should be clipped or encoded as is. If NULL it will default to true.</para>
 		<para><varname>geom_name</varname> is the name of the geometry column in the row data.</para>
 		<para><varname>row</varname> row data with at least a geometry column.</para>
 
@@ -1387,11 +1432,12 @@
 
 	  <refsection>
 		<title>Examples</title>
-		<programlisting><![CDATA[SELECT ST_AsMVT('test', ST_MakeBox2D(ST_Point(0, 0), ST_Point(4096, 4096)), 4096, 0, false, 'geom', q)
-FROM (SELECT 1 AS c1, ST_GeomFromText('POINT(25 17)') AS geom) AS q;
-                              st_asmvt                              
+		<programlisting><![CDATA[SELECT ST_AsMVT('test', 4096, 'geom', q) FROM (SELECT 1 AS c1,
+    ST_AsMVTGeom(ST_GeomFromText('POLYGON ((35 10, 45 45, 15 40, 10 20, 35 10), (20 30, 35 35, 30 20, 20 30))'),
+    ST_MakeBox2D(ST_Point(0, 0), ST_Point(4096, 4096)), 4096, 0, false) AS geom) AS q;
+                              st_asmvt
 --------------------------------------------------------------------
- \x1a1e0a0474657374120c12020000180122040932de3f1a026331220220017802
+ \x1a330a0474657374122112020000180322190946ec3f2214453b0a092832140f091d271a1e09091e13130f1a026331220228017802
 
 		]]>
 		</programlisting>

Modified: trunk/postgis/lwgeom_out_mvt.c
===================================================================
--- trunk/postgis/lwgeom_out_mvt.c	2017-03-06 20:40:10 UTC (rev 15322)
+++ trunk/postgis/lwgeom_out_mvt.c	2017-03-07 20:32:18 UTC (rev 15323)
@@ -36,6 +36,35 @@
 #endif  /* HAVE_LIBPROTOBUF */
 
 /**
+ * Process input parameters to mvt_geom and returned serialized geometry
+ */
+PG_FUNCTION_INFO_V1(ST_AsMVTGeom);
+Datum ST_AsMVTGeom(PG_FUNCTION_ARGS)
+{
+	LWGEOM *lwgeom_in, *lwgeom_out;
+	GSERIALIZED *geom_in, *geom_out;
+	GBOX *bounds;
+	int extent, buffer;
+	bool clip_geom;
+	if (PG_ARGISNULL(0))
+		lwerror("ST_AsMVTGeom: geom cannot be null");
+	geom_in = PG_GETARG_GSERIALIZED_P(0);
+	lwgeom_in = lwgeom_from_gserialized(geom_in);
+	if (PG_ARGISNULL(1))
+		lwerror("ST_AsMVTGeom: parameter bounds cannot be null");
+	bounds = (GBOX *) PG_GETARG_POINTER(1);
+	extent = PG_ARGISNULL(2) ? 4096 : PG_GETARG_INT32(2);
+	buffer = PG_ARGISNULL(3) ? 0 : PG_GETARG_INT32(3);
+	clip_geom = PG_ARGISNULL(4) ? true : PG_GETARG_BOOL(4);
+	lwgeom_out = mvt_geom(lwgeom_in, bounds, extent, buffer, clip_geom);
+	lwgeom_free(lwgeom_in);
+	geom_out = geometry_serialize(lwgeom_out);
+	lwgeom_free(lwgeom_out);
+	PG_FREE_IF_COPY(geom_in, 0);
+	PG_RETURN_POINTER(geom_out);
+}
+
+/**
  * Process input parameters and row data into state
  */
 PG_FUNCTION_INFO_V1(pgis_asmvt_transfn);
@@ -56,26 +85,26 @@
 		ctx = palloc(sizeof(*ctx));
 		if (PG_ARGISNULL(1))
 			lwerror("pgis_asmvt_transfn: parameter name cannot be null");
-		ctx->name = text_to_cstring(PG_GETARG_TEXT_P(1));
-		if (PG_ARGISNULL(2))
-			lwerror("pgis_asmvt_transfn: parameter bounds cannot be null");
-		ctx->bounds = (GBOX *) PG_GETARG_POINTER(2);
-		ctx->extent = PG_ARGISNULL(3) ? 4096 : PG_GETARG_INT32(3);
-		ctx->buffer = PG_ARGISNULL(4) ? 0 : PG_GETARG_INT32(4);
-		ctx->clip_geoms = PG_ARGISNULL(5) ? true : PG_GETARG_BOOL(5);
-		if (PG_ARGISNULL(6))
+		text *name = PG_GETARG_TEXT_P(1);
+		ctx->name = text_to_cstring(name);
+		PG_FREE_IF_COPY(name, 1);
+		ctx->extent = PG_ARGISNULL(2) ? 4096 : PG_GETARG_INT32(2);
+		if (PG_ARGISNULL(3))
 			lwerror("pgis_asmvt_transfn: parameter geom_name cannot be null");
-		ctx->geom_name = text_to_cstring(PG_GETARG_TEXT_P(6));
+		text *geom_name = PG_GETARG_TEXT_P(3);
+		ctx->geom_name = text_to_cstring(geom_name);
+		PG_FREE_IF_COPY(geom_name, 3);
 		mvt_agg_init_context(ctx);
 	} else {
 		ctx = (struct mvt_agg_context *) PG_GETARG_POINTER(0);
 	}
 
-	if (!type_is_rowtype(get_fn_expr_argtype(fcinfo->flinfo, 7)))
+	if (!type_is_rowtype(get_fn_expr_argtype(fcinfo->flinfo, 4)))
 		lwerror("pgis_asmvt_transfn: parameter row cannot be other than a rowtype");
-	ctx->row = PG_GETARG_HEAPTUPLEHEADER(7);
+	ctx->row = PG_GETARG_HEAPTUPLEHEADER(4);
 
 	mvt_agg_transfn(ctx);
+	PG_FREE_IF_COPY(ctx->row, 4);
 	PG_RETURN_POINTER(ctx);
 #endif
 }

Modified: trunk/postgis/mvt.c
===================================================================
--- trunk/postgis/mvt.c	2017-03-06 20:40:10 UTC (rev 15322)
+++ trunk/postgis/mvt.c	2017-03-07 20:32:18 UTC (rev 15323)
@@ -30,7 +30,7 @@
 
 #define FEATURES_CAPACITY_INITIAL 50
 
-enum mvd_cmd_id {
+enum mvt_cmd_id {
 	CMD_MOVE_TO = 1,
 	CMD_LINE_TO = 2,
 	CMD_CLOSE_PATH = 7
@@ -78,7 +78,7 @@
 	UT_hash_handle hh;
 };
 
-static inline uint32_t c_int(enum mvd_cmd_id id, uint32_t count)
+static inline uint32_t c_int(enum mvt_cmd_id id, uint32_t count)
 {
 	return (id & 0x7) | (count << 3);
 }
@@ -88,38 +88,27 @@
 	return (value << 1) ^ (value >> 31);
 }
 
-static inline int32_t scale_translate(double value, double res,
-				      double offset) {
-	return (value - offset) / res;
-}
-
 static uint32_t encode_ptarray(struct mvt_agg_context *ctx, enum mvt_type type,
 			       POINTARRAY *pa, uint32_t *buffer,
 			       int32_t *px, int32_t *py)
 {
-	POINT2D p;
 	uint32_t offset = 0;
 	uint32_t i, c = 0;
 	int32_t dx, dy, x, y;
-	uint32_t cmd_offset;
-	double xres = ctx->xres;
-	double yres = ctx->yres;
-	double xmin = ctx->bounds->xmin;
-	double ymin = ctx->bounds->ymin;
 
-	/* loop points, scale to extent and add to buffer if not repeated */
+	/* loop points and add to buffer */
 	for (i = 0; i < pa->npoints; i++) {
-		/* move offset for command and remember the latest */
+		/* move offset for command */
 		if (i == 0 || (i == 1 && type > MVT_POINT))
-			cmd_offset = offset++;
-		getPoint2d_p(pa, i, &p);
-		x = scale_translate(p.x, xres, xmin);
-		y = ctx->extent - scale_translate(p.y, yres, ymin);
+			offset++;
+		/* skip closing point for rings */
+		if (type == MVT_RING && i == pa->npoints - 1)
+			break;
+		const POINT2D *p = getPoint2d_cp(pa, i);
+		x = p->x;
+		y = p->y;
 		dx = x - *px;
 		dy = y - *py;
-		/* skip point if repeated */
-		if (i > type && dx == 0 && dy == 0)
-			continue;
 		buffer[offset++] = p_int(dx);
 		buffer[offset++] = p_int(dy);
 		*px = x;
@@ -130,12 +119,12 @@
 	/* determine initial move and eventual line command */
 	if (type == MVT_POINT) {
 		/* point or multipoint, use actual number of point count */
-		buffer[cmd_offset] = c_int(CMD_MOVE_TO, c);
+		buffer[0] = c_int(CMD_MOVE_TO, c);
 	} else {
 		/* line or polygon, assume count 1 */
-		buffer[cmd_offset - 3] = c_int(CMD_MOVE_TO, 1);
-		/* line command with move point subtracted from count */ 
-		buffer[cmd_offset] = c_int(CMD_LINE_TO, c - 1);
+		buffer[0] = c_int(CMD_MOVE_TO, 1);
+		/* line command with move point subtracted from count */
+		buffer[3] = c_int(CMD_LINE_TO, c - 1);
 	}
 
 	/* add close command if ring */
@@ -216,7 +205,7 @@
 	feature->type = VECTOR_TILE__TILE__GEOM_TYPE__POLYGON;
 	feature->has_type = 1;
 	for (i = 0; i < lwpoly->nrings; i++)
-		c += 3 + lwpoly->rings[i]->npoints * 2;
+		c += 3 + ((lwpoly->rings[i]->npoints - 1) * 2);
 	feature->geometry = palloc(sizeof(*feature->geometry) * c);
 	for (i = 0; i < lwpoly->nrings; i++)
 		offset += encode_ptarray(ctx, MVT_RING,
@@ -236,89 +225,20 @@
 	feature->has_type = 1;
 	for (i = 0; i < lwmpoly->ngeoms; i++)
 		for (j = 0; poly = lwmpoly->geoms[i], j < poly->nrings; j++)
-			c += 3 + poly->rings[j]->npoints * 2;
+			c += 3 + ((poly->rings[j]->npoints - 1) * 2);
 	feature->geometry = palloc(sizeof(*feature->geometry) * c);
 	for (i = 0; i < lwmpoly->ngeoms; i++)
 		for (j = 0; poly = lwmpoly->geoms[i], j < poly->nrings; j++)
-			offset += encode_ptarray(ctx, MVT_LINE,
+			offset += encode_ptarray(ctx, MVT_RING,
 				poly->rings[j],	feature->geometry + offset,
 				&px, &py);
 	feature->n_geometry = offset;
 }
 
-static bool check_geometry_size(LWGEOM *lwgeom, struct mvt_agg_context *ctx)
+static void encode_geometry(struct mvt_agg_context *ctx, LWGEOM *lwgeom)
 {
-	GBOX bbox;
-	lwgeom_calculate_gbox(lwgeom, &bbox);
-	double w = bbox.xmax - bbox.xmin;
-	double h = bbox.ymax - bbox.ymin;
-	return w >= ctx->xres * 2 && h >= ctx->yres * 2;
-}
-
-static bool clip_geometry(struct mvt_agg_context *ctx)
-{
-	LWGEOM *lwgeom = ctx->lwgeom;
-	GBOX *bounds = ctx->bounds;
 	int type = lwgeom->type;
-	double buffer_map_xunits = ctx->xres * ctx->buffer;
-	double buffer_map_yunits = ctx->yres * ctx->buffer;
-	double x0 = bounds->xmin - buffer_map_xunits;
-	double y0 = bounds->ymin - buffer_map_yunits;
-	double x1 = bounds->xmax + buffer_map_xunits;
-	double y1 = bounds->ymax + buffer_map_yunits;
-#if POSTGIS_GEOS_VERSION < 35
-	LWPOLY *lwenv = lwpoly_construct_envelope(0, x0, y0, x1, y1);
-	lwgeom = lwgeom_intersection(lwgeom, lwpoly_as_lwgeom(lwenv));
-#else
-	lwgeom = lwgeom_clip_by_rect(lwgeom, x0, y0, x1, y1);
-#endif
-	if (lwgeom_is_empty(lwgeom))
-		return false;
-	if (lwgeom->type == COLLECTIONTYPE) {
-		if (type == MULTIPOLYGONTYPE)
-			type = POLYGONTYPE;
-		else if (type == MULTILINETYPE)
-			type = LINETYPE;
-		else if (type == MULTIPOINTTYPE)
-			type = POINTTYPE;
-		lwgeom = lwcollection_as_lwgeom(lwcollection_extract((LWCOLLECTION*)lwgeom, type));
-	}
-	ctx->lwgeom = lwgeom;
-	return true;
-}
 
-static bool coerce_geometry(struct mvt_agg_context *ctx)
-{
-	LWGEOM *lwgeom;
-
-	if (ctx->clip_geoms && !clip_geometry(ctx))
-		return false;
-
-	lwgeom = ctx->lwgeom;
-
-	switch (lwgeom->type) {
-	case POINTTYPE:
-		return true;
-	case LINETYPE:
-	case POLYGONTYPE:
-	case MULTIPOINTTYPE:
-	case MULTILINETYPE:
-	case MULTIPOLYGONTYPE:
-		if (!check_geometry_size(lwgeom, ctx))
-			ctx->lwgeom = lwgeom_centroid(lwgeom);
-		return true;
-	default: lwerror("encode_geometry: '%s' geometry type not supported",
-		lwtype_name(lwgeom->type));
-	}
-
-	return true;
-}
-
-static void encode_geometry(struct mvt_agg_context *ctx)
-{
-	LWGEOM *lwgeom = ctx->lwgeom;
-	int type = lwgeom->type;
-
 	switch (type) {
 	case POINTTYPE:
 		return encode_point(ctx, (LWPOINT*)lwgeom);
@@ -356,7 +276,7 @@
 		char *key = tupdesc->attrs[i]->attname.data;
 		if (strcmp(key, ctx->geom_name) == 0) {
 			ctx->geom_index = i;
-			geom_name_found = true;
+			geom_name_found = 1;
 			continue;
 		}
 		keys[k++] = key;
@@ -445,7 +365,8 @@
 }
 
 static void parse_value_as_string(struct mvt_agg_context *ctx, Oid typoid,
-		Datum datum, uint32_t *tags, uint32_t c, uint32_t k) {
+		Datum datum, uint32_t *tags, uint32_t c, uint32_t k)
+{
 	struct mvt_kv_string_value *kv;
 	Oid foutoid;
 	bool typisvarlena;
@@ -523,24 +444,128 @@
 	ctx->feature->tags = tags;
 }
 
+static void ptarray_mirror_y(POINTARRAY *pa, uint32_t extent)
+{
+	int i;
+	POINT2D *p;
+
+	for (i = 0; i < pa->npoints; i++) {
+		p = (POINT2D *) getPoint_internal(pa, i);
+		p->y = extent - p->y;
+	}
+}
+
+static void lwgeom_mirror_y(LWGEOM *in, uint32_t extent)
+{
+	LWCOLLECTION *col;
+	LWPOLY *poly;
+	int i;
+
+	if ( (!in) || lwgeom_is_empty(in) ) return;
+
+	switch (in->type) {
+	case POINTTYPE:
+		ptarray_mirror_y(lwgeom_as_lwpoint(in)->point, extent);
+		break;
+	case LINETYPE:
+		ptarray_mirror_y(lwgeom_as_lwline(in)->points, extent);
+		break;
+	case POLYGONTYPE:
+		poly = (LWPOLY *) in;
+		for (i=0; i<poly->nrings; i++)
+			ptarray_mirror_y(poly->rings[i], extent);
+		break;
+	case MULTIPOINTTYPE:
+	case MULTILINETYPE:
+	case MULTIPOLYGONTYPE:
+	case COLLECTIONTYPE:
+		col = (LWCOLLECTION *) in;
+		for (i=0; i<col->ngeoms; i++)
+			lwgeom_mirror_y(col->geoms[i], extent);
+		break;
+	default:
+		lwerror("lwgeom_mirror_y: unsupported geometry type: %s",
+		        lwtype_name(in->type));
+		return;
+	}
+
+	lwgeom_drop_bbox(in);
+	lwgeom_add_bbox(in);
+}
+
 /**
+ * Transform a geometry into vector tile coordinate space.
+ *
+ * Makes best effort to keep validity. Might collapse geometry into lower
+ * dimension.
+ */
+LWGEOM *mvt_geom(LWGEOM *lwgeom, GBOX *gbox, uint32_t extent, uint32_t buffer, 
+		 bool clip_geom)
+{
+	double width = gbox->xmax - gbox->xmin;
+	double height = gbox->ymax - gbox->ymin;
+	double resx = width / extent;
+	double resy = height / extent;
+
+	if (width == 0 || height == 0)
+		lwerror("mvt_geom: bounds width or height cannot be 0");
+
+	if (extent == 0)
+		lwerror("mvt_geom: extent cannot be 0");
+
+	if (clip_geom) {
+		double buffer_map_xunits = resx * buffer;
+		double buffer_map_yunits = resy * buffer;
+		double x0 = gbox->xmin - buffer_map_xunits;
+		double y0 = gbox->ymin - buffer_map_yunits;
+		double x1 = gbox->xmax + buffer_map_xunits;
+		double y1 = gbox->ymax + buffer_map_yunits;
+#if POSTGIS_GEOS_VERSION < 35
+		LWPOLY *lwenv = lwpoly_construct_envelope(0, x0, y0, x1, y1);
+		lwgeom = lwgeom_intersection(lwgeom, lwpoly_as_lwgeom(lwenv));
+		lwpoly_free(lwenv);
+#else
+		lwgeom = lwgeom_clip_by_rect(lwgeom, x0, y0, x1, y1);
+#endif
+	}
+
+	POINT4D factors;
+	factors.x = resx;
+	factors.y = resy;
+	factors.z = 1;
+	factors.m = 1;
+
+	lwgeom_scale(lwgeom, &factors);
+
+	gridspec grid;
+	memset(&grid, 0, sizeof(gridspec));
+	grid.ipx = 0;
+	grid.ipy = 0;
+	grid.xsize = 1;
+	grid.ysize = 1;
+
+	LWGEOM *lwgeom_out = lwgeom_grid(lwgeom, &grid);
+
+	if (lwgeom_out == NULL)
+		lwgeom_out = lwgeom_grid(lwgeom_centroid(lwgeom), &grid);
+
+	lwgeom_force_clockwise(lwgeom_out);
+	lwgeom_mirror_y(lwgeom_out, extent);
+	lwgeom_out = lwgeom_make_valid(lwgeom_out);
+
+	return lwgeom_out;
+}
+
+/**
  * Initialize aggregation context.
  */
 void mvt_agg_init_context(struct mvt_agg_context *ctx) 
 {
 	VectorTile__Tile__Layer *layer;
-	double width, height;
 
-	width = ctx->bounds->xmax - ctx->bounds->xmin;
-	height = ctx->bounds->ymax - ctx->bounds->ymin;
-
-	if (width == 0 || height == 0)
-		lwerror("mvt_agg_init_context: bounds width or height cannot be 0");
 	if (ctx->extent == 0)
 		lwerror("mvt_agg_init_context: extent cannot be 0");
 
-	ctx->xres = width / ctx->extent;
-	ctx->yres = height / ctx->extent;
 	ctx->features_capacity = FEATURES_CAPACITY_INITIAL;
 	ctx->string_values_hash = NULL;
 	ctx->float_values_hash = NULL;
@@ -555,6 +580,7 @@
 	vector_tile__tile__layer__init(layer);
 	layer->version = 2;
 	layer->name = ctx->name;
+	layer->has_extent = 1;
 	layer->extent = ctx->extent;
 	layer->features = palloc (ctx->features_capacity *
 		sizeof(*layer->features));
@@ -592,14 +618,13 @@
 	if (!datum)
 		lwerror("mvt_agg_transfn: geometry column cannot be null");
 	GSERIALIZED *gs = (GSERIALIZED *) PG_DETOAST_DATUM(datum);
-	ctx->lwgeom = lwgeom_from_gserialized(gs);
+	LWGEOM *lwgeom = lwgeom_from_gserialized(gs);
 
-	if (!coerce_geometry(ctx))
-		return;
-
 	layer->features[layer->n_features++] = feature;
 
-	encode_geometry(ctx);
+	encode_geometry(ctx, lwgeom);
+	lwgeom_free(lwgeom);
+	// TODO: free detoasted datum?
 	parse_values(ctx);
 }
 

Modified: trunk/postgis/mvt.h
===================================================================
--- trunk/postgis/mvt.h	2017-03-06 20:40:10 UTC (rev 15322)
+++ trunk/postgis/mvt.h	2017-03-07 20:32:18 UTC (rev 15323)
@@ -37,6 +37,7 @@
 #include "access/htup.h"
 #include "../postgis_config.h"
 #include "liblwgeom.h"
+#include "liblwgeom_internal.h"
 #include "lwgeom_pg.h"
 #include "lwgeom_log.h"
 
@@ -45,20 +46,14 @@
 #include "vector_tile.pb-c.h"
 
 struct mvt_agg_context {
-	GBOX *bounds;
 	char *name;
 	uint32_t extent;
-	uint32_t buffer;
-	bool clip_geoms;
 	char *geom_name;
 	uint32_t geom_index;
-	LWGEOM *lwgeom;
 	HeapTupleHeader row;
 	VectorTile__Tile__Feature *feature;
 	VectorTile__Tile__Layer *layer;
 	size_t features_capacity;
-	double xres;
-	double yres;
 	struct mvt_kv_string_value *string_values_hash;
 	struct mvt_kv_float_value *float_values_hash;
 	struct mvt_kv_double_value *double_values_hash;
@@ -69,6 +64,7 @@
 	uint32_t values_hash_i;
 } ;
 
+LWGEOM *mvt_geom(LWGEOM *geom, GBOX *bounds, uint32_t extent, uint32_t buffer, bool clip_geom);
 void mvt_agg_init_context(struct mvt_agg_context *ctx);
 void mvt_agg_transfn(struct mvt_agg_context *ctx);
 uint8_t *mvt_agg_finalfn(struct mvt_agg_context *ctx);

Modified: trunk/postgis/postgis.sql.in
===================================================================
--- trunk/postgis/postgis.sql.in	2017-03-06 20:40:10 UTC (rev 15322)
+++ trunk/postgis/postgis.sql.in	2017-03-07 20:32:18 UTC (rev 15323)
@@ -4387,7 +4387,7 @@
 -----------------------------------------------------------------------
 
 -- Availability: 2.4.0
-CREATE OR REPLACE FUNCTION pgis_asmvt_transfn(internal, text, box2d, int4, int4, bool, text, anyelement)
+CREATE OR REPLACE FUNCTION pgis_asmvt_transfn(internal, text, int4, text, anyelement)
 	RETURNS internal
 	AS 'MODULE_PATHNAME', 'pgis_asmvt_transfn'
 	LANGUAGE c IMMUTABLE;
@@ -4399,7 +4399,7 @@
 	LANGUAGE c IMMUTABLE;
 
 -- Availability: 2.4.0
-CREATE AGGREGATE ST_AsMVT(text, box2d, int4, int4, bool, text, anyelement)
+CREATE AGGREGATE ST_AsMVT(text, int4, text, anyelement)
 (
 	sfunc = pgis_asmvt_transfn,
 	stype = internal,
@@ -4407,6 +4407,12 @@
 );
 
 -- Availability: 2.4.0
+CREATE OR REPLACE FUNCTION ST_AsMVTGeom(geom geometry, bounds box2d, extent int4, buffer int4, clip_geom bool)
+	RETURNS geometry
+	AS 'MODULE_PATHNAME','ST_AsMVTGeom'
+	LANGUAGE 'c' IMMUTABLE  _PARALLEL;
+
+-- Availability: 2.4.0
 CREATE OR REPLACE FUNCTION postgis_libprotobuf_version()
 	RETURNS text
 	AS 'MODULE_PATHNAME','postgis_libprotobuf_version'

Modified: trunk/regress/mvt.sql
===================================================================
--- trunk/regress/mvt.sql	2017-03-06 20:40:10 UTC (rev 15322)
+++ trunk/regress/mvt.sql	2017-03-07 20:32:18 UTC (rev 15323)
@@ -1,79 +1,80 @@
+-- geometry preprocessing tests
+select 'PG1', ST_AsText(ST_AsMVTGeom(
+	ST_Point(1, 2),
+	ST_MakeBox2D(ST_Point(0, 0), ST_Point(4096, 4096)),
+	4096, 0, false));
+select 'PG2', ST_AsText(ST_AsMVTGeom(
+	ST_Point(1, 2),
+	ST_MakeBox2D(ST_Point(0, 0), ST_Point(4096*2, 4096*2)),
+	4096, 0, false));
+select 'PG3', ST_AsText(ST_AsMVTGeom(
+	ST_Point(1, 2),
+	ST_MakeBox2D(ST_Point(0, 0), ST_Point(4096/2, 4096/2)),
+	4096, 0, false));
+select 'PG4', ST_AsText(ST_AsMVTGeom(
+	ST_GeomFromText('POLYGON ((0 0, 10 0, 10 5, 0 -5, 0 0))'),
+	ST_MakeBox2D(ST_Point(0, 0), ST_Point(4096, 4096)),
+	4096, 0, false));
+select 'PG5', ST_AsText(ST_AsMVTGeom(
+	ST_GeomFromText('POLYGON ((0 0, 10 0, 10 5, 0 -5, 0 0))'),
+	ST_MakeBox2D(ST_Point(0, 0), ST_Point(1, 1)),
+	4096, 0, false));
+
 -- geometry encoding tests
-SELECT 'TG1', encode(ST_AsMVT('test',
-    ST_MakeBox2D(ST_Point(0, 0), ST_Point(4096, 4096)), 4096, 0, false, 'geom', q
-), 'base64') FROM (SELECT 1 AS c1,
-    ST_GeomFromText('POINT(25 17)') AS geom) AS q;
-SELECT 'TG2', encode(ST_AsMVT('test',
-    ST_MakeBox2D(ST_Point(0, 0), ST_Point(4096, 4096)), 4096, 0, false, 'geom', q
-), 'base64') FROM (SELECT 1 AS c1,
-    ST_GeomFromText('MULTIPOINT(25 17, 26 18)') AS geom) AS q;
-SELECT 'TG3', encode(ST_AsMVT('test',
-    ST_MakeBox2D(ST_Point(0, 0), ST_Point(4096, 4096)), 4096, 0, false, 'geom', q
-), 'base64') FROM (SELECT 1 AS c1,
-    ST_GeomFromText('LINESTRING(0 0, 1000 1000)') AS geom) AS q;
-SELECT 'TG4', encode(ST_AsMVT('test',
-    ST_MakeBox2D(ST_Point(0, 0), ST_Point(4096, 4096)), 4096, 0, false, 'geom', q
-), 'base64') FROM (SELECT 1 AS c1,
-    ST_GeomFromText('LINESTRING(0 0, 500 500, 1000 1000)') AS geom) AS q;
-SELECT 'TG5', encode(ST_AsMVT('test',
-    ST_MakeBox2D(ST_Point(0, 0), ST_Point(4096, 4096)), 4096, 0, false, 'geom', q
-), 'base64') FROM (SELECT 1 AS c1,
-    ST_GeomFromText('MULTILINESTRING((1 1, 501 501, 1001 1001),(2 2, 502 502, 1002 1002))') AS geom) AS q;
-SELECT 'TG6', encode(ST_AsMVT('test',
-    ST_MakeBox2D(ST_Point(0, 0), ST_Point(4096, 4096)), 4096, 0, false, 'geom', q
-), 'base64') FROM (SELECT 1 AS c1,
-    ST_GeomFromText('POLYGON ((35 10, 45 45, 15 40, 10 20, 35 10), (20 30, 35 35, 30 20, 20 30))') AS geom) AS q;
-SELECT 'TG7', encode(ST_AsMVT('test',
-    ST_MakeBox2D(ST_Point(0, 0), ST_Point(4096, 4096)), 4096, 0, false, 'geom', q
-), 'base64') FROM (SELECT 1 AS c1, 
-    ST_GeomFromText('MULTIPOLYGON (((40 40, 20 45, 45 30, 40 40)), ((20 35, 10 30, 10 10, 30 5, 45 20, 20 35), (30 20, 20 15, 20 25, 30 20)))') AS geom) AS q;
-SELECT 'TG8', encode(ST_AsMVT('test',
-    ST_MakeBox2D(ST_Point(0, 0), ST_Point(4096, 4096)), 4096, 0, true, 'geom', q
-), 'base64') FROM (SELECT 1 AS c1,
-    ST_GeomFromText('POINT(25 17)') AS geom) AS q;
-SELECT 'TG9', encode(ST_AsMVT('test',
-    ST_MakeBox2D(ST_Point(0, 0), ST_Point(4096, 4096)), 4096, 0, true, 'geom', q
-), 'base64') FROM (SELECT 1 AS c1,
-    ST_GeomFromText('MULTIPOINT(25 17, -26 -18)') AS geom) AS q;
+SELECT 'TG1', encode(ST_AsMVT('test', 4096, 'geom', q), 'base64') FROM (SELECT 1 AS c1,
+    ST_AsMVTGeom(ST_GeomFromText('POINT(25 17)'),
+    ST_MakeBox2D(ST_Point(0, 0), ST_Point(4096, 4096)), 4096, 0, false) AS geom) AS q;
+SELECT 'TG2', encode(ST_AsMVT('test', 4096, 'geom', q), 'base64') FROM (SELECT 1 AS c1,
+    ST_AsMVTGeom(ST_GeomFromText('MULTIPOINT(25 17, 26 18)'),
+    ST_MakeBox2D(ST_Point(0, 0), ST_Point(4096, 4096)), 4096, 0, false) AS geom) AS q;
+SELECT 'TG3', encode(ST_AsMVT('test', 4096, 'geom', q), 'base64') FROM (SELECT 1 AS c1,
+    ST_AsMVTGeom(ST_GeomFromText('LINESTRING(0 0, 1000 1000)'),
+    ST_MakeBox2D(ST_Point(0, 0), ST_Point(4096, 4096)), 4096, 0, false) AS geom) AS q;
+SELECT 'TG4', encode(ST_AsMVT('test', 4096, 'geom', q), 'base64') FROM (SELECT 1 AS c1,
+    ST_AsMVTGeom(ST_GeomFromText('LINESTRING(0 0, 500 500, 1000 1000)'),
+    ST_MakeBox2D(ST_Point(0, 0), ST_Point(4096, 4096)), 4096, 0, false) AS geom) AS q;
+SELECT 'TG5', encode(ST_AsMVT('test', 4096, 'geom', q), 'base64') FROM (SELECT 1 AS c1,
+    ST_AsMVTGeom(ST_GeomFromText('MULTILINESTRING((1 1, 501 501, 1001 1001),(2 2, 502 502, 1002 1002))'),
+    ST_MakeBox2D(ST_Point(0, 0), ST_Point(4096, 4096)), 4096, 0, false) AS geom) AS q;
+SELECT 'TG6', encode(ST_AsMVT('test', 4096, 'geom', q), 'base64') FROM (SELECT 1 AS c1,
+    ST_AsMVTGeom(ST_GeomFromText('POLYGON ((35 10, 45 45, 15 40, 10 20, 35 10), (20 30, 35 35, 30 20, 20 30))'),
+    ST_MakeBox2D(ST_Point(0, 0), ST_Point(4096, 4096)), 4096, 0, false) AS geom) AS q;
+SELECT 'TG7', encode(ST_AsMVT('test', 4096, 'geom', q), 'base64') FROM (SELECT 1 AS c1,
+    ST_AsMVTGeom(ST_GeomFromText('MULTIPOLYGON (((40 40, 20 45, 45 30, 40 40)), ((20 35, 10 30, 10 10, 30 5, 45 20, 20 35), (30 20, 20 15, 20 25, 30 20)))'),
+    ST_MakeBox2D(ST_Point(0, 0), ST_Point(4096, 4096)), 4096, 0, false) AS geom) AS q;
+SELECT 'TG8', encode(ST_AsMVT('test', 4096, 'geom', q), 'base64') FROM (SELECT 1 AS c1,
+    ST_AsMVTGeom(ST_GeomFromText('POINT(25 17)'),
+    ST_MakeBox2D(ST_Point(0, 0), ST_Point(4096, 4096)), 4096, 0, false) AS geom) AS q;
+SELECT 'TG9', encode(ST_AsMVT('test', 4096, 'geom', q), 'base64') FROM (SELECT 1 AS c1,
+    ST_AsMVTGeom(ST_GeomFromText('MULTIPOINT(25 17, -26 -18)'),
+    ST_MakeBox2D(ST_Point(0, 0), ST_Point(4096, 4096)), 4096, 0, false) AS geom) AS q;
 
 -- attribute encoding tests
-SELECT 'TA1', encode(ST_AsMVT('test',
-    ST_MakeBox2D(ST_Point(0, 0), ST_Point(4096, 4096)), 4096, 0, false, 'geom', q
-), 'base64') FROM (SELECT 1 AS c1, 'abcd'::text AS c2,
-    ST_GeomFromText('POINT(25 17)') AS geom) AS q;
-SELECT 'TA2', encode(ST_AsMVT('test',
-    ST_MakeBox2D(ST_Point(0, 0), ST_Point(4096, 4096)), 4096, 0, false, 'geom', q
-), 'base64') FROM (SELECT 1.1::double precision AS c1,
-    ST_GeomFromText('POINT(25 17)') AS geom) AS q;
-SELECT 'TA3', encode(ST_AsMVT('test',
-    ST_MakeBox2D(ST_Point(0, 0), ST_Point(4096, 4096)), 4096, 0, false, 'geom', q
-), 'base64') FROM (SELECT NULL::integer AS c1,
-    ST_GeomFromText('POINT(25 17)') AS geom) AS q;
-SELECT 'TA4', encode(ST_AsMVT('test',
-    ST_MakeBox2D(ST_Point(0, 0), ST_Point(4096, 4096)), 4096, 0, false, 'geom', q
-), 'base64') FROM (
-    SELECT 1 AS c1, ST_GeomFromText('POINT(25 17)') AS geom
+SELECT 'TA1', encode(ST_AsMVT('test', 4096, 'geom', q), 'base64') FROM (SELECT 1 AS c1, 'abcd'::text AS c2,
+    ST_AsMVTGeom(ST_GeomFromText('POINT(25 17)'),
+    ST_MakeBox2D(ST_Point(0, 0), ST_Point(4096, 4096)), 4096, 0, false) AS geom) AS q;
+SELECT 'TA2', encode(ST_AsMVT('test', 4096, 'geom', q), 'base64') FROM (SELECT 1.1::double precision AS c1,
+    ST_AsMVTGeom(ST_GeomFromText('POINT(25 17)'),
+    ST_MakeBox2D(ST_Point(0, 0), ST_Point(4096, 4096)), 4096, 0, false) AS geom) AS q;
+SELECT 'TA3', encode(ST_AsMVT('test',  4096, 'geom', q), 'base64') FROM (SELECT NULL::integer AS c1,
+    ST_AsMVTGeom(ST_GeomFromText('POINT(25 17)'),
+    ST_MakeBox2D(ST_Point(0, 0), ST_Point(4096, 4096)), 4096, 0, false) AS geom) AS q;
+SELECT 'TA4', encode(ST_AsMVT('test', 4096, 'geom', q), 'base64') FROM (
+    SELECT 1 AS c1, ST_AsMVTGeom(ST_GeomFromText('POINT(25 17)'),
+    ST_MakeBox2D(ST_Point(0, 0), ST_Point(4096, 4096)), 4096, 0, false) AS geom
     UNION
-    SELECT 2 AS c1, ST_GeomFromText('POINT(25 17)') AS geom) AS q;
-SELECT 'TA5', encode(ST_AsMVT('test',
-    ST_MakeBox2D(ST_Point(0, 0), ST_Point(4096, 4096)), 4096, 0, false, 'geom', q
-), 'base64') FROM (SELECT ST_GeomFromText('POINT(25 17)') AS geom, 1 AS c1, 'abcd'::text AS c2) AS q;
-SELECT 'TA6', encode(ST_AsMVT('test',
-    ST_MakeBox2D(ST_Point(0, 0), ST_Point(4096, 4096)), 4096, 0, false, 'geom', q
-), 'base64') FROM (SELECT 1 AS c1, -1 AS c2,
-    ST_GeomFromText('POINT(25 17)') AS geom) AS q;
+    SELECT 2 AS c1, ST_AsMVTGeom(ST_GeomFromText('POINT(25 17)'),
+    ST_MakeBox2D(ST_Point(0, 0), ST_Point(4096, 4096)), 4096, 0, false) AS geom) AS q;
+SELECT 'TA5', encode(ST_AsMVT('test', 4096, 'geom', q), 'base64') FROM (SELECT 
+    ST_AsMVTGeom(ST_GeomFromText('POINT(25 17)'),
+    ST_MakeBox2D(ST_Point(0, 0), ST_Point(4096, 4096)), 4096, 0, false) AS geom, 1 AS c1, 'abcd'::text AS c2) AS q;
+SELECT 'TA6', encode(ST_AsMVT('test', 4096, 'geom', q), 'base64') FROM (SELECT 1 AS c1, -1 AS c2,
+    ST_AsMVTGeom(ST_GeomFromText('POINT(25 17)'),
+    ST_MakeBox2D(ST_Point(0, 0), ST_Point(4096, 4096)), 4096, 0, false) AS geom) AS q;
 
 -- unsupported input
--- NOTE: disabled test as it's dependant on PostgreSQL error text that cannot be expected to be stable
---SELECT 'TU1';
---SELECT encode(ST_AsMVT('test',
---    ST_MakeBox2D(ST_Point(0, 0), ST_Point(4096, 4096)), 4096, 0, false, 'geom', NULL
---), 'base64');
 SELECT 'TU2';
-SELECT encode(ST_AsMVT('test',
-    ST_MakeBox2D(ST_Point(0, 0), ST_Point(4096, 4096)), 4096, 0, false, 'geom', 1
-), 'base64');
+SELECT encode(ST_AsMVT('test', 4096, 'geom', 1), 'base64');
 SELECT 'TU3';
-SELECT encode(ST_AsMVT('test',
-    ST_MakeBox2D(ST_Point(0, 0), ST_Point(4096, 4096)), 4096, 0, false, 'geom', q
-), 'base64') FROM (SELECT NULL::integer AS c1, NULL AS geom) AS q;
\ No newline at end of file
+SELECT encode(ST_AsMVT('test', 4096, 'geom', q), 'base64')
+    FROM (SELECT NULL::integer AS c1, NULL AS geom) AS q;
\ No newline at end of file

Modified: trunk/regress/mvt_expected
===================================================================
--- trunk/regress/mvt_expected	2017-03-06 20:40:10 UTC (rev 15322)
+++ trunk/regress/mvt_expected	2017-03-07 20:32:18 UTC (rev 15323)
@@ -1,19 +1,25 @@
-TG1|Gh4KBHRlc3QSDBICAAAYASIECTLePxoCYzEiAigBeAI=
-TG2|Gh4KBHRlc3QSDBICAAAYASIECTLePxoCYzEiAigBeAI=
-TG3|GiMKBHRlc3QSERICAAAYAiIJCQCAQArQD88PGgJjMSICKAF4Ag==
-TG4|GicKBHRlc3QSFRICAAAYAiINCQCAQBLoB+cH6AfnBxoCYzEiAigBeAI=
-TG5|GjUKBHRlc3QSIxICAAAYAiIbCQL+PxLoB+cH6AfnBwnND84PEugH5wfoB+cHGgJjMSICKAF4Ag==
-TG6|GjMKBHRlc3QSIRICAAAYAyIZCUbsPyIURTsKCSgyFA8JHScaHgkJHhMTDxoCYzEiAigBeAI=
-TG7|Gj0KBHRlc3QSKxICAAAYAyIjCVCwPxonCTIeCRMJJwoqEwoAKCgKHh0xHQkUHhoTCgATFAoaAmMx
-IgIoAXgC
-TG8|Gh4KBHRlc3QSDBICAAAYASIECTLePxoCYzEiAigBeAI=
-TG9|Gh4KBHRlc3QSDBICAAAYASIECTLePxoCYzEiAigBeAI=
-TA1|GiwKBHRlc3QSDhIEAAABARgBIgQJMt4/GgJjMRoCYzIiAigBIgYKBGFiY2R4Ag==
-TA2|GiUKBHRlc3QSDBICAAAYASIECTLePxoCYzEiCRmamZmZmZnxP3gC
-TA3|GhYKBHRlc3QSCBgBIgQJMt4/GgJjMXgC
-TA4|GjAKBHRlc3QSDBICAAAYASIECTLePxIMEgIAARgBIgQJMt4/GgJjMSICKAEiAigCeAI=
-TA5|GiwKBHRlc3QSDhIEAAABARgBIgQJMt4/GgJjMRoCYzIiAigBIgYKBGFiY2R4Ag==
-TA6|GigKBHRlc3QSDhIEAAABARgBIgQJMt4/GgJjMRoCYzIiAigBIgIwAXgC
+PG1|POINT(1 4094)
+PG2|POINT(2 4092)
+PG3|POINT(0 4095)
+PG4|MULTIPOLYGON(((5 4096,10 4096,10 4091,5 4096)),((0 4096,0 4101,5 4096,0 4096)))
+PG5|POINT(0 4096)
+TG1|GiEKBHRlc3QSDBICAAAYASIECTLePxoCYzEiAigBKIAgeAI=
+TG2|GiMKBHRlc3QSDhICAAAYASIGETLePwIBGgJjMSICKAEogCB4Ag==
+TG3|GiYKBHRlc3QSERICAAAYAiIJCQCAQArQD88PGgJjMSICKAEogCB4Ag==
+TG4|GioKBHRlc3QSFRICAAAYAiINCQCAQBLoB+cH6AfnBxoCYzEiAigBKIAgeAI=
+TG5|GjgKBHRlc3QSIxICAAAYAiIbCQL+PxLoB+cH6AfnBwnND84PEugH5wfoB+cHGgJjMSICKAEogCB4
+Ag==
+TG6|GjIKBHRlc3QSHRICAAAYAyIVCUbsPxoxEwonPAkPCTEeEhQUCh0PGgJjMSICKAEogCB4Ag==
+TG7|Gj0KBHRlc3QSKBICAAAYAyIgCVCwPxIKFDEdDwkAFCIyHh0eJwkAJw8JKBQSEwkAFA8aAmMxIgIo
+ASiAIHgC
+TG8|GiEKBHRlc3QSDBICAAAYASIECTLePxoCYzEiAigBKIAgeAI=
+TG9|GiMKBHRlc3QSDhICAAAYASIGETLeP2VGGgJjMSICKAEogCB4Ag==
+TA1|Gi8KBHRlc3QSDhIEAAABARgBIgQJMt4/GgJjMRoCYzIiAigBIgYKBGFiY2QogCB4Ag==
+TA2|GigKBHRlc3QSDBICAAAYASIECTLePxoCYzEiCRmamZmZmZnxPyiAIHgC
+TA3|GhkKBHRlc3QSCBgBIgQJMt4/GgJjMSiAIHgC
+TA4|GjMKBHRlc3QSDBICAAAYASIECTLePxIMEgIAARgBIgQJMt4/GgJjMSICKAEiAigCKIAgeAI=
+TA5|Gi8KBHRlc3QSDhIEAAABARgBIgQJMt4/GgJjMRoCYzIiAigBIgYKBGFiY2QogCB4Ag==
+TA6|GisKBHRlc3QSDhIEAAABARgBIgQJMt4/GgJjMRoCYzIiAigBIgIwASiAIHgC
 TU2
 ERROR:  pgis_asmvt_transfn: parameter row cannot be other than a rowtype
 TU3



More information about the postgis-tickets mailing list