diff --git a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/vectortile/AbstractVectorTileSearchAction.java b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/vectortile/AbstractVectorTileSearchAction.java index 6dce260c1f2ae..58c3bab938bac 100644 --- a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/vectortile/AbstractVectorTileSearchAction.java +++ b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/vectortile/AbstractVectorTileSearchAction.java @@ -28,14 +28,22 @@ import org.elasticsearch.rest.RestResponse; import org.elasticsearch.rest.RestStatus; import org.elasticsearch.rest.action.RestResponseListener; +import org.elasticsearch.search.aggregations.AggregationBuilder; import org.elasticsearch.search.aggregations.AggregatorFactories; +import org.elasticsearch.search.aggregations.PipelineAggregationBuilder; import org.elasticsearch.search.aggregations.bucket.geogrid.GeoTileUtils; +import org.elasticsearch.search.aggregations.metrics.AvgAggregationBuilder; +import org.elasticsearch.search.aggregations.metrics.CardinalityAggregationBuilder; +import org.elasticsearch.search.aggregations.metrics.MaxAggregationBuilder; +import org.elasticsearch.search.aggregations.metrics.MinAggregationBuilder; +import org.elasticsearch.search.aggregations.metrics.SumAggregationBuilder; import org.elasticsearch.search.builder.SearchSourceBuilder; import org.elasticsearch.search.fetch.subphase.FieldAndFormat; import java.io.IOException; import java.io.OutputStream; import java.util.ArrayList; +import java.util.Collection; import java.util.List; import java.util.Map; import java.util.function.Supplier; @@ -226,9 +234,31 @@ public AggregatorFactories.Builder getAggBuilder() { } public void setAggBuilder(AggregatorFactories.Builder aggBuilder) { - // TODO: validation + for (AggregationBuilder aggregation : aggBuilder.getAggregatorFactories()) { + final String type = aggregation.getType(); + switch (type) { + case MinAggregationBuilder.NAME: + case MaxAggregationBuilder.NAME: + case AvgAggregationBuilder.NAME: + case SumAggregationBuilder.NAME: + case CardinalityAggregationBuilder.NAME: + break; + default: + // top term and percentile should be supported + throw new IllegalArgumentException("Unsupported aggregation of type [" + type + "]"); + } + } + for (PipelineAggregationBuilder aggregation : aggBuilder.getPipelineAggregatorFactories()) { + // should not have pipeline aggregations + final String type = aggregation.getType(); + throw new IllegalArgumentException("Unsupported pipeline aggregation of type [" + type + "]"); + } this.aggBuilder = aggBuilder; } + + public Collection getAggregations() { + return aggBuilder == null ? emptyList() : aggBuilder.getAggregatorFactories(); + } } protected AbstractVectorTileSearchAction(Supplier emptyRequestProvider) { diff --git a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/vectortile/RestVectorTileAction.java b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/vectortile/RestVectorTileAction.java index 8b0909cd5f5a0..2abb2aa04b7d3 100644 --- a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/vectortile/RestVectorTileAction.java +++ b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/vectortile/RestVectorTileAction.java @@ -9,7 +9,6 @@ import com.wdtinc.mapbox_vector_tile.VectorTile; import com.wdtinc.mapbox_vector_tile.adapt.jts.IUserDataConverter; import com.wdtinc.mapbox_vector_tile.build.MvtLayerProps; -import com.wdtinc.mapbox_vector_tile.encoding.MvtValue; import org.elasticsearch.action.search.SearchRequestBuilder; import org.elasticsearch.action.search.SearchResponse; import org.elasticsearch.action.search.SearchResponseSections; @@ -17,35 +16,29 @@ import org.elasticsearch.common.geo.GeoBoundingBox; import org.elasticsearch.common.geo.GeoPoint; import org.elasticsearch.common.geo.GeometryParser; -import org.elasticsearch.common.util.Maps; -import org.elasticsearch.common.xcontent.XContentHelper; -import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.geometry.Geometry; import org.elasticsearch.geometry.Rectangle; import org.elasticsearch.rest.RestRequest; import org.elasticsearch.search.SearchHit; import org.elasticsearch.search.SearchHits; import org.elasticsearch.search.aggregations.Aggregation; +import org.elasticsearch.search.aggregations.AggregationBuilder; import org.elasticsearch.search.aggregations.Aggregations; import org.elasticsearch.search.aggregations.bucket.geogrid.GeoGridAggregationBuilder; import org.elasticsearch.search.aggregations.bucket.geogrid.GeoTileGridAggregationBuilder; import org.elasticsearch.search.aggregations.bucket.geogrid.GeoTileUtils; import org.elasticsearch.search.aggregations.bucket.geogrid.InternalGeoGridBucket; import org.elasticsearch.search.aggregations.bucket.geogrid.InternalGeoTileGrid; -import org.elasticsearch.search.aggregations.metrics.AvgAggregationBuilder; -import org.elasticsearch.search.aggregations.metrics.CardinalityAggregationBuilder; import org.elasticsearch.search.aggregations.metrics.GeoBoundsAggregationBuilder; import org.elasticsearch.search.aggregations.metrics.InternalGeoBounds; -import org.elasticsearch.search.aggregations.metrics.MaxAggregationBuilder; -import org.elasticsearch.search.aggregations.metrics.MinAggregationBuilder; -import org.elasticsearch.search.aggregations.metrics.NumericMetricsAggregation; -import org.elasticsearch.search.aggregations.metrics.SumAggregationBuilder; +import org.elasticsearch.search.aggregations.pipeline.MaxBucketPipelineAggregationBuilder; +import org.elasticsearch.search.aggregations.pipeline.MinBucketPipelineAggregationBuilder; import org.elasticsearch.search.fetch.subphase.FieldAndFormat; import org.elasticsearch.search.profile.SearchProfileShardResults; import java.io.IOException; +import java.util.Collection; import java.util.List; -import java.util.Map; import java.util.stream.Collectors; import static org.elasticsearch.rest.RestRequest.Method.GET; @@ -59,8 +52,11 @@ public class RestVectorTileAction extends AbstractVectorTileSearchAction 0) { - tileBuilder.addLayers(getHitsLayer(s, request)); + tileBuilder.addLayers(buildHitsLayer(s, request)); } // TODO: should be expose the total number of buckets on InternalGeoTileGrid? if (grid != null && grid.getBuckets().size() > 0) { - tileBuilder.addLayers(getAggsLayer(grid, request, geomBuilder)); + tileBuilder.addLayers(buildAggsLayer(grid, request, geomBuilder)); } - tileBuilder.addLayers(getMetaLayer(meta, bounds, request, geomBuilder)); + tileBuilder.addLayers(buildMetaLayer(meta, bounds, request, geomBuilder)); tileBuilder.build().writeTo(b); }; } @@ -150,6 +146,15 @@ private static SearchRequestBuilder searchBuilder(SearchRequestBuilder searchReq aBuilder.subAggregations(request.getAggBuilder()); } searchRequestBuilder.addAggregation(aBuilder); + searchRequestBuilder.addAggregation(new MaxBucketPipelineAggregationBuilder(COUNT_MAX, GRID_FIELD + "._count")); + searchRequestBuilder.addAggregation(new MinBucketPipelineAggregationBuilder(COUNT_MIN, GRID_FIELD + "._count")); + final Collection aggregations = request.getAggregations(); + for (AggregationBuilder aggregation : aggregations) { + searchRequestBuilder.addAggregation( + new MaxBucketPipelineAggregationBuilder(aggregation.getName() + ".max", GRID_FIELD + ">" + aggregation.getName())); + searchRequestBuilder.addAggregation( + new MinBucketPipelineAggregationBuilder(aggregation.getName() + ".min", GRID_FIELD + ">" + aggregation.getName())); + } } if (request.getExactBounds()) { final GeoBoundsAggregationBuilder boundsBuilder = new GeoBoundsAggregationBuilder(BOUNDS_FIELD).field(request.getField()) @@ -159,7 +164,7 @@ private static SearchRequestBuilder searchBuilder(SearchRequestBuilder searchReq return searchRequestBuilder; } - private VectorTile.Tile.Layer.Builder getHitsLayer(SearchResponse response, Request request) { + private VectorTile.Tile.Layer.Builder buildHitsLayer(SearchResponse response, Request request) { final FeatureFactory featureFactory = new FeatureFactory(request.getZ(), request.getX(), request.getY(), request.getExtent()); final GeometryParser parser = new GeometryParser(true, false, false); final VectorTile.Tile.Layer.Builder hitsLayerBuilder = VectorTileUtils.createLayerBuilder(HITS_LAYER, request.getExtent()); @@ -168,12 +173,12 @@ private VectorTile.Tile.Layer.Builder getHitsLayer(SearchResponse response, Requ final IUserDataConverter tags = (userData, layerProps, featureBuilder) -> { // TODO: It would be great if we can add the centroid information for polygons. That information can be // used to place labels inside those geometries - addPropertyToFeature(featureBuilder, layerProps, ID_TAG, searchHit.getId()); + VectorTileUtils.addPropertyToFeature(featureBuilder, layerProps, ID_TAG, searchHit.getId()); if (fields != null) { for (FieldAndFormat field : fields) { - DocumentField documentField = searchHit.field(field.field); + final DocumentField documentField = searchHit.field(field.field); if (documentField != null) { - addPropertyToFeature(featureBuilder, layerProps, field.field, documentField.getValue()); + VectorTileUtils.addPropertyToFeature(featureBuilder, layerProps, field.field, documentField.getValue()); } } } @@ -182,16 +187,16 @@ private VectorTile.Tile.Layer.Builder getHitsLayer(SearchResponse response, Requ final Geometry geometry = parser.parseGeometry(searchHit.field(request.getField()).getValue()); hitsLayerBuilder.addAllFeatures(featureFactory.getFeatures(geometry, tags)); } - addPropertiesToLayer(hitsLayerBuilder, featureFactory.getLayerProps()); + VectorTileUtils.addPropertiesToLayer(hitsLayerBuilder, featureFactory.getLayerProps()); return hitsLayerBuilder; } - private VectorTile.Tile.Layer.Builder getAggsLayer(InternalGeoTileGrid grid, Request request, VectorTileGeometryBuilder geomBuilder) { + private VectorTile.Tile.Layer.Builder buildAggsLayer(InternalGeoTileGrid grid, Request request, VectorTileGeometryBuilder geomBuilder) + throws IOException{ final VectorTile.Tile.Layer.Builder aggLayerBuilder = VectorTileUtils.createLayerBuilder(AGGS_LAYER, request.getExtent()); final MvtLayerProps layerProps = new MvtLayerProps(); final VectorTile.Tile.Feature.Builder featureBuilder = VectorTile.Tile.Feature.newBuilder(); for (InternalGeoGridBucket bucket : grid.getBuckets()) { - final long count = bucket.getDocCount(); featureBuilder.clear(); // Add geometry if (request.getGridType() == GRID_TYPE.GRID) { @@ -203,40 +208,22 @@ private VectorTile.Tile.Layer.Builder getAggsLayer(InternalGeoTileGrid grid, Req geomBuilder.point(featureBuilder, point.lon(), point.lat()); } // Add count as key value pair - addPropertyToFeature(featureBuilder, layerProps, COUNT_TAG, count); - // Add aggregations results as key value pair + VectorTileUtils.addPropertyToFeature(featureBuilder, layerProps, COUNT_TAG, bucket.getDocCount()); for (Aggregation aggregation : bucket.getAggregations()) { - final String type = aggregation.getType(); - switch (type) { - case MinAggregationBuilder.NAME: - case MaxAggregationBuilder.NAME: - case AvgAggregationBuilder.NAME: - case SumAggregationBuilder.NAME: - case CardinalityAggregationBuilder.NAME: - final NumericMetricsAggregation.SingleValue metric = (NumericMetricsAggregation.SingleValue) aggregation; - addPropertyToFeature(featureBuilder, layerProps, "aggs." + aggregation.getName(), metric.value()); - break; - default: - // top term and percentile should be supported - throw new IllegalArgumentException("Unknown feature type [" + type + "]"); - } + VectorTileUtils.addToXContentToFeature(featureBuilder, layerProps, aggregation); } aggLayerBuilder.addFeatures(featureBuilder); } - addPropertiesToLayer(aggLayerBuilder, layerProps); + VectorTileUtils.addPropertiesToLayer(aggLayerBuilder, layerProps); return aggLayerBuilder; } - private VectorTile.Tile.Layer.Builder getMetaLayer( + private VectorTile.Tile.Layer.Builder buildMetaLayer( SearchResponse response, InternalGeoBounds bounds, Request request, VectorTileGeometryBuilder geomBuilder ) throws IOException { - Map responseMap = Maps.flatten( - XContentHelper.convertToMap(XContentHelper.toXContent(response, XContentType.CBOR, false), true, XContentType.CBOR).v2(), - true - ); final VectorTile.Tile.Layer.Builder metaLayerBuilder = VectorTileUtils.createLayerBuilder(META_LAYER, request.getExtent()); final MvtLayerProps layerProps = new MvtLayerProps(); final VectorTile.Tile.Feature.Builder featureBuilder = VectorTile.Tile.Feature.newBuilder(); @@ -248,31 +235,12 @@ private VectorTile.Tile.Layer.Builder getMetaLayer( final Rectangle tile = request.getBoundingBox(); geomBuilder.box(featureBuilder, tile.getMinLon(), tile.getMaxLon(), tile.getMinLat(), tile.getMaxLat()); } - for (Map.Entry entry : responseMap.entrySet()) { - if (entry.getValue() != null) { - addPropertyToFeature(featureBuilder, layerProps, entry.getKey(), entry.getValue()); - } - } + VectorTileUtils.addToXContentToFeature(featureBuilder, layerProps, response); metaLayerBuilder.addFeatures(featureBuilder); - addPropertiesToLayer(metaLayerBuilder, layerProps); + VectorTileUtils.addPropertiesToLayer(metaLayerBuilder, layerProps); return metaLayerBuilder; } - private void addPropertyToFeature(VectorTile.Tile.Feature.Builder feature, MvtLayerProps layerProps, String key, Object value) { - feature.addTags(layerProps.addKey(key)); - feature.addTags(layerProps.addValue(value)); - } - - private void addPropertiesToLayer(VectorTile.Tile.Layer.Builder layer, MvtLayerProps layerProps) { - // Add keys - layer.addAllKeys(layerProps.getKeys()); - // Add values - final Iterable values = layerProps.getVals(); - for (Object value : values) { - layer.addValues(MvtValue.toValue(value)); - } - } - @Override public String getName() { return "vectortile_action"; diff --git a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/vectortile/VectorTileGeometryBuilder.java b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/vectortile/VectorTileGeometryBuilder.java index 5d31a7480da5a..62b356b276178 100644 --- a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/vectortile/VectorTileGeometryBuilder.java +++ b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/vectortile/VectorTileGeometryBuilder.java @@ -57,10 +57,10 @@ public void box(VectorTile.Tile.Feature.Builder featureBuilder, double minLon, d } private int lat(double lat) { - return (int) (pointYScale * (VectorTileUtils.latToSphericalMercator(lat) - rectangle.getMinY())) + extent; + return (int) Math.round(pointYScale * (VectorTileUtils.latToSphericalMercator(lat) - rectangle.getMinY())) + extent; } private int lon(double lon) { - return (int) (pointXScale * (VectorTileUtils.lonToSphericalMercator(lon) - rectangle.getMinX())); + return (int) Math.round(pointXScale * (VectorTileUtils.lonToSphericalMercator(lon) - rectangle.getMinX())); } } diff --git a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/vectortile/VectorTileUtils.java b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/vectortile/VectorTileUtils.java index 361328b2f48d9..7cad6cf043a9c 100644 --- a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/vectortile/VectorTileUtils.java +++ b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/vectortile/VectorTileUtils.java @@ -8,14 +8,26 @@ package org.elasticsearch.xpack.spatial.vectortile; import com.wdtinc.mapbox_vector_tile.VectorTile; +import com.wdtinc.mapbox_vector_tile.build.MvtLayerProps; +import com.wdtinc.mapbox_vector_tile.encoding.MvtValue; +import org.elasticsearch.common.util.Maps; +import org.elasticsearch.common.xcontent.ToXContent; +import org.elasticsearch.common.xcontent.XContentHelper; +import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.geometry.Rectangle; import org.locationtech.jts.geom.Envelope; +import java.io.IOException; +import java.util.Map; + /** * Utility methods For vector tiles. Transforms WGS84 into spherical mercator. */ public class VectorTileUtils { + /** + * Creates a vector layer builder with the provided name and extent. + */ public static VectorTile.Tile.Layer.Builder createLayerBuilder(String layerName, int extent) { final VectorTile.Tile.Layer.Builder layerBuilder = VectorTile.Tile.Layer.newBuilder(); layerBuilder.setVersion(2); @@ -24,6 +36,40 @@ public static VectorTile.Tile.Layer.Builder createLayerBuilder(String layerName, return layerBuilder; } + /** + * Adds the flatten elements of toXContent into the feature as tags. + */ + public static void addToXContentToFeature(VectorTile.Tile.Feature.Builder feature, MvtLayerProps layerProps, ToXContent toXContent) + throws IOException { + final Map map = Maps.flatten( + XContentHelper.convertToMap(XContentHelper.toXContent(toXContent, XContentType.CBOR, false), true, XContentType.CBOR).v2(), + true + ); + for (Map.Entry entry : map.entrySet()) { + if (entry.getValue() != null) { + addPropertyToFeature(feature, layerProps, entry.getKey(), entry.getValue()); + } + } + } + + /** + * Adds the provided key / value pair into the feature as tags. + */ + public static void addPropertyToFeature(VectorTile.Tile.Feature.Builder feature, MvtLayerProps layerProps, String key, Object value) { + feature.addTags(layerProps.addKey(key)); + feature.addTags(layerProps.addValue(value)); + } + + public static void addPropertiesToLayer(VectorTile.Tile.Layer.Builder layer, MvtLayerProps layerProps) { + // Add keys + layer.addAllKeys(layerProps.getKeys()); + // Add values + final Iterable values = layerProps.getVals(); + for (Object value : values) { + layer.addValues(MvtValue.toValue(value)); + } + } + /** * Gets the JTS envelope for z/x/y/ tile in spherical mercator projection. */ diff --git a/x-pack/plugin/spatial/src/yamlRestTest/java/org/elasticsearch/xpack/spatial/VectorTileRestIT.java b/x-pack/plugin/spatial/src/yamlRestTest/java/org/elasticsearch/xpack/spatial/VectorTileRestIT.java index 84e81a620cb92..48fd9887409f0 100644 --- a/x-pack/plugin/spatial/src/yamlRestTest/java/org/elasticsearch/xpack/spatial/VectorTileRestIT.java +++ b/x-pack/plugin/spatial/src/yamlRestTest/java/org/elasticsearch/xpack/spatial/VectorTileRestIT.java @@ -132,8 +132,7 @@ public void testBasicGet() throws Exception { assertThat(tile.getLayersCount(), Matchers.equalTo(3)); assertLayer(tile, HITS_LAYER, 4096, 33, 1); assertLayer(tile, AGGS_LAYER, 4096, 1, 1); - assertLayer(tile, META_LAYER, 4096, 1, 11); - + assertLayer(tile, META_LAYER, 4096, 1, 15); } public void testEmpty() throws Exception { @@ -141,7 +140,7 @@ public void testEmpty() throws Exception { final Request mvtRequest = new Request(HttpGet.METHOD_NAME, INDEX_POINTS + "/_mvt/location/" + z + "/" + x + "/" + newY); final VectorTile.Tile tile = execute(mvtRequest); assertThat(tile.getLayersCount(), Matchers.equalTo(1)); - assertLayer(tile, META_LAYER, 4096, 1, 10); + assertLayer(tile, META_LAYER, 4096, 1, 12); } public void testGridPrecision() throws Exception { @@ -152,7 +151,7 @@ public void testGridPrecision() throws Exception { assertThat(tile.getLayersCount(), Matchers.equalTo(3)); assertLayer(tile, HITS_LAYER, 4096, 33, 1); assertLayer(tile, AGGS_LAYER, 4096, 1, 1); - assertLayer(tile, META_LAYER, 4096, 1, 11); + assertLayer(tile, META_LAYER, 4096, 1, 15); } { final Request mvtRequest = new Request(HttpGet.METHOD_NAME, INDEX_POINTS + "/_mvt/location/" + z + "/" + x + "/" + y); @@ -170,7 +169,7 @@ public void testGridType() throws Exception { assertThat(tile.getLayersCount(), Matchers.equalTo(3)); assertLayer(tile, HITS_LAYER, 4096, 33, 1); assertLayer(tile, AGGS_LAYER, 4096, 1, 1); - assertLayer(tile, META_LAYER, 4096, 1, 11); + assertLayer(tile, META_LAYER, 4096, 1, 15); } { final Request mvtRequest = new Request(HttpGet.METHOD_NAME, INDEX_POINTS + "/_mvt/location/" + z + "/" + x + "/" + y); @@ -179,7 +178,7 @@ public void testGridType() throws Exception { assertThat(tile.getLayersCount(), Matchers.equalTo(3)); assertLayer(tile, HITS_LAYER, 4096, 33, 1); assertLayer(tile, AGGS_LAYER, 4096, 1, 1); - assertLayer(tile, META_LAYER, 4096, 1, 11); + assertLayer(tile, META_LAYER, 4096, 1, 15); } { final Request mvtRequest = new Request(HttpGet.METHOD_NAME, INDEX_POINTS + "/_mvt/location/" + z + "/" + x + "/" + y); @@ -204,7 +203,7 @@ public void testNoHitsLayer() throws Exception { final VectorTile.Tile tile = execute(mvtRequest); assertThat(tile.getLayersCount(), Matchers.equalTo(2)); assertLayer(tile, AGGS_LAYER, 4096, 1, 1); - assertLayer(tile, META_LAYER, 4096, 1, 10); + assertLayer(tile, META_LAYER, 4096, 1, 14); } public void testBasicQueryGet() throws Exception { @@ -222,7 +221,7 @@ public void testBasicQueryGet() throws Exception { assertThat(tile.getLayersCount(), Matchers.equalTo(3)); assertLayer(tile, HITS_LAYER, 4096, 1, 1); assertLayer(tile, AGGS_LAYER, 4096, 1, 1); - assertLayer(tile, META_LAYER, 4096, 1, 11); + assertLayer(tile, META_LAYER, 4096, 1, 15); } public void testBasicShape() throws Exception { @@ -231,7 +230,7 @@ public void testBasicShape() throws Exception { assertThat(tile.getLayersCount(), Matchers.equalTo(3)); assertLayer(tile, HITS_LAYER, 4096, 1, 1); assertLayer(tile, AGGS_LAYER, 4096, 256 * 256, 1); - assertLayer(tile, META_LAYER, 4096, 1, 11); + assertLayer(tile, META_LAYER, 4096, 1, 15); } public void testWithFields() throws Exception { @@ -241,7 +240,7 @@ public void testWithFields() throws Exception { assertThat(tile.getLayersCount(), Matchers.equalTo(3)); assertLayer(tile, HITS_LAYER, 4096, 1, 3); assertLayer(tile, AGGS_LAYER, 4096, 256 * 256, 1); - assertLayer(tile, META_LAYER, 4096, 1, 11); + assertLayer(tile, META_LAYER, 4096, 1, 15); } public void testMinAgg() throws Exception { @@ -259,7 +258,7 @@ public void testMinAgg() throws Exception { assertThat(tile.getLayersCount(), Matchers.equalTo(3)); assertLayer(tile, HITS_LAYER, 4096, 1, 1); assertLayer(tile, AGGS_LAYER, 4096, 256 * 256, 2); - assertLayer(tile, META_LAYER, 4096, 1, 11); + assertLayer(tile, META_LAYER, 4096, 1, 19); } private void assertLayer(VectorTile.Tile tile, String name, int extent, int numFeatures, int numTags) {