Browse Source

Preprocess polygon rings before processing it for decomposition. (#59501)

Adds a preprocess step for polygon rings before they go over the code for decomposition. The process removes from the polygon ring consecutive duplicate points as well as coplanar points that exist in vertical lines.
Ignacio Vera 4 years ago
parent
commit
cdddb3b161

+ 91 - 6
server/src/main/java/org/elasticsearch/common/geo/GeoPolygonDecomposer.java

@@ -64,18 +64,21 @@ public class GeoPolygonDecomposer {
         if (polygon.isEmpty()) {
             return;
         }
-        int numEdges = polygon.getPolygon().length() - 1; // Last point is repeated
+        LinearRing shell = filterRing(polygon.getPolygon());
+        LinearRing[] holes = new LinearRing[polygon.getNumberOfHoles()];
+        int numEdges = shell.length() - 1; // Last point is repeated
         for (int i = 0; i < polygon.getNumberOfHoles(); i++) {
-            numEdges += polygon.getHole(i).length() - 1;
-            validateHole(polygon.getPolygon(), polygon.getHole(i));
+            holes[i] = filterRing(polygon.getHole(i));
+            numEdges += holes[i].length() - 1;
+            validateHole(shell, holes[i]);
         }
 
         Edge[] edges = new Edge[numEdges];
-        Edge[] holeComponents = new Edge[polygon.getNumberOfHoles()];
+        Edge[] holeComponents = new Edge[holes.length];
         final AtomicBoolean translated = new AtomicBoolean(false);
-        int offset = createEdges(0, orientation, polygon.getPolygon(), null, edges, 0, translated);
+        int offset = createEdges(0, orientation, shell, null, edges, 0, translated);
         for (int i = 0; i < polygon.getNumberOfHoles(); i++) {
-            int length = createEdges(i + 1, orientation, polygon.getPolygon(), polygon.getHole(i), edges, offset, translated);
+            int length = createEdges(i + 1, orientation, shell, holes[i], edges, offset, translated);
             holeComponents[i] = edges[offset];
             offset += length;
         }
@@ -88,6 +91,52 @@ public class GeoPolygonDecomposer {
         compose(edges, holeComponents, numHoles, collector);
     }
 
+    /**
+     * This method removes duplicated points and coplanar points on vertical lines (vertical lines
+     * do not cross the dateline).
+     */
+    private static LinearRing filterRing(LinearRing linearRing) {
+        // first we check if there is anything to filter
+        int numPoints = linearRing.length();
+        int count = 2;
+        for (int i = 1; i < numPoints - 1; i++) {
+            if (linearRing.getLon(i - 1) == linearRing.getLon(i)) {
+                if (linearRing.getLat(i - 1) == linearRing.getLat(i)) {
+                    // same point
+                    continue;
+                }
+                if (linearRing.getLon(i - 1) == linearRing.getLon(i + 1) &&
+                    linearRing.getLat(i - 1) > linearRing.getLat(i) != linearRing.getLat(i + 1) > linearRing.getLat(i)) {
+                    // coplanar
+                    continue;
+                }
+            }
+            count++;
+        }
+        if (numPoints == count) {
+            return linearRing;
+        }
+        // Second filter the points
+        double[] lons = new double[count];
+        double[] lats = new double[count];
+        lats[0] = lats[count - 1] = linearRing.getLat(0);
+        lons[0] = lons[count - 1] = linearRing.getLon(0);
+        count = 0;
+        for (int i = 1; i < numPoints - 1; i++) {
+            if (linearRing.getLon(i - 1) == linearRing.getLon(i)) {
+                if (linearRing.getLat(i - 1) == linearRing.getLat(i) ||
+                    linearRing.getLon(i - 1) == linearRing.getLon(i + 1)) {
+                    // filter
+                    continue;
+                }
+            }
+            count++;
+            lats[count] = linearRing.getLat(i);
+            lons[count] = linearRing.getLon(i);
+        }
+        return new LinearRing(lons, lats);
+    }
+
     private static void validateHole(LinearRing shell, LinearRing hole) {
         Set<Point> exterior = new HashSet<>();
         Set<Point> interior = new HashSet<>();
@@ -328,6 +377,7 @@ public class GeoPolygonDecomposer {
     private static int intersections(double dateline, Edge[] edges) {
         int numIntersections = 0;
         assert !Double.isNaN(dateline);
+        int maxComponent = 0;
         for (int i = 0; i < edges.length; i++) {
             Point p1 = edges[i].coordinate;
             Point p2 = edges[i].next.coordinate;
@@ -338,12 +388,47 @@ public class GeoPolygonDecomposer {
             if (!Double.isNaN(position)) {
                 edges[i].intersection(position);
                 numIntersections++;
+                maxComponent = Math.max(maxComponent, edges[i].component);
+            }
+        }
+        if (maxComponent > 0) {
+            // we might detect polygons touching the dateline as intersections
+            // Here we clean them up
+            for (int i = 0; i < maxComponent; i++) {
+                if (clearComponentTouchingDateline(edges, i + 1)) {
+                    numIntersections--;
+                }
             }
         }
         Arrays.sort(edges, INTERSECTION_ORDER);
         return numIntersections;
     }
 
+    /**
+     * Checks the number of dateline intersections detected for a component. If there is only
+     * one, it clears it as it means that the component just touches the dateline.
+     *
+     * @param edges    set of edges that may intersect with the dateline
+     * @param component    The component to check
+     * @return true if the component touches the dateline.
+     */
+    private static boolean clearComponentTouchingDateline(Edge[] edges, int component) {
+        Edge intersection = null;
+        for (int j = 0; j < edges.length; j++) {
+            if (edges[j].intersect != Edge.MAX_COORDINATE && edges[j].component == component) {
+                if (intersection == null) {
+                    intersection = edges[j];
+                } else {
+                    return false;
+                }
+            }
+        }
+        if (intersection != null) {
+            intersection.intersect = Edge.MAX_COORDINATE;
+        }
+        return intersection != null;
+    }
+
 
     private static Edge[] edges(Edge[] edges, int numHoles, List<List<Point[]>> components) {
         ArrayList<Edge> mainEdges = new ArrayList<>(edges.length);

+ 37 - 7
server/src/main/java/org/elasticsearch/common/geo/builders/PolygonBuilder.java

@@ -199,18 +199,21 @@ public class PolygonBuilder extends ShapeBuilder<JtsGeometry, org.elasticsearch.
      * @return coordinates of the polygon
      */
     public Coordinate[][][] coordinates() {
-        int numEdges = shell.coordinates.size()-1; // Last point is repeated
-        for (int i = 0; i < holes.size(); i++) {
-            numEdges += holes.get(i).coordinates.size()-1;
-            validateHole(shell, this.holes.get(i));
+        LineStringBuilder shell = filterRing(this.shell);
+        LineStringBuilder[] holes = new LineStringBuilder[this.holes.size()];
+        int numEdges = shell.coordinates.size() - 1; // Last point is repeated
+        for (int i = 0; i < this.holes.size(); i++) {
+            holes[i] = filterRing(this.holes.get(i));
+            numEdges += holes[i].coordinates.size() - 1;
+            validateHole(shell, holes[i]);
         }
 
         Edge[] edges = new Edge[numEdges];
-        Edge[] holeComponents = new Edge[holes.size()];
+        Edge[] holeComponents = new Edge[holes.length];
         final AtomicBoolean translated = new AtomicBoolean(false);
         int offset = createEdges(0, orientation, shell, null, edges, 0, translated);
-        for (int i = 0; i < holes.size(); i++) {
-            int length = createEdges(i+1, orientation, shell, this.holes.get(i), edges, offset, translated);
+        for (int i = 0; i < holes.length; i++) {
+            int length = createEdges(i+1, orientation, shell, holes[i], edges, offset, translated);
             holeComponents[i] = edges[offset];
             offset += length;
         }
@@ -223,6 +226,33 @@ public class PolygonBuilder extends ShapeBuilder<JtsGeometry, org.elasticsearch.
         return compose(edges, holeComponents, numHoles);
     }
 
+    /**
+     * This method removes duplicated points and coplanar points on vertical lines (vertical lines
+     * do not cross the dateline).
+     */
+    private static LineStringBuilder filterRing(LineStringBuilder linearRing) {
+        int numPoints = linearRing.coordinates.size();
+        List<Coordinate> coordinates = new ArrayList<>();
+        coordinates.add(linearRing.coordinates.get(0));
+        for (int i = 1; i < numPoints - 1; i++) {
+            if (linearRing.coordinates.get(i - 1).x == linearRing.coordinates.get(i).x) {
+                if (linearRing.coordinates.get(i - 1).y == linearRing.coordinates.get(i).y) {
+                    // same point
+                    continue;
+                }
+                if (linearRing.coordinates.get(i - 1).x == linearRing.coordinates.get(i + 1).x &&
+                    linearRing.coordinates.get(i - 1).y > linearRing.coordinates.get(i).y !=
+                        linearRing.coordinates.get(i + 1).y > linearRing.coordinates.get(i).y) {
+                    // coplanar
+                    continue;
+                }
+            }
+            coordinates.add(linearRing.coordinates.get(i));
+        }
+        coordinates.add(linearRing.coordinates.get(numPoints - 1));
+        return new LineStringBuilder(coordinates);
+    }
+
     @Override
     public JtsGeometry buildS4J() {
         return jtsGeometry(buildS4JGeometry(FACTORY, wrapdateline));

+ 37 - 0
server/src/main/java/org/elasticsearch/common/geo/builders/ShapeBuilder.java

@@ -273,6 +273,7 @@ public abstract class ShapeBuilder<T extends Shape, G extends org.elasticsearch.
     protected static int intersections(double dateline, Edge[] edges) {
         int numIntersections = 0;
         assert !Double.isNaN(dateline);
+        int maxComponent = 0;
         for (int i = 0; i < edges.length; i++) {
             Coordinate p1 = edges[i].coordinate;
             Coordinate p2 = edges[i].next.coordinate;
@@ -283,12 +284,48 @@ public abstract class ShapeBuilder<T extends Shape, G extends org.elasticsearch.
             if (!Double.isNaN(position)) {
                 edges[i].intersection(position);
                 numIntersections++;
+                maxComponent = Math.max(maxComponent, edges[i].component);
             }
         }
+        if (maxComponent > 0) {
+            // we might detect polygons touching the dateline as intersections
+            // Here we clean them up
+            for (int i = 0; i < maxComponent; i++) {
+                if (clearComponentTouchingDateline(edges, i + 1)) {
+                    numIntersections--;
+                }
+            }
+        }
+
         Arrays.sort(edges, INTERSECTION_ORDER);
         return numIntersections;
     }
 
+    /**
+     * Checks the number of dateline intersections detected for a component. If there is only
+     * one, it clears it as it means that the component just touches the dateline.
+     *
+     * @param edges    set of edges that may intersect with the dateline
+     * @param component    The component to check
+     * @return true if the component touches the dateline.
+     */
+    private static boolean clearComponentTouchingDateline(Edge[] edges, int component) {
+        Edge intersection = null;
+        for (Edge edge : edges) {
+            if (edge.intersect != Edge.MAX_COORDINATE && edge.component == component) {
+                if (intersection == null) {
+                    intersection = edge;
+                } else {
+                    return false;
+                }
+            }
+        }
+        if (intersection != null) {
+            intersection.intersect = Edge.MAX_COORDINATE;
+        }
+        return intersection != null;
+    }
+
     /**
      * This helper class implements a linked list for {@link Coordinate}. It contains
      * fields for a dateline intersection and component id

+ 72 - 5
server/src/test/java/org/elasticsearch/common/geo/ShapeBuilderTests.java

@@ -566,6 +566,27 @@ public class ShapeBuilderTests extends ESTestCase {
         assertMultiPolygon(buildGeometry(builder.close()), false);
     }
 
+    public void testShapeWithHoleTouchingAtDateline() throws Exception {
+        PolygonBuilder builder = new PolygonBuilder(new CoordinatesBuilder()
+            .coordinate(-180, 90)
+            .coordinate(-180, -90)
+            .coordinate(180, -90)
+            .coordinate(180, 90)
+            .coordinate(-180, 90)
+        );
+        builder.hole(new LineStringBuilder(new CoordinatesBuilder()
+            .coordinate(180.0, -16.14)
+            .coordinate(178.53, -16.64)
+            .coordinate(178.49, -16.82)
+            .coordinate(178.73, -17.02)
+            .coordinate(178.86, -16.86)
+            .coordinate(180.0, -16.14)
+        ));
+
+        assertPolygon(builder.close().buildS4J(), true);
+        assertPolygon(buildGeometry(builder.close()), false);
+    }
+
     public void testShapeWithTangentialHole() {
         // test a shape with one tangential (shared) vertex (should pass)
         PolygonBuilder builder = new PolygonBuilder(new CoordinatesBuilder()
@@ -703,7 +724,7 @@ public class ShapeBuilderTests extends ESTestCase {
         assertMultiPolygon(buildGeometry(builder.close()), false);
      }
 
-    public void testInvalidShapeWithConsecutiveDuplicatePoints() {
+    public void testShapeWithConsecutiveDuplicatePoints() {
         PolygonBuilder builder = new PolygonBuilder(new CoordinatesBuilder()
                 .coordinate(180, 0)
                 .coordinate(176, 4)
@@ -712,10 +733,56 @@ public class ShapeBuilderTests extends ESTestCase {
                 .coordinate(180, 0)
                 );
 
-        Exception e = expectThrows(InvalidShapeException.class, () -> builder.close().buildS4J());
-        assertThat(e.getMessage(), containsString("duplicate consecutive coordinates at: ("));
-        e = expectThrows(InvalidShapeException.class, () -> buildGeometry(builder.close()));
-        assertThat(e.getMessage(), containsString("duplicate consecutive coordinates at: ("));
+        // duplicated points are removed
+        PolygonBuilder expected = new PolygonBuilder(new CoordinatesBuilder()
+            .coordinate(180, 0)
+            .coordinate(176, 4)
+            .coordinate(-176, 4)
+            .coordinate(180, 0)
+        );
+
+        assertEquals(buildGeometry(expected.close()), buildGeometry(builder.close()));
+        assertEquals(expected.close().buildS4J(), builder.close().buildS4J());
+    }
+
+    public void testShapeWithCoplanarVerticalPoints() throws Exception {
+        PolygonBuilder builder = new PolygonBuilder(new CoordinatesBuilder()
+            .coordinate(180, -36)
+            .coordinate(180, 90)
+            .coordinate(-180, 90)
+            .coordinate(-180, 79)
+            .coordinate(16, 58)
+            .coordinate(8, 13)
+            .coordinate(-180, 74)
+            .coordinate(-180, -85)
+            .coordinate(-180, -90)
+            .coordinate(180,  -90)
+            .coordinate(180, -85)
+            .coordinate(26, 6)
+            .coordinate(33, 62)
+            .coordinate(180, -36)
+        );
+
+        //coplanar points on vertical edge are removed.
+        PolygonBuilder expected = new PolygonBuilder(new CoordinatesBuilder()
+            .coordinate(180, -36)
+            .coordinate(180, 90)
+            .coordinate(-180, 90)
+            .coordinate(-180, 79)
+            .coordinate(16, 58)
+            .coordinate(8, 13)
+            .coordinate(-180, 74)
+            .coordinate(-180, -90)
+            .coordinate(180,  -90)
+            .coordinate(180, -85)
+            .coordinate(26, 6)
+            .coordinate(33, 62)
+            .coordinate(180, -36)
+        );
+
+        assertEquals(buildGeometry(expected.close()), buildGeometry(builder.close()));
+        assertEquals(expected.close().buildS4J(), builder.close().buildS4J());
+
     }
 
     public void testPolygon3D() {