diff --git a/server/src/main/java/org/elasticsearch/common/geo/LuceneGeometriesUtils.java b/server/src/main/java/org/elasticsearch/common/geo/LuceneGeometriesUtils.java new file mode 100644 index 0000000000000..c9d4b1c534fef --- /dev/null +++ b/server/src/main/java/org/elasticsearch/common/geo/LuceneGeometriesUtils.java @@ -0,0 +1,449 @@ +/* + * 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.apache.lucene.geo.LatLonGeometry; +import org.apache.lucene.geo.XYGeometry; +import org.elasticsearch.geometry.Circle; +import org.elasticsearch.geometry.Geometry; +import org.elasticsearch.geometry.GeometryCollection; +import org.elasticsearch.geometry.GeometryVisitor; +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.geometry.ShapeType; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.function.Consumer; + +public class LuceneGeometriesUtils { + + interface Quantizer { + double quantizeLat(double lat); + + double quantizeLon(double lon); + + double[] quantizeLats(double[] lats); + + double[] quantizeLons(double[] lons); + } + + static final Quantizer NOOP_QUANTIZER = new Quantizer() { + @Override + public double quantizeLat(double lat) { + return lat; + } + + @Override + public double quantizeLon(double lon) { + return lon; + } + + @Override + public double[] quantizeLats(double[] lats) { + return lats; + } + + @Override + public double[] quantizeLons(double[] lons) { + return lons; + } + }; + + static Quantizer LATLON_QUANTIZER = new Quantizer() { + @Override + public double quantizeLat(double lat) { + return GeoUtils.quantizeLat(lat); + } + + @Override + public double quantizeLon(double lon) { + return GeoUtils.quantizeLon(lon); + } + + @Override + public double[] quantizeLats(double[] lats) { + return Arrays.stream(lats).map(this::quantizeLat).toArray(); + } + + @Override + public double[] quantizeLons(double[] lons) { + return Arrays.stream(lons).map(this::quantizeLon).toArray(); + } + }; + + /** + * Transform an Elasticsearch {@link Geometry} into a lucene {@link LatLonGeometry} + * + * @param geometry the geometry to transform + * @param quantize if true, the coordinates of the geometry will be quantized using lucene quantization. + * This is useful for queries so the latitude and longitude values to match the values on the index. + * @param checker call for every {@link ShapeType} found in the Geometry. It allows to throw an error if a geometry is + * not supported. + * + * @return an array of {@link LatLonGeometry} + */ + public static LatLonGeometry[] toLatLonGeometry(Geometry geometry, boolean quantize, Consumer checker) { + if (geometry == null || geometry.isEmpty()) { + return new LatLonGeometry[0]; + } + if (GeometryNormalizer.needsNormalize(Orientation.CCW, geometry)) { + // make geometry lucene friendly + geometry = GeometryNormalizer.apply(Orientation.CCW, geometry); + } + final List geometries = new ArrayList<>(); + final Quantizer quantizer = quantize ? LATLON_QUANTIZER : NOOP_QUANTIZER; + geometry.visit(new GeometryVisitor<>() { + @Override + public Void visit(Circle circle) { + checker.accept(ShapeType.CIRCLE); + if (circle.isEmpty() == false) { + geometries.add(toLatLonCircle(circle, quantizer)); + } + return null; + } + + @Override + public Void visit(GeometryCollection collection) { + checker.accept(ShapeType.GEOMETRYCOLLECTION); + if (collection.isEmpty() == false) { + for (org.elasticsearch.geometry.Geometry shape : collection) { + shape.visit(this); + } + } + return null; + } + + @Override + public Void visit(org.elasticsearch.geometry.Line line) { + checker.accept(ShapeType.LINESTRING); + if (line.isEmpty() == false) { + geometries.add(toLatLonLine(line, quantizer)); + } + return null; + } + + @Override + public Void visit(LinearRing ring) { + throw new IllegalArgumentException("Found an unsupported shape LinearRing"); + } + + @Override + public Void visit(MultiLine multiLine) { + checker.accept(ShapeType.MULTILINESTRING); + if (multiLine.isEmpty() == false) { + for (Line line : multiLine) { + visit(line); + } + } + return null; + } + + @Override + public Void visit(MultiPoint multiPoint) { + checker.accept(ShapeType.MULTIPOINT); + if (multiPoint.isEmpty() == false) { + for (Point point : multiPoint) { + visit(point); + } + } + return null; + } + + @Override + public Void visit(MultiPolygon multiPolygon) { + checker.accept(ShapeType.MULTIPOLYGON); + if (multiPolygon.isEmpty() == false) { + for (Polygon polygon : multiPolygon) { + visit(polygon); + } + } + return null; + } + + @Override + public Void visit(Point point) { + checker.accept(ShapeType.POINT); + if (point.isEmpty() == false) { + geometries.add(toLatLonPoint(point, quantizer)); + } + return null; + } + + @Override + public Void visit(org.elasticsearch.geometry.Polygon polygon) { + checker.accept(ShapeType.POLYGON); + if (polygon.isEmpty() == false) { + geometries.add(toLatLonPolygon(polygon, quantizer)); + } + return null; + } + + @Override + public Void visit(Rectangle r) { + checker.accept(ShapeType.ENVELOPE); + if (r.isEmpty() == false) { + geometries.add(toLatLonRectangle(r, quantizer)); + } + return null; + } + }); + return geometries.toArray(new LatLonGeometry[0]); + } + + /** + * Transform an Elasticsearch {@link Point} into a lucene {@link org.apache.lucene.geo.Point} + */ + public static org.apache.lucene.geo.Point toLatLonPoint(Point point) { + return toLatLonPoint(point, NOOP_QUANTIZER); + } + + private static org.apache.lucene.geo.Point toLatLonPoint(Point point, Quantizer quantizer) { + return new org.apache.lucene.geo.Point(quantizer.quantizeLat(point.getLat()), quantizer.quantizeLon(point.getLon())); + } + + /** + * Transform an Elasticsearch {@link Line} into a lucene {@link org.apache.lucene.geo.Line} + */ + public static org.apache.lucene.geo.Line toLatLonLine(Line line) { + return toLatLonLine(line, NOOP_QUANTIZER); + } + + private static org.apache.lucene.geo.Line toLatLonLine(Line line, Quantizer quantizer) { + return new org.apache.lucene.geo.Line(quantizer.quantizeLats(line.getLats()), quantizer.quantizeLons(line.getLons())); + } + + /** + * Transform an Elasticsearch {@link Polygon} into a lucene {@link org.apache.lucene.geo.Polygon} + */ + public static org.apache.lucene.geo.Polygon toLatLonPolygon(Polygon polygon) { + return toLatLonPolygon(polygon, NOOP_QUANTIZER); + } + + private static org.apache.lucene.geo.Polygon toLatLonPolygon(Polygon polygon, Quantizer quantizer) { + org.apache.lucene.geo.Polygon[] holes = new org.apache.lucene.geo.Polygon[polygon.getNumberOfHoles()]; + for (int i = 0; i < holes.length; i++) { + holes[i] = new org.apache.lucene.geo.Polygon( + quantizer.quantizeLats(polygon.getHole(i).getY()), + quantizer.quantizeLons(polygon.getHole(i).getX()) + ); + } + return new org.apache.lucene.geo.Polygon( + quantizer.quantizeLats(polygon.getPolygon().getY()), + quantizer.quantizeLons(polygon.getPolygon().getX()), + holes + ); + + } + + /** + * Transform an Elasticsearch {@link Rectangle} into a lucene {@link org.apache.lucene.geo.Rectangle} + */ + public static org.apache.lucene.geo.Rectangle toLatLonRectangle(Rectangle rectangle) { + return toLatLonRectangle(rectangle, NOOP_QUANTIZER); + } + + private static org.apache.lucene.geo.Rectangle toLatLonRectangle(Rectangle r, Quantizer quantizer) { + return new org.apache.lucene.geo.Rectangle( + quantizer.quantizeLat(r.getMinLat()), + quantizer.quantizeLat(r.getMaxLat()), + quantizer.quantizeLon(r.getMinLon()), + quantizer.quantizeLon(r.getMaxLon()) + ); + } + + /** + * Transform an Elasticsearch {@link Circle} into a lucene {@link org.apache.lucene.geo.Circle} + */ + public static org.apache.lucene.geo.Circle toLatLonCircle(Circle circle) { + return toLatLonCircle(circle, NOOP_QUANTIZER); + } + + private static org.apache.lucene.geo.Circle toLatLonCircle(Circle circle, Quantizer quantizer) { + return new org.apache.lucene.geo.Circle( + quantizer.quantizeLat(circle.getLat()), + quantizer.quantizeLon(circle.getLon()), + circle.getRadiusMeters() + ); + } + + /** + * Transform an Elasticsearch {@link Geometry} into a lucene {@link XYGeometry} + * + * @param geometry the geometry to transform. + * @param checker call for every {@link ShapeType} found in the Geometry. It allows to throw an error if + * a geometry is not supported. + * @return an array of {@link XYGeometry} + */ + public static XYGeometry[] toXYGeometry(Geometry geometry, Consumer checker) { + if (geometry == null || geometry.isEmpty()) { + return new XYGeometry[0]; + } + final List geometries = new ArrayList<>(); + geometry.visit(new GeometryVisitor<>() { + @Override + public Void visit(Circle circle) { + checker.accept(ShapeType.CIRCLE); + if (circle.isEmpty() == false) { + geometries.add(toXYCircle(circle)); + } + return null; + } + + @Override + public Void visit(GeometryCollection collection) { + checker.accept(ShapeType.GEOMETRYCOLLECTION); + if (collection.isEmpty() == false) { + for (org.elasticsearch.geometry.Geometry shape : collection) { + shape.visit(this); + } + } + return null; + } + + @Override + public Void visit(org.elasticsearch.geometry.Line line) { + checker.accept(ShapeType.LINESTRING); + if (line.isEmpty() == false) { + geometries.add(toXYLine(line)); + } + return null; + } + + @Override + public Void visit(LinearRing ring) { + throw new IllegalArgumentException("Found an unsupported shape LinearRing"); + } + + @Override + public Void visit(MultiLine multiLine) { + checker.accept(ShapeType.MULTILINESTRING); + if (multiLine.isEmpty() == false) { + for (Line line : multiLine) { + visit(line); + } + } + return null; + } + + @Override + public Void visit(MultiPoint multiPoint) { + checker.accept(ShapeType.MULTIPOINT); + if (multiPoint.isEmpty() == false) { + for (Point point : multiPoint) { + visit(point); + } + } + return null; + } + + @Override + public Void visit(MultiPolygon multiPolygon) { + checker.accept(ShapeType.MULTIPOLYGON); + if (multiPolygon.isEmpty() == false) { + for (Polygon polygon : multiPolygon) { + visit(polygon); + } + } + return null; + } + + @Override + public Void visit(Point point) { + checker.accept(ShapeType.POINT); + if (point.isEmpty() == false) { + geometries.add(toXYPoint(point)); + } + return null; + } + + @Override + public Void visit(org.elasticsearch.geometry.Polygon polygon) { + checker.accept(ShapeType.POLYGON); + if (polygon.isEmpty() == false) { + geometries.add(toXYPolygon(polygon)); + } + return null; + } + + @Override + public Void visit(Rectangle r) { + checker.accept(ShapeType.ENVELOPE); + if (r.isEmpty() == false) { + geometries.add(toXYRectangle(r)); + } + return null; + } + }); + return geometries.toArray(new XYGeometry[0]); + } + + /** + * Transform an Elasticsearch {@link Point} into a lucene {@link org.apache.lucene.geo.XYPoint} + */ + public static org.apache.lucene.geo.XYPoint toXYPoint(Point point) { + return new org.apache.lucene.geo.XYPoint((float) point.getX(), (float) point.getY()); + } + + /** + * Transform an Elasticsearch {@link Line} into a lucene {@link org.apache.lucene.geo.XYLine} + */ + public static org.apache.lucene.geo.XYLine toXYLine(Line line) { + return new org.apache.lucene.geo.XYLine(doubleArrayToFloatArray(line.getX()), doubleArrayToFloatArray(line.getY())); + } + + /** + * Transform an Elasticsearch {@link Polygon} into a lucene {@link org.apache.lucene.geo.XYPolygon} + */ + public static org.apache.lucene.geo.XYPolygon toXYPolygon(Polygon polygon) { + org.apache.lucene.geo.XYPolygon[] holes = new org.apache.lucene.geo.XYPolygon[polygon.getNumberOfHoles()]; + for (int i = 0; i < holes.length; i++) { + holes[i] = new org.apache.lucene.geo.XYPolygon( + doubleArrayToFloatArray(polygon.getHole(i).getX()), + doubleArrayToFloatArray(polygon.getHole(i).getY()) + ); + } + return new org.apache.lucene.geo.XYPolygon( + doubleArrayToFloatArray(polygon.getPolygon().getX()), + doubleArrayToFloatArray(polygon.getPolygon().getY()), + holes + ); + } + + /** + * Transform an Elasticsearch {@link Rectangle} into a lucene {@link org.apache.lucene.geo.XYRectangle} + */ + public static org.apache.lucene.geo.XYRectangle toXYRectangle(Rectangle r) { + return new org.apache.lucene.geo.XYRectangle((float) r.getMinX(), (float) r.getMaxX(), (float) r.getMinY(), (float) r.getMaxY()); + } + + /** + * Transform an Elasticsearch {@link Circle} into a lucene {@link org.apache.lucene.geo.XYCircle} + */ + public static org.apache.lucene.geo.XYCircle toXYCircle(Circle circle) { + return new org.apache.lucene.geo.XYCircle((float) circle.getX(), (float) circle.getY(), (float) circle.getRadiusMeters()); + } + + static float[] doubleArrayToFloatArray(double[] array) { + float[] result = new float[array.length]; + for (int i = 0; i < array.length; ++i) { + result[i] = (float) array[i]; + } + return result; + } + + private LuceneGeometriesUtils() {} +} diff --git a/server/src/main/java/org/elasticsearch/index/mapper/GeoShapeIndexer.java b/server/src/main/java/org/elasticsearch/index/mapper/GeoShapeIndexer.java index 7ec9ec4fd947f..23879282799ab 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/GeoShapeIndexer.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/GeoShapeIndexer.java @@ -13,6 +13,7 @@ import org.apache.lucene.index.IndexableField; import org.elasticsearch.common.geo.GeoUtils; import org.elasticsearch.common.geo.GeometryNormalizer; +import org.elasticsearch.common.geo.LuceneGeometriesUtils; import org.elasticsearch.common.geo.Orientation; import org.elasticsearch.geometry.Circle; import org.elasticsearch.geometry.Geometry; @@ -94,7 +95,7 @@ public Void visit(GeometryCollection collection) { @Override public Void visit(Line line) { - addFields(LatLonShape.createIndexableFields(name, toLuceneLine(line))); + addFields(LatLonShape.createIndexableFields(name, LuceneGeometriesUtils.toLatLonLine(line))); return null; } @@ -135,7 +136,7 @@ public Void visit(Point point) { @Override public Void visit(Polygon polygon) { - addFields(LatLonShape.createIndexableFields(name, toLucenePolygon(polygon), true)); + addFields(LatLonShape.createIndexableFields(name, LuceneGeometriesUtils.toLatLonPolygon(polygon), true)); return null; } @@ -199,22 +200,10 @@ private void addFields(IndexableField[] fields) { } } - private static org.apache.lucene.geo.Polygon toLucenePolygon(Polygon polygon) { - org.apache.lucene.geo.Polygon[] holes = new org.apache.lucene.geo.Polygon[polygon.getNumberOfHoles()]; - for (int i = 0; i < holes.length; i++) { - holes[i] = new org.apache.lucene.geo.Polygon(polygon.getHole(i).getY(), polygon.getHole(i).getX()); - } - return new org.apache.lucene.geo.Polygon(polygon.getPolygon().getY(), polygon.getPolygon().getX(), holes); - } - private static org.apache.lucene.geo.Polygon toLucenePolygon(Rectangle r) { return new org.apache.lucene.geo.Polygon( new double[] { r.getMinLat(), r.getMinLat(), r.getMaxLat(), r.getMaxLat(), r.getMinLat() }, new double[] { r.getMinLon(), r.getMaxLon(), r.getMaxLon(), r.getMinLon(), r.getMinLon() } ); } - - private static org.apache.lucene.geo.Line toLuceneLine(Line line) { - return new org.apache.lucene.geo.Line(line.getLats(), line.getLons()); - } } diff --git a/server/src/main/java/org/elasticsearch/index/mapper/GeoShapeQueryable.java b/server/src/main/java/org/elasticsearch/index/mapper/GeoShapeQueryable.java index beb594d9e9936..3947f009f1aec 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/GeoShapeQueryable.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/GeoShapeQueryable.java @@ -8,32 +8,18 @@ package org.elasticsearch.index.mapper; -import org.apache.lucene.geo.GeoEncodingUtils; import org.apache.lucene.geo.LatLonGeometry; import org.apache.lucene.search.MatchNoDocsQuery; import org.apache.lucene.search.Query; -import org.elasticsearch.common.geo.GeometryNormalizer; -import org.elasticsearch.common.geo.Orientation; +import org.elasticsearch.common.geo.LuceneGeometriesUtils; import org.elasticsearch.common.geo.ShapeRelation; import org.elasticsearch.common.geo.SpatialStrategy; -import org.elasticsearch.geometry.Circle; import org.elasticsearch.geometry.Geometry; -import org.elasticsearch.geometry.GeometryCollection; -import org.elasticsearch.geometry.GeometryVisitor; -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.geometry.ShapeType; import org.elasticsearch.index.query.QueryShardException; import org.elasticsearch.index.query.SearchExecutionContext; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; +import java.util.function.Consumer; /** * Implemented by {@link org.elasticsearch.index.mapper.MappedFieldType} that support @@ -43,10 +29,18 @@ public interface GeoShapeQueryable { Query geoShapeQuery(SearchExecutionContext context, String fieldName, ShapeRelation relation, LatLonGeometry... luceneGeometries); - default Query geoShapeQuery(SearchExecutionContext context, String fieldName, ShapeRelation relation, Geometry shape) { + default Query geoShapeQuery(SearchExecutionContext context, String fieldName, ShapeRelation relation, Geometry geometry) { + final Consumer checker = relation == ShapeRelation.WITHIN ? t -> { + if (t == ShapeType.LINESTRING) { + // Line geometries and WITHIN relation is not supported by Lucene. Throw an error here + // to have same behavior for runtime fields. + throw new IllegalArgumentException("found an unsupported shape Line"); + } + } : t -> {}; final LatLonGeometry[] luceneGeometries; try { - luceneGeometries = toQuantizeLuceneGeometry(shape, relation); + // quantize the geometries to match the values on the index + luceneGeometries = LuceneGeometriesUtils.toLatLonGeometry(geometry, true, checker); } catch (IllegalArgumentException e) { throw new QueryShardException(context, "Exception creating query on Field [" + fieldName + "] " + e.getMessage(), e); } @@ -66,157 +60,4 @@ default Query geoShapeQuery( ) { return geoShapeQuery(context, fieldName, relation, shape); } - - private static double quantizeLat(double lat) { - return GeoEncodingUtils.decodeLatitude(GeoEncodingUtils.encodeLatitude(lat)); - } - - private static double[] quantizeLats(double[] lats) { - return Arrays.stream(lats).map(GeoShapeQueryable::quantizeLat).toArray(); - } - - private static double quantizeLon(double lon) { - return GeoEncodingUtils.decodeLongitude(GeoEncodingUtils.encodeLongitude(lon)); - } - - private static double[] quantizeLons(double[] lons) { - return Arrays.stream(lons).map(GeoShapeQueryable::quantizeLon).toArray(); - } - - /** - * transforms an Elasticsearch {@link Geometry} into a lucene {@link LatLonGeometry} and quantize - * the latitude and longitude values to match the values on the index. - */ - static LatLonGeometry[] toQuantizeLuceneGeometry(Geometry geometry, ShapeRelation relation) { - if (geometry == null) { - return new LatLonGeometry[0]; - } - if (GeometryNormalizer.needsNormalize(Orientation.CCW, geometry)) { - // make geometry lucene friendly - geometry = GeometryNormalizer.apply(Orientation.CCW, geometry); - } - if (geometry.isEmpty()) { - return new LatLonGeometry[0]; - } - final List geometries = new ArrayList<>(); - geometry.visit(new GeometryVisitor<>() { - @Override - public Void visit(Circle circle) { - if (circle.isEmpty() == false) { - geometries.add( - new org.apache.lucene.geo.Circle( - quantizeLat(circle.getLat()), - quantizeLon(circle.getLon()), - circle.getRadiusMeters() - ) - ); - } - return null; - } - - @Override - public Void visit(GeometryCollection collection) { - if (collection.isEmpty() == false) { - for (Geometry shape : collection) { - shape.visit(this); - } - } - return null; - } - - @Override - public Void visit(org.elasticsearch.geometry.Line line) { - if (line.isEmpty() == false) { - if (relation == ShapeRelation.WITHIN) { - // Line geometries and WITHIN relation is not supported by Lucene. Throw an error here - // to have same behavior for runtime fields. - throw new IllegalArgumentException("found an unsupported shape Line"); - } - geometries.add(new org.apache.lucene.geo.Line(quantizeLats(line.getLats()), quantizeLons(line.getLons()))); - } - return null; - } - - @Override - public Void visit(LinearRing ring) { - throw new IllegalArgumentException("Found an unsupported shape LinearRing"); - } - - @Override - public Void visit(MultiLine multiLine) { - if (multiLine.isEmpty() == false) { - for (Line line : multiLine) { - visit(line); - } - } - return null; - } - - @Override - public Void visit(MultiPoint multiPoint) { - if (multiPoint.isEmpty() == false) { - for (Point point : multiPoint) { - visit(point); - } - } - return null; - } - - @Override - public Void visit(MultiPolygon multiPolygon) { - if (multiPolygon.isEmpty() == false) { - for (Polygon polygon : multiPolygon) { - visit(polygon); - } - } - return null; - } - - @Override - public Void visit(Point point) { - if (point.isEmpty() == false) { - geometries.add(new org.apache.lucene.geo.Point(quantizeLat(point.getLat()), quantizeLon(point.getLon()))); - } - return null; - - } - - @Override - public Void visit(org.elasticsearch.geometry.Polygon polygon) { - if (polygon.isEmpty() == false) { - org.apache.lucene.geo.Polygon[] holes = new org.apache.lucene.geo.Polygon[polygon.getNumberOfHoles()]; - for (int i = 0; i < holes.length; i++) { - holes[i] = new org.apache.lucene.geo.Polygon( - quantizeLats(polygon.getHole(i).getY()), - quantizeLons(polygon.getHole(i).getX()) - ); - } - geometries.add( - new org.apache.lucene.geo.Polygon( - quantizeLats(polygon.getPolygon().getY()), - quantizeLons(polygon.getPolygon().getX()), - holes - ) - ); - } - return null; - } - - @Override - public Void visit(Rectangle r) { - if (r.isEmpty() == false) { - geometries.add( - new org.apache.lucene.geo.Rectangle( - quantizeLat(r.getMinLat()), - quantizeLat(r.getMaxLat()), - quantizeLon(r.getMinLon()), - quantizeLon(r.getMaxLon()) - ) - ); - } - return null; - } - }); - return geometries.toArray(new LatLonGeometry[0]); - } } diff --git a/server/src/test/java/org/elasticsearch/common/geo/LuceneGeometriesUtilsTests.java b/server/src/test/java/org/elasticsearch/common/geo/LuceneGeometriesUtilsTests.java new file mode 100644 index 0000000000000..96cc73e2cff4c --- /dev/null +++ b/server/src/test/java/org/elasticsearch/common/geo/LuceneGeometriesUtilsTests.java @@ -0,0 +1,476 @@ +/* + * 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.apache.lucene.geo.LatLonGeometry; +import org.apache.lucene.geo.XYGeometry; +import org.elasticsearch.geo.GeometryTestUtils; +import org.elasticsearch.geo.ShapeTestUtils; +import org.elasticsearch.geometry.Circle; +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.geometry.ShapeType; +import org.elasticsearch.test.ESTestCase; + +import java.util.ArrayList; +import java.util.List; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.instanceOf; + +public class LuceneGeometriesUtilsTests extends ESTestCase { + + public void testLatLonPoint() { + Point point = GeometryTestUtils.randomPoint(); + { + LatLonGeometry[] geometries = LuceneGeometriesUtils.toLatLonGeometry(point, false, t -> assertEquals(ShapeType.POINT, t)); + assertEquals(1, geometries.length); + assertLatLonPoint(point, geometries[0]); + } + { + LatLonGeometry[] geometries = LuceneGeometriesUtils.toLatLonGeometry(point, true, t -> assertEquals(ShapeType.POINT, t)); + assertEquals(1, geometries.length); + assertLatLonPoint(quantize(point), geometries[0]); + } + } + + public void testLatLonMultiPoint() { + MultiPoint multiPoint = GeometryTestUtils.randomMultiPoint(randomBoolean()); + { + int[] counter = new int[] { 0 }; + LatLonGeometry[] geometries = LuceneGeometriesUtils.toLatLonGeometry(multiPoint, false, t -> { + if (counter[0]++ == 0) { + assertEquals(ShapeType.MULTIPOINT, t); + } else { + assertEquals(ShapeType.POINT, t); + } + }); + assertEquals(multiPoint.size(), geometries.length); + for (int i = 0; i < multiPoint.size(); i++) { + assertLatLonPoint(multiPoint.get(i), geometries[i]); + } + } + { + int[] counter = new int[] { 0 }; + LatLonGeometry[] geometries = LuceneGeometriesUtils.toLatLonGeometry(multiPoint, true, t -> { + if (counter[0]++ == 0) { + assertEquals(ShapeType.MULTIPOINT, t); + } else { + assertEquals(ShapeType.POINT, t); + } + }); + assertEquals(multiPoint.size(), geometries.length); + for (int i = 0; i < multiPoint.size(); i++) { + assertLatLonPoint(quantize(multiPoint.get(i)), geometries[i]); + } + } + } + + private void assertLatLonPoint(Point point, LatLonGeometry geometry) { + assertThat(geometry, instanceOf(org.apache.lucene.geo.Point.class)); + org.apache.lucene.geo.Point lalonPoint = (org.apache.lucene.geo.Point) geometry; + assertThat(lalonPoint.getLon(), equalTo(point.getLon())); + assertThat(lalonPoint.getLat(), equalTo(point.getLat())); + assertThat(geometry, equalTo(LuceneGeometriesUtils.toLatLonPoint(point))); + } + + public void testXYPoint() { + Point point = ShapeTestUtils.randomPoint(); + XYGeometry[] geometries = LuceneGeometriesUtils.toXYGeometry(point, t -> assertEquals(ShapeType.POINT, t)); + assertEquals(1, geometries.length); + assertXYPoint(point, geometries[0]); + assertThat(geometries[0], instanceOf(org.apache.lucene.geo.XYPoint.class)); + } + + public void testXYMultiPoint() { + MultiPoint multiPoint = ShapeTestUtils.randomMultiPoint(randomBoolean()); + int[] counter = new int[] { 0 }; + XYGeometry[] geometries = LuceneGeometriesUtils.toXYGeometry(multiPoint, t -> { + if (counter[0]++ == 0) { + assertEquals(ShapeType.MULTIPOINT, t); + } else { + assertEquals(ShapeType.POINT, t); + } + }); + assertEquals(multiPoint.size(), geometries.length); + for (int i = 0; i < multiPoint.size(); i++) { + assertXYPoint(multiPoint.get(i), geometries[i]); + } + } + + private void assertXYPoint(Point point, XYGeometry geometry) { + assertThat(geometry, instanceOf(org.apache.lucene.geo.XYPoint.class)); + org.apache.lucene.geo.XYPoint xyPoint = (org.apache.lucene.geo.XYPoint) geometry; + assertThat(xyPoint.getX(), equalTo((float) point.getX())); + assertThat(xyPoint.getY(), equalTo((float) point.getY())); + assertThat(geometry, equalTo(LuceneGeometriesUtils.toXYPoint(point))); + } + + public void testLatLonLine() { + Line line = GeometryTestUtils.randomLine(randomBoolean()); + { + LatLonGeometry[] geometries = LuceneGeometriesUtils.toLatLonGeometry(line, false, t -> assertEquals(ShapeType.LINESTRING, t)); + assertEquals(1, geometries.length); + assertLatLonLine(line, geometries[0]); + } + { + LatLonGeometry[] geometries = LuceneGeometriesUtils.toLatLonGeometry(line, true, t -> assertEquals(ShapeType.LINESTRING, t)); + assertEquals(1, geometries.length); + assertLatLonLine(quantize(line), geometries[0]); + } + } + + public void testLatLonMultiLine() { + MultiLine multiLine = GeometryTestUtils.randomMultiLine(randomBoolean()); + { + int[] counter = new int[] { 0 }; + LatLonGeometry[] geometries = LuceneGeometriesUtils.toLatLonGeometry(multiLine, false, t -> { + if (counter[0]++ == 0) { + assertEquals(ShapeType.MULTILINESTRING, t); + } else { + assertEquals(ShapeType.LINESTRING, t); + } + }); + assertEquals(multiLine.size(), geometries.length); + for (int i = 0; i < multiLine.size(); i++) { + assertLatLonLine(multiLine.get(i), geometries[i]); + } + } + { + int[] counter = new int[] { 0 }; + LatLonGeometry[] geometries = LuceneGeometriesUtils.toLatLonGeometry(multiLine, true, t -> { + if (counter[0]++ == 0) { + assertEquals(ShapeType.MULTILINESTRING, t); + } else { + assertEquals(ShapeType.LINESTRING, t); + } + }); + assertEquals(multiLine.size(), geometries.length); + for (int i = 0; i < multiLine.size(); i++) { + assertLatLonLine(quantize(multiLine.get(i)), geometries[i]); + } + } + } + + private void assertLatLonLine(Line line, LatLonGeometry geometry) { + assertThat(geometry, instanceOf(org.apache.lucene.geo.Line.class)); + org.apache.lucene.geo.Line lalonLine = (org.apache.lucene.geo.Line) geometry; + assertThat(lalonLine.getLons(), equalTo(line.getLons())); + assertThat(lalonLine.getLats(), equalTo(line.getLats())); + assertThat(geometry, equalTo(LuceneGeometriesUtils.toLatLonLine(line))); + } + + public void testXYLine() { + Line line = ShapeTestUtils.randomLine(randomBoolean()); + XYGeometry[] geometries = LuceneGeometriesUtils.toXYGeometry(line, t -> assertEquals(ShapeType.LINESTRING, t)); + assertEquals(1, geometries.length); + assertXYLine(line, geometries[0]); + } + + public void testXYMultiLine() { + MultiLine multiLine = ShapeTestUtils.randomMultiLine(randomBoolean()); + int[] counter = new int[] { 0 }; + XYGeometry[] geometries = LuceneGeometriesUtils.toXYGeometry(multiLine, t -> { + if (counter[0]++ == 0) { + assertEquals(ShapeType.MULTILINESTRING, t); + } else { + assertEquals(ShapeType.LINESTRING, t); + } + }); + assertEquals(multiLine.size(), geometries.length); + for (int i = 0; i < multiLine.size(); i++) { + assertXYLine(multiLine.get(i), geometries[i]); + } + } + + private void assertXYLine(Line line, XYGeometry geometry) { + assertThat(geometry, instanceOf(org.apache.lucene.geo.XYLine.class)); + org.apache.lucene.geo.XYLine xyLine = (org.apache.lucene.geo.XYLine) geometry; + assertThat(xyLine.getX(), equalTo(LuceneGeometriesUtils.doubleArrayToFloatArray(line.getLons()))); + assertThat(xyLine.getY(), equalTo(LuceneGeometriesUtils.doubleArrayToFloatArray(line.getLats()))); + assertThat(geometry, equalTo(LuceneGeometriesUtils.toXYLine(line))); + } + + public void testLatLonPolygon() { + Polygon polygon = validRandomPolygon(randomBoolean()); + { + LatLonGeometry[] geometries = LuceneGeometriesUtils.toLatLonGeometry(polygon, false, t -> assertEquals(ShapeType.POLYGON, t)); + assertEquals(1, geometries.length); + assertLatLonPolygon(polygon, geometries[0]); + } + { + LatLonGeometry[] geometries = LuceneGeometriesUtils.toLatLonGeometry(polygon, true, t -> assertEquals(ShapeType.POLYGON, t)); + assertEquals(1, geometries.length); + assertLatLonPolygon(quantize(polygon), geometries[0]); + } + } + + public void testLatLonMultiPolygon() { + MultiPolygon multiPolygon = validRandomMultiPolygon(randomBoolean()); + { + int[] counter = new int[] { 0 }; + LatLonGeometry[] geometries = LuceneGeometriesUtils.toLatLonGeometry(multiPolygon, false, t -> { + if (counter[0]++ == 0) { + assertEquals(ShapeType.MULTIPOLYGON, t); + } else { + assertEquals(ShapeType.POLYGON, t); + } + }); + assertEquals(multiPolygon.size(), geometries.length); + for (int i = 0; i < multiPolygon.size(); i++) { + assertLatLonPolygon(multiPolygon.get(i), geometries[i]); + } + } + { + int[] counter = new int[] { 0 }; + LatLonGeometry[] geometries = LuceneGeometriesUtils.toLatLonGeometry(multiPolygon, true, t -> { + if (counter[0]++ == 0) { + assertEquals(ShapeType.MULTIPOLYGON, t); + } else { + assertEquals(ShapeType.POLYGON, t); + } + }); + assertEquals(multiPolygon.size(), geometries.length); + for (int i = 0; i < multiPolygon.size(); i++) { + assertLatLonPolygon(quantize(multiPolygon.get(i)), geometries[i]); + } + } + } + + private void assertLatLonPolygon(Polygon polygon, LatLonGeometry geometry) { + assertThat(geometry, instanceOf(org.apache.lucene.geo.Polygon.class)); + org.apache.lucene.geo.Polygon lalonPolygon = (org.apache.lucene.geo.Polygon) geometry; + assertThat(lalonPolygon.getPolyLons(), equalTo(polygon.getPolygon().getLons())); + assertThat(lalonPolygon.getPolyLats(), equalTo(polygon.getPolygon().getLats())); + assertThat(geometry, equalTo(LuceneGeometriesUtils.toLatLonPolygon(polygon))); + } + + public void testXYPolygon() { + Polygon polygon = ShapeTestUtils.randomPolygon(randomBoolean()); + XYGeometry[] geometries = LuceneGeometriesUtils.toXYGeometry(polygon, t -> assertEquals(ShapeType.POLYGON, t)); + assertEquals(1, geometries.length); + assertXYPolygon(polygon, geometries[0]); + } + + public void testXYMultiPolygon() { + MultiPolygon multiPolygon = ShapeTestUtils.randomMultiPolygon(randomBoolean()); + int[] counter = new int[] { 0 }; + XYGeometry[] geometries = LuceneGeometriesUtils.toXYGeometry(multiPolygon, t -> { + if (counter[0]++ == 0) { + assertEquals(ShapeType.MULTIPOLYGON, t); + } else { + assertEquals(ShapeType.POLYGON, t); + } + }); + assertEquals(multiPolygon.size(), geometries.length); + for (int i = 0; i < multiPolygon.size(); i++) { + assertXYPolygon(multiPolygon.get(i), geometries[i]); + } + } + + private void assertXYPolygon(Polygon polygon, XYGeometry geometry) { + assertThat(geometry, instanceOf(org.apache.lucene.geo.XYPolygon.class)); + org.apache.lucene.geo.XYPolygon xyPolygon = (org.apache.lucene.geo.XYPolygon) geometry; + assertThat(xyPolygon.getPolyX(), equalTo(LuceneGeometriesUtils.doubleArrayToFloatArray(polygon.getPolygon().getX()))); + assertThat(xyPolygon.getPolyY(), equalTo(LuceneGeometriesUtils.doubleArrayToFloatArray(polygon.getPolygon().getY()))); + assertThat(geometry, equalTo(LuceneGeometriesUtils.toXYPolygon(polygon))); + } + + public void testLatLonGeometryCollection() { + boolean hasZ = randomBoolean(); + Point point = GeometryTestUtils.randomPoint(hasZ); + Line line = GeometryTestUtils.randomLine(hasZ); + Polygon polygon = validRandomPolygon(hasZ); + GeometryCollection geometryCollection = new GeometryCollection<>(List.of(point, line, polygon)); + { + int[] counter = new int[] { 0 }; + LatLonGeometry[] geometries = LuceneGeometriesUtils.toLatLonGeometry(geometryCollection, false, t -> { + if (counter[0] == 0) { + assertEquals(ShapeType.GEOMETRYCOLLECTION, t); + } else if (counter[0] == 1) { + assertEquals(ShapeType.POINT, t); + } else if (counter[0] == 2) { + assertEquals(ShapeType.LINESTRING, t); + } else if (counter[0] == 3) { + assertEquals(ShapeType.POLYGON, t); + } else { + fail("Unexpected counter value"); + } + counter[0]++; + }); + assertEquals(geometryCollection.size(), geometries.length); + assertLatLonPoint(point, geometries[0]); + assertLatLonLine(line, geometries[1]); + assertLatLonPolygon(polygon, geometries[2]); + } + { + int[] counter = new int[] { 0 }; + LatLonGeometry[] geometries = LuceneGeometriesUtils.toLatLonGeometry(geometryCollection, true, t -> { + if (counter[0] == 0) { + assertEquals(ShapeType.GEOMETRYCOLLECTION, t); + } else if (counter[0] == 1) { + assertEquals(ShapeType.POINT, t); + } else if (counter[0] == 2) { + assertEquals(ShapeType.LINESTRING, t); + } else if (counter[0] == 3) { + assertEquals(ShapeType.POLYGON, t); + } else { + fail("Unexpected counter value"); + } + counter[0]++; + }); + assertEquals(geometryCollection.size(), geometries.length); + assertLatLonPoint(quantize(point), geometries[0]); + assertLatLonLine(quantize(line), geometries[1]); + assertLatLonPolygon(quantize(polygon), geometries[2]); + } + } + + public void testXYGeometryCollection() { + boolean hasZ = randomBoolean(); + Point point = ShapeTestUtils.randomPoint(hasZ); + Line line = ShapeTestUtils.randomLine(hasZ); + Polygon polygon = ShapeTestUtils.randomPolygon(hasZ); + GeometryCollection geometryCollection = new GeometryCollection<>(List.of(point, line, polygon)); + int[] counter = new int[] { 0 }; + XYGeometry[] geometries = LuceneGeometriesUtils.toXYGeometry(geometryCollection, t -> { + if (counter[0] == 0) { + assertEquals(ShapeType.GEOMETRYCOLLECTION, t); + } else if (counter[0] == 1) { + assertEquals(ShapeType.POINT, t); + } else if (counter[0] == 2) { + assertEquals(ShapeType.LINESTRING, t); + } else if (counter[0] == 3) { + assertEquals(ShapeType.POLYGON, t); + } else { + fail("Unexpected counter value"); + } + counter[0]++; + }); + assertEquals(geometryCollection.size(), geometries.length); + assertXYPoint(point, geometries[0]); + assertXYLine(line, geometries[1]); + assertXYPolygon(polygon, geometries[2]); + } + + private Polygon validRandomPolygon(boolean hasLat) { + return randomValueOtherThanMany( + polygon -> GeometryNormalizer.needsNormalize(Orientation.CCW, polygon), + () -> GeometryTestUtils.randomPolygon(hasLat) + ); + } + + public void testLatLonRectangle() { + Rectangle rectangle = GeometryTestUtils.randomRectangle(); + LatLonGeometry[] geometries = LuceneGeometriesUtils.toLatLonGeometry(rectangle, false, t -> assertEquals(ShapeType.ENVELOPE, t)); + assertEquals(1, geometries.length); + assertLatLonRectangle(rectangle, geometries[0]); + } + + private void assertLatLonRectangle(Rectangle rectangle, LatLonGeometry geometry) { + assertThat(geometry, instanceOf(org.apache.lucene.geo.Rectangle.class)); + org.apache.lucene.geo.Rectangle lalonRectangle = (org.apache.lucene.geo.Rectangle) geometry; + assertThat(lalonRectangle.maxLon, equalTo(rectangle.getMaxLon())); + assertThat(lalonRectangle.minLon, equalTo(rectangle.getMinLon())); + assertThat(lalonRectangle.maxLat, equalTo(rectangle.getMaxLat())); + assertThat(lalonRectangle.minLat, equalTo(rectangle.getMinLat())); + assertThat(geometry, equalTo(LuceneGeometriesUtils.toLatLonRectangle(rectangle))); + } + + public void testXYRectangle() { + Rectangle rectangle = ShapeTestUtils.randomRectangle(); + XYGeometry[] geometries = LuceneGeometriesUtils.toXYGeometry(rectangle, t -> assertEquals(ShapeType.ENVELOPE, t)); + assertEquals(1, geometries.length); + assertXYRectangle(rectangle, geometries[0]); + } + + private void assertXYRectangle(Rectangle rectangle, XYGeometry geometry) { + assertThat(geometry, instanceOf(org.apache.lucene.geo.XYRectangle.class)); + org.apache.lucene.geo.XYRectangle xyRectangle = (org.apache.lucene.geo.XYRectangle) geometry; + assertThat(xyRectangle.maxX, equalTo((float) rectangle.getMaxX())); + assertThat(xyRectangle.minX, equalTo((float) rectangle.getMinX())); + assertThat(xyRectangle.maxY, equalTo((float) rectangle.getMaxY())); + assertThat(xyRectangle.minY, equalTo((float) rectangle.getMinY())); + assertThat(geometry, equalTo(LuceneGeometriesUtils.toXYRectangle(rectangle))); + } + + public void testLatLonCircle() { + Circle circle = GeometryTestUtils.randomCircle(randomBoolean()); + LatLonGeometry[] geometries = LuceneGeometriesUtils.toLatLonGeometry(circle, false, t -> assertEquals(ShapeType.CIRCLE, t)); + assertEquals(1, geometries.length); + assertLatLonCircle(circle, geometries[0]); + } + + private void assertLatLonCircle(Circle circle, LatLonGeometry geometry) { + assertThat(geometry, instanceOf(org.apache.lucene.geo.Circle.class)); + org.apache.lucene.geo.Circle lalonCircle = (org.apache.lucene.geo.Circle) geometry; + assertThat(lalonCircle.getLon(), equalTo(circle.getLon())); + assertThat(lalonCircle.getLat(), equalTo(circle.getLat())); + assertThat(lalonCircle.getRadius(), equalTo(circle.getRadiusMeters())); + assertThat(geometry, equalTo(LuceneGeometriesUtils.toLatLonCircle(circle))); + } + + public void testXYCircle() { + Circle circle = ShapeTestUtils.randomCircle(randomBoolean()); + XYGeometry[] geometries = LuceneGeometriesUtils.toXYGeometry(circle, t -> assertEquals(ShapeType.CIRCLE, t)); + assertEquals(1, geometries.length); + assertXYCircle(circle, geometries[0]); + } + + private void assertXYCircle(Circle circle, XYGeometry geometry) { + assertThat(geometry, instanceOf(org.apache.lucene.geo.XYCircle.class)); + org.apache.lucene.geo.XYCircle xyCircle = (org.apache.lucene.geo.XYCircle) geometry; + assertThat(xyCircle.getX(), equalTo((float) circle.getX())); + assertThat(xyCircle.getY(), equalTo((float) circle.getY())); + assertThat(xyCircle.getRadius(), equalTo((float) circle.getRadiusMeters())); + assertThat(geometry, equalTo(LuceneGeometriesUtils.toXYCircle(circle))); + } + + private MultiPolygon validRandomMultiPolygon(boolean hasLat) { + // make sure we don't generate a polygon that gets splitted across the dateline + return randomValueOtherThanMany( + multiPolygon -> GeometryNormalizer.needsNormalize(Orientation.CCW, multiPolygon), + () -> GeometryTestUtils.randomMultiPolygon(hasLat) + ); + } + + private Point quantize(Point point) { + return new Point(GeoUtils.quantizeLon(point.getLon()), GeoUtils.quantizeLat(point.getLat())); + } + + private Line quantize(Line line) { + return new Line( + LuceneGeometriesUtils.LATLON_QUANTIZER.quantizeLons(line.getLons()), + LuceneGeometriesUtils.LATLON_QUANTIZER.quantizeLats(line.getLats()) + ); + } + + private Polygon quantize(Polygon polygon) { + List holes = new ArrayList<>(polygon.getNumberOfHoles()); + for (int i = 0; i < polygon.getNumberOfHoles(); i++) { + holes.add(quantize(polygon.getHole(i))); + } + return new Polygon(quantize(polygon.getPolygon()), holes); + } + + private LinearRing quantize(LinearRing linearRing) { + return new LinearRing( + LuceneGeometriesUtils.LATLON_QUANTIZER.quantizeLons(linearRing.getLons()), + LuceneGeometriesUtils.LATLON_QUANTIZER.quantizeLats(linearRing.getLats()) + ); + } +} diff --git a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/common/ShapeUtils.java b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/common/ShapeUtils.java deleted file mode 100644 index 289fbe6e707ca..0000000000000 --- a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/common/ShapeUtils.java +++ /dev/null @@ -1,68 +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.spatial.common; - -import org.elasticsearch.geometry.Circle; -import org.elasticsearch.geometry.Line; -import org.elasticsearch.geometry.Point; -import org.elasticsearch.geometry.Polygon; -import org.elasticsearch.geometry.Rectangle; - -/** - * Utility class that transforms Elasticsearch geometry objects to the Lucene representation - */ -public class ShapeUtils { - // no instance: - private ShapeUtils() {} - - public static org.apache.lucene.geo.XYPolygon toLuceneXYPolygon(Polygon polygon) { - org.apache.lucene.geo.XYPolygon[] holes = new org.apache.lucene.geo.XYPolygon[polygon.getNumberOfHoles()]; - for (int i = 0; i < holes.length; i++) { - holes[i] = new org.apache.lucene.geo.XYPolygon( - doubleArrayToFloatArray(polygon.getHole(i).getX()), - doubleArrayToFloatArray(polygon.getHole(i).getY()) - ); - } - return new org.apache.lucene.geo.XYPolygon( - doubleArrayToFloatArray(polygon.getPolygon().getX()), - doubleArrayToFloatArray(polygon.getPolygon().getY()), - holes - ); - } - - public static org.apache.lucene.geo.XYPolygon toLuceneXYPolygon(Rectangle r) { - return new org.apache.lucene.geo.XYPolygon( - new float[] { (float) r.getMinX(), (float) r.getMaxX(), (float) r.getMaxX(), (float) r.getMinX(), (float) r.getMinX() }, - new float[] { (float) r.getMinY(), (float) r.getMinY(), (float) r.getMaxY(), (float) r.getMaxY(), (float) r.getMinY() } - ); - } - - public static org.apache.lucene.geo.XYRectangle toLuceneXYRectangle(Rectangle r) { - return new org.apache.lucene.geo.XYRectangle((float) r.getMinX(), (float) r.getMaxX(), (float) r.getMinY(), (float) r.getMaxY()); - } - - public static org.apache.lucene.geo.XYPoint toLuceneXYPoint(Point point) { - return new org.apache.lucene.geo.XYPoint((float) point.getX(), (float) point.getY()); - } - - public static org.apache.lucene.geo.XYLine toLuceneXYLine(Line line) { - return new org.apache.lucene.geo.XYLine(doubleArrayToFloatArray(line.getX()), doubleArrayToFloatArray(line.getY())); - } - - public static org.apache.lucene.geo.XYCircle toLuceneXYCircle(Circle circle) { - return new org.apache.lucene.geo.XYCircle((float) circle.getX(), (float) circle.getY(), (float) circle.getRadiusMeters()); - } - - private static float[] doubleArrayToFloatArray(double[] array) { - float[] result = new float[array.length]; - for (int i = 0; i < array.length; ++i) { - result[i] = (float) array[i]; - } - return result; - } - -} diff --git a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/mapper/CartesianShapeIndexer.java b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/mapper/CartesianShapeIndexer.java index b8e665c0c768a..c23d63baa5791 100644 --- a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/mapper/CartesianShapeIndexer.java +++ b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/mapper/CartesianShapeIndexer.java @@ -8,6 +8,7 @@ import org.apache.lucene.document.XYShape; import org.apache.lucene.index.IndexableField; +import org.elasticsearch.common.geo.LuceneGeometriesUtils; import org.elasticsearch.geometry.Circle; import org.elasticsearch.geometry.Geometry; import org.elasticsearch.geometry.GeometryCollection; @@ -21,7 +22,6 @@ import org.elasticsearch.geometry.Polygon; import org.elasticsearch.geometry.Rectangle; import org.elasticsearch.index.mapper.ShapeIndexer; -import org.elasticsearch.xpack.spatial.common.ShapeUtils; import java.util.ArrayList; import java.util.Arrays; @@ -70,7 +70,7 @@ public Void visit(GeometryCollection collection) { @Override public Void visit(Line line) { - addFields(XYShape.createIndexableFields(name, ShapeUtils.toLuceneXYLine(line))); + addFields(XYShape.createIndexableFields(name, LuceneGeometriesUtils.toXYLine(line))); return null; } @@ -111,13 +111,13 @@ public Void visit(Point point) { @Override public Void visit(Polygon polygon) { - addFields(XYShape.createIndexableFields(name, ShapeUtils.toLuceneXYPolygon(polygon), true)); + addFields(XYShape.createIndexableFields(name, LuceneGeometriesUtils.toXYPolygon(polygon), true)); return null; } @Override public Void visit(Rectangle r) { - addFields(XYShape.createIndexableFields(name, ShapeUtils.toLuceneXYPolygon(r))); + addFields(XYShape.createIndexableFields(name, toLuceneXYPolygon(r))); return null; } @@ -125,4 +125,11 @@ private void addFields(IndexableField[] fields) { this.fields.addAll(Arrays.asList(fields)); } } + + private static org.apache.lucene.geo.XYPolygon toLuceneXYPolygon(Rectangle r) { + return new org.apache.lucene.geo.XYPolygon( + new float[] { (float) r.getMinX(), (float) r.getMaxX(), (float) r.getMaxX(), (float) r.getMinX(), (float) r.getMinX() }, + new float[] { (float) r.getMinY(), (float) r.getMinY(), (float) r.getMaxY(), (float) r.getMaxY(), (float) r.getMinY() } + ); + } } diff --git a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/query/ShapeQueryPointProcessor.java b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/query/ShapeQueryPointProcessor.java index d455d0f539cfa..a8c084e7e0f01 100644 --- a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/query/ShapeQueryPointProcessor.java +++ b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/query/ShapeQueryPointProcessor.java @@ -8,44 +8,43 @@ import org.apache.lucene.document.XYDocValuesField; import org.apache.lucene.document.XYPointField; -import org.apache.lucene.geo.XYCircle; -import org.apache.lucene.geo.XYRectangle; -import org.apache.lucene.search.BooleanClause; -import org.apache.lucene.search.BooleanQuery; +import org.apache.lucene.geo.XYGeometry; import org.apache.lucene.search.IndexOrDocValuesQuery; import org.apache.lucene.search.Query; +import org.elasticsearch.common.geo.LuceneGeometriesUtils; import org.elasticsearch.common.geo.ShapeRelation; -import org.elasticsearch.geometry.Circle; import org.elasticsearch.geometry.Geometry; -import org.elasticsearch.geometry.GeometryCollection; -import org.elasticsearch.geometry.GeometryVisitor; -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.geometry.ShapeType; import org.elasticsearch.index.mapper.MappedFieldType; import org.elasticsearch.index.query.QueryShardException; import org.elasticsearch.index.query.SearchExecutionContext; -import org.elasticsearch.xpack.spatial.common.ShapeUtils; import org.elasticsearch.xpack.spatial.index.mapper.PointFieldMapper; +import java.util.function.Consumer; + public class ShapeQueryPointProcessor { - public Query shapeQuery(Geometry shape, String fieldName, ShapeRelation relation, SearchExecutionContext context) { - validateIsPointFieldType(fieldName, context); + public Query shapeQuery(Geometry geometry, String fieldName, ShapeRelation relation, SearchExecutionContext context) { + final boolean hasDocValues = validateIsPointFieldType(fieldName, context); // only the intersects relation is supported for indexed cartesian point types if (relation != ShapeRelation.INTERSECTS) { throw new QueryShardException(context, relation + " query relation not supported for Field [" + fieldName + "]."); } - // wrap XYPoint query as a ConstantScoreQuery - return getVectorQueryFromShape(shape, fieldName, relation, context); + final Consumer checker = t -> { + if (t == ShapeType.POINT || t == ShapeType.MULTIPOINT || t == ShapeType.LINESTRING || t == ShapeType.MULTILINESTRING) { + throw new QueryShardException(context, "Field [" + fieldName + "] does not support " + t + " queries"); + } + }; + final XYGeometry[] luceneGeometries = LuceneGeometriesUtils.toXYGeometry(geometry, checker); + Query query = XYPointField.newGeometryQuery(fieldName, luceneGeometries); + if (hasDocValues) { + final Query queryDocValues = XYDocValuesField.newSlowGeometryQuery(fieldName, luceneGeometries); + query = new IndexOrDocValuesQuery(query, queryDocValues); + } + return query; } - private void validateIsPointFieldType(String fieldName, SearchExecutionContext context) { + private boolean validateIsPointFieldType(String fieldName, SearchExecutionContext context) { MappedFieldType fieldType = context.getFieldType(fieldName); if (fieldType instanceof PointFieldMapper.PointFieldType == false) { throw new QueryShardException( @@ -53,118 +52,6 @@ private void validateIsPointFieldType(String fieldName, SearchExecutionContext c "Expected " + PointFieldMapper.CONTENT_TYPE + " field type for Field [" + fieldName + "] but found " + fieldType.typeName() ); } - } - - protected Query getVectorQueryFromShape(Geometry queryShape, String fieldName, ShapeRelation relation, SearchExecutionContext context) { - ShapeVisitor shapeVisitor = new ShapeVisitor(context, fieldName, relation); - return queryShape.visit(shapeVisitor); - } - - private class ShapeVisitor implements GeometryVisitor { - SearchExecutionContext context; - MappedFieldType fieldType; - String fieldName; - ShapeRelation relation; - - ShapeVisitor(SearchExecutionContext context, String fieldName, ShapeRelation relation) { - this.context = context; - this.fieldType = context.getFieldType(fieldName); - this.fieldName = fieldName; - this.relation = relation; - } - - @Override - public Query visit(Circle circle) { - XYCircle xyCircle = ShapeUtils.toLuceneXYCircle(circle); - Query query = XYPointField.newDistanceQuery(fieldName, xyCircle.getX(), xyCircle.getY(), xyCircle.getRadius()); - if (fieldType.hasDocValues()) { - Query dvQuery = XYDocValuesField.newSlowDistanceQuery(fieldName, xyCircle.getX(), xyCircle.getY(), xyCircle.getRadius()); - query = new IndexOrDocValuesQuery(query, dvQuery); - } - return query; - } - - @Override - public Query visit(GeometryCollection collection) { - BooleanQuery.Builder bqb = new BooleanQuery.Builder(); - visit(bqb, collection); - return bqb.build(); - } - - private void visit(BooleanQuery.Builder bqb, GeometryCollection collection) { - BooleanClause.Occur occur = BooleanClause.Occur.FILTER; - for (Geometry shape : collection) { - bqb.add(shape.visit(this), occur); - } - } - - @Override - public Query visit(org.elasticsearch.geometry.Line line) { - throw new QueryShardException(context, "Field [" + fieldName + "] does not support " + ShapeType.LINESTRING + " queries"); - } - - @Override - // don't think this is called directly - public Query visit(LinearRing ring) { - throw new QueryShardException(context, "Field [" + fieldName + "] does not support " + ShapeType.LINEARRING + " queries"); - } - - @Override - public Query visit(MultiLine multiLine) { - throw new QueryShardException(context, "Field [" + fieldName + "] does not support " + ShapeType.MULTILINESTRING + " queries"); - } - - @Override - public Query visit(MultiPoint multiPoint) { - throw new QueryShardException(context, "Field [" + fieldName + "] does not support " + ShapeType.MULTIPOINT + " queries"); - } - - @Override - public Query visit(MultiPolygon multiPolygon) { - org.apache.lucene.geo.XYPolygon[] lucenePolygons = new org.apache.lucene.geo.XYPolygon[multiPolygon.size()]; - for (int i = 0; i < multiPolygon.size(); i++) { - lucenePolygons[i] = ShapeUtils.toLuceneXYPolygon(multiPolygon.get(i)); - } - Query query = XYPointField.newPolygonQuery(fieldName, lucenePolygons); - if (fieldType.hasDocValues()) { - Query dvQuery = XYDocValuesField.newSlowPolygonQuery(fieldName, lucenePolygons); - query = new IndexOrDocValuesQuery(query, dvQuery); - } - return query; - } - - @Override - public Query visit(Point point) { - // not currently supported - throw new QueryShardException(context, "Field [" + fieldName + "] does not support " + ShapeType.POINT + " queries"); - } - - @Override - public Query visit(Polygon polygon) { - org.apache.lucene.geo.XYPolygon lucenePolygon = ShapeUtils.toLuceneXYPolygon(polygon); - Query query = XYPointField.newPolygonQuery(fieldName, lucenePolygon); - if (fieldType.hasDocValues()) { - Query dvQuery = XYDocValuesField.newSlowPolygonQuery(fieldName, lucenePolygon); - query = new IndexOrDocValuesQuery(query, dvQuery); - } - return query; - } - - @Override - public Query visit(Rectangle r) { - XYRectangle xyRectangle = ShapeUtils.toLuceneXYRectangle(r); - Query query = XYPointField.newBoxQuery(fieldName, xyRectangle.minX, xyRectangle.maxX, xyRectangle.minY, xyRectangle.maxY); - if (fieldType.hasDocValues()) { - Query dvQuery = XYDocValuesField.newSlowBoxQuery( - fieldName, - xyRectangle.minX, - xyRectangle.maxX, - xyRectangle.minY, - xyRectangle.maxY - ); - query = new IndexOrDocValuesQuery(query, dvQuery); - } - return query; - } + return fieldType.hasDocValues(); } } diff --git a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/query/ShapeQueryProcessor.java b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/query/ShapeQueryProcessor.java index ac526e6016b23..4bb9e988c0f90 100644 --- a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/query/ShapeQueryProcessor.java +++ b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/query/ShapeQueryProcessor.java @@ -11,34 +11,20 @@ import org.apache.lucene.search.IndexOrDocValuesQuery; import org.apache.lucene.search.MatchNoDocsQuery; import org.apache.lucene.search.Query; +import org.elasticsearch.common.geo.LuceneGeometriesUtils; import org.elasticsearch.common.geo.ShapeRelation; -import org.elasticsearch.geometry.Circle; import org.elasticsearch.geometry.Geometry; -import org.elasticsearch.geometry.GeometryCollection; -import org.elasticsearch.geometry.GeometryVisitor; -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.index.IndexVersions; import org.elasticsearch.index.mapper.MappedFieldType; import org.elasticsearch.index.query.QueryShardException; import org.elasticsearch.index.query.SearchExecutionContext; -import org.elasticsearch.xpack.spatial.common.ShapeUtils; import org.elasticsearch.xpack.spatial.index.mapper.CartesianShapeDocValuesQuery; import org.elasticsearch.xpack.spatial.index.mapper.ShapeFieldMapper; -import java.util.ArrayList; -import java.util.List; - public class ShapeQueryProcessor { public Query shapeQuery( - Geometry shape, + Geometry geometry, String fieldName, ShapeRelation relation, SearchExecutionContext context, @@ -49,10 +35,21 @@ public Query shapeQuery( if (relation == ShapeRelation.CONTAINS && context.indexVersionCreated().before(IndexVersions.V_7_5_0)) { throw new QueryShardException(context, ShapeRelation.CONTAINS + " query relation not supported for Field [" + fieldName + "]."); } - if (shape == null) { + if (geometry == null || geometry.isEmpty()) { return new MatchNoDocsQuery(); } - return getVectorQueryFromShape(shape, fieldName, relation, context, hasDocValues); + final XYGeometry[] luceneGeometries; + try { + luceneGeometries = LuceneGeometriesUtils.toXYGeometry(geometry, t -> {}); + } catch (IllegalArgumentException e) { + throw new QueryShardException(context, "Exception creating query on Field [" + fieldName + "] " + e.getMessage(), e); + } + Query query = XYShape.newGeometryQuery(fieldName, relation.getLuceneRelation(), luceneGeometries); + if (hasDocValues) { + final Query queryDocValues = new CartesianShapeDocValuesQuery(fieldName, relation.getLuceneRelation(), luceneGeometries); + query = new IndexOrDocValuesQuery(query, queryDocValues); + } + return query; } private void validateIsShapeFieldType(String fieldName, SearchExecutionContext context) { @@ -64,119 +61,4 @@ private void validateIsShapeFieldType(String fieldName, SearchExecutionContext c ); } } - - private Query getVectorQueryFromShape( - Geometry queryShape, - String fieldName, - ShapeRelation relation, - SearchExecutionContext context, - boolean hasDocValues - ) { - final LuceneGeometryCollector visitor = new LuceneGeometryCollector(fieldName, context); - queryShape.visit(visitor); - final List geomList = visitor.geometries(); - if (geomList.size() == 0) { - return new MatchNoDocsQuery(); - } - XYGeometry[] geometries = geomList.toArray(new XYGeometry[0]); - Query query = XYShape.newGeometryQuery(fieldName, relation.getLuceneRelation(), geometries); - if (hasDocValues) { - final Query queryDocValues = new CartesianShapeDocValuesQuery(fieldName, relation.getLuceneRelation(), geometries); - query = new IndexOrDocValuesQuery(query, queryDocValues); - } - return query; - } - - private static class LuceneGeometryCollector implements GeometryVisitor { - private final List geometries = new ArrayList<>(); - private final String name; - private final SearchExecutionContext context; - - private LuceneGeometryCollector(String name, SearchExecutionContext context) { - this.name = name; - this.context = context; - } - - List geometries() { - return geometries; - } - - @Override - public Void visit(Circle circle) { - if (circle.isEmpty() == false) { - geometries.add(ShapeUtils.toLuceneXYCircle(circle)); - } - return null; - } - - @Override - public Void visit(GeometryCollection collection) { - for (Geometry shape : collection) { - shape.visit(this); - } - return null; - } - - @Override - public Void visit(Line line) { - if (line.isEmpty() == false) { - geometries.add(ShapeUtils.toLuceneXYLine(line)); - } - return null; - } - - @Override - public Void visit(LinearRing ring) { - throw new QueryShardException(context, "Field [" + name + "] found and unsupported shape LinearRing"); - } - - @Override - public Void visit(MultiLine multiLine) { - for (Line line : multiLine) { - visit(line); - } - return null; - } - - @Override - public Void visit(MultiPoint multiPoint) { - for (Point point : multiPoint) { - visit(point); - } - return null; - } - - @Override - public Void visit(MultiPolygon multiPolygon) { - for (Polygon polygon : multiPolygon) { - visit(polygon); - } - return null; - } - - @Override - public Void visit(Point point) { - if (point.isEmpty() == false) { - geometries.add(ShapeUtils.toLuceneXYPoint(point)); - } - return null; - - } - - @Override - public Void visit(Polygon polygon) { - if (polygon.isEmpty() == false) { - geometries.add(ShapeUtils.toLuceneXYPolygon(polygon)); - } - return null; - } - - @Override - public Void visit(Rectangle r) { - if (r.isEmpty() == false) { - geometries.add(ShapeUtils.toLuceneXYRectangle(r)); - } - return null; - } - } } diff --git a/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/index/mapper/CartesianShapeDocValuesQueryTests.java b/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/index/mapper/CartesianShapeDocValuesQueryTests.java index ae5a6f182274b..f2148799d1b5f 100644 --- a/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/index/mapper/CartesianShapeDocValuesQueryTests.java +++ b/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/index/mapper/CartesianShapeDocValuesQueryTests.java @@ -24,12 +24,12 @@ import org.apache.lucene.store.Directory; import org.apache.lucene.tests.search.CheckHits; import org.apache.lucene.tests.search.QueryUtils; +import org.elasticsearch.common.geo.LuceneGeometriesUtils; import org.elasticsearch.core.IOUtils; import org.elasticsearch.geo.ShapeTestUtils; import org.elasticsearch.geo.XShapeTestUtil; import org.elasticsearch.geometry.Geometry; import org.elasticsearch.test.ESTestCase; -import org.elasticsearch.xpack.spatial.common.ShapeUtils; import org.elasticsearch.xpack.spatial.index.fielddata.CoordinateEncoder; import java.io.IOException; @@ -41,7 +41,7 @@ public class CartesianShapeDocValuesQueryTests extends ESTestCase { private static final String FIELD_NAME = "field"; public void testEqualsAndHashcode() { - XYPolygon polygon = ShapeUtils.toLuceneXYPolygon(ShapeTestUtils.randomPolygon(false)); + XYPolygon polygon = LuceneGeometriesUtils.toXYPolygon(ShapeTestUtils.randomPolygon(false)); Query q1 = new CartesianShapeDocValuesQuery(FIELD_NAME, ShapeField.QueryRelation.INTERSECTS, polygon); Query q2 = new CartesianShapeDocValuesQuery(FIELD_NAME, ShapeField.QueryRelation.INTERSECTS, polygon); QueryUtils.checkEqual(q1, q2); @@ -160,9 +160,9 @@ private XYGeometry[] randomLuceneQueryGeometries() { private XYGeometry randomLuceneQueryGeometry() { return switch (randomInt(3)) { - case 0 -> ShapeUtils.toLuceneXYPolygon(ShapeTestUtils.randomPolygon(false)); - case 1 -> ShapeUtils.toLuceneXYCircle(ShapeTestUtils.randomCircle(false)); - case 2 -> ShapeUtils.toLuceneXYPoint(ShapeTestUtils.randomPoint(false)); + case 0 -> LuceneGeometriesUtils.toXYPolygon(ShapeTestUtils.randomPolygon(false)); + case 1 -> LuceneGeometriesUtils.toXYCircle(ShapeTestUtils.randomCircle(false)); + case 2 -> LuceneGeometriesUtils.toXYPoint(ShapeTestUtils.randomPoint(false)); default -> XShapeTestUtil.nextBox(); }; }