[postgis-tickets] r15317 - Geobuf output support via ST_AsGeobuf

bjorn at wololo.org bjorn at wololo.org
Sun Feb 26 10:16:55 PST 2017


Author: bjornharrtell
Date: 2017-02-26 10:16:54 -0800 (Sun, 26 Feb 2017)
New Revision: 15317

Added:
   trunk/postgis/geobuf.LICENSE
   trunk/postgis/geobuf.c
   trunk/postgis/geobuf.h
   trunk/postgis/geobuf.proto
   trunk/postgis/lwgeom_out_geobuf.c
   trunk/regress/geobuf.sql
   trunk/regress/geobuf_expected
Modified:
   trunk/NEWS
   trunk/doc/reference_output.xml
   trunk/postgis/Makefile.in
   trunk/postgis/postgis.sql.in
   trunk/regress/Makefile.in
Log:
Geobuf output support via ST_AsGeobuf
Closes #3599

Modified: trunk/NEWS
===================================================================
--- trunk/NEWS	2017-02-26 16:42:56 UTC (rev 15316)
+++ trunk/NEWS	2017-02-26 18:16:54 UTC (rev 15317)
@@ -3,6 +3,7 @@
 
  * New Features *
 
+  - #3599, Geobuf output support via ST_AsGeobuf (Björn Harrtell)
   - #3661, Mapbox vector tile output support via ST_AsMVT (Björn Harrtell / CartoDB)
 
 PostGIS 2.3.0

Modified: trunk/doc/reference_output.xml
===================================================================
--- trunk/doc/reference_output.xml	2017-02-26 16:42:56 UTC (rev 15316)
+++ trunk/doc/reference_output.xml	2017-02-26 18:16:54 UTC (rev 15317)
@@ -1300,6 +1300,50 @@
 	  </refsection>
 	</refentry>
 
+	<refentry id="ST_AsGeobuf">
+	  <refnamediv>
+		<refname>ST_AsGeobuf</refname>
+
+		<refpurpose>Return a Geobuf representation of a set of rows.</refpurpose>
+	  </refnamediv>
+	  <refsynopsisdiv>
+		<funcsynopsis>
+			<funcprototype>
+				<funcdef>bytea <function>ST_AsGeobuf</function></funcdef>
+				<paramdef><type>text </type> <parameter>geom_name</parameter></paramdef>
+				<paramdef><type>anyelement </type> <parameter>row</parameter></paramdef>
+			</funcprototype>
+		</funcsynopsis>
+	  </refsynopsisdiv>
+
+	  <refsection>
+		<title>Description</title>
+
+		<para>
+			Return a Geobuf representation (<ulink url="https://github.com/mapbox/geobuf">https://github.com/mapbox/geobuf</ulink>) of a set of rows corresponding to a FeatureCollection.
+			Every input geometry is analyzed to determine maximum precision for optimal storage.
+			Note that Geobuf in its current form cannot be streamed so the full output will be assembled in memory.
+		</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>
+
+		<para>Availability: 2.4.0</para>
+	  </refsection>
+
+	  <refsection>
+		<title>Examples</title>
+		<programlisting><![CDATA[encode(ST_AsGeobuf('geom', q), 'base64')
+    FROM (SELECT ST_GeomFromText('POLYGON((0 0,0 1,1 1,1 0,0 0))') as geom) AS q;
+ st_asgeobuf
+----------------------------------
+ GAAiEAoOCgwIBBoIAAAAAgIAAAE=
+
+		]]>
+		</programlisting>
+	  </refsection>
+	</refentry>
+
 	<refentry id="ST_AsMVT">
 	  <refnamediv>
 		<refname>ST_AsMVT</refname>

Modified: trunk/postgis/Makefile.in
===================================================================
--- trunk/postgis/Makefile.in	2017-02-26 16:42:56 UTC (rev 15316)
+++ trunk/postgis/Makefile.in	2017-02-26 18:16:54 UTC (rev 15317)
@@ -51,7 +51,7 @@
 endif
 
 ifeq (@HAVE_PROTOBUF@,yes)
-PROTOBUF_OBJ=vector_tile.pb-c.o
+PROTOBUF_OBJ=vector_tile.pb-c.o geobuf.pb-c.o
 endif
 
 # SQL preprocessor
@@ -104,6 +104,8 @@
 	$(PROTOBUF_OBJ) \
 	mvt.o \
 	lwgeom_out_mvt.o \
+	geobuf.o \
+	lwgeom_out_geobuf.o
 
 # Objects to build using PGXS
 OBJS=$(PG_OBJS)
@@ -135,7 +137,9 @@
 	sfcgal_upgrade.sql.in \
 	sfcgal_upgrade.sql \
 	vector_tile.pb-c.c \
-	vector_tile.pb-c.h
+	vector_tile.pb-c.h \
+	geobuf.pb-c.c \
+	geobuf.pb-c.h
 
 # PGXS information
 PG_CONFIG = @PG_CONFIG@
@@ -186,9 +190,15 @@
 vector_tile.pb-c.c vector_tile.pb-c.h: vector_tile.proto
 	$(PROTOCC) --c_out=. $<
 
+# Generate Geobuf encoder/decoder using protobuf-c compiler 
+geobuf.pb-c.c geobuf.pb-c.h: geobuf.proto
+	$(PROTOCC) --c_out=. $<
+
 ifeq (@HAVE_PROTOBUF@,yes)
 lwgeom_out_mvt.o: vector_tile.pb-c.h
 mvt.o: vector_tile.pb-c.h
+lwgeom_out_geobuf.o: geobuf.pb-c.h
+geobuf.o: geobuf.pb-c.h
 endif
 
 # Borrow the $libdir substitution from PGXS but customise by running the preprocessor

Added: trunk/postgis/geobuf.LICENSE
===================================================================
--- trunk/postgis/geobuf.LICENSE	                        (rev 0)
+++ trunk/postgis/geobuf.LICENSE	2017-02-26 18:16:54 UTC (rev 15317)
@@ -0,0 +1,15 @@
+ISC License
+
+Copyright (c) 2015, Mapbox
+
+Permission to use, copy, modify, and/or distribute this software for any
+purpose with or without fee is hereby granted, provided that the above
+copyright notice and this permission notice appear in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
\ No newline at end of file

Added: trunk/postgis/geobuf.c
===================================================================
--- trunk/postgis/geobuf.c	                        (rev 0)
+++ trunk/postgis/geobuf.c	2017-02-26 18:16:54 UTC (rev 15317)
@@ -0,0 +1,600 @@
+/**********************************************************************
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ *
+ **********************************************************************
+ *
+ * Copyright (C) 2016-2017 Björn Harrtell <bjorn at wololo.org>
+ *
+ **********************************************************************/
+
+#include <math.h>
+#include "geobuf.h"
+
+#ifdef HAVE_LIBPROTOBUF
+
+#define FEATURES_CAPACITY_INITIAL 50
+#define MAX_PRECISION 1e6
+
+static Data__Geometry *encode_geometry(struct geobuf_agg_context *ctx, LWGEOM *lwgeom);
+
+static Data__Geometry *galloc(Data__Geometry__Type type) {
+	Data__Geometry *geometry;
+	geometry = palloc (sizeof (Data__Geometry));
+	data__geometry__init(geometry);
+	geometry->type = type;
+	return geometry;
+}
+
+static TupleDesc get_tuple_desc(struct geobuf_agg_context *ctx)
+{
+	Oid tupType = HeapTupleHeaderGetTypeId(ctx->row);
+	int32 tupTypmod = HeapTupleHeaderGetTypMod(ctx->row);
+	TupleDesc tupdesc = lookup_rowtype_tupdesc(tupType, tupTypmod);
+	return tupdesc;
+}
+
+static void encode_keys(struct geobuf_agg_context *ctx)
+{
+	TupleDesc tupdesc = get_tuple_desc(ctx);
+	int natts = tupdesc->natts;
+	char **keys = palloc(natts * sizeof(*keys));
+	uint32_t i, k = 0;
+	bool geom_name_found = false;
+	for (i = 0; i < natts; i++) {
+		char *key = tupdesc->attrs[i]->attname.data;
+		if (strcmp(key, ctx->geom_name) == 0) {
+			ctx->geom_index = i;
+			geom_name_found = true;
+			continue;
+		}
+		keys[k++] = key;
+	}
+	if (!geom_name_found)
+		lwerror("encode_keys: no column with specificed geom_name found");
+	ctx->data->n_keys = k;
+	ctx->data->keys = keys;
+	ReleaseTupleDesc(tupdesc);
+}
+
+
+static void set_int_value(Data__Value *value, int64 intval) {
+	if (intval >= 0) {
+		value->value_type_case = DATA__VALUE__VALUE_TYPE_POS_INT_VALUE;
+		value->pos_int_value = intval;
+	} else {
+		value->value_type_case = DATA__VALUE__VALUE_TYPE_NEG_INT_VALUE;
+		value->neg_int_value = abs(intval);
+	}
+}
+
+static void encode_properties(struct geobuf_agg_context *ctx, Data__Feature *feature) {
+	uint32_t *properties;
+	Data__Value **values;
+	uint32_t i, k = 0, c = 0;
+	TupleDesc tupdesc = get_tuple_desc(ctx);
+	int natts = tupdesc->natts;
+	properties = palloc(sizeof (*properties) * (natts - 1) * 2);
+	values = palloc (sizeof (*values) * (natts - 1));
+
+	for (i = 0; i < natts; i++) {
+		Data__Value *value;
+		char *type, *string_value, *key;
+		Datum datum;
+		bool isnull;
+
+		if (i == ctx->geom_index)
+			continue;
+		k++;
+
+		key = tupdesc->attrs[i]->attname.data;
+
+		value = palloc (sizeof (*value));
+		data__value__init(value);
+
+		type = SPI_gettype(tupdesc, i + 1);
+		datum = GetAttributeByNum(ctx->row, i + 1, &isnull);
+		if (isnull)
+			continue;
+		Oid typoid = getBaseType(tupdesc->attrs[i]->atttypid);
+		if (strcmp(type, "int2") == 0) {
+			set_int_value(value, DatumGetInt16(datum));
+		} else if (strcmp(type, "int4") == 0) {
+			set_int_value(value, DatumGetInt32(datum));
+		} else if (strcmp(type, "int8") == 0) {
+			set_int_value(value, DatumGetInt64(datum));
+		} else if (strcmp(type, "float4") == 0) {
+			value->value_type_case = DATA__VALUE__VALUE_TYPE_DOUBLE_VALUE;
+			value->double_value = DatumGetFloat4(datum);
+		} else if (strcmp(type, "float8") == 0) {
+			value->value_type_case = DATA__VALUE__VALUE_TYPE_DOUBLE_VALUE;
+			value->double_value = DatumGetFloat8(datum);
+		} else {
+			Oid foutoid;
+			bool typisvarlena;
+			getTypeOutputInfo(typoid, &foutoid, &typisvarlena);
+			string_value = OidOutputFunctionCall(foutoid, datum);
+			value->value_type_case = DATA__VALUE__VALUE_TYPE_STRING_VALUE;
+			value->string_value = string_value;
+		}
+		properties[c * 2] = k - 1;
+		properties[c * 2 + 1] = c;
+		values[c++] = value;
+	}
+
+	ReleaseTupleDesc(tupdesc);
+
+	feature->n_values = c;
+	feature->values = values;
+	feature->n_properties = c * 2;
+	feature->properties = properties;
+}
+
+static int64_t *encode_coords(struct geobuf_agg_context *ctx, POINTARRAY *pa, int64_t *coords, int len, int offset) {
+	int i, c;
+	POINT4D pt;
+	int64_t sum[] = { 0, 0, 0, 0 };
+
+	if (offset == 0)
+		coords = palloc(sizeof (int64_t) * len * ctx->dimensions);
+	else
+		coords = repalloc(coords, sizeof (int64_t) * ((len * ctx->dimensions) + offset));
+
+	c = offset;
+	for (i = 0; i < len; i++) {
+		getPoint4d_p(pa, i, &pt);
+		sum[0] += coords[c++] = ceil(pt.x * ctx->e) - sum[0];
+		sum[1] += coords[c++] = ceil(pt.y * ctx->e) - sum[1];
+		if (ctx->dimensions == 3)
+			sum[2] += coords[c++] = ceil(pt.z * ctx->e) - sum[2];
+		else if (ctx->dimensions == 4)
+			sum[3] += coords[c++] = ceil(pt.m * ctx->e) - sum[3];
+	}
+	return coords;
+}
+
+static Data__Geometry *encode_point(struct geobuf_agg_context *ctx, LWPOINT *lwpoint) {
+	int npoints;
+	Data__Geometry *geometry;
+	POINTARRAY *pa;
+
+	geometry = galloc(DATA__GEOMETRY__TYPE__POINT);
+
+	pa = lwpoint->point;
+	npoints = pa->npoints;
+
+	if (npoints == 0)
+		return geometry;
+
+	geometry->n_coords = npoints * ctx->dimensions;
+	geometry->coords = encode_coords(ctx, pa, NULL, 1, 0);
+
+	return geometry;
+}
+
+static Data__Geometry *encode_mpoint(struct geobuf_agg_context *ctx, LWMPOINT *lwmpoint) {
+	int i, ngeoms;
+	POINTARRAY *pa;
+	Data__Geometry *geometry;
+
+	geometry = galloc(DATA__GEOMETRY__TYPE__MULTIPOINT);
+
+	ngeoms = lwmpoint->ngeoms;
+
+	if (ngeoms == 0)
+		return geometry;
+
+	pa = ptarray_construct_empty(0, 0, ngeoms);
+
+	for (i = 0; i < ngeoms; i++) {
+		POINT4D pt;
+		getPoint4d_p(lwmpoint->geoms[i]->point, 0, &pt);
+		ptarray_append_point(pa, &pt, 0);
+	}
+
+	geometry->n_coords = ngeoms * ctx->dimensions;
+	geometry->coords = encode_coords(ctx, pa, NULL, ngeoms, 0);
+
+	return geometry;
+}
+
+static Data__Geometry *encode_line(struct geobuf_agg_context *ctx, LWLINE *lwline) {
+	POINTARRAY *pa;
+	Data__Geometry *geometry;
+
+	geometry = galloc(DATA__GEOMETRY__TYPE__LINESTRING);
+
+	pa = lwline->points;
+
+	if (pa->npoints == 0)
+		return geometry;
+
+	geometry->n_coords = pa->npoints * ctx->dimensions;
+	geometry->coords = encode_coords(ctx, pa, NULL, pa->npoints, 0);
+
+	return geometry;
+}
+
+static Data__Geometry *encode_mline(struct geobuf_agg_context *ctx, LWMLINE *lwmline) {
+	int i, offset, ngeoms;
+	POINTARRAY *pa;
+	Data__Geometry *geometry;
+	uint32_t *lengths;
+	int64_t *coords = NULL;
+
+	geometry = galloc(DATA__GEOMETRY__TYPE__MULTILINESTRING);
+
+	ngeoms = lwmline->ngeoms;
+
+	if (ngeoms == 0)
+		return geometry;
+
+	lengths = palloc (sizeof (uint32_t) * ngeoms);
+
+	offset = 0;
+	for (i = 0; i < ngeoms; i++) {
+		pa = lwmline->geoms[i]->points;
+		coords = encode_coords(ctx, pa, coords, pa->npoints, offset);
+		offset += pa->npoints * ctx->dimensions;
+		lengths[i] = pa->npoints;
+	}
+
+	if (ngeoms > 1) {
+		geometry->n_lengths = ngeoms;
+		geometry->lengths = lengths;
+	}
+
+	geometry->n_coords = offset;
+	geometry->coords = coords;
+
+	return geometry;
+}
+
+static Data__Geometry *encode_poly(struct geobuf_agg_context *ctx, LWPOLY *lwpoly) {
+	int i, len, nrings, offset;
+	POINTARRAY *pa;
+	Data__Geometry *geometry;
+	uint32_t *lengths;
+	int64_t *coords = NULL;
+
+	geometry = galloc(DATA__GEOMETRY__TYPE__POLYGON);
+
+	nrings = lwpoly->nrings;
+
+	if (nrings == 0)
+		return geometry;
+
+	lengths = palloc (sizeof (uint32_t) * nrings);
+
+	offset = 0;
+	for (i = 0; i < nrings; i++) {
+		pa = lwpoly->rings[i];
+		len = pa->npoints - 1;
+		coords = encode_coords(ctx, pa, coords, len, offset);
+		offset += len * ctx->dimensions;
+		lengths[i] = len;
+	}
+
+	if (nrings > 1) {
+		geometry->n_lengths = nrings;
+		geometry->lengths = lengths;
+	}
+
+	geometry->n_coords = offset;
+	geometry->coords = coords;
+
+	return geometry;
+}
+
+static Data__Geometry *encode_mpoly(struct geobuf_agg_context *ctx, LWMPOLY* lwmpoly) {
+	int i, j, c, len, offset, n_lengths, ngeoms, nrings;
+	POINTARRAY *pa;
+	Data__Geometry *geometry;
+	uint32_t *lengths;
+	int64_t *coords = NULL;
+
+	geometry = galloc(DATA__GEOMETRY__TYPE__MULTIPOLYGON);
+
+	ngeoms = lwmpoly->ngeoms;
+
+	if (ngeoms == 0) return geometry;
+
+	n_lengths = 1;
+	for (i = 0; i < ngeoms; i++) {
+		nrings = lwmpoly->geoms[i]->nrings;
+		n_lengths++;
+		for (j = 0; j < nrings; j++)
+			n_lengths++;
+	}
+
+	lengths = palloc (sizeof (uint32_t) * n_lengths);
+
+	c = 0;
+	offset = 0;
+	lengths[c++] = ngeoms;
+	for (i = 0; i < ngeoms; i++) {
+		nrings = lwmpoly->geoms[i]->nrings;
+		lengths[c++] = nrings;
+		for (j = 0; j < nrings; j++) {
+			pa = lwmpoly->geoms[i]->rings[j];
+			len = pa->npoints - 1;
+			coords = encode_coords(ctx, pa, coords, len, offset);
+			offset += len * ctx->dimensions;
+			lengths[c++] = len;
+		}
+	}
+
+	if (c > 1) {
+		geometry->n_lengths = n_lengths;
+		geometry->lengths = lengths;
+	}
+
+	geometry->n_coords = offset;
+	geometry->coords = coords;
+
+	return geometry;
+}
+
+static Data__Geometry *encode_collection(struct geobuf_agg_context *ctx, LWCOLLECTION* lwcollection) {
+	int i, ngeoms;
+	Data__Geometry *geometry, **geometries;
+
+	geometry = galloc(DATA__GEOMETRY__TYPE__GEOMETRYCOLLECTION);
+
+	ngeoms = lwcollection->ngeoms;
+
+	if (ngeoms == 0)
+		return geometry;
+
+	geometries = palloc (sizeof (Data__Geometry *) * ngeoms);
+	for (i = 0; i < ngeoms; i++) {
+		LWGEOM *lwgeom = lwcollection->geoms[i];
+		Data__Geometry *geom = encode_geometry(ctx, lwgeom);
+		geometries[i] = geom;
+	}
+
+	geometry->n_geometries = ngeoms;
+	geometry->geometries = geometries;
+
+	return geometry;
+}
+
+static Data__Geometry *encode_geometry(struct geobuf_agg_context *ctx, LWGEOM *lwgeom) {
+	int type = lwgeom->type;
+	switch (type)
+	{
+	case POINTTYPE:
+		return encode_point(ctx, (LWPOINT*)lwgeom);
+	case LINETYPE:
+		return encode_line(ctx, (LWLINE*)lwgeom);
+	case POLYGONTYPE:
+		return encode_poly(ctx, (LWPOLY*)lwgeom);
+	case MULTIPOINTTYPE:
+		return encode_mpoint(ctx, (LWMPOINT*)lwgeom);
+	case MULTILINETYPE:
+		return encode_mline(ctx, (LWMLINE*)lwgeom);
+	case MULTIPOLYGONTYPE:
+		return encode_mpoly(ctx, (LWMPOLY*)lwgeom);
+	case COLLECTIONTYPE:
+		return encode_collection(ctx, (LWCOLLECTION*)lwgeom);
+	default:
+		lwerror("encode_geometry: '%s' geometry type not supported",
+				lwtype_name(type));
+	}
+	return NULL;
+}
+
+static void analyze_val(struct geobuf_agg_context *ctx, double val) {
+	if (ceil(val * ctx->e) / ctx->e != val && ctx->e < MAX_PRECISION)
+		ctx->e *= 10;
+}
+
+static void analyze_pa(struct geobuf_agg_context *ctx, POINTARRAY *pa) {
+	int i;
+	POINT4D pt;
+	for (i = 0; i < pa->npoints; i++) {
+		getPoint4d_p(pa, i, &pt);
+		analyze_val(ctx, pt.x);
+		analyze_val(ctx, pt.y);
+		if (ctx->dimensions == 3)
+			analyze_val(ctx, pt.z);
+		else if (ctx->dimensions == 4)
+			analyze_val(ctx, pt.m);
+	}
+}
+
+static void analyze_geometry(struct geobuf_agg_context *ctx, LWGEOM *lwgeom) {
+	int i, type;
+	LWLINE *lwline;
+	LWPOLY *lwpoly;
+	LWCOLLECTION *lwcollection;
+	type = lwgeom->type;
+	switch (type)
+	{
+	case POINTTYPE:
+	case LINETYPE:
+		lwline = (LWLINE*) lwgeom;
+		analyze_pa(ctx, lwline->points);
+		break;
+	case POLYGONTYPE:
+		lwpoly = (LWPOLY*) lwgeom;
+		for (i = 0; i < lwpoly->nrings; i++)
+			analyze_pa(ctx, lwpoly->rings[i]);
+		break;
+	case MULTIPOINTTYPE:
+	case MULTILINETYPE:
+	case MULTIPOLYGONTYPE:
+	case COLLECTIONTYPE:
+		lwcollection = (LWCOLLECTION*) lwgeom;
+		for (i = 0; i < lwcollection->ngeoms; i++)
+			analyze_geometry(ctx, lwcollection->geoms[i]);
+		break;
+	default:
+		lwerror("analyze_geometry: '%s' geometry type not supported",
+			lwtype_name(type));
+	}
+}
+
+static void analyze_geometry_flags(struct geobuf_agg_context *ctx, LWGEOM *lwgeom) {
+	if (!ctx->has_dimensions) {
+		if (FLAGS_GET_Z(lwgeom->flags) || FLAGS_GET_M(lwgeom->flags))
+			ctx->dimensions = 3;
+		else if (FLAGS_GET_ZM(lwgeom->flags))
+			ctx->dimensions = 4;
+		else
+			ctx->dimensions = 2;
+		ctx->has_dimensions = 1;
+	}
+}
+
+/**
+ *
+ */
+static Data__Feature *encode_feature(struct geobuf_agg_context *ctx) {
+	Data__Feature *feature;
+
+	feature = palloc (sizeof (Data__Feature));
+	data__feature__init(feature);
+
+	encode_properties(ctx, feature);
+	return feature;
+}
+
+/**
+ * Initialize aggregation context.
+ */
+void geobuf_agg_init_context(struct geobuf_agg_context *ctx)
+{
+	Data *data;
+	Data__FeatureCollection *fc;
+
+	ctx->has_dimensions = 0;
+	ctx->dimensions = 2;
+	ctx->has_precision = 0;
+	ctx->precision = MAX_PRECISION;
+	ctx->e = 1;
+	ctx->features_capacity = FEATURES_CAPACITY_INITIAL;
+
+	data = palloc(sizeof(*data));
+	data__init(data);
+
+	fc = palloc(sizeof(*fc));
+	data__feature_collection__init(fc);
+
+	fc->features = palloc (ctx->features_capacity *
+		sizeof(*fc->features));
+
+	ctx->lwgeoms = palloc (ctx->features_capacity *
+		sizeof(*ctx->lwgeoms));
+
+	data->data_type_case = DATA__DATA_TYPE_FEATURE_COLLECTION;
+	data->feature_collection = fc;
+
+	ctx->data = data;
+}
+
+/**
+ * Aggregation step.
+ *
+ * Expands features array if needed by a factor of 2.
+ * Allocates a new feature, increment feature counter and
+ * encode properties into it.
+ */
+void geobuf_agg_transfn(struct geobuf_agg_context *ctx)
+{
+	LWGEOM *lwgeom;
+	bool isnull;
+	Datum datum;
+	Data__FeatureCollection *fc = ctx->data->feature_collection;
+	Data__Feature **features = fc->features;
+	Data__Feature *feature;
+	GSERIALIZED *gs;
+	if (fc->n_features >= ctx->features_capacity) {
+		size_t new_capacity = ctx->features_capacity * 2;
+		fc->features = repalloc(fc->features, new_capacity *
+			sizeof(*fc->features));
+		ctx->lwgeoms = repalloc(ctx->lwgeoms, new_capacity *
+			sizeof(*ctx->lwgeoms));
+		ctx->features_capacity = new_capacity;
+	}
+
+	/* inspect row and encode keys assuming static schema */
+	if (fc->n_features == 0)
+		encode_keys(ctx);
+
+	datum = GetAttributeByNum(ctx->row, ctx->geom_index + 1, &isnull);
+	if (!datum)
+		lwerror("geobuf_agg_transfn: geometry column cannot be null");
+	gs = (GSERIALIZED *) PG_DETOAST_DATUM(datum);
+	lwgeom = lwgeom_from_gserialized(gs);
+
+	feature = encode_feature(ctx);
+
+	/* inspect geometry flags assuming static schema */
+	if (fc->n_features == 0)
+		analyze_geometry_flags(ctx, lwgeom);
+
+	analyze_geometry(ctx, lwgeom);
+
+	ctx->lwgeoms[fc->n_features] = lwgeom;
+	fc->features[fc->n_features++] = feature;
+}
+
+/**
+ * Finalize aggregation.
+ *
+ * Encode into Data message and return it packed as a bytea.
+ */
+uint8_t *geobuf_agg_finalfn(struct geobuf_agg_context *ctx)
+{
+	int i;
+	Data *data;
+	Data__FeatureCollection *fc;
+
+	data = ctx->data;
+	fc = data->feature_collection;
+
+	/* check and set dimensions if not default */
+	if (ctx->dimensions != 2) {
+		data->has_dimensions = ctx->has_dimensions;
+		data->dimensions = ctx->dimensions;
+	}
+	lwdebug(3, "data->dimensions: %d", data->dimensions);
+
+	/* check and set precision if not default */
+	if (ctx->e > MAX_PRECISION)
+		ctx->e = MAX_PRECISION;
+	ctx->precision = ceil(log(ctx->e) / log(10));
+	lwdebug(3, "ctx->precision: %d", ctx->precision);
+	if (ctx->precision != 6) {
+		data->has_precision = 1;
+		data->precision = ctx->precision;
+	}
+
+	for (i = 0; i < fc->n_features; i++)
+		fc->features[i]->geometry = encode_geometry(ctx, ctx->lwgeoms[i]);
+
+	size_t len = data__get_packed_size(data);
+	uint8_t *buf = palloc(sizeof(*buf) * (len + VARHDRSZ));
+	data__pack(data, buf + VARHDRSZ);
+
+	SET_VARSIZE(buf, VARHDRSZ + len);
+
+	return buf;
+}
+
+#endif

Added: trunk/postgis/geobuf.h
===================================================================
--- trunk/postgis/geobuf.h	                        (rev 0)
+++ trunk/postgis/geobuf.h	2017-02-26 18:16:54 UTC (rev 15317)
@@ -0,0 +1,69 @@
+/**********************************************************************
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ *
+ **********************************************************************
+ *
+ * Copyright (C) 2016-2017 Björn Harrtell <bjorn at wololo.org>
+ *
+ **********************************************************************/
+
+#ifndef GEOBUF_H_
+#define GEOBUF_H_ 1
+
+#include <stdlib.h>
+#include "postgres.h"
+#include "utils/builtins.h"
+#include "utils/array.h"
+#include "utils/typcache.h"
+#include "utils/lsyscache.h"
+#include "catalog/pg_type.h" 
+#include "executor/spi.h"
+#include "executor/executor.h"
+#include "access/htup_details.h"
+#include "access/htup.h"
+#include "../postgis_config.h"
+#include "liblwgeom.h"
+#include "lwgeom_pg.h"
+#include "lwgeom_log.h"
+
+#ifdef HAVE_LIBPROTOBUF
+
+#include "geobuf.pb-c.h"
+
+struct geobuf_agg_context {
+	char *geom_name;
+	uint32_t geom_index;
+	HeapTupleHeader row;
+	LWGEOM **lwgeoms;
+        Data *data;
+        Data__Feature *feature;
+	size_t features_capacity;
+        uint32_t e;
+        protobuf_c_boolean has_precision;
+        uint32_t precision;
+        protobuf_c_boolean has_dimensions;
+        uint32_t dimensions;
+};
+
+void geobuf_agg_init_context(struct geobuf_agg_context *ctx);
+void geobuf_agg_transfn(struct geobuf_agg_context *ctx);
+uint8_t *geobuf_agg_finalfn(struct geobuf_agg_context *ctx);
+
+#endif  /* HAVE_LIBPROTOBUF */
+
+#endif

Added: trunk/postgis/geobuf.proto
===================================================================
--- trunk/postgis/geobuf.proto	                        (rev 0)
+++ trunk/postgis/geobuf.proto	2017-02-26 18:16:54 UTC (rev 15317)
@@ -0,0 +1,66 @@
+option optimize_for = LITE_RUNTIME;
+
+message Data {
+    repeated string keys = 1; // global arrays of unique keys
+
+    optional uint32 dimensions = 2 [default = 2]; // max coordinate dimensions
+    optional uint32 precision = 3 [default = 6]; // number of digits after decimal point for coordinates
+
+    oneof data_type {
+        FeatureCollection feature_collection = 4;
+        Feature feature = 5;
+        Geometry geometry = 6;
+    }
+
+    message Feature {
+        required Geometry geometry = 1;
+
+        oneof id_type {
+            string id = 11;
+            sint64 int_id = 12;
+        }
+
+        repeated Value values = 13; // unique values
+        repeated uint32 properties = 14 [packed = true]; // pairs of key/value indexes
+        repeated uint32 custom_properties = 15 [packed = true]; // arbitrary properties
+    }
+
+    message Geometry {
+        required Type type = 1;
+
+        repeated uint32 lengths = 2 [packed = true]; // coordinate structure in lengths
+        repeated sint64 coords = 3 [packed = true]; // delta-encoded integer values
+        repeated Geometry geometries = 4;
+
+        repeated Value values = 13;
+        repeated uint32 custom_properties = 15 [packed = true];
+
+        enum Type {
+            POINT = 0;
+            MULTIPOINT = 1;
+            LINESTRING = 2;
+            MULTILINESTRING = 3;
+            POLYGON = 4;
+            MULTIPOLYGON = 5;
+            GEOMETRYCOLLECTION = 6;
+        }
+    }
+
+    message FeatureCollection {
+        repeated Feature features = 1;
+
+        repeated Value values = 13;
+        repeated uint32 custom_properties = 15 [packed = true];
+    }
+
+    message Value {
+        oneof value_type {
+            string string_value = 1;
+            double double_value = 2;
+            uint64 pos_int_value = 3;
+            uint64 neg_int_value = 4;
+            bool bool_value = 5;
+            string json_value = 6;
+        }
+    }
+}

Added: trunk/postgis/lwgeom_out_geobuf.c
===================================================================
--- trunk/postgis/lwgeom_out_geobuf.c	                        (rev 0)
+++ trunk/postgis/lwgeom_out_geobuf.c	2017-02-26 18:16:54 UTC (rev 15317)
@@ -0,0 +1,99 @@
+/**********************************************************************
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ *
+ **********************************************************************
+ *
+ * Copyright (C) 2016-2017 Björn Harrtell <bjorn at wololo.org>
+ *
+ **********************************************************************/
+
+#include "geobuf.h"
+
+/**
+ * @file
+ * Geobuf export functions
+ */
+
+#include "postgres.h"
+#include "utils/builtins.h"
+#include "executor/spi.h"
+
+#include "../postgis_config.h"
+#include "lwgeom_pg.h"
+#include "lwgeom_log.h"
+#include "liblwgeom.h"
+#include "geobuf.h"
+
+/**
+ * Process input parameters and row data into state
+ */
+PG_FUNCTION_INFO_V1(pgis_asgeobuf_transfn);
+Datum pgis_asgeobuf_transfn(PG_FUNCTION_ARGS)
+{
+#ifndef HAVE_LIBPROTOBUF
+	lwerror("Missing libprotobuf-c");
+	PG_RETURN_NULL();
+#else
+	MemoryContext aggcontext;
+	struct geobuf_agg_context *ctx;
+
+	if (!AggCheckCallContext(fcinfo, &aggcontext))
+		lwerror("pgis_asmvt_transfn: called in non-aggregate context");
+	MemoryContextSwitchTo(aggcontext);
+
+	if (PG_ARGISNULL(0)) {
+		ctx = palloc(sizeof(*ctx));
+		if (PG_ARGISNULL(1))
+			lwerror("pgis_asgeobuf_transfn: parameter geom_name cannot be null");
+		ctx->geom_name = text_to_cstring(PG_GETARG_TEXT_P(1));
+		geobuf_agg_init_context(ctx);
+	} else {
+		ctx = (struct geobuf_agg_context *) PG_GETARG_POINTER(0);
+	}
+
+	if (!type_is_rowtype(get_fn_expr_argtype(fcinfo->flinfo, 2)))
+		lwerror("pgis_asgeobuf_transfn: parameter row cannot be other than a rowtype");
+	ctx->row = PG_GETARG_HEAPTUPLEHEADER(2);
+
+	geobuf_agg_transfn(ctx);
+	PG_RETURN_POINTER(ctx);
+#endif
+}
+
+/**
+ * Encode final state to Geobuf
+ */
+PG_FUNCTION_INFO_V1(pgis_asgeobuf_finalfn);
+Datum pgis_asgeobuf_finalfn(PG_FUNCTION_ARGS)
+{
+#ifndef HAVE_LIBPROTOBUF
+	lwerror("Missing libprotobuf-c");
+	PG_RETURN_NULL();
+#else
+	struct geobuf_agg_context *ctx;
+	if (!AggCheckCallContext(fcinfo, NULL))
+		lwerror("pgis_asmvt_finalfn called in non-aggregate context");
+
+	if (PG_ARGISNULL(0))
+		PG_RETURN_NULL();
+
+	ctx = (struct geobuf_agg_context *) PG_GETARG_POINTER(0);
+	uint8_t *buf = geobuf_agg_finalfn(ctx);
+	PG_RETURN_BYTEA_P(buf);
+#endif
+}

Modified: trunk/postgis/postgis.sql.in
===================================================================
--- trunk/postgis/postgis.sql.in	2017-02-26 16:42:56 UTC (rev 15316)
+++ trunk/postgis/postgis.sql.in	2017-02-26 18:16:54 UTC (rev 15317)
@@ -4412,6 +4412,33 @@
 	AS 'MODULE_PATHNAME','postgis_libprotobuf_version'
 	LANGUAGE 'c' IMMUTABLE STRICT _PARALLEL;
 
+
+-----------------------------------------------------------------------
+-- GEOBUF OUTPUT
+-- Availability: 2.4.0
+-----------------------------------------------------------------------
+
+-- Availability: 2.4.0
+CREATE OR REPLACE FUNCTION pgis_asgeobuf_transfn(internal, text, anyelement)
+	RETURNS internal
+	AS 'MODULE_PATHNAME', 'pgis_asgeobuf_transfn'
+	LANGUAGE c IMMUTABLE;
+
+-- Availability: 2.4.0
+CREATE OR REPLACE FUNCTION pgis_asgeobuf_finalfn(internal)
+	RETURNS bytea
+	AS 'MODULE_PATHNAME', 'pgis_asgeobuf_finalfn'
+	LANGUAGE c IMMUTABLE;
+
+-- Availability: 2.4.0
+CREATE AGGREGATE ST_AsGeobuf(text, anyelement)
+(
+	sfunc = pgis_asgeobuf_transfn,
+	stype = internal,
+	finalfunc = pgis_asgeobuf_finalfn
+);
+
+
 ------------------------------------------------------------------------
 -- GeoHash (geohash.org)
 ------------------------------------------------------------------------

Modified: trunk/regress/Makefile.in
===================================================================
--- trunk/regress/Makefile.in	2017-02-26 16:42:56 UTC (rev 15316)
+++ trunk/regress/Makefile.in	2017-02-26 18:16:54 UTC (rev 15317)
@@ -242,9 +242,10 @@
 
 ifeq ($(HAVE_PROTOBUF),yes)
 	# protobuf-c adds:
-	# ST_AsMVT
+	# ST_AsMVT, ST_AsGeobuf
 	TESTS += \
-		mvt
+		mvt \
+		geobuf
 endif
 
 ifeq ($(HAVE_SFCGAL),yes)

Added: trunk/regress/geobuf.sql
===================================================================
--- trunk/regress/geobuf.sql	                        (rev 0)
+++ trunk/regress/geobuf.sql	2017-02-26 18:16:54 UTC (rev 15317)
@@ -0,0 +1,19 @@
+--set client_min_messages to DEBUG3;
+SELECT 'T1', encode(ST_AsGeobuf('geom', q), 'base64')
+    FROM (SELECT ST_MakePoint(1.1, 2.1) AS geom) AS q;
+SELECT 'T2', encode(ST_AsGeobuf('geom', q), 'base64')
+    FROM (SELECT 'test' as test_str, 1 as test_pos_int, -1 as test_neg_int, 1.1 as test_numeric, 1.1::float as test_float, ST_MakeLine(ST_MakePoint(1,1), ST_MakePoint(2,2)) as geom) AS q;
+SELECT 'T3', encode(ST_AsGeobuf('geom', q), 'base64')
+    FROM (SELECT ST_GeomFromText('POLYGON((0 0,0 1,1 1,1 0,0 0))') as geom) AS q;
+SELECT 'T4', encode(ST_AsGeobuf('geom', q), 'base64')
+    FROM (SELECT ST_GeomFromText('POLYGON((0 0,0 5,5 5,5 0,0 0), (1 1,1 2,2 2,2 1,1 1))') as geom) AS q;
+SELECT 'T5', encode(ST_AsGeobuf('geom', q), 'base64')
+    FROM (SELECT ST_GeomFromText('MULTIPOINT (10 40, 40 30, 20 20, 30 10)') as geom) AS q;
+SELECT 'T6', encode(ST_AsGeobuf('geom', q), 'base64')
+    FROM (SELECT ST_GeomFromText('MULTILINESTRING ((10 10, 20 20, 10 40), (40 40, 30 30, 40 20, 30 10))') as geom) AS q;
+SELECT 'T7', encode(ST_AsGeobuf('geom', q), 'base64')
+    FROM (SELECT 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 'T8', encode(ST_AsGeobuf('geom', q), 'base64')
+    FROM (SELECT ST_GeomFromText('GEOMETRYCOLLECTION(POINT(4 6),LINESTRING(4 6,7 10))') as geom) AS q;
+SELECT 'T9', encode(ST_AsGeobuf('geom', q), 'base64')
+    FROM (SELECT ST_MakePoint(1, 2, 3) as geom) AS q;

Added: trunk/regress/geobuf_expected
===================================================================
--- trunk/regress/geobuf_expected	                        (rev 0)
+++ trunk/regress/geobuf_expected	2017-02-26 18:16:54 UTC (rev 15317)
@@ -0,0 +1,11 @@
+T1|GAEiCgoICgYIABoCFio=
+T2|Cgh0ZXN0X3N0cgoMdGVzdF9wb3NfaW50Cgx0ZXN0X25lZ19pbnQKDHRlc3RfbnVtZXJpYwoKdGVz
+dF9mbG9hdBgAIjoKOAoICAIaBAICAgJqBgoEdGVzdGoCGAFqAiABagUKAzEuMWoJEZqZmZmZmfE/
+cgoAAAEBAgIDAwQE
+T3|GAAiEAoOCgwIBBoIAAAAAgIAAAE=
+T4|GAAiHAoaChgIBBICBAQaEAAAAAoKAAAJAgIAAgIAAAE=
+T5|GAAiEAoOCgwIARoIFFA8EycTFBM=
+T6|GAAiGgoYChYIAxICAwQaDhQUFBQTKFBQExMUExMT
+T7|GAAiJgokCiIIBRIGAgEDAgUDGhZQUCcKMh0oRhMJACcoCR4ePCgTCQAU
+T8|GAAiGAoWChQIBiIGCAAaAggMIggIAhoECAwGCA==
+T9|EAMYACILCgkKBwgAGgMCBAY=



More information about the postgis-tickets mailing list