Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Speed up creation of vector tiles features #75874

Merged
merged 5 commits into from
Aug 4, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import org.locationtech.jts.geom.Envelope;
import org.locationtech.jts.geom.GeometryFactory;
import org.locationtech.jts.geom.LineString;
import org.locationtech.jts.simplify.TopologyPreservingSimplifier;

import java.util.ArrayList;
import java.util.List;
Expand All @@ -57,15 +58,21 @@ public FeatureFactory(int z, int x, int y, int extent) {
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);
// pixel precision of the tile in the mercator projection.
final double pixelPrecision = 2 * SphericalMercatorUtils.MERCATOR_BOUNDS / ((1L << z) * extent);
this.clipEnvelope.expandBy(pixelPrecision, pixelPrecision);
this.builder = new JTSGeometryBuilder(geomFactory, geomFactory.toGeometry(tileEnvelope), pixelPrecision);
// TODO: Not sure what is the difference between extent and tile size?
this.layerParams = new MvtLayerParams(extent, extent);
}

public List<byte[]> getFeatures(Geometry geometry) {
final org.locationtech.jts.geom.Geometry jtsGeometry = geometry.visit(builder);
if (jtsGeometry.isValid() == false) {
return List.of();
}
final TileGeomResult tileGeom = JtsAdapter.createTileGeom(
JtsAdapter.flatFeatureList(geometry.visit(builder)),
JtsAdapter.flatFeatureList(jtsGeometry),
tileEnvelope,
clipEnvelope,
geomFactory,
Expand All @@ -82,8 +89,12 @@ public List<byte[]> getFeatures(Geometry geometry) {
private static class JTSGeometryBuilder implements GeometryVisitor<org.locationtech.jts.geom.Geometry, IllegalArgumentException> {

private final GeometryFactory geomFactory;
private final org.locationtech.jts.geom.Geometry tile;
private final double pixelPrecision;

JTSGeometryBuilder(GeometryFactory geomFactory) {
JTSGeometryBuilder(GeometryFactory geomFactory, org.locationtech.jts.geom.Geometry tile, double pixelPrecision) {
this.pixelPrecision = pixelPrecision;
this.tile = tile;
this.geomFactory = geomFactory;
}

Expand Down Expand Up @@ -152,16 +163,26 @@ private LineString buildLine(Line line) {

@Override
public org.locationtech.jts.geom.Geometry visit(Polygon polygon) throws RuntimeException {
return buildPolygon(polygon);
final org.locationtech.jts.geom.Polygon jtsPolygon = buildPolygon(polygon);
if (jtsPolygon.contains(tile)) {
// shortcut, we return the tile
return tile;
}
return TopologyPreservingSimplifier.simplify(jtsPolygon, pixelPrecision);
}

@Override
public org.locationtech.jts.geom.Geometry visit(MultiPolygon multiPolygon) throws RuntimeException {
final org.locationtech.jts.geom.Polygon[] polygons = new org.locationtech.jts.geom.Polygon[multiPolygon.size()];
for (int i = 0; i < multiPolygon.size(); i++) {
polygons[i] = buildPolygon(multiPolygon.get(i));
final org.locationtech.jts.geom.Polygon jtsPolygon = buildPolygon(multiPolygon.get(i));
if (jtsPolygon.contains(tile)) {
// shortcut, we return the tile
return tile;
}
polygons[i] = jtsPolygon;
}
return geomFactory.createMultiPolygon(polygons);
return TopologyPreservingSimplifier.simplify(geomFactory.createMultiPolygon(polygons), pixelPrecision);
}

private org.locationtech.jts.geom.Polygon buildPolygon(Polygon polygon) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,24 @@
import org.elasticsearch.geometry.Point;
import org.elasticsearch.geometry.Polygon;
import org.elasticsearch.geometry.Rectangle;
import org.elasticsearch.geometry.utils.StandardValidator;
import org.elasticsearch.geometry.utils.WellKnownText;
import org.elasticsearch.search.aggregations.bucket.geogrid.GeoTileUtils;
import org.elasticsearch.test.ESTestCase;
import org.hamcrest.Matchers;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.zip.GZIPInputStream;

public class FeatureFactoryTests extends ESTestCase {

Expand Down Expand Up @@ -151,4 +159,45 @@ private MultiPolygon buildMultiPolygon(Rectangle r) {
private GeometryCollection<Geometry> buildGeometryCollection(Rectangle r) {
return new GeometryCollection<>(List.of(buildPolygon(r), buildLine(r)));
}

public void testStackOverflowError() throws IOException, ParseException {
// The provided polygon contains 49K points and we have observed that for some tiles and some extent values,
// it makes the library we are using to compute features to fail with a StackOverFlowError. This test just makes
// sure the fix in place avoids that error.
final InputStream is = new GZIPInputStream(getClass().getResourceAsStream("polygon.wkt.gz"));
final BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8));
final Geometry geometry = WellKnownText.fromWKT(StandardValidator.instance(true), true, reader.readLine());
for (int i = 0; i < 10; i++) {
final int z = randomIntBetween(0, 4);
final int x = randomIntBetween(0, (1 << z) - 1);
final int y = randomIntBetween(0, (1 << z) - 1);
final int extent = randomIntBetween(128, 8012);
final FeatureFactory builder = new FeatureFactory(z, x, y, extent);
try {
builder.getFeatures(geometry);
} catch (StackOverflowError error) {
fail("stackoverflow error thrown at " + z + "/" + x + "/" + y + "@" + extent);
}
}
}

public void testTileInsidePolygon() throws Exception {
final int z = randomIntBetween(0, 4);
final int x = randomIntBetween(0, (1 << z) - 1);
final int y = randomIntBetween(0, (1 << z) - 1);
final int extent = randomIntBetween(128, 8012);
final FeatureFactory builder = new FeatureFactory(z, x, y, extent);
final Rectangle rectangle = GeoTileUtils.toBoundingBox(x, y, z);
final double minX = Math.max(-180, rectangle.getMinX() - 1);
final double maxX = Math.min(180, rectangle.getMaxX() + 1);
final double minY = Math.max(-GeoTileUtils.LATITUDE_MASK, rectangle.getMinY() - 1);
final double maxY = Math.min(GeoTileUtils.LATITUDE_MASK, rectangle.getMaxY() + 1);
Polygon bigPolygon = new Polygon(
new LinearRing(new double[] { minX, maxX, maxX, minX, minX }, new double[] { minY, minY, maxY, maxY, minY })
);
final List<byte[]> bytes = builder.getFeatures(bigPolygon);
assertThat(bytes, Matchers.iterableWithSize(1));
final VectorTile.Tile.Feature feature = VectorTile.Tile.Feature.parseFrom(bytes.get(0));
assertThat(feature.getType(), Matchers.equalTo(VectorTile.Tile.GeomType.POLYGON));
}
}
Binary file not shown.