Browse Source

GeoShapeValue can determine the spatial relationship with a LatLonGeometry (#89415)

This PR adds a new method to GeoShape value called GeoRelation relate(LatLonGeometry latLonGeometry) that 
replaces boolean intersects(Geometry geometry).
Ignacio Vera 3 năm trước cách đây
mục cha
commit
5d6af5890f

+ 7 - 3
x-pack/plugin/spatial/src/internalClusterTest/java/org/elasticsearch/xpack/spatial/search/GeoShapeScriptDocValuesIT.java

@@ -6,12 +6,12 @@
  */
 package org.elasticsearch.xpack.spatial.search;
 
+import org.apache.lucene.geo.Circle;
 import org.elasticsearch.action.search.SearchResponse;
 import org.elasticsearch.common.document.DocumentField;
 import org.elasticsearch.common.geo.GeoBoundingBox;
 import org.elasticsearch.common.geo.Orientation;
 import org.elasticsearch.geo.GeometryTestUtils;
-import org.elasticsearch.geometry.Circle;
 import org.elasticsearch.geometry.Geometry;
 import org.elasticsearch.geometry.Line;
 import org.elasticsearch.geometry.LinearRing;
@@ -32,6 +32,7 @@ import org.elasticsearch.xcontent.XContentFactory;
 import org.elasticsearch.xpack.core.LocalStateCompositeXPackPlugin;
 import org.elasticsearch.xpack.spatial.LocalStateSpatialPlugin;
 import org.elasticsearch.xpack.spatial.index.fielddata.DimensionalShapeType;
+import org.elasticsearch.xpack.spatial.index.fielddata.GeoRelation;
 import org.elasticsearch.xpack.spatial.index.fielddata.GeoShapeValues;
 import org.elasticsearch.xpack.spatial.util.GeoTestUtils;
 import org.hamcrest.Matchers;
@@ -273,8 +274,11 @@ public class GeoShapeScriptDocValuesIT extends ESSingleNodeTestCase {
 
         // Check label position is in the geometry, but with a tolerance constructed as a circle of 1m radius to handle quantization
         Point labelPosition = new Point(fields.get("label_lon").getValue(), fields.get("label_lat").getValue());
-        Circle tolerance = new Circle(labelPosition.getX(), labelPosition.getY(), 1);
-        assertTrue("Expect label position " + labelPosition + " to intersect geometry " + geometry, value.intersects(tolerance));
+        Circle tolerance = new Circle(labelPosition.getY(), labelPosition.getX(), 1);
+        assertTrue(
+            "Expect label position " + labelPosition + " to intersect geometry " + geometry,
+            value.relate(tolerance) != GeoRelation.QUERY_DISJOINT
+        );
 
         // Check that the label position is the expected one, or the centroid in certain polygon cases
         if (expectedLabelPosition != null) {

+ 20 - 19
x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/fielddata/GeoShapeValues.java

@@ -9,16 +9,14 @@ package org.elasticsearch.xpack.spatial.index.fielddata;
 
 import org.apache.lucene.document.ShapeField;
 import org.apache.lucene.geo.LatLonGeometry;
+import org.apache.lucene.geo.Point;
 import org.apache.lucene.util.BytesRef;
 import org.elasticsearch.common.geo.GeoPoint;
 import org.elasticsearch.common.geo.Orientation;
-import org.elasticsearch.common.geo.ShapeRelation;
 import org.elasticsearch.geometry.Geometry;
-import org.elasticsearch.geometry.Point;
 import org.elasticsearch.geometry.utils.GeographyValidator;
 import org.elasticsearch.geometry.utils.WellKnownText;
 import org.elasticsearch.index.mapper.GeoShapeIndexer;
-import org.elasticsearch.index.mapper.GeoShapeQueryable;
 import org.elasticsearch.search.aggregations.support.ValuesSourceType;
 import org.elasticsearch.xcontent.ToXContentFragment;
 import org.elasticsearch.xcontent.XContentBuilder;
@@ -93,11 +91,13 @@ public abstract class GeoShapeValues {
         private final GeometryDocValueReader reader;
         private final BoundingBox boundingBox;
         private final Tile2DVisitor tile2DVisitor;
+        private final LatLonGeometryRelationVisitor component2DRelationVisitor;
 
         public GeoShapeValue() {
             this.reader = new GeometryDocValueReader();
             this.boundingBox = new BoundingBox();
             this.tile2DVisitor = new Tile2DVisitor();
+            this.component2DRelationVisitor = new LatLonGeometryRelationVisitor(CoordinateEncoder.GEO);
         }
 
         /**
@@ -117,8 +117,16 @@ public abstract class GeoShapeValues {
          */
         public GeoPoint labelPosition() throws IOException {
             // For polygons we prefer to use the centroid, as long as it is within the polygon
-            if (reader.getDimensionalShapeType() == DimensionalShapeType.POLYGON && intersects(new Point(lon(), lat()))) {
-                return new GeoPoint(lat(), lon());
+            if (reader.getDimensionalShapeType() == DimensionalShapeType.POLYGON) {
+                Component2DVisitor visitor = Component2DVisitor.getVisitor(
+                    LatLonGeometry.create(new Point(lat(), lon())),
+                    ShapeField.QueryRelation.INTERSECTS,
+                    CoordinateEncoder.GEO
+                );
+                reader.visit(visitor);
+                if (visitor.matches()) {
+                    return new GeoPoint(lat(), lon());
+                }
             }
             // For all other cases, use the first triangle (or line or point) in the tree which will always intersect the shape
             LabelPositionVisitor<GeoPoint> visitor = new LabelPositionVisitor<>(CoordinateEncoder.GEO, (x, y) -> new GeoPoint(y, x));
@@ -137,21 +145,14 @@ public abstract class GeoShapeValues {
         }
 
         /**
-         * Determine if the current shape value intersects the specified geometry.
-         * Note that the intersection must be true in quantized space, so it is possible that
-         * points on the edges of geometries will return false due to quantization shifting them off the geometry.
-         * To deal with this, one option is to pass in a circle around the point with a 1m radius
-         * which is enough to cover the resolution of the quantization.
+         * Determine the {@link GeoRelation} between the current shape and a {@link LatLonGeometry}. It only supports
+         * simple geometries, therefore it will fail if the LatLonGeometry is a {@link org.apache.lucene.geo.Rectangle}
+         * that crosses the dateline.
          */
-        public boolean intersects(Geometry geometry) throws IOException {
-            LatLonGeometry[] latLonGeometries = GeoShapeQueryable.toQuantizeLuceneGeometry(geometry, ShapeRelation.INTERSECTS);
-            Component2DVisitor visitor = Component2DVisitor.getVisitor(
-                LatLonGeometry.create(latLonGeometries),
-                ShapeField.QueryRelation.INTERSECTS,
-                CoordinateEncoder.GEO
-            );
-            reader.visit(visitor);
-            return visitor.matches();
+        public GeoRelation relate(LatLonGeometry latLonGeometry) throws IOException {
+            component2DRelationVisitor.reset(latLonGeometry);
+            reader.visit(component2DRelationVisitor);
+            return component2DRelationVisitor.relation();
         }
 
         public DimensionalShapeType dimensionalShapeType() {

+ 116 - 0
x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/fielddata/LatLonGeometryRelationVisitor.java

@@ -0,0 +1,116 @@
+/*
+ * 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.index.fielddata;
+
+import org.apache.lucene.geo.Component2D;
+import org.apache.lucene.geo.LatLonGeometry;
+import org.apache.lucene.index.PointValues;
+
+/**
+ * A reusable tree reader visitor for a previous serialized {@link org.elasticsearch.geometry.Geometry} using
+ * {@link TriangleTreeWriter}.
+ *
+ * This class supports checking {@link LatLonGeometry} relations against a serialized triangle tree.
+ * It does not support bounding boxes crossing the dateline.
+ *
+ */
+class LatLonGeometryRelationVisitor extends TriangleTreeReader.DecodedVisitor {
+
+    private GeoRelation relation;
+    private Component2D component2D;
+
+    LatLonGeometryRelationVisitor(CoordinateEncoder encoder) {
+        super(encoder);
+    }
+
+    public void reset(LatLonGeometry latLonGeometry) {
+        component2D = LatLonGeometry.create(latLonGeometry);
+        relation = GeoRelation.QUERY_DISJOINT;
+    }
+
+    /**
+     * return the computed relation.
+     */
+    public GeoRelation relation() {
+        return relation;
+    }
+
+    @Override
+    void visitDecodedPoint(double x, double y) {
+        if (component2D.contains(x, y)) {
+            if (component2D.withinPoint(x, y) == Component2D.WithinRelation.CANDIDATE) {
+                relation = GeoRelation.QUERY_INSIDE;
+            } else {
+                relation = GeoRelation.QUERY_CROSSES;
+            }
+        }
+    }
+
+    @Override
+    void visitDecodedLine(double aX, double aY, double bX, double bY, byte metadata) {
+        if (component2D.intersectsLine(aX, aY, bX, bY)) {
+            final boolean ab = (metadata & 1 << 4) == 1 << 4;
+            if (component2D.withinLine(aX, aY, ab, bX, bY) == Component2D.WithinRelation.CANDIDATE) {
+                relation = GeoRelation.QUERY_INSIDE;
+            } else {
+                relation = GeoRelation.QUERY_CROSSES;
+            }
+        }
+    }
+
+    @Override
+    void visitDecodedTriangle(double aX, double aY, double bX, double bY, double cX, double cY, byte metadata) {
+        if (component2D.intersectsTriangle(aX, aY, bX, bY, cX, cY)) {
+            boolean ab = (metadata & 1 << 4) == 1 << 4;
+            boolean bc = (metadata & 1 << 5) == 1 << 5;
+            boolean ca = (metadata & 1 << 6) == 1 << 6;
+            if (component2D.withinTriangle(aX, aY, ab, bX, bY, bc, cX, cY, ca) == Component2D.WithinRelation.CANDIDATE) {
+                relation = GeoRelation.QUERY_INSIDE;
+            } else {
+                relation = GeoRelation.QUERY_CROSSES;
+            }
+        }
+    }
+
+    @Override
+    public boolean push() {
+        return relation != GeoRelation.QUERY_CROSSES;
+    }
+
+    @Override
+    public boolean pushDecodedX(double minX) {
+        return component2D.getMaxX() >= minX;
+    }
+
+    @Override
+    public boolean pushDecodedY(double minY) {
+        return component2D.getMaxY() >= minY;
+    }
+
+    @Override
+    public boolean pushDecoded(double maxX, double maxY) {
+        return component2D.getMinY() <= maxY && component2D.getMinX() <= maxX;
+    }
+
+    @Override
+    @SuppressWarnings("HiddenField")
+    public boolean pushDecoded(double minX, double minY, double maxX, double maxY) {
+        PointValues.Relation rel = component2D.relate(minX, maxX, minY, maxY);
+        if (rel == PointValues.Relation.CELL_OUTSIDE_QUERY) {
+            // shapes are disjoint
+            relation = GeoRelation.QUERY_DISJOINT;
+            return false;
+        }
+        if (rel == PointValues.Relation.CELL_INSIDE_QUERY) {
+            // the rectangle fully contains the shape
+            relation = GeoRelation.QUERY_CROSSES;
+            return false;
+        }
+        return true;
+    }
+}

+ 5 - 5
x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/index/fielddata/GeometryDocValueTests.java

@@ -7,10 +7,10 @@
 
 package org.elasticsearch.xpack.spatial.index.fielddata;
 
+import org.apache.lucene.geo.Circle;
 import org.elasticsearch.common.geo.GeoPoint;
 import org.elasticsearch.common.geo.GeometryNormalizer;
 import org.elasticsearch.common.geo.Orientation;
-import org.elasticsearch.geometry.Circle;
 import org.elasticsearch.geometry.Geometry;
 import org.elasticsearch.geometry.GeometryCollection;
 import org.elasticsearch.geometry.LinearRing;
@@ -182,8 +182,8 @@ public class GeometryDocValueTests extends ESTestCase {
         double centroidY = CoordinateEncoder.GEO.decodeY(reader.getCentroidY());
         assertEquals(centroidX, labelPosition.lon(), 0.0000001);
         assertEquals(centroidY, labelPosition.lat(), 0.0000001);
-        Circle tolerance = new Circle(centroidX, centroidY, 1);
-        assertTrue("Expect label position to be within the geometry", shapeValue.intersects(tolerance));
+        Circle tolerance = new Circle(centroidY, centroidX, 1);
+        assertTrue("Expect label position to be within the geometry", shapeValue.relate(tolerance) != GeoRelation.QUERY_DISJOINT);
     }
 
     public void testFranceLabelPosition() throws Exception {
@@ -197,8 +197,8 @@ public class GeometryDocValueTests extends ESTestCase {
         double centroidY = CoordinateEncoder.GEO.decodeY(reader.getCentroidY());
         assertEquals(centroidX, labelPosition.lon(), 0.0000001);
         assertEquals(centroidY, labelPosition.lat(), 0.0000001);
-        Circle tolerance = new Circle(centroidX, centroidY, 1);
-        assertTrue("Expect label position to be within the geometry", shapeValue.intersects(tolerance));
+        Circle tolerance = new Circle(centroidY, centroidX, 1);
+        assertTrue("Expect label position to be within the geometry", shapeValue.relate(tolerance) != GeoRelation.QUERY_DISJOINT);
     }
 
     private Geometry loadResourceAsGeometry(String filename) throws IOException, ParseException {

+ 78 - 0
x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/index/fielddata/LatLonGeometryRelationVisitorTests.java

@@ -0,0 +1,78 @@
+/*
+ * 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.index.fielddata;
+
+import org.apache.lucene.document.ShapeField;
+import org.apache.lucene.geo.Component2D;
+import org.apache.lucene.geo.LatLonGeometry;
+import org.apache.lucene.tests.geo.GeoTestUtil;
+import org.elasticsearch.geo.GeometryTestUtils;
+import org.elasticsearch.geometry.Geometry;
+import org.elasticsearch.test.ESTestCase;
+import org.elasticsearch.xpack.spatial.util.GeoTestUtils;
+
+import java.util.function.Supplier;
+
+import static org.hamcrest.Matchers.equalTo;
+
+public class LatLonGeometryRelationVisitorTests extends ESTestCase {
+
+    public void testPoint() throws Exception {
+        doTestShapes(GeoTestUtil::nextPoint);
+    }
+
+    public void testLine() throws Exception {
+        doTestShapes(GeoTestUtil::nextLine);
+    }
+
+    public void testPolygon() throws Exception {
+        doTestShapes(GeoTestUtil::nextPolygon);
+    }
+
+    private void doTestShapes(Supplier<LatLonGeometry> supplier) throws Exception {
+        Geometry geometry = GeometryTestUtils.randomGeometryWithoutCircle(0, false);
+        GeoShapeValues.GeoShapeValue geoShapeValue = GeoTestUtils.geoShapeValue(geometry);
+        GeometryDocValueReader reader = GeoTestUtils.geometryDocValueReader(geometry, CoordinateEncoder.GEO);
+        for (int i = 0; i < 1000; i++) {
+            LatLonGeometry latLonGeometry = supplier.get();
+            GeoRelation relation = geoShapeValue.relate(latLonGeometry);
+            Component2D component2D = LatLonGeometry.create(latLonGeometry);
+            Component2DVisitor contains = Component2DVisitor.getVisitor(
+                component2D,
+                ShapeField.QueryRelation.CONTAINS,
+                CoordinateEncoder.GEO
+            );
+            reader.visit(contains);
+            Component2DVisitor intersects = Component2DVisitor.getVisitor(
+                component2D,
+                ShapeField.QueryRelation.INTERSECTS,
+                CoordinateEncoder.GEO
+            );
+            reader.visit(intersects);
+            Component2DVisitor disjoint = Component2DVisitor.getVisitor(
+                component2D,
+                ShapeField.QueryRelation.DISJOINT,
+                CoordinateEncoder.GEO
+            );
+            reader.visit(disjoint);
+            if (relation == GeoRelation.QUERY_INSIDE) {
+                assertThat(contains.matches(), equalTo(true));
+                assertThat(intersects.matches(), equalTo(true));
+                assertThat(disjoint.matches(), equalTo(false));
+            } else if (relation == GeoRelation.QUERY_CROSSES) {
+                assertThat(contains.matches(), equalTo(false));
+                assertThat(intersects.matches(), equalTo(true));
+                assertThat(disjoint.matches(), equalTo(false));
+            } else {
+                assertThat(contains.matches(), equalTo(false));
+                assertThat(intersects.matches(), equalTo(false));
+                assertThat(disjoint.matches(), equalTo(true));
+            }
+        }
+    }
+}