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

Add 'mvt' field type format to geo fields #75367

Merged
merged 15 commits into from
Jul 28, 2021
Merged
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

package org.elasticsearch.common.geo;

import org.elasticsearch.geometry.Geometry;
import org.elasticsearch.search.aggregations.bucket.geogrid.GeoTileUtils;

import java.util.List;
import java.util.Locale;
import java.util.function.Function;

/**
* Output formatters for geo fields. Adds support for vector tiles.
*/
public class GeoFormatterFactory {

@FunctionalInterface
public interface VectorTileEngine<T> {
/**
* Returns a formatter for a specific tile.
*/
Function<List<T>, List<Object>> getFormatter(int z, int x, int y, int extent);
}

private static final String MVT = "mvt";

/**
* Returns a formatter by name
*/
public static <T> Function<List<T>, List<Object>> getFormatter(String format, Function<T, Geometry> toGeometry,
VectorTileEngine<T> mvt) {
final int start = format.indexOf('(');
if (start == -1) {
return GeometryFormatterFactory.getFormatter(format, toGeometry);
}
final String formatName = format.substring(0, start);
if (MVT.equals(formatName) == false) {
throw new IllegalArgumentException("Invalid format: " + formatName);
}
final String param = format.substring(start + 1, format.length() - 1);
// we expect either z/x/y or z/x/y@extent
final String[] parts = param.split("@", 3);
if (parts.length > 2) {
throw new IllegalArgumentException(
"Invalid mvt formatter parameter [" + param + "]. Must have the form \"zoom/x/y\" or \"zoom/x/y@extent\"."
);
}
final int extent = parts.length == 2 ? Integer.parseInt(parts[1]) : 4096;
final String[] tileBits = parts[0].split("/", 4);
if (tileBits.length != 3) {
throw new IllegalArgumentException(
"Invalid tile string [" + parts[0] + "]. Must be three integers in a form \"zoom/x/y\"."
);
}
final int z = GeoTileUtils.checkPrecisionRange(Integer.parseInt(tileBits[0]));
final int tiles = 1 << z;
final int x = Integer.parseInt(tileBits[1]);
final int y = Integer.parseInt(tileBits[2]);
if (x < 0 || y < 0 || x >= tiles || y >= tiles) {
throw new IllegalArgumentException(String.format(Locale.ROOT, "Zoom/X/Y combination is not valid: %d/%d/%d", z, x, y));
}
return mvt.getFormatter(z, x, y, extent);
}
}
Original file line number Diff line number Diff line change
@@ -1,25 +1,25 @@
/*
* 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.
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
iverase marked this conversation as resolved.
Show resolved Hide resolved

package org.elasticsearch.xpack.vectortile.feature;
package org.elasticsearch.common.geo;

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.io.UncheckedIOException;
import java.util.Comparator;
import java.util.List;

/**
* Similar to {@link FeatureFactory} but only supports points and rectangles. It is just
* more efficient for those shapes and it does not use external dependencies.
* Transforms points and rectangles objects in WGS84 into mvt features.
*/
public class SimpleFeatureFactory {

Expand Down Expand Up @@ -63,11 +63,11 @@ public byte[] point(double lon, double lat) throws IOException {
/**
* Returns a {@code byte[]} containing the mvt representation of the provided points
*/
public byte[] points(List<Point> multiPoint) throws IOException {
multiPoint.sort(Comparator.comparingDouble(Point::getLon).thenComparingDouble(Point::getLat));
public byte[] points(List<GeoPoint> multiPoint) {
multiPoint.sort(Comparator.comparingDouble(GeoPoint::getLon).thenComparingDouble(GeoPoint::getLat));
final int[] commands = new int[2 * multiPoint.size() + 1];
int pos = 1, prevLon = 0, prevLat = 0, numPoints = 0;
for (Point point : multiPoint) {
for (GeoPoint point : multiPoint) {
final int posLon = lon(point.getLon());
if (posLon > extent || posLon < 0) {
continue;
Expand All @@ -89,7 +89,11 @@ public byte[] points(List<Point> multiPoint) throws IOException {
return EMPTY;
}
commands[0] = encodeCommand(MOVETO, numPoints);
return writeCommands(commands, 1, pos);
try {
return writeCommands(commands, 1, pos);
} catch (IOException ioe) {
throw new UncheckedIOException(ioe);
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
/*
* 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.
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
iverase marked this conversation as resolved.
Show resolved Hide resolved

package org.elasticsearch.xpack.vectortile.feature;
package org.elasticsearch.common.geo;

import org.elasticsearch.geometry.Rectangle;

/**
* Utility functions to transforms WGS84 coordinates into spherical mercator.
*/
class SphericalMercatorUtils {
public class SphericalMercatorUtils {

private static double MERCATOR_FACTOR = 20037508.34 / 180.0;
private static final double MERCATOR_FACTOR = 20037508.34 / 180.0;

/**
* Transforms WGS84 longitude to a Spherical mercator longitude
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,13 @@
import org.elasticsearch.ElasticsearchParseException;
import org.elasticsearch.common.CheckedBiFunction;
import org.elasticsearch.common.Explicit;
import org.elasticsearch.common.geo.GeoFormatterFactory;
import org.elasticsearch.common.geo.GeoPoint;
import org.elasticsearch.common.geo.GeoShapeUtils;
import org.elasticsearch.common.geo.GeoUtils;
import org.elasticsearch.common.geo.GeometryFormatterFactory;
import org.elasticsearch.common.geo.ShapeRelation;
import org.elasticsearch.common.geo.SimpleFeatureFactory;
import org.elasticsearch.common.unit.DistanceUnit;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.common.xcontent.support.MapXContentParser;
Expand Down Expand Up @@ -243,7 +245,11 @@ public String typeName() {

@Override
protected Function<List<GeoPoint>, List<Object>> getFormatter(String format) {
return GeometryFormatterFactory.getFormatter(format, p -> new Point(p.lon(), p.lat()));
return GeoFormatterFactory.getFormatter(format, p -> new Point(p.getLon(), p.getLat()),
(z, x, y, extent) -> {
final SimpleFeatureFactory featureFactory = new SimpleFeatureFactory(z, x, y, extent);
return points -> List.of(featureFactory.points(points));
});
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
/*
* 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.
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

package org.elasticsearch.xpack.vectortile.feature;
package org.elasticsearch.common.geo;

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;
Expand Down Expand Up @@ -48,7 +48,7 @@ public void testPoint() throws IOException {
}
}

public void testMultiPoint() throws IOException {
public void testMultiPoint() {
int z = randomIntBetween(3, 10);
int x = randomIntBetween(0, (1 << z) - 1);
int y = randomIntBetween(0, (1 << z) - 1);
Expand All @@ -57,20 +57,20 @@ public void testMultiPoint() throws IOException {
Rectangle rectangle = GeoTileUtils.toBoundingBox(x, y, z);
int numPoints = randomIntBetween(2, 10);
{
List<Point> points = new ArrayList<>();
List<GeoPoint> 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));
points.add(new GeoPoint(lat, lon));
for (int i = 0; i < numPoints - 1; i++) {
points.add(new Point(GeoTestUtil.nextLongitude(), GeoTestUtil.nextLatitude()));
points.add(new GeoPoint(GeoTestUtil.nextLatitude(), GeoTestUtil.nextLongitude()));
}
assertThat(builder.points(points).length, Matchers.greaterThan(0));
}
{
int xNew = randomValueOtherThanMany(v -> Math.abs(v - x) < 2, () -> randomIntBetween(0, (1 << z) - 1));
int yNew = randomValueOtherThanMany(v -> Math.abs(v - y) < 2, () -> randomIntBetween(0, (1 << z) - 1));
Rectangle rectangleNew = GeoTileUtils.toBoundingBox(xNew, yNew, z);
List<Point> points = new ArrayList<>();
List<GeoPoint> points = new ArrayList<>();
for (int i = 0; i < numPoints; i++) {
double lat = randomValueOtherThanMany(
(l) -> rectangleNew.getMinY() >= l || rectangleNew.getMaxY() <= l,
Expand All @@ -80,7 +80,7 @@ public void testMultiPoint() throws IOException {
(l) -> rectangleNew.getMinX() >= l || rectangleNew.getMaxX() <= l,
GeoTestUtil::nextLongitude
);
points.add(new Point(lon, lat));
points.add(new GeoPoint(lat, lon));
}
assertThat(builder.points(points).length, Matchers.equalTo(0));
}
Expand All @@ -95,24 +95,24 @@ public void testPointsMethodConsistency() throws IOException {
Rectangle rectangle = GeoTileUtils.toBoundingBox(x, y, z);
int extraPoints = randomIntBetween(1, 10);
{
List<Point> points = new ArrayList<>();
List<GeoPoint> 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));
points.add(new GeoPoint(lat, lon));
assertArrayEquals(builder.points(points), builder.point(lon, lat));
for (int i = 0; i < extraPoints; i++) {
points.add(new Point(lon, lat));
points.add(new GeoPoint(lat, lon));
}
assertArrayEquals(builder.points(points), builder.point(lon, lat));
}
{
List<Point> points = new ArrayList<>();
List<GeoPoint> 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));
points.add(new GeoPoint(lat, lon));
assertArrayEquals(builder.points(points), builder.point(lon, lat));
for (int i = 0; i < extraPoints; i++) {
points.add(new Point(lon, lat));
points.add(new GeoPoint(lat, lon));
}
assertArrayEquals(builder.points(points), builder.point(lon, lat));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,14 @@

package org.elasticsearch.index.mapper;

import org.apache.lucene.geo.GeoTestUtil;
import org.elasticsearch.common.geo.GeoPoint;
import org.elasticsearch.common.geo.SimpleFeatureFactory;
import org.elasticsearch.script.ScriptCompiler;
import org.hamcrest.Matchers;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

Expand Down Expand Up @@ -54,4 +59,36 @@ public void testFetchSourceValue() throws IOException {
sourceValue = "malformed";
assertEquals(List.of(), fetchSourceValue(mapper, sourceValue, null));
}

public void testFetchVectorTile() throws IOException {
MappedFieldType mapper
= new GeoPointFieldMapper.Builder("field", ScriptCompiler.NONE, false).build(new ContentPath()).fieldType();
final int z = randomIntBetween(1, 10);
int x = randomIntBetween(0, (1 << z) - 1);
int y = randomIntBetween(0, (1 << z) - 1);
final SimpleFeatureFactory featureFactory;
final String mvtString;
if (randomBoolean()) {
int extent = randomIntBetween(1 << 8, 1 << 14);
mvtString = "mvt(" + z + "/" + x + "/" + y + "@" + extent + ")";
featureFactory = new SimpleFeatureFactory(z, x, y, extent);
} else {
mvtString = "mvt(" + z + "/" + x + "/" + y + ")";
featureFactory = new SimpleFeatureFactory(z, x, y, 4096);
}
List<GeoPoint> geoPoints = new ArrayList<>();
List<List<Double>> values = new ArrayList<>();
for (int i = 0; i < randomIntBetween(1, 10); i++) {
final double lat = GeoTestUtil.nextLatitude();
final double lon = GeoTestUtil.nextLongitude();
List<?> sourceValue = fetchSourceValue(mapper, List.of(lon, lat), mvtString);
assertThat(sourceValue.size(), Matchers.equalTo(1));
assertThat(sourceValue.get(0), Matchers.equalTo(featureFactory.point(lon, lat)));
geoPoints.add(new GeoPoint(lat, lon));
values.add(List.of(lon, lat));
}
List<?> sourceValue = fetchSourceValue(mapper, values, mvtString);
assertThat(sourceValue.size(), Matchers.equalTo(1));
assertThat(sourceValue.get(0), Matchers.equalTo(featureFactory.points(geoPoints)));
}
}
1 change: 1 addition & 0 deletions x-pack/plugin/spatial/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ dependencies {
testImplementation(testArtifact(project(xpackModule('core'))))
yamlRestTestImplementation(testArtifact(project(xpackModule('core'))))
api project(path: ':modules:geo')
api project(path: xpackModule('vector-tile'))
iverase marked this conversation as resolved.
Show resolved Hide resolved
restTestConfig project(path: ':modules:geo', configuration: 'restTests')
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,15 @@
import org.apache.lucene.search.Query;
import org.elasticsearch.Version;
import org.elasticsearch.common.Explicit;
import org.elasticsearch.common.geo.GeoFormatterFactory;
import org.elasticsearch.common.geo.GeoShapeUtils;
import org.elasticsearch.common.geo.GeometryFormatterFactory;
import org.elasticsearch.common.geo.GeometryParser;
import org.elasticsearch.common.geo.Orientation;
import org.elasticsearch.common.geo.ShapeRelation;
import org.elasticsearch.common.logging.DeprecationCategory;
import org.elasticsearch.common.logging.DeprecationLogger;
import org.elasticsearch.geometry.Geometry;
import org.elasticsearch.geometry.GeometryCollection;
import org.elasticsearch.index.fielddata.IndexFieldData;
import org.elasticsearch.index.mapper.AbstractShapeGeometryFieldMapper;
import org.elasticsearch.index.mapper.ContentPath;
Expand All @@ -40,6 +41,7 @@
import org.elasticsearch.search.lookup.SearchLookup;
import org.elasticsearch.xpack.spatial.index.fielddata.plain.AbstractLatLonShapeIndexFieldData;
import org.elasticsearch.xpack.spatial.search.aggregations.support.GeoShapeValuesSourceType;
import org.elasticsearch.xpack.vectortile.feature.FeatureFactory;

import java.io.IOException;
import java.util.Arrays;
Expand Down Expand Up @@ -174,7 +176,17 @@ public Query geoShapeQuery(Geometry shape, String fieldName, ShapeRelation relat

@Override
protected Function<List<Geometry>, List<Object>> getFormatter(String format) {
return GeometryFormatterFactory.getFormatter(format, Function.identity());
return GeoFormatterFactory.getFormatter(format, Function.identity(),
(z, x, y, extent) -> {
final FeatureFactory featureFactory = new FeatureFactory(z, x, y, extent);
return geometries -> {
if (geometries.size() == 1) {
return List.of(featureFactory.getFeature(geometries.get(0)));
} else {
return List.of(featureFactory.getFeature(new GeometryCollection<>(geometries)));
}
};
});
}
}

Expand Down
Loading