[postgis-tickets] r15304 - Mapbox vector tile output support via ST_AsMVT

Sandro Santilli strk at kbt.io
Mon Feb 6 08:33:23 PST 2017


Author: strk
Date: 2017-02-06 08:33:23 -0800 (Mon, 06 Feb 2017)
New Revision: 15304

Added:
   trunk/postgis/lwgeom_out_mvt.c
   trunk/postgis/mvt.c
   trunk/postgis/mvt.h
   trunk/postgis/postgis_libprotobuf.c
   trunk/postgis/uthash.h
   trunk/postgis/vector_tile.LICENSE
   trunk/postgis/vector_tile.proto
   trunk/regress/mvt.sql
   trunk/regress/mvt_expected
Modified:
   trunk/NEWS
   trunk/configure.ac
   trunk/doc/reference_output.xml
   trunk/postgis/Makefile.in
   trunk/postgis/postgis.sql.in
   trunk/postgis_config.h.in
   trunk/regress/Makefile.in
Log:
Mapbox vector tile output support via ST_AsMVT

Implementation by Bj?\195?\182rn Harrtell / CARTO

References #3661

Modified: trunk/NEWS
===================================================================
--- trunk/NEWS	2017-01-31 05:05:06 UTC (rev 15303)
+++ trunk/NEWS	2017-02-06 16:33:23 UTC (rev 15304)
@@ -1,7 +1,10 @@
 PostGIS 2.4.0
 2017/xx/xx
 
+ * New Features *
 
+  - #3661, Mapbox vector tile output support via ST_AsMVT (Björn Harrtell / CartoDB)
+
 PostGIS 2.3.0
 2016/09/26
 

Modified: trunk/configure.ac
===================================================================
--- trunk/configure.ac	2017-01-31 05:05:06 UTC (rev 15303)
+++ trunk/configure.ac	2017-02-06 16:33:23 UTC (rev 15304)
@@ -899,6 +899,69 @@
 
 
 dnl ===========================================================================
+dnl Detect if protobuf-c installed
+dnl ===========================================================================
+
+CHECK_PROTOBUF=yes
+HAVE_PROTOBUF=no
+
+AC_ARG_WITH([protobuf],
+	[AS_HELP_STRING([--without-protobuf], [build without protobuf-c support])],
+	[CHECK_PROTOBUF="$withval"], [])
+
+if test "$CHECK_PROTOBUF" != "no"; then dnl {
+
+AC_ARG_WITH([protobufdir],
+	[AS_HELP_STRING([--with-protobufdir=PATH], [specify the protobuf-c installation directory])],
+	[PROTOBUFDIR="$withval"], [PROTOBUFDIR=])
+
+if test ! "x$PROTOBUFDIR" = "x"; then
+	dnl Make sure that the directory exists
+	if test "x$PROTOBUFDIR" = "xyes"; then
+		AC_MSG_ERROR([you must specify a parameter to --with-protobufdir, e.g. --with-protobufdir=/path/to])
+	else
+		AC_MSG_RESULT([Using user-specified protobuf-c directory: $PROTOBUFDIR])
+
+		dnl Add the include directory to PROTOBUF_CPPFLAGS
+		PROTOBUF_CPPFLAGS="-I$PROTOBUFDIR/include"
+		PROTOBUF_LDFLAGS="-L$PROTOBUFDIR/lib"
+	fi
+fi
+
+dnl Check that we can find the protobuf/protobuf.h header file
+CPPFLAGS_SAVE="$CPPFLAGS"
+CPPFLAGS="$PROTOBUF_CPPFLAGS"
+AC_CHECK_HEADER([protobuf-c/protobuf-c.h], [HAVE_PROTOBUF=yes], [])	
+CPPFLAGS="$CPPFLAGS_SAVE"
+
+dnl Ensure we can link against libprotobuf-c
+LIBS_SAVE="$LIBS"
+LIBS="$PROTOBUF_LDFLAGS"
+AC_CHECK_LIB([protobuf-c], [protobuf_c_message_check], [HAVE_PROTOBUF=yes; PROTOBUF_LDFLAGS="${PROTOBUF_LDFLAGS} -lprotobuf-c"], [HAVE_PROTOBUF=no])
+LIBS="$LIBS_SAVE"
+
+dnl Ensure libprotobuf-c is of minimum required version
+PKG_CHECK_MODULES([PROTOBUFC], [libprotobuf-c >= 1.1.0], [HAVE_PROTOBUF=yes], [HAVE_PROTOBUF=no])
+
+if test "$HAVE_PROTOBUF" = "yes"; then
+	AC_PATH_PROG(PROTOCC, protoc-c)
+	if test "x$PROTOCC" = "x"; then
+	  AC_MSG_WARN([Protobuf compiler missing, disabling protobuf support.])
+	  HAVE_PROTOBUF=no
+	else
+	  AC_DEFINE([HAVE_LIBPROTOBUF], 1, [Define to 1 if libprotobuf-c is present])
+	fi
+fi
+
+AC_SUBST([PROTOBUF_CPPFLAGS])
+AC_SUBST([PROTOBUF_LDFLAGS])
+AC_SUBST([HAVE_PROTOBUF])
+
+fi dnl }
+
+
+
+dnl ===========================================================================
 dnl Detect GTK+2.0 for GUI
 dnl ===========================================================================
 
@@ -1051,10 +1114,10 @@
     AC_MSG_RESULT([ADDRESS_STANDARDIZER support: disabled])
 fi
 
-CPPFLAGS="$PGSQL_CPPFLAGS $GEOS_CPPFLAGS $PROJ_CPPFLAGS $XML2_CPPFLAGS $SFCGAL_CPPFLAGS $JSON_CPPFLAGS $PCRE_CPPFLAGS $CPPFLAGS"
+CPPFLAGS="$PGSQL_CPPFLAGS $GEOS_CPPFLAGS $PROJ_CPPFLAGS $PROTOBUF_CPPFLAGS $XML2_CPPFLAGS $SFCGAL_CPPFLAGS $JSON_CPPFLAGS $PCRE_CPPFLAGS $CPPFLAGS"
 dnl AC_MSG_RESULT([CPPFLAGS: $CPPFLAGS])
 
-SHLIB_LINK="$PGSQL_LDFLAGS $GEOS_LDFLAGS $PROJ_LDFLAGS -lgeos_c -lproj $JSON_LDFLAGS $XML2_LDFLAGS $SFCGAL_LDFLAGS $PCRE_LDFLAGS $EXCLUDELIBS_LDFLAGS"
+SHLIB_LINK="$PGSQL_LDFLAGS $GEOS_LDFLAGS $PROJ_LDFLAGS -lgeos_c -lproj $JSON_LDFLAGS $PROTOBUF_LDFLAGS $XML2_LDFLAGS $SFCGAL_LDFLAGS $PCRE_LDFLAGS $EXCLUDELIBS_LDFLAGS"
 AC_SUBST([SHLIB_LINK])
 dnl AC_MSG_RESULT([SHLIB_LINK: $SHLIB_LINK])
 
@@ -1396,6 +1459,7 @@
 AC_MSG_RESULT([  Libxml2 config:       ${XML2CONFIG}])
 AC_MSG_RESULT([  Libxml2 version:      ${POSTGIS_LIBXML2_VERSION}])
 AC_MSG_RESULT([  JSON-C support:       ${HAVE_JSON}])
+AC_MSG_RESULT([  protobuf-c support:   ${HAVE_PROTOBUF}])
 AC_MSG_RESULT([  PCRE support:         ${HAVE_PCRE}])
 AC_MSG_RESULT([  PostGIS debug level:  ${POSTGIS_DEBUG_LEVEL}])
 AC_MSG_RESULT([  Perl:                 ${PERL}])

Modified: trunk/doc/reference_output.xml
===================================================================
--- trunk/doc/reference_output.xml	2017-01-31 05:05:06 UTC (rev 15303)
+++ trunk/doc/reference_output.xml	2017-02-06 16:33:23 UTC (rev 15304)
@@ -1300,4 +1300,58 @@
 	  </refsection>
 	</refentry>
 
+	<refentry id="ST_AsMVT">
+	  <refnamediv>
+		<refname>ST_AsMVT</refname>
+
+		<refpurpose>Return a Mapbox Vector Tile 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>
+		</funcsynopsis>
+	  </refsynopsisdiv>
+
+	  <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.
+		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.
+		</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>
+
+		<para>Availability: 2.4.0</para>
+	  </refsection>
+
+	  <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                              
+--------------------------------------------------------------------
+ \x1a1e0a0474657374120c12020000180122040932de3f1a026331220220017802
+
+		]]>
+		</programlisting>
+	  </refsection>
+	</refentry>
+
   </sect1>

Modified: trunk/postgis/Makefile.in
===================================================================
--- trunk/postgis/Makefile.in	2017-01-31 05:05:06 UTC (rev 15303)
+++ trunk/postgis/Makefile.in	2017-02-06 16:33:23 UTC (rev 15304)
@@ -50,6 +50,9 @@
 BRIN_OBJ= brin_2d.o brin_nd.o brin_common.o
 endif
 
+ifeq (@HAVE_PROTOBUF@,yes)
+PROTOBUF_OBJ=vector_tile.pb-c.o
+endif
 
 # SQL preprocessor
 SQLPP = @SQLPP@
@@ -96,7 +99,11 @@
 	geography_btree.o \
 	geography_measurement.o \
 	geography_measurement_trees.o \
-	geometry_inout.o
+	geometry_inout.o \
+	postgis_libprotobuf.o \
+	$(PROTOBUF_OBJ) \
+	mvt.o \
+	lwgeom_out_mvt.o \
 
 # Objects to build using PGXS
 OBJS=$(PG_OBJS)
@@ -126,7 +133,9 @@
 	postgis_upgrade.sql.in \
 	postgis_upgrade.sql \
 	sfcgal_upgrade.sql.in \
-	sfcgal_upgrade.sql 
+	sfcgal_upgrade.sql \
+	vector_tile.pb-c.c \
+	vector_tile.pb-c.h
 
 # PGXS information
 PG_CONFIG = @PG_CONFIG@
@@ -170,6 +179,18 @@
 ../libpgcommon/libpgcommon.a:
 	$(MAKE) -C ../libpgcommon libpgcommon.a
 
+# Get protobuf-c compiler command
+PROTOCC=@PROTOCC@
+
+# Generate Mapbox Vector Tile encoder/decoder using protobuf-c compiler 
+vector_tile.pb-c.c vector_tile.pb-c.h: vector_tile.proto
+	$(PROTOCC) --c_out=. $<
+
+ifeq (@HAVE_PROTOBUF@,yes)
+lwgeom_out_mvt.o: vector_tile.pb-c.h
+mvt.o: vector_tile.pb-c.h
+endif
+
 # Borrow the $libdir substitution from PGXS but customise by running the preprocessor
 # and adding the version number
 # replace @extschema at . with nothing, this is only used as placeholder for extension install

Added: trunk/postgis/lwgeom_out_mvt.c
===================================================================
--- trunk/postgis/lwgeom_out_mvt.c	                        (rev 0)
+++ trunk/postgis/lwgeom_out_mvt.c	2017-02-06 16:33:23 UTC (rev 15304)
@@ -0,0 +1,104 @@
+/**********************************************************************
+ *
+ * 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 "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 "mvt.h"
+
+#ifdef HAVE_LIBPROTOBUF
+#include "vector_tile.pb-c.h"
+#endif  /* HAVE_LIBPROTOBUF */
+
+/**
+ * Process input parameters and row data into state
+ */
+PG_FUNCTION_INFO_V1(pgis_asmvt_transfn);
+Datum pgis_asmvt_transfn(PG_FUNCTION_ARGS)
+{
+#ifndef HAVE_LIBPROTOBUF
+	lwerror("Missing libprotobuf-c");
+	PG_RETURN_NULL();
+#else
+	MemoryContext aggcontext;
+	struct mvt_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_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))
+			lwerror("pgis_asmvt_transfn: parameter geom_name cannot be null");
+		ctx->geom_name = text_to_cstring(PG_GETARG_TEXT_P(6));
+		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)))
+		lwerror("pgis_asmvt_transfn: parameter row cannot be other than a rowtype");
+	ctx->row = PG_GETARG_HEAPTUPLEHEADER(7);
+
+	mvt_agg_transfn(ctx);
+	PG_RETURN_POINTER(ctx);
+#endif
+}
+
+/**
+ * Encode final state to Mapbox Vector Tile
+ */
+PG_FUNCTION_INFO_V1(pgis_asmvt_finalfn);
+Datum pgis_asmvt_finalfn(PG_FUNCTION_ARGS)
+{
+#ifndef HAVE_LIBPROTOBUF
+	lwerror("Missing libprotobuf-c");
+	PG_RETURN_NULL();
+#else
+	struct mvt_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 mvt_agg_context *) PG_GETARG_POINTER(0);
+	uint8_t *buf = mvt_agg_finalfn(ctx);
+	PG_RETURN_BYTEA_P(buf);
+#endif
+}

Added: trunk/postgis/mvt.c
===================================================================
--- trunk/postgis/mvt.c	                        (rev 0)
+++ trunk/postgis/mvt.c	2017-02-06 16:33:23 UTC (rev 15304)
@@ -0,0 +1,614 @@
+/**********************************************************************
+ *
+ * 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 "mvt.h"
+
+#ifdef HAVE_LIBPROTOBUF
+
+#include "uthash.h"
+
+#define FEATURES_CAPACITY_INITIAL 50
+
+enum mvd_cmd_id {
+	CMD_MOVE_TO = 1,
+	CMD_LINE_TO = 2,
+	CMD_CLOSE_PATH = 7
+};
+
+enum mvt_type {
+	MVT_POINT = 1,
+	MVT_LINE = 2,
+	MVT_RING = 3
+};
+
+struct mvt_kv_string_value {
+	char *string_value;
+	uint32_t id;
+	UT_hash_handle hh;
+};
+
+struct mvt_kv_float_value {
+	float float_value;
+	uint32_t id;
+	UT_hash_handle hh;
+};
+
+struct mvt_kv_double_value {
+	double double_value;
+	uint32_t id;
+	UT_hash_handle hh;
+};
+
+struct mvt_kv_int_value {
+	int64_t int_value;
+	uint32_t id;
+	UT_hash_handle hh;
+};
+
+struct mvt_kv_uint_value {
+	uint64_t uint_value;
+	uint32_t id;
+	UT_hash_handle hh;
+};
+
+struct mvt_kv_sint_value {
+	int64_t sint_value;
+	uint32_t id;
+	UT_hash_handle hh;
+};
+
+struct mvt_kv_bool_value {
+	bool bool_value;
+	uint32_t id;
+	UT_hash_handle hh;
+};
+
+static inline uint32_t c_int(enum mvd_cmd_id id, uint32_t count)
+{
+	return (id & 0x7) | (count << 3);
+}
+
+static inline uint32_t p_int(int32_t value)
+{
+	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 */
+	for (i = 0; i < pa->npoints; i++) {
+		/* move offset for command and remember the latest */
+		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);
+		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;
+		*py = y;
+		c++;
+	}
+
+	/* 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);
+	} 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);
+	}
+
+	/* add close command if ring */
+	if (type == MVT_RING)
+		buffer[offset++] = c_int(CMD_CLOSE_PATH, 1);
+
+	return offset;
+}
+
+static uint32_t encode_ptarray_initial(struct mvt_agg_context *ctx,
+				       enum mvt_type type,
+				       POINTARRAY *pa, uint32_t *buffer)
+{
+	int32_t px = 0, py = 0;
+	return encode_ptarray(ctx, type, pa, buffer, &px, &py);
+}
+
+static void encode_point(struct mvt_agg_context *ctx, LWPOINT *point)
+{
+	VectorTile__Tile__Feature *feature = ctx->feature;
+	feature->type = VECTOR_TILE__TILE__GEOM_TYPE__POINT;
+	feature->has_type = 1;
+	feature->n_geometry = 3;
+	feature->geometry = palloc(sizeof(*feature->geometry) * 3);
+	encode_ptarray_initial(ctx, MVT_POINT, point->point, feature->geometry);
+}
+
+static void encode_mpoint(struct mvt_agg_context *ctx, LWMPOINT *mpoint)
+{
+	size_t c;
+	VectorTile__Tile__Feature *feature = ctx->feature;
+	// NOTE: inefficient shortcut LWMPOINT->LWLINE
+	LWLINE *lwline = lwline_from_lwmpoint(mpoint->srid, mpoint);
+	feature->type = VECTOR_TILE__TILE__GEOM_TYPE__POINT;
+	feature->has_type = 1;
+	c = 1 + lwline->points->npoints * 2;
+	feature->geometry = palloc(sizeof(*feature->geometry) * c);
+	feature->n_geometry = encode_ptarray_initial(ctx, MVT_POINT,
+		lwline->points, feature->geometry);
+}
+
+static void encode_line(struct mvt_agg_context *ctx, LWLINE *lwline)
+{
+	size_t c;
+	VectorTile__Tile__Feature *feature = ctx->feature;
+	feature->type = VECTOR_TILE__TILE__GEOM_TYPE__LINESTRING;
+	feature->has_type = 1;
+	c = 2 + lwline->points->npoints * 2;
+	feature->geometry = palloc(sizeof(*feature->geometry) * c);
+	feature->n_geometry = encode_ptarray_initial(ctx, MVT_LINE,
+		lwline->points, feature->geometry);
+}
+
+static void encode_mline(struct mvt_agg_context *ctx, LWMLINE *lwmline)
+{
+	uint32_t i;
+	int32_t px = 0, py = 0;
+	size_t c = 0, offset = 0;
+	VectorTile__Tile__Feature *feature = ctx->feature;
+	feature->type = VECTOR_TILE__TILE__GEOM_TYPE__LINESTRING;
+	feature->has_type = 1;
+	for (i = 0; i < lwmline->ngeoms; i++)
+		c += 2 + lwmline->geoms[i]->points->npoints * 2;
+	feature->geometry = palloc(sizeof(*feature->geometry) * c);
+	for (i = 0; i < lwmline->ngeoms; i++)
+		offset += encode_ptarray(ctx, MVT_LINE, 
+			lwmline->geoms[i]->points,
+			feature->geometry + offset, &px, &py);
+	feature->n_geometry = offset;
+}
+
+static void encode_poly(struct mvt_agg_context *ctx, LWPOLY *lwpoly)
+{
+	uint32_t i;
+	int32_t px = 0, py = 0;
+	size_t c = 0, offset = 0;
+	VectorTile__Tile__Feature *feature = ctx->feature;
+	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;
+	feature->geometry = palloc(sizeof(*feature->geometry) * c);
+	for (i = 0; i < lwpoly->nrings; i++)
+		offset += encode_ptarray(ctx, MVT_RING,
+			lwpoly->rings[i],
+			feature->geometry + offset, &px, &py);
+	feature->n_geometry = offset;
+}
+
+static void encode_mpoly(struct mvt_agg_context *ctx, LWMPOLY *lwmpoly)
+{
+	uint32_t i, j;
+	int32_t px = 0, py = 0;
+	size_t c = 0, offset = 0;
+	LWPOLY *poly;
+	VectorTile__Tile__Feature *feature = ctx->feature;
+	feature->type = VECTOR_TILE__TILE__GEOM_TYPE__POLYGON;
+	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;
+	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,
+				poly->rings[j],	feature->geometry + offset,
+				&px, &py);
+	feature->n_geometry = offset;
+}
+
+static bool check_geometry_size(LWGEOM *lwgeom, struct mvt_agg_context *ctx)
+{
+	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);
+	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);
+	default: lwerror("encode_geometry: '%s' geometry type not supported",
+		lwtype_name(type));
+	}
+}
+
+static TupleDesc get_tuple_desc(struct mvt_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 mvt_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->layer->n_keys = k;
+	ctx->layer->keys = keys;
+	ReleaseTupleDesc(tupdesc);
+}
+
+static VectorTile__Tile__Value *create_value() {
+	VectorTile__Tile__Value *value = palloc(sizeof(*value));
+	vector_tile__tile__value__init(value);
+	return value;
+}
+
+#define MVT_CREATE_VALUES(kvtype, hash, hasfield, valuefield) \
+{ \
+	struct kvtype *kv; \
+	for (kv = ctx->hash; kv != NULL; kv=kv->hh.next) { \
+		VectorTile__Tile__Value *value = create_value(); \
+		value->hasfield = 1; \
+		value->valuefield = kv->valuefield; \
+		values[kv->id] = value; \
+	} \
+}
+
+static void encode_values(struct mvt_agg_context *ctx)
+{
+	VectorTile__Tile__Value **values;
+	values = palloc(ctx->values_hash_i * sizeof(*values));
+	struct mvt_kv_string_value *kv;
+	for (kv = ctx->string_values_hash; kv != NULL; kv=kv->hh.next) {
+		VectorTile__Tile__Value *value = create_value();
+		value->string_value = kv->string_value;
+		values[kv->id] = value;
+	}
+	MVT_CREATE_VALUES(mvt_kv_float_value,
+		float_values_hash, has_float_value, float_value);
+	MVT_CREATE_VALUES(mvt_kv_double_value,
+		double_values_hash, has_double_value, double_value);
+	MVT_CREATE_VALUES(mvt_kv_int_value,
+		int_values_hash, has_int_value, int_value);
+	MVT_CREATE_VALUES(mvt_kv_uint_value,
+		uint_values_hash, has_uint_value, uint_value);
+	MVT_CREATE_VALUES(mvt_kv_sint_value,
+		sint_values_hash, has_sint_value, sint_value);
+	MVT_CREATE_VALUES(mvt_kv_bool_value,
+		bool_values_hash, has_bool_value, bool_value);
+
+	ctx->layer->n_values = ctx->values_hash_i;
+	ctx->layer->values = values;
+}
+
+
+#define MVT_PARSE_VALUE(type, kvtype, hash, valuefield, datumfunc, size) \
+{ \
+	struct kvtype *kv; \
+	type value = datumfunc(datum); \
+	HASH_FIND(hh, ctx->hash, &value, size, kv); \
+	if (!kv) { \
+		kv = palloc(sizeof(*kv)); \
+		kv->id = ctx->values_hash_i++; \
+		kv->valuefield = value; \
+		HASH_ADD(hh, ctx->hash, valuefield, size, kv); \
+	} \
+	tags[c*2] = k - 1; \
+	tags[c*2+1] = kv->id; \
+}
+
+static void parse_values(struct mvt_agg_context *ctx)
+{
+	uint32_t n_keys = ctx->layer->n_keys;
+	uint32_t *tags = palloc(n_keys * 2 * sizeof(*tags));
+	bool isnull;
+	uint32_t i, k = 0, c = 0;
+	TupleDesc tupdesc = get_tuple_desc(ctx);
+	int natts = tupdesc->natts;
+
+	for (i = 0; i < natts; i++) {
+		if (i == ctx->geom_index)
+			continue;
+		k++;
+		Datum datum = GetAttributeByNum(ctx->row, i+1, &isnull);
+		if (isnull)
+			continue;
+		Oid typoid = getBaseType(tupdesc->attrs[i]->atttypid);
+		switch (typoid) {
+		case BOOLOID:
+			MVT_PARSE_VALUE(bool, mvt_kv_bool_value,
+				bool_values_hash, bool_value,
+				DatumGetBool, sizeof(bool));
+			break;
+		case INT2OID:
+			MVT_PARSE_VALUE(int16_t, mvt_kv_int_value,
+				int_values_hash, int_value,
+				DatumGetInt16, sizeof(int64_t));
+			break;
+		case INT4OID:
+			MVT_PARSE_VALUE(int32_t, mvt_kv_int_value,
+				int_values_hash, int_value,
+				DatumGetInt32, sizeof(int64_t));
+			break;
+		case INT8OID:
+			MVT_PARSE_VALUE(int64_t, mvt_kv_int_value,
+				int_values_hash, int_value,
+				DatumGetInt64, sizeof(int64_t));
+			break;
+		case FLOAT4OID:
+			MVT_PARSE_VALUE(float, mvt_kv_float_value,
+				float_values_hash, float_value,
+				DatumGetFloat4, sizeof(float));
+			break;
+		case FLOAT8OID:
+			MVT_PARSE_VALUE(double, mvt_kv_double_value,
+				double_values_hash, double_value,
+				DatumGetFloat8, sizeof(double));
+			break;
+		case NUMERICOID:
+			lwerror("parse_values: numeric type not supported");
+			break;
+		case VARCHAROID:
+		case CHAROID:
+		case BPCHAROID:
+		case TEXTOID:
+			MVT_PARSE_VALUE(char *, mvt_kv_string_value,
+				string_values_hash, string_value,
+				TextDatumGetCString, strlen(value));
+			break;
+		default:
+			continue;
+		}
+		c++;
+	}
+
+	ReleaseTupleDesc(tupdesc);
+
+	ctx->feature->n_tags = c * 2;
+	ctx->feature->tags = tags;
+}
+
+/**
+ * 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;
+	ctx->double_values_hash = NULL;
+	ctx->int_values_hash = NULL;
+	ctx->uint_values_hash = NULL;
+	ctx->sint_values_hash = NULL;
+	ctx->bool_values_hash = NULL;
+	ctx->values_hash_i = 0;
+
+	layer = palloc(sizeof(*layer));
+	vector_tile__tile__layer__init(layer);
+	layer->version = 2;
+	layer->name = ctx->name;
+	layer->extent = ctx->extent;
+	layer->features = palloc (ctx->features_capacity *
+		sizeof(*layer->features));
+
+	ctx->layer = layer;
+}
+
+/**
+ * Aggregation step.
+ *
+ * Expands features array if needed by a factor of 2.
+ * Allocates a new feature, increment feature counter and
+ * encode geometry and properties into it.
+ */
+void mvt_agg_transfn(struct mvt_agg_context *ctx)
+{
+	VectorTile__Tile__Layer *layer = ctx->layer;
+	VectorTile__Tile__Feature **features = layer->features;
+	if (layer->n_features >= ctx->features_capacity) {
+		size_t new_capacity = ctx->features_capacity * 2;
+		layer->features = repalloc(layer->features, new_capacity *
+			sizeof(*layer->features));
+		ctx->features_capacity = new_capacity;
+	}
+
+	VectorTile__Tile__Feature *feature = palloc(sizeof(*feature));
+	vector_tile__tile__feature__init(feature);
+
+	ctx->feature = feature;
+	if (layer->n_features == 0)
+		encode_keys(ctx);
+
+	bool isnull;
+	Datum datum = GetAttributeByNum(ctx->row, ctx->geom_index + 1, &isnull);
+	if (!datum)
+		lwerror("mvt_agg_transfn: geometry column cannot be null");
+	GSERIALIZED *gs = (GSERIALIZED *) PG_DETOAST_DATUM(datum);
+	ctx->lwgeom = lwgeom_from_gserialized(gs);
+
+	if (!coerce_geometry(ctx))
+		return;
+
+	layer->features[layer->n_features++] = feature;
+
+	encode_geometry(ctx);
+	parse_values(ctx);
+}
+
+/**
+ * Finalize aggregation.
+ *
+ * Encode keys and values and put the aggregated Layer message into
+ * a Tile message and returns it packed as a bytea.
+ */
+uint8_t *mvt_agg_finalfn(struct mvt_agg_context *ctx)
+{
+	encode_values(ctx);
+
+	VectorTile__Tile__Layer *layers[1];
+	layers[0] = ctx->layer;
+
+	VectorTile__Tile tile = VECTOR_TILE__TILE__INIT;
+	tile.n_layers = 1;
+	tile.layers = layers;
+
+	size_t len = vector_tile__tile__get_packed_size(&tile);
+	uint8_t *buf = palloc(sizeof(*buf) * (len + VARHDRSZ));
+	vector_tile__tile__pack(&tile, buf + VARHDRSZ);
+
+	SET_VARSIZE(buf, VARHDRSZ + len);
+
+	return buf;
+}
+
+#endif
\ No newline at end of file

Added: trunk/postgis/mvt.h
===================================================================
--- trunk/postgis/mvt.h	                        (rev 0)
+++ trunk/postgis/mvt.h	2017-02-06 16:33:23 UTC (rev 15304)
@@ -0,0 +1,78 @@
+/**********************************************************************
+ *
+ * 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 MVT_H_
+#define MVT_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/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 "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;
+	struct mvt_kv_int_value *int_values_hash;
+	struct mvt_kv_uint_value *uint_values_hash;
+	struct mvt_kv_sint_value *sint_values_hash;
+	struct mvt_kv_bool_value *bool_values_hash;
+	uint32_t values_hash_i;
+} ;
+
+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);
+
+#endif  /* HAVE_LIBPROTOBUF */
+
+#endif
\ No newline at end of file

Modified: trunk/postgis/postgis.sql.in
===================================================================
--- trunk/postgis/postgis.sql.in	2017-01-31 05:05:06 UTC (rev 15303)
+++ trunk/postgis/postgis.sql.in	2017-02-06 16:33:23 UTC (rev 15304)
@@ -2861,6 +2861,7 @@
 	rast_scr_ver text;
 	topo_scr_ver text;
 	json_lib_ver text;
+	protobuf_lib_ver text;
 	sfcgal_lib_ver text;
 	sfcgal_scr_ver text;
 BEGIN
@@ -2868,6 +2869,7 @@
 	SELECT postgis_proj_version() INTO projver;
 	SELECT postgis_geos_version() INTO geosver;
 	SELECT postgis_libjson_version() INTO json_lib_ver;
+	SELECT postgis_libprotobuf_version() INTO protobuf_lib_ver;
 	BEGIN
 		SELECT postgis_gdal_version() INTO gdalver;
 	EXCEPTION
@@ -2957,6 +2959,10 @@
 		fullver = fullver || ' LIBJSON="' || json_lib_ver || '"';
 	END IF;
 
+	IF protobuf_lib_ver IS NOT NULL THEN
+		fullver = fullver || ' LIBPROTOBUF="' || protobuf_lib_ver || '"';
+	END IF;
+
 	-- fullver = fullver || ' DBPROC="' || dbproc || '"';
 	-- fullver = fullver || ' RELPROC="' || relproc || '"';
 
@@ -4375,6 +4381,37 @@
 	AS $$ SELECT @extschema at .ST_AsGeoJson($2::@extschema at .geometry, $3::int4, $4::int4); $$
 	LANGUAGE 'sql' IMMUTABLE STRICT _PARALLEL;
 
+-----------------------------------------------------------------------
+-- Mapbox Vector Tile OUTPUT
+-- Availability: 2.4.0
+-----------------------------------------------------------------------
+
+-- Availability: 2.4.0
+CREATE OR REPLACE FUNCTION pgis_asmvt_transfn(internal, text, box2d, int4, int4, bool, text, anyelement)
+	RETURNS internal
+	AS 'MODULE_PATHNAME', 'pgis_asmvt_transfn'
+	LANGUAGE c IMMUTABLE;
+
+-- Availability: 2.4.0
+CREATE OR REPLACE FUNCTION pgis_asmvt_finalfn(internal)
+	RETURNS bytea
+	AS 'MODULE_PATHNAME', 'pgis_asmvt_finalfn'
+	LANGUAGE c IMMUTABLE;
+
+-- Availability: 2.4.0
+CREATE AGGREGATE ST_AsMVT(text, box2d, int4, int4, bool, text, anyelement)
+(
+	sfunc = pgis_asmvt_transfn,
+	stype = internal,
+	finalfunc = pgis_asmvt_finalfn
+);
+
+-- Availability: 2.4.0
+CREATE OR REPLACE FUNCTION postgis_libprotobuf_version()
+	RETURNS text
+	AS 'MODULE_PATHNAME','postgis_libprotobuf_version'
+	LANGUAGE 'c' IMMUTABLE STRICT _PARALLEL;
+
 ------------------------------------------------------------------------
 -- GeoHash (geohash.org)
 ------------------------------------------------------------------------

Added: trunk/postgis/postgis_libprotobuf.c
===================================================================
--- trunk/postgis/postgis_libprotobuf.c	                        (rev 0)
+++ trunk/postgis/postgis_libprotobuf.c	2017-02-06 16:33:23 UTC (rev 15304)
@@ -0,0 +1,20 @@
+#include "postgres.h"
+#include "utils/builtins.h"
+#include "../postgis_config.h"
+#include "lwgeom_pg.h"
+
+#ifdef HAVE_LIBPROTOBUF
+#include <protobuf-c/protobuf-c.h>
+#endif
+
+PG_FUNCTION_INFO_V1(postgis_libprotobuf_version);
+Datum postgis_libprotobuf_version(PG_FUNCTION_ARGS)
+{
+#ifndef HAVE_LIBPROTOBUF
+	PG_RETURN_NULL();
+#else /* HAVE_LIBPROTOBUF  */
+	const char *ver = protobuf_c_version();
+	text *result = cstring2text(ver);
+	PG_RETURN_POINTER(result);
+#endif
+}
\ No newline at end of file

Added: trunk/postgis/uthash.h
===================================================================
--- trunk/postgis/uthash.h	                        (rev 0)
+++ trunk/postgis/uthash.h	2017-02-06 16:33:23 UTC (rev 15304)
@@ -0,0 +1,1070 @@
+/*
+Copyright (c) 2003-2016, Troy D. Hanson     http://troydhanson.github.com/uthash/
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
+IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
+OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+#ifndef UTHASH_H
+#define UTHASH_H
+
+#define UTHASH_VERSION 2.0.1
+
+#include <string.h>   /* memcmp,strlen */
+#include <stddef.h>   /* ptrdiff_t */
+#include <stdlib.h>   /* exit() */
+
+/* These macros use decltype or the earlier __typeof GNU extension.
+   As decltype is only available in newer compilers (VS2010 or gcc 4.3+
+   when compiling c++ source) this code uses whatever method is needed
+   or, for VS2008 where neither is available, uses casting workarounds. */
+#if defined(_MSC_VER)   /* MS compiler */
+#if _MSC_VER >= 1600 && defined(__cplusplus)  /* VS2010 or newer in C++ mode */
+#define DECLTYPE(x) (decltype(x))
+#else                   /* VS2008 or older (or VS2010 in C mode) */
+#define NO_DECLTYPE
+#define DECLTYPE(x)
+#endif
+#elif defined(__BORLANDC__) || defined(__LCC__) || defined(__WATCOMC__)
+#define NO_DECLTYPE
+#define DECLTYPE(x)
+#else                   /* GNU, Sun and other compilers */
+#define DECLTYPE(x) (__typeof(x))
+#endif
+
+#ifdef NO_DECLTYPE
+#define DECLTYPE_ASSIGN(dst,src)                                                 \
+do {                                                                             \
+  char **_da_dst = (char**)(&(dst));                                             \
+  *_da_dst = (char*)(src);                                                       \
+} while (0)
+#else
+#define DECLTYPE_ASSIGN(dst,src)                                                 \
+do {                                                                             \
+  (dst) = DECLTYPE(dst)(src);                                                    \
+} while (0)
+#endif
+
+/* a number of the hash function use uint32_t which isn't defined on Pre VS2010 */
+#if defined(_WIN32)
+#if defined(_MSC_VER) && _MSC_VER >= 1600
+#include <stdint.h>
+#elif defined(__WATCOMC__) || defined(__MINGW32__) || defined(__CYGWIN__)
+#include <stdint.h>
+#else
+typedef unsigned int uint32_t;
+typedef unsigned char uint8_t;
+#endif
+#elif defined(__GNUC__) && !defined(__VXWORKS__)
+#include <stdint.h>
+#else
+typedef unsigned int uint32_t;
+typedef unsigned char uint8_t;
+#endif
+
+#ifndef uthash_fatal
+#define uthash_fatal(msg) exit(-1)        /* fatal error (out of memory,etc) */
+#endif
+#ifndef uthash_malloc
+#define uthash_malloc(sz) malloc(sz)      /* malloc fcn                      */
+#endif
+#ifndef uthash_free
+#define uthash_free(ptr,sz) free(ptr)     /* free fcn                        */
+#endif
+#ifndef uthash_strlen
+#define uthash_strlen(s) strlen(s)
+#endif
+#ifndef uthash_memcmp
+#define uthash_memcmp(a,b,n) memcmp(a,b,n)
+#endif
+
+#ifndef uthash_noexpand_fyi
+#define uthash_noexpand_fyi(tbl)          /* can be defined to log noexpand  */
+#endif
+#ifndef uthash_expand_fyi
+#define uthash_expand_fyi(tbl)            /* can be defined to log expands   */
+#endif
+
+/* initial number of buckets */
+#define HASH_INITIAL_NUM_BUCKETS 32U     /* initial number of buckets        */
+#define HASH_INITIAL_NUM_BUCKETS_LOG2 5U /* lg2 of initial number of buckets */
+#define HASH_BKT_CAPACITY_THRESH 10U     /* expand when bucket count reaches */
+
+/* calculate the element whose hash handle address is hhp */
+#define ELMT_FROM_HH(tbl,hhp) ((void*)(((char*)(hhp)) - ((tbl)->hho)))
+/* calculate the hash handle from element address elp */
+#define HH_FROM_ELMT(tbl,elp) ((UT_hash_handle *)(((char*)(elp)) + ((tbl)->hho)))
+
+#define HASH_VALUE(keyptr,keylen,hashv)                                          \
+do {                                                                             \
+  HASH_FCN(keyptr, keylen, hashv);                                               \
+} while (0)
+
+#define HASH_FIND_BYHASHVALUE(hh,head,keyptr,keylen,hashval,out)                 \
+do {                                                                             \
+  (out) = NULL;                                                                  \
+  if (head) {                                                                    \
+    unsigned _hf_bkt;                                                            \
+    HASH_TO_BKT(hashval, (head)->hh.tbl->num_buckets, _hf_bkt);                  \
+    if (HASH_BLOOM_TEST((head)->hh.tbl, hashval) != 0) {                         \
+      HASH_FIND_IN_BKT((head)->hh.tbl, hh, (head)->hh.tbl->buckets[ _hf_bkt ], keyptr, keylen, hashval, out); \
+    }                                                                            \
+  }                                                                              \
+} while (0)
+
+#define HASH_FIND(hh,head,keyptr,keylen,out)                                     \
+do {                                                                             \
+  unsigned _hf_hashv;                                                            \
+  HASH_VALUE(keyptr, keylen, _hf_hashv);                                         \
+  HASH_FIND_BYHASHVALUE(hh, head, keyptr, keylen, _hf_hashv, out);               \
+} while (0)
+
+#ifdef HASH_BLOOM
+#define HASH_BLOOM_BITLEN (1UL << HASH_BLOOM)
+#define HASH_BLOOM_BYTELEN (HASH_BLOOM_BITLEN/8UL) + (((HASH_BLOOM_BITLEN%8UL)!=0UL) ? 1UL : 0UL)
+#define HASH_BLOOM_MAKE(tbl)                                                     \
+do {                                                                             \
+  (tbl)->bloom_nbits = HASH_BLOOM;                                               \
+  (tbl)->bloom_bv = (uint8_t*)uthash_malloc(HASH_BLOOM_BYTELEN);                 \
+  if (!((tbl)->bloom_bv))  { uthash_fatal( "out of memory"); }                   \
+  memset((tbl)->bloom_bv, 0, HASH_BLOOM_BYTELEN);                                \
+  (tbl)->bloom_sig = HASH_BLOOM_SIGNATURE;                                       \
+} while (0)
+
+#define HASH_BLOOM_FREE(tbl)                                                     \
+do {                                                                             \
+  uthash_free((tbl)->bloom_bv, HASH_BLOOM_BYTELEN);                              \
+} while (0)
+
+#define HASH_BLOOM_BITSET(bv,idx) (bv[(idx)/8U] |= (1U << ((idx)%8U)))
+#define HASH_BLOOM_BITTEST(bv,idx) (bv[(idx)/8U] & (1U << ((idx)%8U)))
+
+#define HASH_BLOOM_ADD(tbl,hashv)                                                \
+  HASH_BLOOM_BITSET((tbl)->bloom_bv, (hashv & (uint32_t)((1ULL << (tbl)->bloom_nbits) - 1U)))
+
+#define HASH_BLOOM_TEST(tbl,hashv)                                               \
+  HASH_BLOOM_BITTEST((tbl)->bloom_bv, (hashv & (uint32_t)((1ULL << (tbl)->bloom_nbits) - 1U)))
+
+#else
+#define HASH_BLOOM_MAKE(tbl)
+#define HASH_BLOOM_FREE(tbl)
+#define HASH_BLOOM_ADD(tbl,hashv)
+#define HASH_BLOOM_TEST(tbl,hashv) (1)
+#define HASH_BLOOM_BYTELEN 0U
+#endif
+
+#define HASH_MAKE_TABLE(hh,head)                                                 \
+do {                                                                             \
+  (head)->hh.tbl = (UT_hash_table*)uthash_malloc(                                \
+                  sizeof(UT_hash_table));                                        \
+  if (!((head)->hh.tbl))  { uthash_fatal( "out of memory"); }                    \
+  memset((head)->hh.tbl, 0, sizeof(UT_hash_table));                              \
+  (head)->hh.tbl->tail = &((head)->hh);                                          \
+  (head)->hh.tbl->num_buckets = HASH_INITIAL_NUM_BUCKETS;                        \
+  (head)->hh.tbl->log2_num_buckets = HASH_INITIAL_NUM_BUCKETS_LOG2;              \
+  (head)->hh.tbl->hho = (char*)(&(head)->hh) - (char*)(head);                    \
+  (head)->hh.tbl->buckets = (UT_hash_bucket*)uthash_malloc(                      \
+          HASH_INITIAL_NUM_BUCKETS*sizeof(struct UT_hash_bucket));               \
+  if (! (head)->hh.tbl->buckets) { uthash_fatal( "out of memory"); }             \
+  memset((head)->hh.tbl->buckets, 0,                                             \
+          HASH_INITIAL_NUM_BUCKETS*sizeof(struct UT_hash_bucket));               \
+  HASH_BLOOM_MAKE((head)->hh.tbl);                                               \
+  (head)->hh.tbl->signature = HASH_SIGNATURE;                                    \
+} while (0)
+
+#define HASH_REPLACE_BYHASHVALUE_INORDER(hh,head,fieldname,keylen_in,hashval,add,replaced,cmpfcn) \
+do {                                                                             \
+  (replaced) = NULL;                                                             \
+  HASH_FIND_BYHASHVALUE(hh, head, &((add)->fieldname), keylen_in, hashval, replaced); \
+  if (replaced) {                                                                \
+     HASH_DELETE(hh, head, replaced);                                            \
+  }                                                                              \
+  HASH_ADD_KEYPTR_BYHASHVALUE_INORDER(hh, head, &((add)->fieldname), keylen_in, hashval, add, cmpfcn); \
+} while (0)
+
+#define HASH_REPLACE_BYHASHVALUE(hh,head,fieldname,keylen_in,hashval,add,replaced) \
+do {                                                                             \
+  (replaced) = NULL;                                                             \
+  HASH_FIND_BYHASHVALUE(hh, head, &((add)->fieldname), keylen_in, hashval, replaced); \
+  if (replaced) {                                                                \
+     HASH_DELETE(hh, head, replaced);                                            \
+  }                                                                              \
+  HASH_ADD_KEYPTR_BYHASHVALUE(hh, head, &((add)->fieldname), keylen_in, hashval, add); \
+} while (0)
+
+#define HASH_REPLACE(hh,head,fieldname,keylen_in,add,replaced)                   \
+do {                                                                             \
+  unsigned _hr_hashv;                                                            \
+  HASH_VALUE(&((add)->fieldname), keylen_in, _hr_hashv);                         \
+  HASH_REPLACE_BYHASHVALUE(hh, head, fieldname, keylen_in, _hr_hashv, add, replaced); \
+} while (0)
+
+#define HASH_REPLACE_INORDER(hh,head,fieldname,keylen_in,add,replaced,cmpfcn)    \
+do {                                                                             \
+  unsigned _hr_hashv;                                                            \
+  HASH_VALUE(&((add)->fieldname), keylen_in, _hr_hashv);                         \
+  HASH_REPLACE_BYHASHVALUE_INORDER(hh, head, fieldname, keylen_in, _hr_hashv, add, replaced, cmpfcn); \
+} while (0)
+
+#define HASH_APPEND_LIST(hh, head, add)                                          \
+do {                                                                             \
+  (add)->hh.next = NULL;                                                         \
+  (add)->hh.prev = ELMT_FROM_HH((head)->hh.tbl, (head)->hh.tbl->tail);           \
+  (head)->hh.tbl->tail->next = (add);                                            \
+  (head)->hh.tbl->tail = &((add)->hh);                                           \
+} while (0)
+
+#define HASH_ADD_KEYPTR_BYHASHVALUE_INORDER(hh,head,keyptr,keylen_in,hashval,add,cmpfcn) \
+do {                                                                             \
+  unsigned _ha_bkt;                                                              \
+  (add)->hh.hashv = (hashval);                                                   \
+  (add)->hh.key = (char*) (keyptr);                                              \
+  (add)->hh.keylen = (unsigned) (keylen_in);                                     \
+  if (!(head)) {                                                                 \
+    (add)->hh.next = NULL;                                                       \
+    (add)->hh.prev = NULL;                                                       \
+    (head) = (add);                                                              \
+    HASH_MAKE_TABLE(hh, head);                                                   \
+  } else {                                                                       \
+    struct UT_hash_handle *_hs_iter = &(head)->hh;                               \
+    (add)->hh.tbl = (head)->hh.tbl;                                              \
+    do {                                                                         \
+      if (cmpfcn(DECLTYPE(head) ELMT_FROM_HH((head)->hh.tbl, _hs_iter), add) > 0) \
+        break;                                                                   \
+    } while ((_hs_iter = _hs_iter->next));                                       \
+    if (_hs_iter) {                                                              \
+      (add)->hh.next = _hs_iter;                                                 \
+      if (((add)->hh.prev = _hs_iter->prev)) {                                   \
+        HH_FROM_ELMT((head)->hh.tbl, _hs_iter->prev)->next = (add);              \
+      } else {                                                                   \
+        (head) = (add);                                                          \
+      }                                                                          \
+      _hs_iter->prev = (add);                                                    \
+    } else {                                                                     \
+      HASH_APPEND_LIST(hh, head, add);                                           \
+    }                                                                            \
+  }                                                                              \
+  (head)->hh.tbl->num_items++;                                                   \
+  HASH_TO_BKT(hashval, (head)->hh.tbl->num_buckets, _ha_bkt);                    \
+  HASH_ADD_TO_BKT((head)->hh.tbl->buckets[_ha_bkt], &(add)->hh);                 \
+  HASH_BLOOM_ADD((head)->hh.tbl, hashval);                                       \
+  HASH_EMIT_KEY(hh, head, keyptr, keylen_in);                                    \
+  HASH_FSCK(hh, head);                                                           \
+} while (0)
+
+#define HASH_ADD_KEYPTR_INORDER(hh,head,keyptr,keylen_in,add,cmpfcn)             \
+do {                                                                             \
+  unsigned _hs_hashv;                                                            \
+  HASH_VALUE(keyptr, keylen_in, _hs_hashv);                                      \
+  HASH_ADD_KEYPTR_BYHASHVALUE_INORDER(hh, head, keyptr, keylen_in, _hs_hashv, add, cmpfcn); \
+} while (0)
+
+#define HASH_ADD_BYHASHVALUE_INORDER(hh,head,fieldname,keylen_in,hashval,add,cmpfcn) \
+  HASH_ADD_KEYPTR_BYHASHVALUE_INORDER(hh, head, &((add)->fieldname), keylen_in, hashval, add, cmpfcn)
+
+#define HASH_ADD_INORDER(hh,head,fieldname,keylen_in,add,cmpfcn)                 \
+  HASH_ADD_KEYPTR_INORDER(hh, head, &((add)->fieldname), keylen_in, add, cmpfcn)
+
+#define HASH_ADD_KEYPTR_BYHASHVALUE(hh,head,keyptr,keylen_in,hashval,add)        \
+do {                                                                             \
+  unsigned _ha_bkt;                                                              \
+  (add)->hh.hashv = (hashval);                                                   \
+  (add)->hh.key = (char*) (keyptr);                                              \
+  (add)->hh.keylen = (unsigned) (keylen_in);                                     \
+  if (!(head)) {                                                                 \
+    (add)->hh.next = NULL;                                                       \
+    (add)->hh.prev = NULL;                                                       \
+    (head) = (add);                                                              \
+    HASH_MAKE_TABLE(hh, head);                                                   \
+  } else {                                                                       \
+    (add)->hh.tbl = (head)->hh.tbl;                                              \
+    HASH_APPEND_LIST(hh, head, add);                                             \
+  }                                                                              \
+  (head)->hh.tbl->num_items++;                                                   \
+  HASH_TO_BKT(hashval, (head)->hh.tbl->num_buckets, _ha_bkt);                    \
+  HASH_ADD_TO_BKT((head)->hh.tbl->buckets[_ha_bkt], &(add)->hh);                 \
+  HASH_BLOOM_ADD((head)->hh.tbl, hashval);                                       \
+  HASH_EMIT_KEY(hh, head, keyptr, keylen_in);                                    \
+  HASH_FSCK(hh, head);                                                           \
+} while (0)
+
+#define HASH_ADD_KEYPTR(hh,head,keyptr,keylen_in,add)                            \
+do {                                                                             \
+  unsigned _ha_hashv;                                                            \
+  HASH_VALUE(keyptr, keylen_in, _ha_hashv);                                      \
+  HASH_ADD_KEYPTR_BYHASHVALUE(hh, head, keyptr, keylen_in, _ha_hashv, add);      \
+} while (0)
+
+#define HASH_ADD_BYHASHVALUE(hh,head,fieldname,keylen_in,hashval,add)            \
+  HASH_ADD_KEYPTR_BYHASHVALUE(hh, head, &((add)->fieldname), keylen_in, hashval, add)
+
+#define HASH_ADD(hh,head,fieldname,keylen_in,add)                                \
+  HASH_ADD_KEYPTR(hh, head, &((add)->fieldname), keylen_in, add)
+
+#define HASH_TO_BKT(hashv,num_bkts,bkt)                                          \
+do {                                                                             \
+  bkt = ((hashv) & ((num_bkts) - 1U));                                           \
+} while (0)
+
+/* delete "delptr" from the hash table.
+ * "the usual" patch-up process for the app-order doubly-linked-list.
+ * The use of _hd_hh_del below deserves special explanation.
+ * These used to be expressed using (delptr) but that led to a bug
+ * if someone used the same symbol for the head and deletee, like
+ *  HASH_DELETE(hh,users,users);
+ * We want that to work, but by changing the head (users) below
+ * we were forfeiting our ability to further refer to the deletee (users)
+ * in the patch-up process. Solution: use scratch space to
+ * copy the deletee pointer, then the latter references are via that
+ * scratch pointer rather than through the repointed (users) symbol.
+ */
+#define HASH_DELETE(hh,head,delptr)                                              \
+do {                                                                             \
+    struct UT_hash_handle *_hd_hh_del;                                           \
+    if ( ((delptr)->hh.prev == NULL) && ((delptr)->hh.next == NULL) )  {         \
+        uthash_free((head)->hh.tbl->buckets,                                     \
+                    (head)->hh.tbl->num_buckets*sizeof(struct UT_hash_bucket) ); \
+        HASH_BLOOM_FREE((head)->hh.tbl);                                         \
+        uthash_free((head)->hh.tbl, sizeof(UT_hash_table));                      \
+        head = NULL;                                                             \
+    } else {                                                                     \
+        unsigned _hd_bkt;                                                        \
+        _hd_hh_del = &((delptr)->hh);                                            \
+        if ((delptr) == ELMT_FROM_HH((head)->hh.tbl,(head)->hh.tbl->tail)) {     \
+            (head)->hh.tbl->tail =                                               \
+                (UT_hash_handle*)((ptrdiff_t)((delptr)->hh.prev) +               \
+                (head)->hh.tbl->hho);                                            \
+        }                                                                        \
+        if ((delptr)->hh.prev != NULL) {                                         \
+            ((UT_hash_handle*)((ptrdiff_t)((delptr)->hh.prev) +                  \
+                    (head)->hh.tbl->hho))->next = (delptr)->hh.next;             \
+        } else {                                                                 \
+            DECLTYPE_ASSIGN(head,(delptr)->hh.next);                             \
+        }                                                                        \
+        if (_hd_hh_del->next != NULL) {                                          \
+            ((UT_hash_handle*)((ptrdiff_t)_hd_hh_del->next +                     \
+                    (head)->hh.tbl->hho))->prev =                                \
+                    _hd_hh_del->prev;                                            \
+        }                                                                        \
+        HASH_TO_BKT( _hd_hh_del->hashv, (head)->hh.tbl->num_buckets, _hd_bkt);   \
+        HASH_DEL_IN_BKT(hh,(head)->hh.tbl->buckets[_hd_bkt], _hd_hh_del);        \
+        (head)->hh.tbl->num_items--;                                             \
+    }                                                                            \
+    HASH_FSCK(hh,head);                                                          \
+} while (0)
+
+
+/* convenience forms of HASH_FIND/HASH_ADD/HASH_DEL */
+#define HASH_FIND_STR(head,findstr,out)                                          \
+    HASH_FIND(hh,head,findstr,(unsigned)uthash_strlen(findstr),out)
+#define HASH_ADD_STR(head,strfield,add)                                          \
+    HASH_ADD(hh,head,strfield[0],(unsigned)uthash_strlen(add->strfield),add)
+#define HASH_REPLACE_STR(head,strfield,add,replaced)                             \
+    HASH_REPLACE(hh,head,strfield[0],(unsigned)uthash_strlen(add->strfield),add,replaced)
+#define HASH_FIND_INT(head,findint,out)                                          \
+    HASH_FIND(hh,head,findint,sizeof(int),out)
+#define HASH_ADD_INT(head,intfield,add)                                          \
+    HASH_ADD(hh,head,intfield,sizeof(int),add)
+#define HASH_REPLACE_INT(head,intfield,add,replaced)                             \
+    HASH_REPLACE(hh,head,intfield,sizeof(int),add,replaced)
+#define HASH_FIND_PTR(head,findptr,out)                                          \
+    HASH_FIND(hh,head,findptr,sizeof(void *),out)
+#define HASH_ADD_PTR(head,ptrfield,add)                                          \
+    HASH_ADD(hh,head,ptrfield,sizeof(void *),add)
+#define HASH_REPLACE_PTR(head,ptrfield,add,replaced)                             \
+    HASH_REPLACE(hh,head,ptrfield,sizeof(void *),add,replaced)
+#define HASH_DEL(head,delptr)                                                    \
+    HASH_DELETE(hh,head,delptr)
+
+/* HASH_FSCK checks hash integrity on every add/delete when HASH_DEBUG is defined.
+ * This is for uthash developer only; it compiles away if HASH_DEBUG isn't defined.
+ */
+#ifdef HASH_DEBUG
+#define HASH_OOPS(...) do { fprintf(stderr,__VA_ARGS__); exit(-1); } while (0)
+#define HASH_FSCK(hh,head)                                                       \
+do {                                                                             \
+    struct UT_hash_handle *_thh;                                                 \
+    if (head) {                                                                  \
+        unsigned _bkt_i;                                                         \
+        unsigned _count;                                                         \
+        char *_prev;                                                             \
+        _count = 0;                                                              \
+        for( _bkt_i = 0; _bkt_i < (head)->hh.tbl->num_buckets; _bkt_i++) {       \
+            unsigned _bkt_count = 0;                                             \
+            _thh = (head)->hh.tbl->buckets[_bkt_i].hh_head;                      \
+            _prev = NULL;                                                        \
+            while (_thh) {                                                       \
+               if (_prev != (char*)(_thh->hh_prev)) {                            \
+                   HASH_OOPS("invalid hh_prev %p, actual %p\n",                  \
+                    _thh->hh_prev, _prev );                                      \
+               }                                                                 \
+               _bkt_count++;                                                     \
+               _prev = (char*)(_thh);                                            \
+               _thh = _thh->hh_next;                                             \
+            }                                                                    \
+            _count += _bkt_count;                                                \
+            if ((head)->hh.tbl->buckets[_bkt_i].count !=  _bkt_count) {          \
+               HASH_OOPS("invalid bucket count %u, actual %u\n",                 \
+                (head)->hh.tbl->buckets[_bkt_i].count, _bkt_count);              \
+            }                                                                    \
+        }                                                                        \
+        if (_count != (head)->hh.tbl->num_items) {                               \
+            HASH_OOPS("invalid hh item count %u, actual %u\n",                   \
+                (head)->hh.tbl->num_items, _count );                             \
+        }                                                                        \
+        /* traverse hh in app order; check next/prev integrity, count */         \
+        _count = 0;                                                              \
+        _prev = NULL;                                                            \
+        _thh =  &(head)->hh;                                                     \
+        while (_thh) {                                                           \
+           _count++;                                                             \
+           if (_prev !=(char*)(_thh->prev)) {                                    \
+              HASH_OOPS("invalid prev %p, actual %p\n",                          \
+                    _thh->prev, _prev );                                         \
+           }                                                                     \
+           _prev = (char*)ELMT_FROM_HH((head)->hh.tbl, _thh);                    \
+           _thh = ( _thh->next ?  (UT_hash_handle*)((char*)(_thh->next) +        \
+                                  (head)->hh.tbl->hho) : NULL );                 \
+        }                                                                        \
+        if (_count != (head)->hh.tbl->num_items) {                               \
+            HASH_OOPS("invalid app item count %u, actual %u\n",                  \
+                (head)->hh.tbl->num_items, _count );                             \
+        }                                                                        \
+    }                                                                            \
+} while (0)
+#else
+#define HASH_FSCK(hh,head)
+#endif
+
+/* When compiled with -DHASH_EMIT_KEYS, length-prefixed keys are emitted to
+ * the descriptor to which this macro is defined for tuning the hash function.
+ * The app can #include <unistd.h> to get the prototype for write(2). */
+#ifdef HASH_EMIT_KEYS
+#define HASH_EMIT_KEY(hh,head,keyptr,fieldlen)                                   \
+do {                                                                             \
+    unsigned _klen = fieldlen;                                                   \
+    write(HASH_EMIT_KEYS, &_klen, sizeof(_klen));                                \
+    write(HASH_EMIT_KEYS, keyptr, (unsigned long)fieldlen);                      \
+} while (0)
+#else
+#define HASH_EMIT_KEY(hh,head,keyptr,fieldlen)
+#endif
+
+/* default to Jenkin's hash unless overridden e.g. DHASH_FUNCTION=HASH_SAX */
+#define HASH_FCN HASH_JEN
+
+/* The Bernstein hash function, used in Perl prior to v5.6. Note (x<<5+x)=x*33. */
+#define HASH_BER(key,keylen,hashv)                                               \
+do {                                                                             \
+  unsigned _hb_keylen=(unsigned)keylen;                                          \
+  const unsigned char *_hb_key=(const unsigned char*)(key);                      \
+  (hashv) = 0;                                                                   \
+  while (_hb_keylen-- != 0U) {                                                   \
+      (hashv) = (((hashv) << 5) + (hashv)) + *_hb_key++;                         \
+  }                                                                              \
+} while (0)
+
+
+/* SAX/FNV/OAT/JEN hash functions are macro variants of those listed at
+ * http://eternallyconfuzzled.com/tuts/algorithms/jsw_tut_hashing.aspx */
+#define HASH_SAX(key,keylen,hashv)                                               \
+do {                                                                             \
+  unsigned _sx_i;                                                                \
+  const unsigned char *_hs_key=(const unsigned char*)(key);                      \
+  hashv = 0;                                                                     \
+  for(_sx_i=0; _sx_i < keylen; _sx_i++) {                                        \
+      hashv ^= (hashv << 5) + (hashv >> 2) + _hs_key[_sx_i];                     \
+  }                                                                              \
+} while (0)
+/* FNV-1a variation */
+#define HASH_FNV(key,keylen,hashv)                                               \
+do {                                                                             \
+  unsigned _fn_i;                                                                \
+  const unsigned char *_hf_key=(const unsigned char*)(key);                      \
+  hashv = 2166136261U;                                                           \
+  for(_fn_i=0; _fn_i < keylen; _fn_i++) {                                        \
+      hashv = hashv ^ _hf_key[_fn_i];                                            \
+      hashv = hashv * 16777619U;                                                 \
+  }                                                                              \
+} while (0)
+
+#define HASH_OAT(key,keylen,hashv)                                               \
+do {                                                                             \
+  unsigned _ho_i;                                                                \
+  const unsigned char *_ho_key=(const unsigned char*)(key);                      \
+  hashv = 0;                                                                     \
+  for(_ho_i=0; _ho_i < keylen; _ho_i++) {                                        \
+      hashv += _ho_key[_ho_i];                                                   \
+      hashv += (hashv << 10);                                                    \
+      hashv ^= (hashv >> 6);                                                     \
+  }                                                                              \
+  hashv += (hashv << 3);                                                         \
+  hashv ^= (hashv >> 11);                                                        \
+  hashv += (hashv << 15);                                                        \
+} while (0)
+
+#define HASH_JEN_MIX(a,b,c)                                                      \
+do {                                                                             \
+  a -= b; a -= c; a ^= ( c >> 13 );                                              \
+  b -= c; b -= a; b ^= ( a << 8 );                                               \
+  c -= a; c -= b; c ^= ( b >> 13 );                                              \
+  a -= b; a -= c; a ^= ( c >> 12 );                                              \
+  b -= c; b -= a; b ^= ( a << 16 );                                              \
+  c -= a; c -= b; c ^= ( b >> 5 );                                               \
+  a -= b; a -= c; a ^= ( c >> 3 );                                               \
+  b -= c; b -= a; b ^= ( a << 10 );                                              \
+  c -= a; c -= b; c ^= ( b >> 15 );                                              \
+} while (0)
+
+#define HASH_JEN(key,keylen,hashv)                                               \
+do {                                                                             \
+  unsigned _hj_i,_hj_j,_hj_k;                                                    \
+  unsigned const char *_hj_key=(unsigned const char*)(key);                      \
+  hashv = 0xfeedbeefu;                                                           \
+  _hj_i = _hj_j = 0x9e3779b9u;                                                   \
+  _hj_k = (unsigned)(keylen);                                                    \
+  while (_hj_k >= 12U) {                                                         \
+    _hj_i +=    (_hj_key[0] + ( (unsigned)_hj_key[1] << 8 )                      \
+        + ( (unsigned)_hj_key[2] << 16 )                                         \
+        + ( (unsigned)_hj_key[3] << 24 ) );                                      \
+    _hj_j +=    (_hj_key[4] + ( (unsigned)_hj_key[5] << 8 )                      \
+        + ( (unsigned)_hj_key[6] << 16 )                                         \
+        + ( (unsigned)_hj_key[7] << 24 ) );                                      \
+    hashv += (_hj_key[8] + ( (unsigned)_hj_key[9] << 8 )                         \
+        + ( (unsigned)_hj_key[10] << 16 )                                        \
+        + ( (unsigned)_hj_key[11] << 24 ) );                                     \
+                                                                                 \
+     HASH_JEN_MIX(_hj_i, _hj_j, hashv);                                          \
+                                                                                 \
+     _hj_key += 12;                                                              \
+     _hj_k -= 12U;                                                               \
+  }                                                                              \
+  hashv += (unsigned)(keylen);                                                   \
+  switch ( _hj_k ) {                                                             \
+     case 11: hashv += ( (unsigned)_hj_key[10] << 24 ); /* FALLTHROUGH */        \
+     case 10: hashv += ( (unsigned)_hj_key[9] << 16 );  /* FALLTHROUGH */        \
+     case 9:  hashv += ( (unsigned)_hj_key[8] << 8 );   /* FALLTHROUGH */        \
+     case 8:  _hj_j += ( (unsigned)_hj_key[7] << 24 );  /* FALLTHROUGH */        \
+     case 7:  _hj_j += ( (unsigned)_hj_key[6] << 16 );  /* FALLTHROUGH */        \
+     case 6:  _hj_j += ( (unsigned)_hj_key[5] << 8 );   /* FALLTHROUGH */        \
+     case 5:  _hj_j += _hj_key[4];                      /* FALLTHROUGH */        \
+     case 4:  _hj_i += ( (unsigned)_hj_key[3] << 24 );  /* FALLTHROUGH */        \
+     case 3:  _hj_i += ( (unsigned)_hj_key[2] << 16 );  /* FALLTHROUGH */        \
+     case 2:  _hj_i += ( (unsigned)_hj_key[1] << 8 );   /* FALLTHROUGH */        \
+     case 1:  _hj_i += _hj_key[0];                                               \
+  }                                                                              \
+  HASH_JEN_MIX(_hj_i, _hj_j, hashv);                                             \
+} while (0)
+
+/* The Paul Hsieh hash function */
+#undef get16bits
+#if (defined(__GNUC__) && defined(__i386__)) || defined(__WATCOMC__)             \
+  || defined(_MSC_VER) || defined (__BORLANDC__) || defined (__TURBOC__)
+#define get16bits(d) (*((const uint16_t *) (d)))
+#endif
+
+#if !defined (get16bits)
+#define get16bits(d) ((((uint32_t)(((const uint8_t *)(d))[1])) << 8)             \
+                       +(uint32_t)(((const uint8_t *)(d))[0]) )
+#endif
+#define HASH_SFH(key,keylen,hashv)                                               \
+do {                                                                             \
+  unsigned const char *_sfh_key=(unsigned const char*)(key);                     \
+  uint32_t _sfh_tmp, _sfh_len = (uint32_t)keylen;                                \
+                                                                                 \
+  unsigned _sfh_rem = _sfh_len & 3U;                                             \
+  _sfh_len >>= 2;                                                                \
+  hashv = 0xcafebabeu;                                                           \
+                                                                                 \
+  /* Main loop */                                                                \
+  for (;_sfh_len > 0U; _sfh_len--) {                                             \
+    hashv    += get16bits (_sfh_key);                                            \
+    _sfh_tmp  = ((uint32_t)(get16bits (_sfh_key+2)) << 11) ^ hashv;              \
+    hashv     = (hashv << 16) ^ _sfh_tmp;                                        \
+    _sfh_key += 2U*sizeof (uint16_t);                                            \
+    hashv    += hashv >> 11;                                                     \
+  }                                                                              \
+                                                                                 \
+  /* Handle end cases */                                                         \
+  switch (_sfh_rem) {                                                            \
+    case 3: hashv += get16bits (_sfh_key);                                       \
+            hashv ^= hashv << 16;                                                \
+            hashv ^= (uint32_t)(_sfh_key[sizeof (uint16_t)]) << 18;              \
+            hashv += hashv >> 11;                                                \
+            break;                                                               \
+    case 2: hashv += get16bits (_sfh_key);                                       \
+            hashv ^= hashv << 11;                                                \
+            hashv += hashv >> 17;                                                \
+            break;                                                               \
+    case 1: hashv += *_sfh_key;                                                  \
+            hashv ^= hashv << 10;                                                \
+            hashv += hashv >> 1;                                                 \
+  }                                                                              \
+                                                                                 \
+    /* Force "avalanching" of final 127 bits */                                  \
+    hashv ^= hashv << 3;                                                         \
+    hashv += hashv >> 5;                                                         \
+    hashv ^= hashv << 4;                                                         \
+    hashv += hashv >> 17;                                                        \
+    hashv ^= hashv << 25;                                                        \
+    hashv += hashv >> 6;                                                         \
+} while (0)
+
+#ifdef HASH_USING_NO_STRICT_ALIASING
+/* The MurmurHash exploits some CPU's (x86,x86_64) tolerance for unaligned reads.
+ * For other types of CPU's (e.g. Sparc) an unaligned read causes a bus error.
+ * MurmurHash uses the faster approach only on CPU's where we know it's safe.
+ *
+ * Note the preprocessor built-in defines can be emitted using:
+ *
+ *   gcc -m64 -dM -E - < /dev/null                  (on gcc)
+ *   cc -## a.c (where a.c is a simple test file)   (Sun Studio)
+ */
+#if (defined(__i386__) || defined(__x86_64__)  || defined(_M_IX86))
+#define MUR_GETBLOCK(p,i) p[i]
+#else /* non intel */
+#define MUR_PLUS0_ALIGNED(p) (((unsigned long)p & 3UL) == 0UL)
+#define MUR_PLUS1_ALIGNED(p) (((unsigned long)p & 3UL) == 1UL)
+#define MUR_PLUS2_ALIGNED(p) (((unsigned long)p & 3UL) == 2UL)
+#define MUR_PLUS3_ALIGNED(p) (((unsigned long)p & 3UL) == 3UL)
+#define WP(p) ((uint32_t*)((unsigned long)(p) & ~3UL))
+#if (defined(__BIG_ENDIAN__) || defined(SPARC) || defined(__ppc__) || defined(__ppc64__))
+#define MUR_THREE_ONE(p) ((((*WP(p))&0x00ffffff) << 8) | (((*(WP(p)+1))&0xff000000) >> 24))
+#define MUR_TWO_TWO(p)   ((((*WP(p))&0x0000ffff) <<16) | (((*(WP(p)+1))&0xffff0000) >> 16))
+#define MUR_ONE_THREE(p) ((((*WP(p))&0x000000ff) <<24) | (((*(WP(p)+1))&0xffffff00) >>  8))
+#else /* assume little endian non-intel */
+#define MUR_THREE_ONE(p) ((((*WP(p))&0xffffff00) >> 8) | (((*(WP(p)+1))&0x000000ff) << 24))
+#define MUR_TWO_TWO(p)   ((((*WP(p))&0xffff0000) >>16) | (((*(WP(p)+1))&0x0000ffff) << 16))
+#define MUR_ONE_THREE(p) ((((*WP(p))&0xff000000) >>24) | (((*(WP(p)+1))&0x00ffffff) <<  8))
+#endif
+#define MUR_GETBLOCK(p,i) (MUR_PLUS0_ALIGNED(p) ? ((p)[i]) :           \
+                            (MUR_PLUS1_ALIGNED(p) ? MUR_THREE_ONE(p) : \
+                             (MUR_PLUS2_ALIGNED(p) ? MUR_TWO_TWO(p) :  \
+                                                      MUR_ONE_THREE(p))))
+#endif
+#define MUR_ROTL32(x,r) (((x) << (r)) | ((x) >> (32 - (r))))
+#define MUR_FMIX(_h) \
+do {                 \
+  _h ^= _h >> 16;    \
+  _h *= 0x85ebca6bu; \
+  _h ^= _h >> 13;    \
+  _h *= 0xc2b2ae35u; \
+  _h ^= _h >> 16;    \
+} while (0)
+
+#define HASH_MUR(key,keylen,hashv)                                     \
+do {                                                                   \
+  const uint8_t *_mur_data = (const uint8_t*)(key);                    \
+  const int _mur_nblocks = (int)(keylen) / 4;                          \
+  uint32_t _mur_h1 = 0xf88D5353u;                                      \
+  uint32_t _mur_c1 = 0xcc9e2d51u;                                      \
+  uint32_t _mur_c2 = 0x1b873593u;                                      \
+  uint32_t _mur_k1 = 0;                                                \
+  const uint8_t *_mur_tail;                                            \
+  const uint32_t *_mur_blocks = (const uint32_t*)(_mur_data+(_mur_nblocks*4)); \
+  int _mur_i;                                                          \
+  for(_mur_i = -_mur_nblocks; _mur_i!=0; _mur_i++) {                   \
+    _mur_k1 = MUR_GETBLOCK(_mur_blocks,_mur_i);                        \
+    _mur_k1 *= _mur_c1;                                                \
+    _mur_k1 = MUR_ROTL32(_mur_k1,15);                                  \
+    _mur_k1 *= _mur_c2;                                                \
+                                                                       \
+    _mur_h1 ^= _mur_k1;                                                \
+    _mur_h1 = MUR_ROTL32(_mur_h1,13);                                  \
+    _mur_h1 = (_mur_h1*5U) + 0xe6546b64u;                              \
+  }                                                                    \
+  _mur_tail = (const uint8_t*)(_mur_data + (_mur_nblocks*4));          \
+  _mur_k1=0;                                                           \
+  switch((keylen) & 3U) {                                              \
+    case 3: _mur_k1 ^= (uint32_t)_mur_tail[2] << 16; /* FALLTHROUGH */ \
+    case 2: _mur_k1 ^= (uint32_t)_mur_tail[1] << 8;  /* FALLTHROUGH */ \
+    case 1: _mur_k1 ^= (uint32_t)_mur_tail[0];                         \
+    _mur_k1 *= _mur_c1;                                                \
+    _mur_k1 = MUR_ROTL32(_mur_k1,15);                                  \
+    _mur_k1 *= _mur_c2;                                                \
+    _mur_h1 ^= _mur_k1;                                                \
+  }                                                                    \
+  _mur_h1 ^= (uint32_t)(keylen);                                       \
+  MUR_FMIX(_mur_h1);                                                   \
+  hashv = _mur_h1;                                                     \
+} while (0)
+#endif  /* HASH_USING_NO_STRICT_ALIASING */
+
+/* iterate over items in a known bucket to find desired item */
+#define HASH_FIND_IN_BKT(tbl,hh,head,keyptr,keylen_in,hashval,out)               \
+do {                                                                             \
+  if ((head).hh_head != NULL) {                                                  \
+    DECLTYPE_ASSIGN(out, ELMT_FROM_HH(tbl, (head).hh_head));                     \
+  } else {                                                                       \
+    (out) = NULL;                                                                \
+  }                                                                              \
+  while ((out) != NULL) {                                                        \
+    if ((out)->hh.hashv == (hashval) && (out)->hh.keylen == (keylen_in)) {       \
+      if (uthash_memcmp((out)->hh.key, keyptr, keylen_in) == 0) {                \
+        break;                                                                   \
+      }                                                                          \
+    }                                                                            \
+    if ((out)->hh.hh_next != NULL) {                                             \
+      DECLTYPE_ASSIGN(out, ELMT_FROM_HH(tbl, (out)->hh.hh_next));                \
+    } else {                                                                     \
+      (out) = NULL;                                                              \
+    }                                                                            \
+  }                                                                              \
+} while (0)
+
+/* add an item to a bucket  */
+#define HASH_ADD_TO_BKT(head,addhh)                                              \
+do {                                                                             \
+ head.count++;                                                                   \
+ (addhh)->hh_next = head.hh_head;                                                \
+ (addhh)->hh_prev = NULL;                                                        \
+ if (head.hh_head != NULL) { (head).hh_head->hh_prev = (addhh); }                \
+ (head).hh_head=addhh;                                                           \
+ if ((head.count >= ((head.expand_mult+1U) * HASH_BKT_CAPACITY_THRESH))          \
+     && ((addhh)->tbl->noexpand != 1U)) {                                        \
+       HASH_EXPAND_BUCKETS((addhh)->tbl);                                        \
+ }                                                                               \
+} while (0)
+
+/* remove an item from a given bucket */
+#define HASH_DEL_IN_BKT(hh,head,hh_del)                                          \
+    (head).count--;                                                              \
+    if ((head).hh_head == hh_del) {                                              \
+      (head).hh_head = hh_del->hh_next;                                          \
+    }                                                                            \
+    if (hh_del->hh_prev) {                                                       \
+        hh_del->hh_prev->hh_next = hh_del->hh_next;                              \
+    }                                                                            \
+    if (hh_del->hh_next) {                                                       \
+        hh_del->hh_next->hh_prev = hh_del->hh_prev;                              \
+    }
+
+/* Bucket expansion has the effect of doubling the number of buckets
+ * and redistributing the items into the new buckets. Ideally the
+ * items will distribute more or less evenly into the new buckets
+ * (the extent to which this is true is a measure of the quality of
+ * the hash function as it applies to the key domain).
+ *
+ * With the items distributed into more buckets, the chain length
+ * (item count) in each bucket is reduced. Thus by expanding buckets
+ * the hash keeps a bound on the chain length. This bounded chain
+ * length is the essence of how a hash provides constant time lookup.
+ *
+ * The calculation of tbl->ideal_chain_maxlen below deserves some
+ * explanation. First, keep in mind that we're calculating the ideal
+ * maximum chain length based on the *new* (doubled) bucket count.
+ * In fractions this is just n/b (n=number of items,b=new num buckets).
+ * Since the ideal chain length is an integer, we want to calculate
+ * ceil(n/b). We don't depend on floating point arithmetic in this
+ * hash, so to calculate ceil(n/b) with integers we could write
+ *
+ *      ceil(n/b) = (n/b) + ((n%b)?1:0)
+ *
+ * and in fact a previous version of this hash did just that.
+ * But now we have improved things a bit by recognizing that b is
+ * always a power of two. We keep its base 2 log handy (call it lb),
+ * so now we can write this with a bit shift and logical AND:
+ *
+ *      ceil(n/b) = (n>>lb) + ( (n & (b-1)) ? 1:0)
+ *
+ */
+#define HASH_EXPAND_BUCKETS(tbl)                                                 \
+do {                                                                             \
+    unsigned _he_bkt;                                                            \
+    unsigned _he_bkt_i;                                                          \
+    struct UT_hash_handle *_he_thh, *_he_hh_nxt;                                 \
+    UT_hash_bucket *_he_new_buckets, *_he_newbkt;                                \
+    _he_new_buckets = (UT_hash_bucket*)uthash_malloc(                            \
+             2UL * tbl->num_buckets * sizeof(struct UT_hash_bucket));            \
+    if (!_he_new_buckets) { uthash_fatal( "out of memory"); }                    \
+    memset(_he_new_buckets, 0,                                                   \
+            2UL * tbl->num_buckets * sizeof(struct UT_hash_bucket));             \
+    tbl->ideal_chain_maxlen =                                                    \
+       (tbl->num_items >> (tbl->log2_num_buckets+1U)) +                          \
+       (((tbl->num_items & ((tbl->num_buckets*2U)-1U)) != 0U) ? 1U : 0U);        \
+    tbl->nonideal_items = 0;                                                     \
+    for(_he_bkt_i = 0; _he_bkt_i < tbl->num_buckets; _he_bkt_i++)                \
+    {                                                                            \
+        _he_thh = tbl->buckets[ _he_bkt_i ].hh_head;                             \
+        while (_he_thh != NULL) {                                                \
+           _he_hh_nxt = _he_thh->hh_next;                                        \
+           HASH_TO_BKT( _he_thh->hashv, tbl->num_buckets*2U, _he_bkt);           \
+           _he_newbkt = &(_he_new_buckets[ _he_bkt ]);                           \
+           if (++(_he_newbkt->count) > tbl->ideal_chain_maxlen) {                \
+             tbl->nonideal_items++;                                              \
+             _he_newbkt->expand_mult = _he_newbkt->count /                       \
+                                        tbl->ideal_chain_maxlen;                 \
+           }                                                                     \
+           _he_thh->hh_prev = NULL;                                              \
+           _he_thh->hh_next = _he_newbkt->hh_head;                               \
+           if (_he_newbkt->hh_head != NULL) { _he_newbkt->hh_head->hh_prev =     \
+                _he_thh; }                                                       \
+           _he_newbkt->hh_head = _he_thh;                                        \
+           _he_thh = _he_hh_nxt;                                                 \
+        }                                                                        \
+    }                                                                            \
+    uthash_free( tbl->buckets, tbl->num_buckets*sizeof(struct UT_hash_bucket) ); \
+    tbl->num_buckets *= 2U;                                                      \
+    tbl->log2_num_buckets++;                                                     \
+    tbl->buckets = _he_new_buckets;                                              \
+    tbl->ineff_expands = (tbl->nonideal_items > (tbl->num_items >> 1)) ?         \
+        (tbl->ineff_expands+1U) : 0U;                                            \
+    if (tbl->ineff_expands > 1U) {                                               \
+        tbl->noexpand=1;                                                         \
+        uthash_noexpand_fyi(tbl);                                                \
+    }                                                                            \
+    uthash_expand_fyi(tbl);                                                      \
+} while (0)
+
+
+/* This is an adaptation of Simon Tatham's O(n log(n)) mergesort */
+/* Note that HASH_SORT assumes the hash handle name to be hh.
+ * HASH_SRT was added to allow the hash handle name to be passed in. */
+#define HASH_SORT(head,cmpfcn) HASH_SRT(hh,head,cmpfcn)
+#define HASH_SRT(hh,head,cmpfcn)                                                 \
+do {                                                                             \
+  unsigned _hs_i;                                                                \
+  unsigned _hs_looping,_hs_nmerges,_hs_insize,_hs_psize,_hs_qsize;               \
+  struct UT_hash_handle *_hs_p, *_hs_q, *_hs_e, *_hs_list, *_hs_tail;            \
+  if (head != NULL) {                                                            \
+      _hs_insize = 1;                                                            \
+      _hs_looping = 1;                                                           \
+      _hs_list = &((head)->hh);                                                  \
+      while (_hs_looping != 0U) {                                                \
+          _hs_p = _hs_list;                                                      \
+          _hs_list = NULL;                                                       \
+          _hs_tail = NULL;                                                       \
+          _hs_nmerges = 0;                                                       \
+          while (_hs_p != NULL) {                                                \
+              _hs_nmerges++;                                                     \
+              _hs_q = _hs_p;                                                     \
+              _hs_psize = 0;                                                     \
+              for ( _hs_i = 0; _hs_i  < _hs_insize; _hs_i++ ) {                  \
+                  _hs_psize++;                                                   \
+                  _hs_q = (UT_hash_handle*)((_hs_q->next != NULL) ?              \
+                          ((void*)((char*)(_hs_q->next) +                        \
+                          (head)->hh.tbl->hho)) : NULL);                         \
+                  if (! (_hs_q) ) { break; }                                     \
+              }                                                                  \
+              _hs_qsize = _hs_insize;                                            \
+              while ((_hs_psize > 0U) || ((_hs_qsize > 0U) && (_hs_q != NULL))) {\
+                  if (_hs_psize == 0U) {                                         \
+                      _hs_e = _hs_q;                                             \
+                      _hs_q = (UT_hash_handle*)((_hs_q->next != NULL) ?          \
+                              ((void*)((char*)(_hs_q->next) +                    \
+                              (head)->hh.tbl->hho)) : NULL);                     \
+                      _hs_qsize--;                                               \
+                  } else if ( (_hs_qsize == 0U) || (_hs_q == NULL) ) {           \
+                      _hs_e = _hs_p;                                             \
+                      if (_hs_p != NULL){                                        \
+                        _hs_p = (UT_hash_handle*)((_hs_p->next != NULL) ?        \
+                                ((void*)((char*)(_hs_p->next) +                  \
+                                (head)->hh.tbl->hho)) : NULL);                   \
+                       }                                                         \
+                      _hs_psize--;                                               \
+                  } else if ((                                                   \
+                      cmpfcn(DECLTYPE(head)(ELMT_FROM_HH((head)->hh.tbl,_hs_p)), \
+                             DECLTYPE(head)(ELMT_FROM_HH((head)->hh.tbl,_hs_q))) \
+                             ) <= 0) {                                           \
+                      _hs_e = _hs_p;                                             \
+                      if (_hs_p != NULL){                                        \
+                        _hs_p = (UT_hash_handle*)((_hs_p->next != NULL) ?        \
+                               ((void*)((char*)(_hs_p->next) +                   \
+                               (head)->hh.tbl->hho)) : NULL);                    \
+                       }                                                         \
+                      _hs_psize--;                                               \
+                  } else {                                                       \
+                      _hs_e = _hs_q;                                             \
+                      _hs_q = (UT_hash_handle*)((_hs_q->next != NULL) ?          \
+                              ((void*)((char*)(_hs_q->next) +                    \
+                              (head)->hh.tbl->hho)) : NULL);                     \
+                      _hs_qsize--;                                               \
+                  }                                                              \
+                  if ( _hs_tail != NULL ) {                                      \
+                      _hs_tail->next = ((_hs_e != NULL) ?                        \
+                            ELMT_FROM_HH((head)->hh.tbl,_hs_e) : NULL);          \
+                  } else {                                                       \
+                      _hs_list = _hs_e;                                          \
+                  }                                                              \
+                  if (_hs_e != NULL) {                                           \
+                  _hs_e->prev = ((_hs_tail != NULL) ?                            \
+                     ELMT_FROM_HH((head)->hh.tbl,_hs_tail) : NULL);              \
+                  }                                                              \
+                  _hs_tail = _hs_e;                                              \
+              }                                                                  \
+              _hs_p = _hs_q;                                                     \
+          }                                                                      \
+          if (_hs_tail != NULL){                                                 \
+            _hs_tail->next = NULL;                                               \
+          }                                                                      \
+          if ( _hs_nmerges <= 1U ) {                                             \
+              _hs_looping=0;                                                     \
+              (head)->hh.tbl->tail = _hs_tail;                                   \
+              DECLTYPE_ASSIGN(head,ELMT_FROM_HH((head)->hh.tbl, _hs_list));      \
+          }                                                                      \
+          _hs_insize *= 2U;                                                      \
+      }                                                                          \
+      HASH_FSCK(hh,head);                                                        \
+ }                                                                               \
+} while (0)
+
+/* This function selects items from one hash into another hash.
+ * The end result is that the selected items have dual presence
+ * in both hashes. There is no copy of the items made; rather
+ * they are added into the new hash through a secondary hash
+ * hash handle that must be present in the structure. */
+#define HASH_SELECT(hh_dst, dst, hh_src, src, cond)                              \
+do {                                                                             \
+  unsigned _src_bkt, _dst_bkt;                                                   \
+  void *_last_elt=NULL, *_elt;                                                   \
+  UT_hash_handle *_src_hh, *_dst_hh, *_last_elt_hh=NULL;                         \
+  ptrdiff_t _dst_hho = ((char*)(&(dst)->hh_dst) - (char*)(dst));                 \
+  if (src != NULL) {                                                             \
+    for(_src_bkt=0; _src_bkt < (src)->hh_src.tbl->num_buckets; _src_bkt++) {     \
+      for(_src_hh = (src)->hh_src.tbl->buckets[_src_bkt].hh_head;                \
+          _src_hh != NULL;                                                       \
+          _src_hh = _src_hh->hh_next) {                                          \
+          _elt = ELMT_FROM_HH((src)->hh_src.tbl, _src_hh);                       \
+          if (cond(_elt)) {                                                      \
+            _dst_hh = (UT_hash_handle*)(((char*)_elt) + _dst_hho);               \
+            _dst_hh->key = _src_hh->key;                                         \
+            _dst_hh->keylen = _src_hh->keylen;                                   \
+            _dst_hh->hashv = _src_hh->hashv;                                     \
+            _dst_hh->prev = _last_elt;                                           \
+            _dst_hh->next = NULL;                                                \
+            if (_last_elt_hh != NULL) { _last_elt_hh->next = _elt; }             \
+            if (dst == NULL) {                                                   \
+              DECLTYPE_ASSIGN(dst,_elt);                                         \
+              HASH_MAKE_TABLE(hh_dst,dst);                                       \
+            } else {                                                             \
+              _dst_hh->tbl = (dst)->hh_dst.tbl;                                  \
+            }                                                                    \
+            HASH_TO_BKT(_dst_hh->hashv, _dst_hh->tbl->num_buckets, _dst_bkt);    \
+            HASH_ADD_TO_BKT(_dst_hh->tbl->buckets[_dst_bkt],_dst_hh);            \
+            (dst)->hh_dst.tbl->num_items++;                                      \
+            _last_elt = _elt;                                                    \
+            _last_elt_hh = _dst_hh;                                              \
+          }                                                                      \
+      }                                                                          \
+    }                                                                            \
+  }                                                                              \
+  HASH_FSCK(hh_dst,dst);                                                         \
+} while (0)
+
+#define HASH_CLEAR(hh,head)                                                      \
+do {                                                                             \
+  if (head != NULL) {                                                            \
+    uthash_free((head)->hh.tbl->buckets,                                         \
+                (head)->hh.tbl->num_buckets*sizeof(struct UT_hash_bucket));      \
+    HASH_BLOOM_FREE((head)->hh.tbl);                                             \
+    uthash_free((head)->hh.tbl, sizeof(UT_hash_table));                          \
+    (head)=NULL;                                                                 \
+  }                                                                              \
+} while (0)
+
+#define HASH_OVERHEAD(hh,head)                                                   \
+ ((head != NULL) ? (                                                             \
+ (size_t)(((head)->hh.tbl->num_items   * sizeof(UT_hash_handle))   +             \
+          ((head)->hh.tbl->num_buckets * sizeof(UT_hash_bucket))   +             \
+           sizeof(UT_hash_table)                                   +             \
+           (HASH_BLOOM_BYTELEN))) : 0U)
+
+#ifdef NO_DECLTYPE
+#define HASH_ITER(hh,head,el,tmp)                                                \
+for(((el)=(head)), ((*(char**)(&(tmp)))=(char*)((head!=NULL)?(head)->hh.next:NULL)); \
+  (el) != NULL; ((el)=(tmp)), ((*(char**)(&(tmp)))=(char*)((tmp!=NULL)?(tmp)->hh.next:NULL)))
+#else
+#define HASH_ITER(hh,head,el,tmp)                                                \
+for(((el)=(head)), ((tmp)=DECLTYPE(el)((head!=NULL)?(head)->hh.next:NULL));      \
+  (el) != NULL; ((el)=(tmp)), ((tmp)=DECLTYPE(el)((tmp!=NULL)?(tmp)->hh.next:NULL)))
+#endif
+
+/* obtain a count of items in the hash */
+#define HASH_COUNT(head) HASH_CNT(hh,head)
+#define HASH_CNT(hh,head) ((head != NULL)?((head)->hh.tbl->num_items):0U)
+
+typedef struct UT_hash_bucket {
+   struct UT_hash_handle *hh_head;
+   unsigned count;
+
+   /* expand_mult is normally set to 0. In this situation, the max chain length
+    * threshold is enforced at its default value, HASH_BKT_CAPACITY_THRESH. (If
+    * the bucket's chain exceeds this length, bucket expansion is triggered).
+    * However, setting expand_mult to a non-zero value delays bucket expansion
+    * (that would be triggered by additions to this particular bucket)
+    * until its chain length reaches a *multiple* of HASH_BKT_CAPACITY_THRESH.
+    * (The multiplier is simply expand_mult+1). The whole idea of this
+    * multiplier is to reduce bucket expansions, since they are expensive, in
+    * situations where we know that a particular bucket tends to be overused.
+    * It is better to let its chain length grow to a longer yet-still-bounded
+    * value, than to do an O(n) bucket expansion too often.
+    */
+   unsigned expand_mult;
+
+} UT_hash_bucket;
+
+/* random signature used only to find hash tables in external analysis */
+#define HASH_SIGNATURE 0xa0111fe1u
+#define HASH_BLOOM_SIGNATURE 0xb12220f2u
+
+typedef struct UT_hash_table {
+   UT_hash_bucket *buckets;
+   unsigned num_buckets, log2_num_buckets;
+   unsigned num_items;
+   struct UT_hash_handle *tail; /* tail hh in app order, for fast append    */
+   ptrdiff_t hho; /* hash handle offset (byte pos of hash handle in element */
+
+   /* in an ideal situation (all buckets used equally), no bucket would have
+    * more than ceil(#items/#buckets) items. that's the ideal chain length. */
+   unsigned ideal_chain_maxlen;
+
+   /* nonideal_items is the number of items in the hash whose chain position
+    * exceeds the ideal chain maxlen. these items pay the penalty for an uneven
+    * hash distribution; reaching them in a chain traversal takes >ideal steps */
+   unsigned nonideal_items;
+
+   /* ineffective expands occur when a bucket doubling was performed, but
+    * afterward, more than half the items in the hash had nonideal chain
+    * positions. If this happens on two consecutive expansions we inhibit any
+    * further expansion, as it's not helping; this happens when the hash
+    * function isn't a good fit for the key domain. When expansion is inhibited
+    * the hash will still work, albeit no longer in constant time. */
+   unsigned ineff_expands, noexpand;
+
+   uint32_t signature; /* used only to find hash tables in external analysis */
+#ifdef HASH_BLOOM
+   uint32_t bloom_sig; /* used only to test bloom exists in external analysis */
+   uint8_t *bloom_bv;
+   uint8_t bloom_nbits;
+#endif
+
+} UT_hash_table;
+
+typedef struct UT_hash_handle {
+   struct UT_hash_table *tbl;
+   void *prev;                       /* prev element in app order      */
+   void *next;                       /* next element in app order      */
+   struct UT_hash_handle *hh_prev;   /* previous hh in bucket order    */
+   struct UT_hash_handle *hh_next;   /* next hh in bucket order        */
+   void *key;                        /* ptr to enclosing struct's key  */
+   unsigned keylen;                  /* enclosing struct's key len     */
+   unsigned hashv;                   /* result of hash-fcn(key)        */
+} UT_hash_handle;
+
+#endif /* UTHASH_H */

Added: trunk/postgis/vector_tile.LICENSE
===================================================================
--- trunk/postgis/vector_tile.LICENSE	                        (rev 0)
+++ trunk/postgis/vector_tile.LICENSE	2017-02-06 16:33:23 UTC (rev 15304)
@@ -0,0 +1,48 @@
+THE WORK (AS DEFINED BELOW) IS PROVIDED UNDER THE TERMS OF THIS CREATIVE COMMONS PUBLIC LICENSE ("CCPL" OR "LICENSE"). THE WORK IS PROTECTED BY COPYRIGHT AND/OR OTHER APPLICABLE LAW. ANY USE OF THE WORK OTHER THAN AS AUTHORIZED UNDER THIS LICENSE OR COPYRIGHT LAW IS PROHIBITED.
+
+BY EXERCISING ANY RIGHTS TO THE WORK PROVIDED HERE, YOU ACCEPT AND AGREE TO BE BOUND BY THE TERMS OF THIS LICENSE. TO THE EXTENT THIS LICENSE MAY BE CONSIDERED TO BE A CONTRACT, THE LICENSOR GRANTS YOU THE RIGHTS CONTAINED HERE IN CONSIDERATION OF YOUR ACCEPTANCE OF SUCH TERMS AND CONDITIONS.
+
+1. Definitions
+
+"Collective Work" means a work, such as a periodical issue, anthology or encyclopedia, in which the Work in its entirety in unmodified form, along with one or more other contributions, constituting separate and independent works in themselves, are assembled into a collective whole. A work that constitutes a Collective Work will not be considered a Derivative Work (as defined below) for the purposes of this License.
+"Derivative Work" means a work based upon the Work or upon the Work and other pre-existing works, such as a translation, musical arrangement, dramatization, fictionalization, motion picture version, sound recording, art reproduction, abridgment, condensation, or any other form in which the Work may be recast, transformed, or adapted, except that a work that constitutes a Collective Work will not be considered a Derivative Work for the purpose of this License. For the avoidance of doubt, where the Work is a musical composition or sound recording, the synchronization of the Work in timed-relation with a moving image ("synching") will be considered a Derivative Work for the purpose of this License.
+"Licensor" means the individual, individuals, entity or entities that offers the Work under the terms of this License.
+"Original Author" means the individual, individuals, entity or entities who created the Work.
+"Work" means the copyrightable work of authorship offered under the terms of this License.
+"You" means an individual or entity exercising rights under this License who has not previously violated the terms of this License with respect to the Work, or who has received express permission from the Licensor to exercise rights under this License despite a previous violation.
+2. Fair Use Rights. Nothing in this license is intended to reduce, limit, or restrict any rights arising from fair use, first sale or other limitations on the exclusive rights of the copyright owner under copyright law or other applicable laws.
+
+3. License Grant. Subject to the terms and conditions of this License, Licensor hereby grants You a worldwide, royalty-free, non-exclusive, perpetual (for the duration of the applicable copyright) license to exercise the rights in the Work as stated below:
+
+to reproduce the Work, to incorporate the Work into one or more Collective Works, and to reproduce the Work as incorporated in the Collective Works;
+to create and reproduce Derivative Works provided that any such Derivative Work, including any translation in any medium, takes reasonable steps to clearly label, demarcate or otherwise identify that changes were made to the original Work. For example, a translation could be marked "The original work was translated from English to Spanish," or a modification could indicate "The original work has been modified.";;
+to distribute copies or phonorecords of, display publicly, perform publicly, and perform publicly by means of a digital audio transmission the Work including as incorporated in Collective Works;
+to distribute copies or phonorecords of, display publicly, perform publicly, and perform publicly by means of a digital audio transmission Derivative Works.
+For the avoidance of doubt, where the Work is a musical composition:
+
+Performance Royalties Under Blanket Licenses. Licensor waives the exclusive right to collect, whether individually or, in the event that Licensor is a member of a performance rights society (e.g. ASCAP, BMI, SESAC), via that society, royalties for the public performance or public digital performance (e.g. webcast) of the Work.
+Mechanical Rights and Statutory Royalties. Licensor waives the exclusive right to collect, whether individually or via a music rights agency or designated agent (e.g. Harry Fox Agency), royalties for any phonorecord You create from the Work ("cover version") and distribute, subject to the compulsory license created by 17 USC Section 115 of the US Copyright Act (or the equivalent in other jurisdictions).
+Webcasting Rights and Statutory Royalties. For the avoidance of doubt, where the Work is a sound recording, Licensor waives the exclusive right to collect, whether individually or via a performance-rights society (e.g. SoundExchange), royalties for the public digital performance (e.g. webcast) of the Work, subject to the compulsory license created by 17 USC Section 114 of the US Copyright Act (or the equivalent in other jurisdictions).
+The above rights may be exercised in all media and formats whether now known or hereafter devised. The above rights include the right to make such modifications as are technically necessary to exercise the rights in other media and formats. All rights not expressly granted by Licensor are hereby reserved.
+
+4. Restrictions. The license granted in Section 3 above is expressly made subject to and limited by the following restrictions:
+
+You may distribute, publicly display, publicly perform, or publicly digitally perform the Work only under the terms of this License, and You must include a copy of, or the Uniform Resource Identifier for, this License with every copy or phonorecord of the Work You distribute, publicly display, publicly perform, or publicly digitally perform. You may not offer or impose any terms on the Work that restrict the terms of this License or the ability of a recipient of the Work to exercise the rights granted to that recipient under the terms of the License. You may not sublicense the Work. You must keep intact all notices that refer to this License and to the disclaimer of warranties. When You distribute, publicly display, publicly perform, or publicly digitally perform the Work, You may not impose any technological measures on the Work that restrict the ability of a recipient of the Work from You to exercise the rights granted to that recipient under the terms of the License. This Section
  4(a) applies to the Work as incorporated in a Collective Work, but this does not require the Collective Work apart from the Work itself to be made subject to the terms of this License. If You create a Collective Work, upon notice from any Licensor You must, to the extent practicable, remove from the Collective Work any credit as required by Section 4(b), as requested. If You create a Derivative Work, upon notice from any Licensor You must, to the extent practicable, remove from the Derivative Work any credit as required by Section 4(b), as requested.
+If You distribute, publicly display, publicly perform, or publicly digitally perform the Work (as defined in Section 1 above) or any Derivative Works (as defined in Section 1 above) or Collective Works (as defined in Section 1 above), You must, unless a request has been made pursuant to Section 4(a), keep intact all copyright notices for the Work and provide, reasonable to the medium or means You are utilizing: (i) the name of the Original Author (or pseudonym, if applicable) if supplied, and/or (ii) if the Original Author and/or Licensor designate another party or parties (e.g. a sponsor institute, publishing entity, journal) for attribution ("Attribution Parties") in Licensor's copyright notice, terms of service or by other reasonable means, the name of such party or parties; the title of the Work if supplied; to the extent reasonably practicable, the Uniform Resource Identifier, if any, that Licensor specifies to be associated with the Work, unless such URI does not refer to the 
 copyright notice or licensing information for the Work; and, consistent with Section 3(b) in the case of a Derivative Work, a credit identifying the use of the Work in the Derivative Work (e.g., "French translation of the Work by Original Author," or "Screenplay based on original Work by Original Author"). The credit required by this Section 4(b) may be implemented in any reasonable manner; provided, however, that in the case of a Derivative Work or Collective Work, at a minimum such credit will appear, if a credit for all contributing authors of the Derivative Work or Collective Work appears, then as part of these credits and in a manner at least as prominent as the credits for the other contributing authors. For the avoidance of doubt, You may only use the credit required by this Section for the purpose of attribution in the manner set out above and, by exercising Your rights under this License, You may not implicitly or explicitly assert or imply any connection with, sponsorship 
 or endorsement by the Original Author, Licensor and/or Attribution Parties, as appropriate, of You or Your use of the Work, without the separate, express prior written permission of the Original Author, Licensor and/or Attribution Parties.
+5. Representations, Warranties and Disclaimer
+
+UNLESS OTHERWISE MUTUALLY AGREED TO BY THE PARTIES IN WRITING, LICENSOR OFFERS THE WORK AS-IS AND ONLY TO THE EXTENT OF ANY RIGHTS HELD IN THE LICENSED WORK BY THE LICENSOR. THE LICENSOR MAKES NO REPRESENTATIONS OR WARRANTIES OF ANY KIND CONCERNING THE WORK, EXPRESS, IMPLIED, STATUTORY OR OTHERWISE, INCLUDING, WITHOUT LIMITATION, WARRANTIES OF TITLE, MARKETABILITY, MERCHANTIBILITY, FITNESS FOR A PARTICULAR PURPOSE, NONINFRINGEMENT, OR THE ABSENCE OF LATENT OR OTHER DEFECTS, ACCURACY, OR THE PRESENCE OF ABSENCE OF ERRORS, WHETHER OR NOT DISCOVERABLE. SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OF IMPLIED WARRANTIES, SO SUCH EXCLUSION MAY NOT APPLY TO YOU.
+
+6. Limitation on Liability. EXCEPT TO THE EXTENT REQUIRED BY APPLICABLE LAW, IN NO EVENT WILL LICENSOR BE LIABLE TO YOU ON ANY LEGAL THEORY FOR ANY SPECIAL, INCIDENTAL, CONSEQUENTIAL, PUNITIVE OR EXEMPLARY DAMAGES ARISING OUT OF THIS LICENSE OR THE USE OF THE WORK, EVEN IF LICENSOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
+
+7. Termination
+
+This License and the rights granted hereunder will terminate automatically upon any breach by You of the terms of this License. Individuals or entities who have received Derivative Works (as defined in Section 1 above) or Collective Works (as defined in Section 1 above) from You under this License, however, will not have their licenses terminated provided such individuals or entities remain in full compliance with those licenses. Sections 1, 2, 5, 6, 7, and 8 will survive any termination of this License.
+Subject to the above terms and conditions, the license granted here is perpetual (for the duration of the applicable copyright in the Work). Notwithstanding the above, Licensor reserves the right to release the Work under different license terms or to stop distributing the Work at any time; provided, however that any such election will not serve to withdraw this License (or any other license that has been, or is required to be, granted under the terms of this License), and this License will continue in full force and effect unless terminated as stated above.
+8. Miscellaneous
+
+Each time You distribute or publicly digitally perform the Work (as defined in Section 1 above) or a Collective Work (as defined in Section 1 above), the Licensor offers to the recipient a license to the Work on the same terms and conditions as the license granted to You under this License.
+Each time You distribute or publicly digitally perform a Derivative Work, Licensor offers to the recipient a license to the original Work on the same terms and conditions as the license granted to You under this License.
+If any provision of this License is invalid or unenforceable under applicable law, it shall not affect the validity or enforceability of the remainder of the terms of this License, and without further action by the parties to this agreement, such provision shall be reformed to the minimum extent necessary to make such provision valid and enforceable.
+No term or provision of this License shall be deemed waived and no breach consented to unless such waiver or consent shall be in writing and signed by the party to be charged with such waiver or consent.
+This License constitutes the entire agreement between the parties with respect to the Work licensed here. There are no understandings, agreements or representations with respect to the Work not specified here. Licensor shall not be bound by any additional provisions that may appear in any communication from You. This License may not be modified without the mutual written agreement of the Licensor and You.
\ No newline at end of file

Added: trunk/postgis/vector_tile.proto
===================================================================
--- trunk/postgis/vector_tile.proto	                        (rev 0)
+++ trunk/postgis/vector_tile.proto	2017-02-06 16:33:23 UTC (rev 15304)
@@ -0,0 +1,78 @@
+package vector_tile;
+
+option optimize_for = LITE_RUNTIME;
+
+message Tile {
+
+        // GeomType is described in section 4.3.4 of the specification
+        enum GeomType {
+             UNKNOWN = 0;
+             POINT = 1;
+             LINESTRING = 2;
+             POLYGON = 3;
+        }
+
+        // Variant type encoding
+        // The use of values is described in section 4.1 of the specification
+        message Value {
+                // Exactly one of these values must be present in a valid message
+                optional string string_value = 1;
+                optional float float_value = 2;
+                optional double double_value = 3;
+                optional int64 int_value = 4;
+                optional uint64 uint_value = 5;
+                optional sint64 sint_value = 6;
+                optional bool bool_value = 7;
+
+                extensions 8 to max;
+        }
+
+        // Features are described in section 4.2 of the specification
+        message Feature {
+                optional uint64 id = 1 [ default = 0 ];
+
+                // Tags of this feature are encoded as repeated pairs of
+                // integers.
+                // A detailed description of tags is located in sections
+                // 4.2 and 4.4 of the specification
+                repeated uint32 tags = 2 [ packed = true ];
+
+                // The type of geometry stored in this feature.
+                optional GeomType type = 3 [ default = UNKNOWN ];
+
+                // Contains a stream of commands and parameters (vertices).
+                // A detailed description on geometry encoding is located in 
+                // section 4.3 of the specification.
+                repeated uint32 geometry = 4 [ packed = true ];
+        }
+
+        // Layers are described in section 4.1 of the specification
+        message Layer {
+                // Any compliant implementation must first read the version
+                // number encoded in this message and choose the correct
+                // implementation for this version number before proceeding to
+                // decode other parts of this message.
+                required uint32 version = 15 [ default = 1 ];
+
+                required string name = 1;
+
+                // The actual features in this tile.
+                repeated Feature features = 2;
+
+                // Dictionary encoding for keys
+                repeated string keys = 3;
+
+                // Dictionary encoding for values
+                repeated Value values = 4;
+
+                // Although this is an "optional" field it is required by the specification.
+                // See https://github.com/mapbox/vector-tile-spec/issues/47
+                optional uint32 extent = 5 [ default = 4096 ];
+
+                extensions 16 to max;
+        }
+
+        repeated Layer layers = 3;
+
+        extensions 16 to 8191;
+}

Modified: trunk/postgis_config.h.in
===================================================================
--- trunk/postgis_config.h.in	2017-01-31 05:05:06 UTC (rev 15303)
+++ trunk/postgis_config.h.in	2017-02-06 16:33:23 UTC (rev 15304)
@@ -54,6 +54,9 @@
 /* Define to 1 if you have the `libiconvctl' function. */
 #undef HAVE_LIBICONVCTL
 
+/* Define to 1 if libprotobuf-c is present */
+#undef HAVE_LIBPROTOBUF
+
 /* Define to 1 if libjson is present */
 #undef HAVE_LIBJSON
 

Modified: trunk/regress/Makefile.in
===================================================================
--- trunk/regress/Makefile.in	2017-01-31 05:05:06 UTC (rev 15303)
+++ trunk/regress/Makefile.in	2017-02-06 16:33:23 UTC (rev 15304)
@@ -22,6 +22,7 @@
 POSTGIS_MAJOR_VERSION=@POSTGIS_MAJOR_VERSION@
 POSTGIS_MINOR_VERSION=@POSTGIS_MINOR_VERSION@
 HAVE_JSON=@HAVE_JSON@
+HAVE_PROTOBUF=@HAVE_PROTOBUF@
 HAVE_SFCGAL=@HAVE_SFCGAL@
 HAVE_BRIN=@HAVE_BRIN@
 MINGWBUILD=@MINGWBUILD@
@@ -239,6 +240,13 @@
 		regress_brin_index_geography
 endif
 
+ifeq ($(HAVE_PROTOBUF),yes)
+	# protobuf-c adds:
+	# ST_AsMVT
+	TESTS += \
+		mvt
+endif
+
 ifeq ($(HAVE_SFCGAL),yes)
 	# SFCGAL additionnal backend
 	TESTS += \

Added: trunk/regress/mvt.sql
===================================================================
--- trunk/regress/mvt.sql	                        (rev 0)
+++ trunk/regress/mvt.sql	2017-02-06 16:33:23 UTC (rev 15304)
@@ -0,0 +1,74 @@
+-- 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;
+
+-- 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
+    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;
+
+-- unsupported input
+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 '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

Added: trunk/regress/mvt_expected
===================================================================
--- trunk/regress/mvt_expected	                        (rev 0)
+++ trunk/regress/mvt_expected	2017-02-06 16:33:23 UTC (rev 15304)
@@ -0,0 +1,21 @@
+TG1|Gh4KBHRlc3QSDBICAAAYASIECTLePxoCYzEiAiABeAI=
+TG2|Gh4KBHRlc3QSDBICAAAYASIECTLePxoCYzEiAiABeAI=
+TG3|GiMKBHRlc3QSERICAAAYAiIJCQCAQArQD88PGgJjMSICIAF4Ag==
+TG4|GicKBHRlc3QSFRICAAAYAiINCQCAQBLoB+cH6AfnBxoCYzEiAiABeAI=
+TG5|GjUKBHRlc3QSIxICAAAYAiIbCQL+PxLoB+cH6AfnBwnND84PEugH5wfoB+cHGgJjMSICIAF4Ag==
+TG6|GjMKBHRlc3QSIRICAAAYAyIZCUbsPyIURTsKCSgyFA8JHScaHgkJHhMTDxoCYzEiAiABeAI=
+TG7|Gj0KBHRlc3QSKxICAAAYAyIjCVCwPxonCTIeCRMJJwoqEwoAKCgKHh0xHQkUHhoTCgATFAoaAmMx
+IgIgAXgC
+TG8|Gh4KBHRlc3QSDBICAAAYASIECTLePxoCYzEiAiABeAI=
+TG9|Gh4KBHRlc3QSDBICAAAYASIECTLePxoCYzEiAiABeAI=
+TA1|GiwKBHRlc3QSDhIEAAABARgBIgQJMt4/GgJjMRoCYzIiAiABIgYKBGFiY2R4Ag==
+TA2|GiUKBHRlc3QSDBICAAAYASIECTLePxoCYzEiCRmamZmZmZnxP3gC
+TA3|GhYKBHRlc3QSCBgBIgQJMt4/GgJjMXgC
+TA4|GjAKBHRlc3QSDBICAAAYASIECTLePxIMEgIAARgBIgQJMt4/GgJjMSICIAEiAiACeAI=
+TA5|GiwKBHRlc3QSDhIEAAABARgBIgQJMt4/GgJjMRoCYzIiAiABIgYKBGFiY2R4Ag==
+TU1
+ERROR:  could not determine polymorphic type because input has type "unknown"
+TU2
+ERROR:  pgis_asmvt_transfn: parameter row cannot be other than a rowtype
+TU3
+ERROR:  mvt_agg_transfn: geometry column cannot be null



More information about the postgis-tickets mailing list