From e5eb0475d5ba776f842cafb6c7b080f746979504 Mon Sep 17 00:00:00 2001 From: Ignacio Vera Date: Wed, 14 Jul 2021 07:47:21 +0200 Subject: [PATCH] Refactor SimpleFeatureFactory so it has no external dependencies (#75232) SimpleFeatureFactory has no external dependencies now. --- .../vectortile/feature/FeatureFactory.java | 29 ++- .../feature/FeatureFactoryUtils.java | 63 ----- .../feature/SimpleFeatureFactory.java | 151 ++++++++--- .../feature/SphericalMercatorUtils.java | 50 ++++ .../vectortile/rest/RestVectorTileAction.java | 8 +- .../FeatureFactoriesConsistencyTests.java | 99 +++++++ .../feature/FeatureFactoryTests.java | 241 ++++++++++++++---- .../feature/SimpleFeatureFactoryTests.java | 124 +++++++++ 8 files changed, 602 insertions(+), 163 deletions(-) delete mode 100644 x-pack/plugin/vector-tile/src/main/java/org/elasticsearch/xpack/vectortile/feature/FeatureFactoryUtils.java create mode 100644 x-pack/plugin/vector-tile/src/main/java/org/elasticsearch/xpack/vectortile/feature/SphericalMercatorUtils.java create mode 100644 x-pack/plugin/vector-tile/src/test/java/org/elasticsearch/xpack/vectortile/feature/FeatureFactoriesConsistencyTests.java create mode 100644 x-pack/plugin/vector-tile/src/test/java/org/elasticsearch/xpack/vectortile/feature/SimpleFeatureFactoryTests.java diff --git a/x-pack/plugin/vector-tile/src/main/java/org/elasticsearch/xpack/vectortile/feature/FeatureFactory.java b/x-pack/plugin/vector-tile/src/main/java/org/elasticsearch/xpack/vectortile/feature/FeatureFactory.java index 7f3d1884fe280..570e8f5b1c1cd 100644 --- a/x-pack/plugin/vector-tile/src/main/java/org/elasticsearch/xpack/vectortile/feature/FeatureFactory.java +++ b/x-pack/plugin/vector-tile/src/main/java/org/elasticsearch/xpack/vectortile/feature/FeatureFactory.java @@ -27,6 +27,7 @@ import org.elasticsearch.geometry.Point; import org.elasticsearch.geometry.Polygon; import org.elasticsearch.geometry.Rectangle; +import org.elasticsearch.search.aggregations.bucket.geogrid.GeoTileUtils; import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.Envelope; import org.locationtech.jts.geom.GeometryFactory; @@ -49,8 +50,9 @@ public class FeatureFactory { private final Envelope clipEnvelope; public FeatureFactory(int z, int x, int y, int extent) { - this.tileEnvelope = FeatureFactoryUtils.getJTSTileBounds(z, x, y); - this.clipEnvelope = FeatureFactoryUtils.getJTSTileBounds(z, x, y); + final Rectangle r = SphericalMercatorUtils.recToSphericalMercator(GeoTileUtils.toBoundingBox(x, y, z)); + this.tileEnvelope = new Envelope(r.getMinX(), r.getMaxX(), r.getMinY(), r.getMaxY()); + this.clipEnvelope = new Envelope(tileEnvelope); this.clipEnvelope.expandBy(tileEnvelope.getWidth() * 0.1d, tileEnvelope.getHeight() * 0.1d); this.builder = new JTSGeometryBuilder(geomFactory); // TODO: Not sure what is the difference between extent and tile size? @@ -89,7 +91,8 @@ public org.locationtech.jts.geom.Geometry visit(Circle circle) { @Override public org.locationtech.jts.geom.Geometry visit(GeometryCollection collection) { - throw new IllegalArgumentException("Circle is not supported"); + // TODO: Geometry collections are not supported by the vector tile specification. + throw new IllegalArgumentException("GeometryCollection is not supported"); } @Override @@ -112,8 +115,8 @@ public org.locationtech.jts.geom.Geometry visit(MultiPoint multiPoint) throws Ru } private org.locationtech.jts.geom.Point buildPoint(Point point) { - final double x = FeatureFactoryUtils.lonToSphericalMercator(point.getX()); - final double y = FeatureFactoryUtils.latToSphericalMercator(point.getY()); + final double x = SphericalMercatorUtils.lonToSphericalMercator(point.getX()); + final double y = SphericalMercatorUtils.latToSphericalMercator(point.getY()); return geomFactory.createPoint(new Coordinate(x, y)); } @@ -134,8 +137,8 @@ public org.locationtech.jts.geom.Geometry visit(MultiLine multiLine) throws Runt private LineString buildLine(Line line) { final Coordinate[] coordinates = new Coordinate[line.length()]; for (int i = 0; i < line.length(); i++) { - final double x = FeatureFactoryUtils.lonToSphericalMercator(line.getX(i)); - final double y = FeatureFactoryUtils.latToSphericalMercator(line.getY(i)); + final double x = SphericalMercatorUtils.lonToSphericalMercator(line.getX(i)); + final double y = SphericalMercatorUtils.latToSphericalMercator(line.getY(i)); coordinates[i] = new Coordinate(x, y); } return geomFactory.createLineString(coordinates); @@ -170,8 +173,8 @@ private org.locationtech.jts.geom.Polygon buildPolygon(Polygon polygon) { private org.locationtech.jts.geom.LinearRing buildLinearRing(LinearRing ring) throws RuntimeException { final Coordinate[] coordinates = new Coordinate[ring.length()]; for (int i = 0; i < ring.length(); i++) { - final double x = FeatureFactoryUtils.lonToSphericalMercator(ring.getX(i)); - final double y = FeatureFactoryUtils.latToSphericalMercator(ring.getY(i)); + final double x = SphericalMercatorUtils.lonToSphericalMercator(ring.getX(i)); + final double y = SphericalMercatorUtils.latToSphericalMercator(ring.getY(i)); coordinates[i] = new Coordinate(x, y); } return geomFactory.createLinearRing(coordinates); @@ -180,10 +183,10 @@ private org.locationtech.jts.geom.LinearRing buildLinearRing(LinearRing ring) th @Override public org.locationtech.jts.geom.Geometry visit(Rectangle rectangle) throws RuntimeException { // TODO: handle degenerated rectangles? - final double xMin = FeatureFactoryUtils.lonToSphericalMercator(rectangle.getMinX()); - final double yMin = FeatureFactoryUtils.latToSphericalMercator(rectangle.getMinY()); - final double xMax = FeatureFactoryUtils.lonToSphericalMercator(rectangle.getMaxX()); - final double yMax = FeatureFactoryUtils.latToSphericalMercator(rectangle.getMaxY()); + final double xMin = SphericalMercatorUtils.lonToSphericalMercator(rectangle.getMinX()); + final double yMin = SphericalMercatorUtils.latToSphericalMercator(rectangle.getMinY()); + final double xMax = SphericalMercatorUtils.lonToSphericalMercator(rectangle.getMaxX()); + final double yMax = SphericalMercatorUtils.latToSphericalMercator(rectangle.getMaxY()); final Coordinate[] coordinates = new Coordinate[5]; coordinates[0] = new Coordinate(xMin, yMin); coordinates[1] = new Coordinate(xMax, yMin); diff --git a/x-pack/plugin/vector-tile/src/main/java/org/elasticsearch/xpack/vectortile/feature/FeatureFactoryUtils.java b/x-pack/plugin/vector-tile/src/main/java/org/elasticsearch/xpack/vectortile/feature/FeatureFactoryUtils.java deleted file mode 100644 index ced22f16f6d51..0000000000000 --- a/x-pack/plugin/vector-tile/src/main/java/org/elasticsearch/xpack/vectortile/feature/FeatureFactoryUtils.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -package org.elasticsearch.xpack.vectortile.feature; - -import org.elasticsearch.geometry.Rectangle; -import org.locationtech.jts.geom.Envelope; - -/** - * Utility functions to transforms WGS84 coordinates into spherical mercator. - */ -class FeatureFactoryUtils { - - private FeatureFactoryUtils() { - // no instances - } - - /** - * Gets the JTS envelope for z/x/y/ tile in spherical mercator projection. - */ - public static Envelope getJTSTileBounds(int z, int x, int y) { - return new Envelope(getLong(x, z), getLong(x + 1, z), getLat(y, z), getLat(y + 1, z)); - } - - /** - * Gets the {@link org.elasticsearch.geometry.Geometry} envelope for z/x/y/ tile - * in spherical mercator projection. - */ - public static Rectangle getTileBounds(int z, int x, int y) { - return new Rectangle(getLong(x, z), getLong(x + 1, z), getLat(y, z), getLat(y + 1, z)); - } - - private static double getLong(int x, int zoom) { - return lonToSphericalMercator(Math.scalb(x, -zoom) * 360 - 180); - } - - private static double getLat(int y, int zoom) { - double r2d = 180 / Math.PI; - double n = Math.PI - 2 * Math.PI * y / Math.pow(2, zoom); - return latToSphericalMercator(r2d * Math.atan(0.5 * (Math.exp(n) - Math.exp(-n)))); - } - - private static double MERCATOR_FACTOR = 20037508.34 / 180.0; - - /** - * Transforms WGS84 longitude to a Spherical mercator longitude - */ - public static double lonToSphericalMercator(double lon) { - return lon * MERCATOR_FACTOR; - } - - /** - * Transforms WGS84 latitude to a Spherical mercator latitude - */ - public static double latToSphericalMercator(double lat) { - double y = Math.log(Math.tan((90 + lat) * Math.PI / 360)) / (Math.PI / 180); - return y * MERCATOR_FACTOR; - } -} diff --git a/x-pack/plugin/vector-tile/src/main/java/org/elasticsearch/xpack/vectortile/feature/SimpleFeatureFactory.java b/x-pack/plugin/vector-tile/src/main/java/org/elasticsearch/xpack/vectortile/feature/SimpleFeatureFactory.java index 53abad9ec1435..b797ee495ab29 100644 --- a/x-pack/plugin/vector-tile/src/main/java/org/elasticsearch/xpack/vectortile/feature/SimpleFeatureFactory.java +++ b/x-pack/plugin/vector-tile/src/main/java/org/elasticsearch/xpack/vectortile/feature/SimpleFeatureFactory.java @@ -7,66 +7,157 @@ package org.elasticsearch.xpack.vectortile.feature; -import com.wdtinc.mapbox_vector_tile.VectorTile; -import com.wdtinc.mapbox_vector_tile.encoding.GeomCmd; -import com.wdtinc.mapbox_vector_tile.encoding.GeomCmdHdr; - import org.apache.lucene.util.BitUtil; +import org.elasticsearch.common.io.stream.BytesStreamOutput; +import org.elasticsearch.geometry.Point; import org.elasticsearch.geometry.Rectangle; +import org.elasticsearch.search.aggregations.bucket.geogrid.GeoTileUtils; + +import java.io.IOException; +import java.util.Comparator; +import java.util.List; /** * Similar to {@link FeatureFactory} but only supports points and rectangles. It is just - * much more efficient for those shapes. + * more efficient for those shapes and it does not use external dependencies. */ public class SimpleFeatureFactory { private final int extent; private final double pointXScale, pointYScale, pointXTranslate, pointYTranslate; + private static final int MOVETO = 1; + private static final int LINETO = 2; + private static final int CLOSEPATH = 7; + + private static final byte[] EMPTY = new byte[0]; + public SimpleFeatureFactory(int z, int x, int y, int extent) { this.extent = extent; - final Rectangle rectangle = FeatureFactoryUtils.getTileBounds(z, x, y); + final Rectangle rectangle = SphericalMercatorUtils.recToSphericalMercator(GeoTileUtils.toBoundingBox(x, y, z)); pointXScale = (double) extent / (rectangle.getMaxLon() - rectangle.getMinLon()); pointYScale = (double) -extent / (rectangle.getMaxLat() - rectangle.getMinLat()); pointXTranslate = -pointXScale * rectangle.getMinX(); pointYTranslate = -pointYScale * rectangle.getMinY(); } - public void point(VectorTile.Tile.Feature.Builder featureBuilder, double lon, double lat) { - featureBuilder.setType(VectorTile.Tile.GeomType.POINT); - featureBuilder.addGeometry(GeomCmdHdr.cmdHdr(GeomCmd.MoveTo, 1)); - featureBuilder.addGeometry(BitUtil.zigZagEncode(lon(lon))); - featureBuilder.addGeometry(BitUtil.zigZagEncode(lat(lat))); + /** + * Returns a {@code byte[]} containing the mvt representation of the provided point + */ + public byte[] point(double lon, double lat) throws IOException { + final int posLon = lon(lon); + if (posLon > extent || posLon < 0) { + return EMPTY; + } + final int posLat = lat(lat); + if (posLat > extent || posLat < 0) { + return EMPTY; + } + final int[] commands = new int[3]; + commands[0] = encodeCommand(MOVETO, 1); + commands[1] = BitUtil.zigZagEncode(posLon); + commands[2] = BitUtil.zigZagEncode(posLat); + return writeCommands(commands, 1, 3); + } + + /** + * Returns a {@code byte[]} containing the mvt representation of the provided points + */ + public byte[] points(List multiPoint) throws IOException { + multiPoint.sort(Comparator.comparingDouble(Point::getLon).thenComparingDouble(Point::getLat)); + final int[] commands = new int[2 * multiPoint.size() + 1]; + int pos = 1, prevLon = 0, prevLat = 0, numPoints = 0; + for (int i = 0; i < multiPoint.size(); i++) { + final Point point = multiPoint.get(i); + final int posLon = lon(point.getLon()); + if (posLon > extent || posLon < 0) { + continue; + } + final int posLat = lat(point.getLat()); + if (posLat > extent || posLat < 0) { + continue; + } + if (i == 0 || posLon != prevLon || posLat != prevLat) { + commands[pos++] = BitUtil.zigZagEncode(posLon - prevLon); + commands[pos++] = BitUtil.zigZagEncode(posLat - prevLat); + prevLon = posLon; + prevLat = posLat; + numPoints++; + } + } + if (numPoints == 0) { + return EMPTY; + } + commands[0] = encodeCommand(MOVETO, numPoints); + return writeCommands(commands, 1, pos); } - public void box(VectorTile.Tile.Feature.Builder featureBuilder, double minLon, double maxLon, double minLat, double maxLat) { - featureBuilder.setType(VectorTile.Tile.GeomType.POLYGON); - final int minX = lon(minLon); - final int minY = lat(minLat); - final int maxX = lon(maxLon); - final int maxY = lat(maxLat); - featureBuilder.addGeometry(GeomCmdHdr.cmdHdr(GeomCmd.MoveTo, 1)); - featureBuilder.addGeometry(BitUtil.zigZagEncode(minX)); - featureBuilder.addGeometry(BitUtil.zigZagEncode(minY)); - featureBuilder.addGeometry(GeomCmdHdr.cmdHdr(GeomCmd.LineTo, 3)); + /** + * Returns a {@code byte[]} containing the mvt representation of the provided rectangle + */ + public byte[] box(double minLon, double maxLon, double minLat, double maxLat) throws IOException { + int[] commands = new int[11]; + final int minX = Math.max(0, lon(minLon)); + if (minX > extent) { + return EMPTY; + } + final int minY = Math.min(extent, lat(minLat)); + if (minY > extent) { + return EMPTY; + } + final int maxX = Math.min(extent, lon(maxLon)); + if (maxX < 0 || minX == maxX) { + return EMPTY; + } + final int maxY = Math.max(0, lat(maxLat)); + if (maxY < 0 || minY == maxY) { + return EMPTY; + } + commands[0] = encodeCommand(MOVETO, 1); + commands[1] = BitUtil.zigZagEncode(minX); + commands[2] = BitUtil.zigZagEncode(minY); + commands[3] = encodeCommand(LINETO, 3); // 1 - featureBuilder.addGeometry(BitUtil.zigZagEncode(maxX - minX)); - featureBuilder.addGeometry(BitUtil.zigZagEncode(0)); + commands[4] = BitUtil.zigZagEncode(maxX - minX); + commands[5] = BitUtil.zigZagEncode(0); // 2 - featureBuilder.addGeometry(BitUtil.zigZagEncode(0)); - featureBuilder.addGeometry(BitUtil.zigZagEncode(maxY - minY)); + commands[6] = BitUtil.zigZagEncode(0); + commands[7] = BitUtil.zigZagEncode(maxY - minY); // 3 - featureBuilder.addGeometry(BitUtil.zigZagEncode(minX - maxX)); - featureBuilder.addGeometry(BitUtil.zigZagEncode(0)); + commands[8] = BitUtil.zigZagEncode(minX - maxX); + commands[9] = BitUtil.zigZagEncode(0); // close - featureBuilder.addGeometry(GeomCmdHdr.cmdHdr(GeomCmd.ClosePath, 1)); + commands[10] = encodeCommand(CLOSEPATH, 1); + return writeCommands(commands, 3, 11); } private int lat(double lat) { - return (int) Math.round(pointYScale * FeatureFactoryUtils.latToSphericalMercator(lat) + pointYTranslate) + extent; + return (int) Math.round(pointYScale * SphericalMercatorUtils.latToSphericalMercator(lat) + pointYTranslate) + extent; } private int lon(double lon) { - return (int) Math.round(pointXScale * FeatureFactoryUtils.lonToSphericalMercator(lon) + pointXTranslate); + return (int) Math.round(pointXScale * SphericalMercatorUtils.lonToSphericalMercator(lon) + pointXTranslate); + } + + private static int encodeCommand(int id, int length) { + return (id & 0x7) | (length << 3); + } + + private static byte[] writeCommands(final int[] commands, final int type, final int length) throws IOException { + try (BytesStreamOutput output = new BytesStreamOutput()) { + for (int i = 0; i < length; i++) { + output.writeVInt(commands[i]); + } + final int dataSize = output.size(); + output.reset(); + output.writeVInt(24); + output.writeVInt(type); + output.writeVInt(34); + output.writeVInt(dataSize); + for (int i = 0; i < length; i++) { + output.writeVInt(commands[i]); + } + return output.copyBytes().array(); + } } } diff --git a/x-pack/plugin/vector-tile/src/main/java/org/elasticsearch/xpack/vectortile/feature/SphericalMercatorUtils.java b/x-pack/plugin/vector-tile/src/main/java/org/elasticsearch/xpack/vectortile/feature/SphericalMercatorUtils.java new file mode 100644 index 0000000000000..da0949582d36c --- /dev/null +++ b/x-pack/plugin/vector-tile/src/main/java/org/elasticsearch/xpack/vectortile/feature/SphericalMercatorUtils.java @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.vectortile.feature; + +import org.elasticsearch.geometry.Rectangle; + +/** + * Utility functions to transforms WGS84 coordinates into spherical mercator. + */ +class SphericalMercatorUtils { + + private static double MERCATOR_FACTOR = 20037508.34 / 180.0; + + /** + * Transforms WGS84 longitude to a Spherical mercator longitude + */ + public static double lonToSphericalMercator(double lon) { + return lon * MERCATOR_FACTOR; + } + + /** + * Transforms WGS84 latitude to a Spherical mercator latitude + */ + public static double latToSphericalMercator(double lat) { + double y = Math.log(Math.tan((90 + lat) * Math.PI / 360)) / (Math.PI / 180); + return y * MERCATOR_FACTOR; + } + + /** + * Transforms WGS84 rectangle to a Spherical mercator rectangle + */ + public static Rectangle recToSphericalMercator(Rectangle r) { + return new Rectangle( + lonToSphericalMercator(r.getMinLon()), + lonToSphericalMercator(r.getMaxLon()), + latToSphericalMercator(r.getMaxLat()), + latToSphericalMercator(r.getMinLat()) + ); + + } + + private SphericalMercatorUtils() { + // no instances + } +} diff --git a/x-pack/plugin/vector-tile/src/main/java/org/elasticsearch/xpack/vectortile/rest/RestVectorTileAction.java b/x-pack/plugin/vector-tile/src/main/java/org/elasticsearch/xpack/vectortile/rest/RestVectorTileAction.java index 66b13f4ac00eb..7dc4284e93440 100644 --- a/x-pack/plugin/vector-tile/src/main/java/org/elasticsearch/xpack/vectortile/rest/RestVectorTileAction.java +++ b/x-pack/plugin/vector-tile/src/main/java/org/elasticsearch/xpack/vectortile/rest/RestVectorTileAction.java @@ -259,11 +259,11 @@ private static VectorTile.Tile.Layer.Builder buildAggsLayer( // Add geometry if (request.getGridType() == VectorTileRequest.GRID_TYPE.GRID) { final Rectangle r = GeoTileUtils.toBoundingBox(bucket.getKeyAsString()); - geomBuilder.box(featureBuilder, r.getMinLon(), r.getMaxLon(), r.getMinLat(), r.getMaxLat()); + featureBuilder.mergeFrom(geomBuilder.box(r.getMinLon(), r.getMaxLon(), r.getMinLat(), r.getMaxLat())); } else { // TODO: it should be the centroid of the data? final GeoPoint point = (GeoPoint) bucket.getKey(); - geomBuilder.point(featureBuilder, point.lon(), point.lat()); + featureBuilder.mergeFrom(geomBuilder.point(point.lon(), point.lat())); } // Add count as key value pair VectorTileUtils.addPropertyToFeature(featureBuilder, layerProps, COUNT_TAG, bucket.getDocCount()); @@ -288,10 +288,10 @@ private static VectorTile.Tile.Layer.Builder buildMetaLayer( if (bounds != null && bounds.topLeft() != null) { final GeoPoint topLeft = bounds.topLeft(); final GeoPoint bottomRight = bounds.bottomRight(); - geomBuilder.box(featureBuilder, topLeft.lon(), bottomRight.lon(), bottomRight.lat(), topLeft.lat()); + featureBuilder.mergeFrom(geomBuilder.box(topLeft.lon(), bottomRight.lon(), bottomRight.lat(), topLeft.lat())); } else { final Rectangle tile = request.getBoundingBox(); - geomBuilder.box(featureBuilder, tile.getMinLon(), tile.getMaxLon(), tile.getMinLat(), tile.getMaxLat()); + featureBuilder.mergeFrom(geomBuilder.box(tile.getMinLon(), tile.getMaxLon(), tile.getMinLat(), tile.getMaxLat())); } VectorTileUtils.addToXContentToFeature(featureBuilder, layerProps, response); metaLayerBuilder.addFeatures(featureBuilder); diff --git a/x-pack/plugin/vector-tile/src/test/java/org/elasticsearch/xpack/vectortile/feature/FeatureFactoriesConsistencyTests.java b/x-pack/plugin/vector-tile/src/test/java/org/elasticsearch/xpack/vectortile/feature/FeatureFactoriesConsistencyTests.java new file mode 100644 index 0000000000000..c3a65137c68b3 --- /dev/null +++ b/x-pack/plugin/vector-tile/src/test/java/org/elasticsearch/xpack/vectortile/feature/FeatureFactoriesConsistencyTests.java @@ -0,0 +1,99 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.vectortile.feature; + +import com.wdtinc.mapbox_vector_tile.VectorTile; +import com.wdtinc.mapbox_vector_tile.adapt.jts.UserDataIgnoreConverter; + +import org.apache.lucene.geo.GeoTestUtil; +import org.elasticsearch.geometry.MultiPoint; +import org.elasticsearch.geometry.Point; +import org.elasticsearch.geometry.Rectangle; +import org.elasticsearch.search.aggregations.bucket.geogrid.GeoTileUtils; +import org.elasticsearch.test.ESTestCase; +import org.hamcrest.Matchers; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class FeatureFactoriesConsistencyTests extends ESTestCase { + + public void testPoint() throws IOException { + int z = randomIntBetween(1, 10); + int x = randomIntBetween(0, (1 << z) - 1); + int y = randomIntBetween(0, (1 << z) - 1); + int extent = randomIntBetween(1 << 8, 1 << 14); + // check if we might have numerical error due to floating point arithmetic + assumeFalse("", hasNumericalError(z, x, y, extent)); + Rectangle rectangle = GeoTileUtils.toBoundingBox(x, y, z); + SimpleFeatureFactory builder = new SimpleFeatureFactory(z, x, y, extent); + FeatureFactory factory = new FeatureFactory(z, x, y, extent); + List points = new ArrayList<>(); + for (int i = 0; i < 10; i++) { + double lat = randomValueOtherThanMany((l) -> rectangle.getMinY() > l || rectangle.getMaxY() < l, GeoTestUtil::nextLatitude); + double lon = randomValueOtherThanMany((l) -> rectangle.getMinX() > l || rectangle.getMaxX() < l, GeoTestUtil::nextLongitude); + byte[] b1 = builder.point(lon, lat); + Point point = new Point(lon, lat); + List features = factory.getFeatures(point, new UserDataIgnoreConverter()); + assertThat(features.size(), Matchers.equalTo(1)); + byte[] b2 = features.get(0).toByteArray(); + assertArrayEquals(b1, b2); + points.add(point); + } + byte[] b1 = builder.points(points); + List features = factory.getFeatures(new MultiPoint(points), new UserDataIgnoreConverter()); + assertThat(features.size(), Matchers.equalTo(1)); + byte[] b2 = features.get(0).toByteArray(); + assertArrayEquals(b1, b2); + } + + public void testIssue74341() throws IOException { + int z = 1; + int x = 0; + int y = 0; + int extent = 1730; + // this is the typical case we need to guard from. + assertThat(hasNumericalError(z, x, y, extent), Matchers.equalTo(true)); + double lon = -171.0; + double lat = 0.9999999403953552; + SimpleFeatureFactory builder = new SimpleFeatureFactory(z, x, y, extent); + FeatureFactory factory = new FeatureFactory(z, x, y, extent); + byte[] b1 = builder.point(lon, lat); + Point point = new Point(lon, lat); + List features = factory.getFeatures(point, new UserDataIgnoreConverter()); + assertThat(features.size(), Matchers.equalTo(1)); + byte[] b2 = features.get(0).toByteArray(); + assertThat(Arrays.equals(b1, b2), Matchers.equalTo(false)); + } + + private boolean hasNumericalError(int z, int x, int y, int extent) { + final Rectangle rectangle = SphericalMercatorUtils.recToSphericalMercator(GeoTileUtils.toBoundingBox(x, y, z)); + final double xDiff = rectangle.getMaxLon() - rectangle.getMinLon(); + final double yDiff = rectangle.getMaxLat() - rectangle.getMinLat(); + return (double) -extent / yDiff != -1d / (yDiff / (double) extent) || (double) extent / xDiff != 1d / (xDiff / (double) extent); + } + + public void testRectangle() throws IOException { + int z = randomIntBetween(1, 10); + int x = randomIntBetween(0, (1 << z) - 1); + int y = randomIntBetween(0, (1 << z) - 1); + int extent = randomIntBetween(1 << 8, 1 << 14); + SimpleFeatureFactory builder = new SimpleFeatureFactory(z, x, y, extent); + FeatureFactory factory = new FeatureFactory(z, x, y, extent); + Rectangle r = GeoTileUtils.toBoundingBox(x, y, z); + for (int i = 0; i < extent; i++) { + byte[] b1 = builder.box(r.getMinLon(), r.getMaxLon(), r.getMinLat(), r.getMaxLat()); + List features = factory.getFeatures(r, new UserDataIgnoreConverter()); + assertThat(features.size(), Matchers.equalTo(1)); + byte[] b2 = features.get(0).toByteArray(); + assertArrayEquals(extent + "", b1, b2); + } + } +} diff --git a/x-pack/plugin/vector-tile/src/test/java/org/elasticsearch/xpack/vectortile/feature/FeatureFactoryTests.java b/x-pack/plugin/vector-tile/src/test/java/org/elasticsearch/xpack/vectortile/feature/FeatureFactoryTests.java index 7076bb8f6ec6d..8301ee745ebc9 100644 --- a/x-pack/plugin/vector-tile/src/test/java/org/elasticsearch/xpack/vectortile/feature/FeatureFactoryTests.java +++ b/x-pack/plugin/vector-tile/src/test/java/org/elasticsearch/xpack/vectortile/feature/FeatureFactoryTests.java @@ -11,13 +11,22 @@ import com.wdtinc.mapbox_vector_tile.adapt.jts.UserDataIgnoreConverter; import org.apache.lucene.geo.GeoTestUtil; +import org.elasticsearch.geometry.Geometry; +import org.elasticsearch.geometry.GeometryCollection; +import org.elasticsearch.geometry.Line; +import org.elasticsearch.geometry.LinearRing; +import org.elasticsearch.geometry.MultiLine; +import org.elasticsearch.geometry.MultiPoint; +import org.elasticsearch.geometry.MultiPolygon; import org.elasticsearch.geometry.Point; +import org.elasticsearch.geometry.Polygon; import org.elasticsearch.geometry.Rectangle; import org.elasticsearch.search.aggregations.bucket.geogrid.GeoTileUtils; import org.elasticsearch.test.ESTestCase; import org.hamcrest.Matchers; -import java.util.Arrays; +import java.util.ArrayList; +import java.util.Collections; import java.util.List; public class FeatureFactoryTests extends ESTestCase { @@ -27,71 +36,197 @@ public void testPoint() { int x = randomIntBetween(0, (1 << z) - 1); int y = randomIntBetween(0, (1 << z) - 1); int extent = randomIntBetween(1 << 8, 1 << 14); - // check if we might have numerical error due to floating point arithmetic - assumeFalse("", hasNumericalError(z, x, y, extent)); + FeatureFactory builder = new FeatureFactory(z, x, y, extent); Rectangle rectangle = GeoTileUtils.toBoundingBox(x, y, z); - SimpleFeatureFactory builder = new SimpleFeatureFactory(z, x, y, extent); - FeatureFactory factory = new FeatureFactory(z, x, y, extent); - VectorTile.Tile.Feature.Builder featureBuilder = VectorTile.Tile.Feature.newBuilder(); - for (int i = 0; i < 10; i++) { - featureBuilder.clear(); + { double lat = randomValueOtherThanMany((l) -> rectangle.getMinY() > l || rectangle.getMaxY() < l, GeoTestUtil::nextLatitude); double lon = randomValueOtherThanMany((l) -> rectangle.getMinX() > l || rectangle.getMaxX() < l, GeoTestUtil::nextLongitude); - builder.point(featureBuilder, lon, lat); - byte[] b1 = featureBuilder.build().toByteArray(); - Point point = new Point(lon, lat); - List features = factory.getFeatures(point, new UserDataIgnoreConverter()); + List features = builder.getFeatures(new Point(lon, lat), new UserDataIgnoreConverter()); assertThat(features.size(), Matchers.equalTo(1)); - byte[] b2 = features.get(0).toByteArray(); - assertArrayEquals(b1, b2); + VectorTile.Tile.Feature feature = features.get(0); + assertThat(feature.getType(), Matchers.equalTo(VectorTile.Tile.GeomType.POINT)); + } + { + double lat = randomValueOtherThanMany((l) -> rectangle.getMinY() <= l && rectangle.getMaxY() >= l, GeoTestUtil::nextLatitude); + double lon = randomValueOtherThanMany((l) -> rectangle.getMinX() <= l && rectangle.getMaxX() >= l, GeoTestUtil::nextLongitude); + List features = builder.getFeatures(new Point(lon, lat), new UserDataIgnoreConverter()); + assertThat(features.size(), Matchers.equalTo(0)); + } + } + + public void testMultiPoint() { + int z = randomIntBetween(1, 10); + int x = randomIntBetween(0, (1 << z) - 1); + int y = randomIntBetween(0, (1 << z) - 1); + int extent = randomIntBetween(1 << 8, 1 << 14); + FeatureFactory builder = new FeatureFactory(z, x, y, extent); + Rectangle rectangle = GeoTileUtils.toBoundingBox(x, y, z); + int numPoints = randomIntBetween(2, 10); + { + List points = new ArrayList<>(); + double lat = randomValueOtherThanMany((l) -> rectangle.getMinY() > l || rectangle.getMaxY() < l, GeoTestUtil::nextLatitude); + double lon = randomValueOtherThanMany((l) -> rectangle.getMinX() > l || rectangle.getMaxX() < l, GeoTestUtil::nextLongitude); + points.add(new Point(lon, lat)); + for (int i = 0; i < numPoints - 1; i++) { + points.add(new Point(GeoTestUtil.nextLongitude(), GeoTestUtil.nextLatitude())); + } + List features = builder.getFeatures(new MultiPoint(points), new UserDataIgnoreConverter()); + assertThat(features.size(), Matchers.equalTo(1)); + VectorTile.Tile.Feature feature = features.get(0); + assertThat(feature.getType(), Matchers.equalTo(VectorTile.Tile.GeomType.POINT)); + } + { + List points = new ArrayList<>(); + for (int i = 0; i < numPoints; i++) { + double lat = randomValueOtherThanMany( + (l) -> rectangle.getMinY() <= l && rectangle.getMaxY() >= l, + GeoTestUtil::nextLatitude + ); + double lon = randomValueOtherThanMany( + (l) -> rectangle.getMinX() <= l && rectangle.getMaxX() >= l, + GeoTestUtil::nextLongitude + ); + points.add(new Point(lon, lat)); + } + List features = builder.getFeatures(new MultiPoint(points), new UserDataIgnoreConverter()); + assertThat(features.size(), Matchers.equalTo(0)); } } - public void testIssue74341() { - int z = 1; - int x = 0; - int y = 0; - int extent = 1730; - // this is the typical case we need to guard from. - assertThat(hasNumericalError(z, x, y, extent), Matchers.equalTo(true)); - double lon = -171.0; - double lat = 0.9999999403953552; - SimpleFeatureFactory builder = new SimpleFeatureFactory(z, x, y, extent); - FeatureFactory factory = new FeatureFactory(z, x, y, extent); - VectorTile.Tile.Feature.Builder featureBuilder = VectorTile.Tile.Feature.newBuilder(); - builder.point(featureBuilder, lon, lat); - byte[] b1 = featureBuilder.build().toByteArray(); - Point point = new Point(lon, lat); - List features = factory.getFeatures(point, new UserDataIgnoreConverter()); - assertThat(features.size(), Matchers.equalTo(1)); - byte[] b2 = features.get(0).toByteArray(); - assertThat(Arrays.equals(b1, b2), Matchers.equalTo(false)); + public void testRectangle() { + int z = randomIntBetween(3, 10); + int x = randomIntBetween(2, (1 << z) - 1); + int y = randomIntBetween(2, (1 << z) - 1); + int extent = randomIntBetween(1 << 8, 1 << 14); + FeatureFactory builder = new FeatureFactory(z, x, y, extent); + { + Rectangle r = GeoTileUtils.toBoundingBox(x, y, z); + List features = builder.getFeatures(r, new UserDataIgnoreConverter()); + assertThat(features.size(), Matchers.equalTo(1)); + VectorTile.Tile.Feature feature = features.get(0); + assertThat(feature.getType(), Matchers.equalTo(VectorTile.Tile.GeomType.POLYGON)); + } + { + Rectangle r = GeoTileUtils.toBoundingBox(x - 2, y, z); + List features = builder.getFeatures(r, new UserDataIgnoreConverter()); + assertThat(features.size(), Matchers.equalTo(0)); + } } - private boolean hasNumericalError(int z, int x, int y, int extent) { - final Rectangle rectangle = FeatureFactoryUtils.getTileBounds(z, x, y); - final double xDiff = rectangle.getMaxLon() - rectangle.getMinLon(); - final double yDiff = rectangle.getMaxLat() - rectangle.getMinLat(); - return (double) -extent / yDiff != -1d / (yDiff / (double) extent) || (double) extent / xDiff != 1d / (xDiff / (double) extent); + public void testLine() { + int z = randomIntBetween(3, 10); + int x = randomIntBetween(2, (1 << z) - 1); + int y = randomIntBetween(2, (1 << z) - 1); + int extent = randomIntBetween(1 << 8, 1 << 14); + FeatureFactory builder = new FeatureFactory(z, x, y, extent); + { + Rectangle r = GeoTileUtils.toBoundingBox(x, y, z); + List features = builder.getFeatures(buildLine(r), new UserDataIgnoreConverter()); + assertThat(features.size(), Matchers.equalTo(1)); + VectorTile.Tile.Feature feature = features.get(0); + assertThat(feature.getType(), Matchers.equalTo(VectorTile.Tile.GeomType.LINESTRING)); + } + { + Rectangle r = GeoTileUtils.toBoundingBox(x - 2, y, z); + List features = builder.getFeatures(buildLine(r), new UserDataIgnoreConverter()); + assertThat(features.size(), Matchers.equalTo(0)); + } } - public void testRectangle() { - int z = randomIntBetween(1, 10); - int x = randomIntBetween(0, (1 << z) - 1); - int y = randomIntBetween(0, (1 << z) - 1); + public void testMultiLine() { + int z = randomIntBetween(3, 10); + int x = randomIntBetween(2, (1 << z) - 1); + int y = randomIntBetween(2, (1 << z) - 1); int extent = randomIntBetween(1 << 8, 1 << 14); - SimpleFeatureFactory builder = new SimpleFeatureFactory(z, x, y, extent); - FeatureFactory factory = new FeatureFactory(z, x, y, extent); - Rectangle r = GeoTileUtils.toBoundingBox(x, y, z); - VectorTile.Tile.Feature.Builder featureBuilder = VectorTile.Tile.Feature.newBuilder(); - for (int i = 0; i < extent; i++) { - featureBuilder.clear(); - builder.box(featureBuilder, r.getMinLon(), r.getMaxLon(), r.getMinLat(), r.getMaxLat()); - byte[] b1 = featureBuilder.build().toByteArray(); - List features = factory.getFeatures(r, new UserDataIgnoreConverter()); + FeatureFactory builder = new FeatureFactory(z, x, y, extent); + { + Rectangle r = GeoTileUtils.toBoundingBox(x, y, z); + List features = builder.getFeatures(buildMultiLine(r), new UserDataIgnoreConverter()); assertThat(features.size(), Matchers.equalTo(1)); - byte[] b2 = features.get(0).toByteArray(); - assertArrayEquals(extent + "", b1, b2); + VectorTile.Tile.Feature feature = features.get(0); + assertThat(feature.getType(), Matchers.equalTo(VectorTile.Tile.GeomType.LINESTRING)); } + { + Rectangle r = GeoTileUtils.toBoundingBox(x - 2, y, z); + List features = builder.getFeatures(buildMultiLine(r), new UserDataIgnoreConverter()); + assertThat(features.size(), Matchers.equalTo(0)); + } + } + + public void testPolygon() { + int z = randomIntBetween(3, 10); + int x = randomIntBetween(2, (1 << z) - 1); + int y = randomIntBetween(2, (1 << z) - 1); + int extent = randomIntBetween(1 << 8, 1 << 14); + FeatureFactory builder = new FeatureFactory(z, x, y, extent); + { + Rectangle r = GeoTileUtils.toBoundingBox(x, y, z); + List features = builder.getFeatures(buildPolygon(r), new UserDataIgnoreConverter()); + assertThat(features.size(), Matchers.equalTo(1)); + VectorTile.Tile.Feature feature = features.get(0); + assertThat(feature.getType(), Matchers.equalTo(VectorTile.Tile.GeomType.POLYGON)); + } + { + Rectangle r = GeoTileUtils.toBoundingBox(x - 2, y, z); + List features = builder.getFeatures(buildPolygon(r), new UserDataIgnoreConverter()); + assertThat(features.size(), Matchers.equalTo(0)); + } + } + + public void testMultiPolygon() { + int z = randomIntBetween(3, 10); + int x = randomIntBetween(2, (1 << z) - 1); + int y = randomIntBetween(2, (1 << z) - 1); + int extent = randomIntBetween(1 << 8, 1 << 14); + FeatureFactory builder = new FeatureFactory(z, x, y, extent); + { + Rectangle r = GeoTileUtils.toBoundingBox(x, y, z); + List features = builder.getFeatures(buildMultiPolygon(r), new UserDataIgnoreConverter()); + assertThat(features.size(), Matchers.equalTo(1)); + VectorTile.Tile.Feature feature = features.get(0); + assertThat(feature.getType(), Matchers.equalTo(VectorTile.Tile.GeomType.POLYGON)); + } + { + Rectangle r = GeoTileUtils.toBoundingBox(x - 2, y, z); + List features = builder.getFeatures(buildMultiPolygon(r), new UserDataIgnoreConverter()); + assertThat(features.size(), Matchers.equalTo(0)); + } + } + + public void testGeometryCollection() { + int z = randomIntBetween(3, 10); + int x = randomIntBetween(2, (1 << z) - 1); + int y = randomIntBetween(2, (1 << z) - 1); + int extent = randomIntBetween(1 << 8, 1 << 14); + FeatureFactory builder = new FeatureFactory(z, x, y, extent); + { + Rectangle r = GeoTileUtils.toBoundingBox(x, y, z); + List geometries = List.of(buildPolygon(r), buildLine(r)); + IllegalArgumentException ex = expectThrows( + IllegalArgumentException.class, + () -> builder.getFeatures(new GeometryCollection<>(geometries), new UserDataIgnoreConverter()) + ); + assertThat(ex.getMessage(), Matchers.equalTo("GeometryCollection is not supported")); + } + } + + private Line buildLine(Rectangle r) { + return new Line(new double[] { r.getMinX(), r.getMaxX() }, new double[] { r.getMinY(), r.getMaxY() }); + } + + private MultiLine buildMultiLine(Rectangle r) { + return new MultiLine(Collections.singletonList(buildLine(r))); + } + + private Polygon buildPolygon(Rectangle r) { + LinearRing ring = new LinearRing( + new double[] { r.getMinX(), r.getMaxX(), r.getMaxX(), r.getMinX(), r.getMinX() }, + new double[] { r.getMinY(), r.getMinY(), r.getMaxY(), r.getMaxY(), r.getMinY() } + ); + return new Polygon(ring); + } + + private MultiPolygon buildMultiPolygon(Rectangle r) { + return new MultiPolygon(Collections.singletonList(buildPolygon(r))); } } diff --git a/x-pack/plugin/vector-tile/src/test/java/org/elasticsearch/xpack/vectortile/feature/SimpleFeatureFactoryTests.java b/x-pack/plugin/vector-tile/src/test/java/org/elasticsearch/xpack/vectortile/feature/SimpleFeatureFactoryTests.java new file mode 100644 index 0000000000000..704614b89c800 --- /dev/null +++ b/x-pack/plugin/vector-tile/src/test/java/org/elasticsearch/xpack/vectortile/feature/SimpleFeatureFactoryTests.java @@ -0,0 +1,124 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.vectortile.feature; + +import org.apache.lucene.geo.GeoTestUtil; +import org.elasticsearch.geometry.Point; +import org.elasticsearch.geometry.Rectangle; +import org.elasticsearch.search.aggregations.bucket.geogrid.GeoTileUtils; +import org.elasticsearch.test.ESTestCase; +import org.hamcrest.Matchers; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +public class SimpleFeatureFactoryTests extends ESTestCase { + + public void testPoint() throws IOException { + int z = randomIntBetween(1, 10); + int x = randomIntBetween(0, (1 << z) - 1); + int y = randomIntBetween(0, (1 << z) - 1); + int extent = randomIntBetween(1 << 8, 1 << 14); + SimpleFeatureFactory builder = new SimpleFeatureFactory(z, x, y, extent); + Rectangle rectangle = GeoTileUtils.toBoundingBox(x, y, z); + { + double lat = randomValueOtherThanMany((l) -> rectangle.getMinY() > l || rectangle.getMaxY() < l, GeoTestUtil::nextLatitude); + double lon = randomValueOtherThanMany((l) -> rectangle.getMinX() > l || rectangle.getMaxX() < l, GeoTestUtil::nextLongitude); + assertThat(builder.point(lon, lat).length, Matchers.greaterThan(0)); + } + { + double lat = randomValueOtherThanMany((l) -> rectangle.getMinY() <= l && rectangle.getMaxY() >= l, GeoTestUtil::nextLatitude); + double lon = randomValueOtherThanMany((l) -> rectangle.getMinX() <= l && rectangle.getMaxX() >= l, GeoTestUtil::nextLongitude); + assertThat(builder.point(lon, lat).length, Matchers.equalTo(0)); + } + } + + public void testMultiPoint() throws IOException { + int z = randomIntBetween(1, 10); + int x = randomIntBetween(0, (1 << z) - 1); + int y = randomIntBetween(0, (1 << z) - 1); + int extent = randomIntBetween(1 << 8, 1 << 14); + SimpleFeatureFactory builder = new SimpleFeatureFactory(z, x, y, extent); + Rectangle rectangle = GeoTileUtils.toBoundingBox(x, y, z); + int numPoints = randomIntBetween(2, 10); + { + List points = new ArrayList<>(); + double lat = randomValueOtherThanMany((l) -> rectangle.getMinY() > l || rectangle.getMaxY() < l, GeoTestUtil::nextLatitude); + double lon = randomValueOtherThanMany((l) -> rectangle.getMinX() > l || rectangle.getMaxX() < l, GeoTestUtil::nextLongitude); + points.add(new Point(lon, lat)); + for (int i = 0; i < numPoints - 1; i++) { + points.add(new Point(GeoTestUtil.nextLongitude(), GeoTestUtil.nextLatitude())); + } + assertThat(builder.points(points).length, Matchers.greaterThan(0)); + } + { + List points = new ArrayList<>(); + for (int i = 0; i < numPoints; i++) { + double lat = randomValueOtherThanMany( + (l) -> rectangle.getMinY() <= l && rectangle.getMaxY() >= l, + GeoTestUtil::nextLatitude + ); + double lon = randomValueOtherThanMany( + (l) -> rectangle.getMinX() <= l && rectangle.getMaxX() >= l, + GeoTestUtil::nextLongitude + ); + points.add(new Point(lon, lat)); + } + assertThat(builder.points(points).length, Matchers.equalTo(0)); + } + } + + public void testPointsMethodConsistency() throws IOException { + int z = randomIntBetween(1, 10); + int x = randomIntBetween(0, (1 << z) - 1); + int y = randomIntBetween(0, (1 << z) - 1); + int extent = randomIntBetween(1 << 8, 1 << 14); + SimpleFeatureFactory builder = new SimpleFeatureFactory(z, x, y, extent); + Rectangle rectangle = GeoTileUtils.toBoundingBox(x, y, z); + int extraPoints = randomIntBetween(1, 10); + { + List points = new ArrayList<>(); + double lat = randomValueOtherThanMany((l) -> rectangle.getMinY() > l || rectangle.getMaxY() < l, GeoTestUtil::nextLatitude); + double lon = randomValueOtherThanMany((l) -> rectangle.getMinX() > l || rectangle.getMaxX() < l, GeoTestUtil::nextLongitude); + points.add(new Point(lon, lat)); + assertArrayEquals(builder.points(points), builder.point(lon, lat)); + for (int i = 0; i < extraPoints; i++) { + points.add(new Point(lon, lat)); + } + assertArrayEquals(builder.points(points), builder.point(lon, lat)); + } + { + List points = new ArrayList<>(); + double lat = randomValueOtherThanMany((l) -> rectangle.getMinY() <= l && rectangle.getMaxY() >= l, GeoTestUtil::nextLatitude); + double lon = randomValueOtherThanMany((l) -> rectangle.getMinX() <= l && rectangle.getMaxX() >= l, GeoTestUtil::nextLongitude); + points.add(new Point(lon, lat)); + assertArrayEquals(builder.points(points), builder.point(lon, lat)); + for (int i = 0; i < extraPoints; i++) { + points.add(new Point(lon, lat)); + } + assertArrayEquals(builder.points(points), builder.point(lon, lat)); + } + } + + public void testRectangle() throws IOException { + int z = randomIntBetween(3, 10); + int x = randomIntBetween(1, (1 << z) - 1); + int y = randomIntBetween(1, (1 << z) - 1); + int extent = randomIntBetween(1 << 8, 1 << 14); + SimpleFeatureFactory builder = new SimpleFeatureFactory(z, x, y, extent); + { + Rectangle r = GeoTileUtils.toBoundingBox(x, y, z); + assertThat(builder.box(r.getMinLon(), r.getMaxLon(), r.getMinLat(), r.getMaxLat()).length, Matchers.greaterThan(0)); + } + { + Rectangle r = GeoTileUtils.toBoundingBox(x - 1, y, z); + assertThat(builder.box(r.getMinLon(), r.getMaxLon(), r.getMinLat(), r.getMaxLat()).length, Matchers.equalTo(0)); + } + } +}