From cd5a3348642ee68ef38866b7eb092f73e2d8f2e3 Mon Sep 17 00:00:00 2001 From: Igor Motov Date: Tue, 16 Jul 2019 09:37:04 -0400 Subject: [PATCH] Geo: extract dateline handling logic from ShapeBuilders (#44187) Extracts dateline decomposition logic from ShapeBuilder into a separate utility class that is used on the indexing side. The search side will be handled as part of another PR at this time we will remove the decomposition logic from ShapeBuilders as well. This PR also doesn't change any existing logic including bugs. Relates to #40908 --- .../org/elasticsearch/common/geo/GeoJson.java | 16 +- .../common/geo/GeometryIndexer.java | 933 ++++++++++++++++++ .../index/mapper/GeoShapeFieldMapper.java | 15 +- .../common/geo/BaseGeoParsingTestCase.java | 24 +- .../common/geo/GeoJsonParserTests.java | 11 +- .../common/geo/GeoJsonShapeParserTests.java | 73 +- .../common/geo/GeoWKTShapeParserTests.java | 23 +- .../common/geo/GeometryIndexerTests.java | 239 +++++ .../geo/builders/LineStringBuilderTests.java | 3 +- .../index/mapper/ExternalMapper.java | 5 +- 10 files changed, 1269 insertions(+), 73 deletions(-) create mode 100644 server/src/main/java/org/elasticsearch/common/geo/GeometryIndexer.java create mode 100644 server/src/test/java/org/elasticsearch/common/geo/GeometryIndexerTests.java diff --git a/server/src/main/java/org/elasticsearch/common/geo/GeoJson.java b/server/src/main/java/org/elasticsearch/common/geo/GeoJson.java index 4508d38935857..247e5d86cebd6 100644 --- a/server/src/main/java/org/elasticsearch/common/geo/GeoJson.java +++ b/server/src/main/java/org/elasticsearch/common/geo/GeoJson.java @@ -228,8 +228,12 @@ private XContentBuilder coordinatesToXContent(Polygon polygon) throws IOExceptio private static Geometry createGeometry(String type, List geometries, CoordinateNode coordinates, Boolean orientation, boolean defaultOrientation, boolean coerce, DistanceUnit.Distance radius) { - - ShapeType shapeType = ShapeType.forName(type); + ShapeType shapeType; + if ("bbox".equalsIgnoreCase(type)) { + shapeType = ShapeType.ENVELOPE; + } else { + shapeType = ShapeType.forName(type); + } if (shapeType == ShapeType.GEOMETRYCOLLECTION) { if (geometries == null) { throw new ElasticsearchParseException("geometries not included"); @@ -484,7 +488,7 @@ public MultiPoint asMultiPoint() { return new MultiPoint(points); } - private double[][] asLineComponents(boolean orientation, boolean coerce) { + private double[][] asLineComponents(boolean orientation, boolean coerce, boolean close) { if (coordinate != null) { throw new ElasticsearchException("expected a list of points but got a point"); } @@ -495,7 +499,7 @@ private double[][] asLineComponents(boolean orientation, boolean coerce) { boolean needsClosing; int resultSize; - if (coerce && children.get(0).asPoint().equals(children.get(children.size() - 1).asPoint()) == false) { + if (close && coerce && children.get(0).asPoint().equals(children.get(children.size() - 1).asPoint()) == false) { needsClosing = true; resultSize = children.size() + 1; } else { @@ -531,12 +535,12 @@ private double[][] asLineComponents(boolean orientation, boolean coerce) { } public Line asLineString(boolean coerce) { - double[][] components = asLineComponents(true, coerce); + double[][] components = asLineComponents(true, coerce, false); return new Line(components[0], components[1], components[2]); } public LinearRing asLinearRing(boolean orientation, boolean coerce) { - double[][] components = asLineComponents(orientation, coerce); + double[][] components = asLineComponents(orientation, coerce, true); return new LinearRing(components[0], components[1], components[2]); } diff --git a/server/src/main/java/org/elasticsearch/common/geo/GeometryIndexer.java b/server/src/main/java/org/elasticsearch/common/geo/GeometryIndexer.java new file mode 100644 index 0000000000000..8351ef7805e90 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/common/geo/GeometryIndexer.java @@ -0,0 +1,933 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + + +package org.elasticsearch.common.geo; + +import org.elasticsearch.common.collect.Tuple; +import org.elasticsearch.geo.geometry.Circle; +import org.elasticsearch.geo.geometry.Geometry; +import org.elasticsearch.geo.geometry.GeometryCollection; +import org.elasticsearch.geo.geometry.GeometryVisitor; +import org.elasticsearch.geo.geometry.Line; +import org.elasticsearch.geo.geometry.LinearRing; +import org.elasticsearch.geo.geometry.MultiLine; +import org.elasticsearch.geo.geometry.MultiPoint; +import org.elasticsearch.geo.geometry.MultiPolygon; +import org.elasticsearch.geo.geometry.Point; +import org.elasticsearch.geo.geometry.Polygon; +import org.elasticsearch.geo.geometry.Rectangle; +import org.locationtech.spatial4j.exception.InvalidShapeException; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; + +import static org.apache.lucene.geo.GeoUtils.orient; +import static org.elasticsearch.common.geo.GeoUtils.normalizeLat; +import static org.elasticsearch.common.geo.GeoUtils.normalizeLon; + +/** + * Utility class that converts geometries into Lucene-compatible form + */ +public final class GeometryIndexer { + + private static final double DATELINE = 180; + + protected static final Comparator INTERSECTION_ORDER = Comparator.comparingDouble(o -> o.intersect.getLat()); + + private final boolean orientation; + + public GeometryIndexer(boolean orientation) { + this.orientation = orientation; + } + + public Geometry prepareForIndexing(Geometry geometry) { + if (geometry == null) { + return null; + } + + return geometry.visit(new GeometryVisitor() { + @Override + public Geometry visit(Circle circle) { + throw new UnsupportedOperationException("CIRCLE geometry is not supported"); + } + + @Override + public Geometry visit(GeometryCollection collection) { + if (collection.isEmpty()) { + return GeometryCollection.EMPTY; + } + List shapes = new ArrayList<>(collection.size()); + + // Flatten collection and convert each geometry to Lucene-friendly format + for (Geometry shape : collection) { + shapes.add(shape.visit(this)); + } + + if (shapes.size() == 1) { + return shapes.get(0); + } else { + return new GeometryCollection<>(shapes); + } + } + + @Override + public Geometry visit(Line line) { + // decompose linestrings crossing dateline into array of Lines + List lines = decomposeGeometry(line, new ArrayList<>()); + if (lines.size() == 1) { + return lines.get(0); + } else { + return new MultiLine(lines); + } + } + + @Override + public Geometry visit(LinearRing ring) { + throw new UnsupportedOperationException("cannot index linear ring [" + ring + "] directly"); + } + + @Override + public Geometry visit(MultiLine multiLine) { + List lines = new ArrayList<>(); + for (Line line : multiLine) { + decomposeGeometry(line, lines); + } + if (lines.isEmpty()) { + return GeometryCollection.EMPTY; + } else if (lines.size() == 1) { + return lines.get(0); + } else { + return new MultiLine(lines); + } + } + + @Override + public Geometry visit(MultiPoint multiPoint) { + if (multiPoint.isEmpty()) { + return MultiPoint.EMPTY; + } else if (multiPoint.size() == 1) { + return multiPoint.get(0).visit(this); + } else { + List points = new ArrayList<>(); + for (Point point : multiPoint) { + points.add((Point) point.visit(this)); + } + return new MultiPoint(points); + } + } + + @Override + public Geometry visit(MultiPolygon multiPolygon) { + List polygons = new ArrayList<>(); + for (Polygon polygon : multiPolygon) { + polygons.addAll(decompose(polygon, orientation)); + } + if (polygons.size() == 1) { + return polygons.get(0); + } else { + return new MultiPolygon(polygons); + } + } + + @Override + public Geometry visit(Point point) { + //TODO: Just remove altitude for now. We need to add normalization later + return new Point(point.getLat(), point.getLon()); + } + + @Override + public Geometry visit(Polygon polygon) { + List polygons = decompose(polygon, orientation); + if (polygons.size() == 1) { + return polygons.get(0); + } else { + return new MultiPolygon(polygons); + } + } + + @Override + public Geometry visit(Rectangle rectangle) { + return rectangle; + } + }); + } + + /** + * Calculate the intersection of a line segment and a vertical dateline. + * + * @param p1x longitude of the start-point of the line segment + * @param p2x longitude of the end-point of the line segment + * @param dateline x-coordinate of the vertical dateline + * @return position of the intersection in the open range (0..1] if the line + * segment intersects with the line segment. Otherwise this method + * returns {@link Double#NaN} + */ + protected static double intersection(double p1x, double p2x, double dateline) { + if (p1x == p2x && p1x != dateline) { + return Double.NaN; + } else if (p1x == p2x && p1x == dateline) { + return 1.0; + } else { + final double t = (dateline - p1x) / (p2x - p1x); + if (t > 1 || t <= 0) { + return Double.NaN; + } else { + return t; + } + } + } + + /** + * Splits the specified line by datelines and adds them to the supplied lines array + */ + private List decomposeGeometry(Line line, List lines) { + + for (Line partPlus : decompose(+DATELINE, line)) { + for (Line partMinus : decompose(-DATELINE, partPlus)) { + double[] lats = new double[partMinus.length()]; + double[] lons = new double[partMinus.length()]; + for (int i = 0; i < partMinus.length(); i++) { + lats[i] = normalizeLat(partMinus.getLat(i)); + lons[i] = normalizeLon(partMinus.getLon(i)); + } + lines.add(new Line(lats, lons)); + } + } + return lines; + } + + /** + * Decompose a linestring given as array of coordinates at a vertical line. + * + * @param dateline x-axis intercept of the vertical line + * @param line linestring that should be decomposed + * @return array of linestrings given as coordinate arrays + */ + private List decompose(double dateline, Line line) { + double[] lons = line.getLons(); + double[] lats = line.getLats(); + return decompose(dateline, lons, lats); + } + + /** + * Decompose a linestring given as two arrays of coordinates at a vertical line. + */ + private List decompose(double dateline, double[] lons, double[] lats) { + int offset = 0; + ArrayList parts = new ArrayList<>(); + + double lastLon = lons[0]; + double shift = lastLon > DATELINE ? DATELINE : (lastLon < -DATELINE ? -DATELINE : 0); + + for (int i = 1; i < lons.length; i++) { + double t = intersection(lastLon, lons[i], dateline); + if (Double.isNaN(t) == false) { + double[] partLons = Arrays.copyOfRange(lons, offset, i + 1); + double[] partLats = Arrays.copyOfRange(lats, offset, i + 1); + if (t < 1) { + Point intersection = position(new Point(lats[i - 1], lons[i - 1]), new Point(lats[i], lons[i]), t); + partLons[partLons.length - 1] = intersection.getLon(); + partLats[partLats.length - 1] = intersection.getLat(); + + lons[offset + i - 1] = intersection.getLon(); + lats[offset + i - 1] = intersection.getLat(); + + shift(shift, lons); + offset = i - 1; + shift = lons[i] > DATELINE ? DATELINE : (lons[i] < -DATELINE ? -DATELINE : 0); + } else { + shift(shift, partLons); + offset = i; + } + parts.add(new Line(partLats, partLons)); + } + } + + if (offset == 0) { + shift(shift, lons); + parts.add(new Line(lats, lons)); + } else if (offset < lons.length - 1) { + double[] partLons = Arrays.copyOfRange(lons, offset, lons.length); + double[] partLats = Arrays.copyOfRange(lats, offset, lats.length); + shift(shift, partLons); + parts.add(new Line(partLats, partLons)); + } + return parts; + } + + /** + * shifts all coordinates by (- shift * 2) + */ + private static void shift(double shift, double[] lons) { + if (shift != 0) { + for (int j = 0; j < lons.length; j++) { + lons[j] = lons[j] - 2 * shift; + } + } + } + + protected static Point shift(Point coordinate, double dateline) { + if (dateline == 0) { + return coordinate; + } else { + return new Point(coordinate.getLat(), -2 * dateline + coordinate.getLon()); + } + } + + private List decompose(Polygon polygon, boolean orientation) { + int numEdges = polygon.getPolygon().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)); + } + + Edge[] edges = new Edge[numEdges]; + Edge[] holeComponents = new Edge[polygon.getNumberOfHoles()]; + final AtomicBoolean translated = new AtomicBoolean(false); + int offset = createEdges(0, orientation, polygon.getPolygon(), 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); + holeComponents[i] = edges[offset]; + offset += length; + } + + int numHoles = holeComponents.length; + + numHoles = merge(edges, 0, intersections(+DATELINE, edges), holeComponents, numHoles); + numHoles = merge(edges, 0, intersections(-DATELINE, edges), holeComponents, numHoles); + + return compose(edges, holeComponents, numHoles); + } + + private void validateHole(LinearRing shell, LinearRing hole) { + Set exterior = new HashSet<>(); + Set interior = new HashSet<>(); + for (int i = 0; i < shell.length(); i++) { + exterior.add(new Point(shell.getLat(i), shell.getLon(i))); + } + for (int i = 0; i < hole.length(); i++) { + interior.remove(new Point(hole.getLat(i), hole.getLon(i))); + } + exterior.retainAll(interior); + if (exterior.size() >= 2) { + throw new IllegalArgumentException("Invalid polygon, interior cannot share more than one point with the exterior"); + } + } + + /** + * This helper class implements a linked list for {@link Point}. It contains + * fields for a dateline intersection and component id + */ + private static final class Edge { + Point coordinate; // coordinate of the start point + Edge next; // next segment + Point intersect; // potential intersection with dateline + int component = -1; // id of the component this edge belongs to + public static final Point MAX_COORDINATE = new Point(Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY); + + protected Edge(Point coordinate, Edge next, Point intersection) { + this.coordinate = coordinate; + // use setter to catch duplicate point cases + this.setNext(next); + this.intersect = intersection; + if (next != null) { + this.component = next.component; + } + } + + protected Edge(Point coordinate, Edge next) { + this(coordinate, next, Edge.MAX_COORDINATE); + } + + protected void setNext(Edge next) { + // don't bother setting next if its null + if (next != null) { + // self-loop throws an invalid shape + if (this.coordinate.equals(next.coordinate)) { + throw new InvalidShapeException("Provided shape has duplicate consecutive coordinates at: " + this.coordinate); + } + this.next = next; + } + } + + /** + * Set the intersection of this line segment to the given position + * + * @param position position of the intersection [0..1] + * @return the {@link Point} of the intersection + */ + protected Point intersection(double position) { + return intersect = position(coordinate, next.coordinate, position); + } + + @Override + public String toString() { + return "Edge[Component=" + component + "; start=" + coordinate + " " + "; intersection=" + intersect + "]"; + } + } + + protected static Point position(Point p1, Point p2, double position) { + if (position == 0) { + return p1; + } else if (position == 1) { + return p2; + } else { + final double x = p1.getLon() + position * (p2.getLon() - p1.getLon()); + final double y = p1.getLat() + position * (p2.getLat() - p1.getLat()); + return new Point(y, x); + } + } + + private int createEdges(int component, boolean orientation, LinearRing shell, + LinearRing hole, Edge[] edges, int offset, final AtomicBoolean translated) { + // inner rings (holes) have an opposite direction than the outer rings + // XOR will invert the orientation for outer ring cases (Truth Table:, T/T = F, T/F = T, F/T = T, F/F = F) + boolean direction = (component == 0 ^ orientation); + // set the points array accordingly (shell or hole) + Point[] points = (hole != null) ? points(hole) : points(shell); + ring(component, direction, orientation == false, points, 0, edges, offset, points.length - 1, translated); + return points.length - 1; + } + + private Point[] points(LinearRing linearRing) { + Point[] points = new Point[linearRing.length()]; + for (int i = 0; i < linearRing.length(); i++) { + points[i] = new Point(linearRing.getLat(i), linearRing.getLon(i)); + } + return points; + } + + /** + * Create a connected list of a list of coordinates + * + * @param points array of point + * @param offset index of the first point + * @param length number of points + * @return Array of edges + */ + private Edge[] ring(int component, boolean direction, boolean handedness, + Point[] points, int offset, Edge[] edges, int toffset, int length, final AtomicBoolean translated) { + + boolean orientation = getOrientation(points, offset, length); + + // OGC requires shell as ccw (Right-Handedness) and holes as cw (Left-Handedness) + // since GeoJSON doesn't specify (and doesn't need to) GEO core will assume OGC standards + // thus if orientation is computed as cw, the logic will translate points across dateline + // and convert to a right handed system + + // compute the bounding box and calculate range + double[] range = range(points, offset, length); + final double rng = range[1] - range[0]; + // translate the points if the following is true + // 1. shell orientation is cw and range is greater than a hemisphere (180 degrees) but not spanning 2 hemispheres + // (translation would result in a collapsed poly) + // 2. the shell of the candidate hole has been translated (to preserve the coordinate system) + boolean incorrectOrientation = component == 0 && handedness != orientation; + if ((incorrectOrientation && (rng > DATELINE && rng != 2 * DATELINE)) || (translated.get() && component != 0)) { + translate(points); + // flip the translation bit if the shell is being translated + if (component == 0) { + translated.set(true); + } + // correct the orientation post translation (ccw for shell, cw for holes) + if (component == 0 || (component != 0 && handedness == orientation)) { + orientation = !orientation; + } + } + return concat(component, direction ^ orientation, points, offset, edges, toffset, length); + } + + /** + * Transforms coordinates in the eastern hemisphere (-180:0) to a (180:360) range + */ + private static void translate(Point[] points) { + for (int i = 0; i < points.length; i++) { + if (points[i].getLon() < 0) { + points[i] = new Point(points[i].getLat(), points[i].getLon() + 2 * DATELINE); + } + } + } + + /** + * @return whether the points are clockwise (true) or anticlockwise (false) + */ + private static boolean getOrientation(Point[] points, int offset, int length) { + // calculate the direction of the points: find the southernmost point + // and check its neighbors orientation. + + final int top = top(points, offset, length); + final int prev = (top + length - 1) % length; + final int next = (top + 1) % length; + + final int determinantSign = orient( + points[offset + prev].getLon(), points[offset + prev].getLat(), + points[offset + top].getLon(), points[offset + top].getLat(), + points[offset + next].getLon(), points[offset + next].getLat()); + + if (determinantSign == 0) { + // Points are collinear, but `top` is not in the middle if so, so the edges either side of `top` are intersecting. + throw new InvalidShapeException("Cannot determine orientation: edges adjacent to (" + + points[offset + top].getLon() + "," + points[offset + top].getLat() + ") coincide"); + } + + return determinantSign < 0; + } + + /** + * @return the (offset) index of the point that is furthest west amongst + * those points that are the furthest south in the set. + */ + private static int top(Point[] points, int offset, int length) { + int top = 0; // we start at 1 here since top points to 0 + for (int i = 1; i < length; i++) { + if (points[offset + i].getLat() < points[offset + top].getLat()) { + top = i; + } else if (points[offset + i].getLat() == points[offset + top].getLat()) { + if (points[offset + i].getLon() < points[offset + top].getLon()) { + top = i; + } + } + } + return top; + } + + + private static double[] range(Point[] points, int offset, int length) { + double minX = points[0].getLon(); + double maxX = minX; + double minY = points[0].getLat(); + double maxY = minY; + // compute the bounding coordinates (@todo: cleanup brute force) + for (int i = 1; i < length; ++i) { + Point point = points[offset + i]; + if (point.getLon() < minX) { + minX = point.getLon(); + } + if (point.getLon() > maxX) { + maxX = point.getLon(); + } + if (point.getLat() < minY) { + minY = point.getLat(); + } + if (point.getLat() > maxY) { + maxY = point.getLat(); + } + } + return new double[]{minX, maxX, minY, maxY}; + } + + private int merge(Edge[] intersections, int offset, int length, Edge[] holes, int numHoles) { + // Intersections appear pairwise. On the first edge the inner of + // of the polygon is entered. On the second edge the outer face + // is entered. Other kinds of intersections are discard by the + // intersection function + + for (int i = 0; i < length; i += 2) { + Edge e1 = intersections[offset + i + 0]; + Edge e2 = intersections[offset + i + 1]; + + // If two segments are connected maybe a hole must be deleted + // Since Edges of components appear pairwise we need to check + // the second edge only (the first edge is either polygon or + // already handled) + if (e2.component > 0) { + //TODO: Check if we could save the set null step + numHoles--; + holes[e2.component - 1] = holes[numHoles]; + holes[numHoles] = null; + } + // only connect edges if intersections are pairwise + // 1. per the comment above, the edge array is sorted by y-value of the intersection + // with the dateline. Two edges have the same y intercept when they cross the + // dateline thus they appear sequentially (pairwise) in the edge array. Two edges + // do not have the same y intercept when we're forming a multi-poly from a poly + // that wraps the dateline (but there are 2 ordered intercepts). + // The connect method creates a new edge for these paired edges in the linked list. + // For boundary conditions (e.g., intersect but not crossing) there is no sibling edge + // to connect. Thus the first logic check enforces the pairwise rule + // 2. the second logic check ensures the two candidate edges aren't already connected by an + // existing edge along the dateline - this is necessary due to a logic change in + // ShapeBuilder.intersection that computes dateline edges as valid intersect points + // in support of OGC standards + if (e1.intersect != Edge.MAX_COORDINATE && e2.intersect != Edge.MAX_COORDINATE + && !(e1.next.next.coordinate.equals(e2.coordinate) && Math.abs(e1.next.coordinate.getLon()) == DATELINE + && Math.abs(e2.coordinate.getLon()) == DATELINE)) { + connect(e1, e2); + } + } + return numHoles; + } + + private void connect(Edge in, Edge out) { + assert in != null && out != null; + assert in != out; + // Connecting two Edges by inserting the point at + // dateline intersection and connect these by adding + // two edges between this points. One per direction + if (in.intersect != in.next.coordinate) { + // NOTE: the order of the object creation is crucial here! Don't change it! + // first edge has no point on dateline + Edge e1 = new Edge(in.intersect, in.next); + + if (out.intersect != out.next.coordinate) { + // second edge has no point on dateline + Edge e2 = new Edge(out.intersect, out.next); + in.next = new Edge(in.intersect, e2, in.intersect); + } else { + // second edge intersects with dateline + in.next = new Edge(in.intersect, out.next, in.intersect); + } + out.next = new Edge(out.intersect, e1, out.intersect); + } else if (in.next != out && in.coordinate != out.intersect) { + // first edge intersects with dateline + Edge e2 = new Edge(out.intersect, in.next, out.intersect); + + if (out.intersect != out.next.coordinate) { + // second edge has no point on dateline + Edge e1 = new Edge(out.intersect, out.next); + in.next = new Edge(in.intersect, e1, in.intersect); + + } else { + // second edge intersects with dateline + in.next = new Edge(in.intersect, out.next, in.intersect); + } + out.next = e2; + } + } + + /** + * Concatenate a set of points to a polygon + * + * @param component component id of the polygon + * @param direction direction of the ring + * @param points list of points to concatenate + * @param pointOffset index of the first point + * @param edges Array of edges to write the result to + * @param edgeOffset index of the first edge in the result + * @param length number of points to use + * @return the edges creates + */ + private static Edge[] concat(int component, boolean direction, Point[] points, final int pointOffset, Edge[] edges, + final int edgeOffset, int length) { + assert edges.length >= length + edgeOffset; + assert points.length >= length + pointOffset; + edges[edgeOffset] = new Edge(new Point(points[pointOffset].getLat(), points[pointOffset].getLon()), null); + for (int i = 1; i < length; i++) { + Point nextPoint = new Point(points[pointOffset + i].getLat(), points[pointOffset + i].getLon()); + if (direction) { + edges[edgeOffset + i] = new Edge(nextPoint, edges[edgeOffset + i - 1]); + edges[edgeOffset + i].component = component; + } else if (!edges[edgeOffset + i - 1].coordinate.equals(nextPoint)) { + edges[edgeOffset + i - 1].next = edges[edgeOffset + i] = new Edge(nextPoint, null); + edges[edgeOffset + i - 1].component = component; + } else { + throw new InvalidShapeException("Provided shape has duplicate consecutive coordinates at: " + nextPoint); + } + } + + if (direction) { + edges[edgeOffset].setNext(edges[edgeOffset + length - 1]); + edges[edgeOffset].component = component; + } else { + edges[edgeOffset + length - 1].setNext(edges[edgeOffset]); + edges[edgeOffset + length - 1].component = component; + } + + return edges; + } + + /** + * Calculate all intersections of line segments and a vertical line. The + * Array of edges will be ordered asc by the y-coordinate of the + * intersections of edges. + * + * @param dateline + * x-coordinate of the dateline + * @param edges + * set of edges that may intersect with the dateline + * @return number of intersecting edges + */ + protected static int intersections(double dateline, Edge[] edges) { + int numIntersections = 0; + assert !Double.isNaN(dateline); + for (int i = 0; i < edges.length; i++) { + Point p1 = edges[i].coordinate; + Point p2 = edges[i].next.coordinate; + assert !Double.isNaN(p2.getLon()) && !Double.isNaN(p1.getLon()); + edges[i].intersect = Edge.MAX_COORDINATE; + + double position = intersection(p1.getLon(), p2.getLon(), dateline); + if (!Double.isNaN(position)) { + edges[i].intersection(position); + numIntersections++; + } + } + Arrays.sort(edges, INTERSECTION_ORDER); + return numIntersections; + } + + + private static Edge[] edges(Edge[] edges, int numHoles, List> components) { + ArrayList mainEdges = new ArrayList<>(edges.length); + + for (int i = 0; i < edges.length; i++) { + if (edges[i].component >= 0) { + double[] partitionPoint = new double[3]; + int length = component(edges[i], -(components.size()+numHoles+1), mainEdges, partitionPoint); + List component = new ArrayList<>(); + component.add(coordinates(edges[i], new Point[length+1], partitionPoint)); + components.add(component); + } + } + + return mainEdges.toArray(new Edge[mainEdges.size()]); + } + + private static List compose(Edge[] edges, Edge[] holes, int numHoles) { + final List> components = new ArrayList<>(); + assign(holes, holes(holes, numHoles), numHoles, edges(edges, numHoles, components), components); + return buildPoints(components); + } + + private static void assign(Edge[] holes, Point[][] points, int numHoles, Edge[] edges, List> components) { + // Assign Hole to related components + // To find the new component the hole belongs to all intersections of the + // polygon edges with a vertical line are calculated. This vertical line + // is an arbitrary point of the hole. The polygon edge next to this point + // is part of the polygon the hole belongs to. + for (int i = 0; i < numHoles; i++) { + // To do the assignment we assume (and later, elsewhere, check) that each hole is within + // a single component, and the components do not overlap. Based on this assumption, it's + // enough to find a component that contains some vertex of the hole, and + // holes[i].coordinate is such a vertex, so we use that one. + + // First, we sort all the edges according to their order of intersection with the line + // of longitude through holes[i].coordinate, in order from south to north. Edges that do + // not intersect this line are sorted to the end of the array and of no further interest + // here. + final Edge current = new Edge(holes[i].coordinate, holes[i].next); + current.intersect = current.coordinate; + final int intersections = intersections(current.coordinate.getLon(), edges); + + if (intersections == 0) { + // There were no edges that intersect the line of longitude through + // holes[i].coordinate, so there's no way this hole is within the polygon. + throw new InvalidShapeException("Invalid shape: Hole is not within polygon"); + } + + // Next we do a binary search to find the position of holes[i].coordinate in the array. + // The binary search returns the index of an exact match, or (-insertionPoint - 1) if + // the vertex lies between the intersections of edges[insertionPoint] and + // edges[insertionPoint+1]. The latter case is vastly more common. + + final int pos; + boolean sharedVertex = false; + if (((pos = Arrays.binarySearch(edges, 0, intersections, current, INTERSECTION_ORDER)) >= 0) + && !(sharedVertex = (edges[pos].intersect.equals(current.coordinate)))) { + // The binary search returned an exact match, but we checked again using compareTo() + // and it didn't match after all. + + // TODO Can this actually happen? Needs a test to exercise it, or else needs to be removed. + throw new InvalidShapeException("Invalid shape: Hole is not within polygon"); + } + + final int index; + if (sharedVertex) { + // holes[i].coordinate lies exactly on an edge. + index = 0; // TODO Should this be pos instead of 0? This assigns exact matches to the southernmost component. + } else if (pos == -1) { + // holes[i].coordinate is strictly south of all intersections. Assign it to the + // southernmost component, and allow later validation to spot that it is not + // entirely within the chosen component. + index = 0; + } else { + // holes[i].coordinate is strictly north of at least one intersection. Assign it to + // the component immediately to its south. + index = -(pos + 2); + } + + final int component = -edges[index].component - numHoles - 1; + + components.get(component).add(points[i]); + } + } + + /** + * This method sets the component id of all edges in a ring to a given id and shifts the + * coordinates of this component according to the dateline + * + * @param edge An arbitrary edge of the component + * @param id id to apply to the component + * @param edges a list of edges to which all edges of the component will be added (could be null) + * @return number of edges that belong to this component + */ + private static int component(final Edge edge, final int id, final ArrayList edges, double[] partitionPoint) { + // find a coordinate that is not part of the dateline + Edge any = edge; + while(any.coordinate.getLon() == +DATELINE || any.coordinate.getLon() == -DATELINE) { + if((any = any.next) == edge) { + break; + } + } + + double shiftOffset = any.coordinate.getLon() > DATELINE ? DATELINE : (any.coordinate.getLon() < -DATELINE ? -DATELINE : 0); + + // run along the border of the component, collect the + // edges, shift them according to the dateline and + // update the component id + int length = 0, connectedComponents = 0; + // if there are two connected components, splitIndex keeps track of where to split the edge array + // start at 1 since the source coordinate is shared + int splitIndex = 1; + Edge current = edge; + Edge prev = edge; + // bookkeep the source and sink of each visited coordinate + HashMap> visitedEdge = new HashMap<>(); + do { + current.coordinate = shift(current.coordinate, shiftOffset); + current.component = id; + + if (edges != null) { + // found a closed loop - we have two connected components so we need to slice into two distinct components + if (visitedEdge.containsKey(current.coordinate)) { + partitionPoint[0] = current.coordinate.getLon(); + partitionPoint[1] = current.coordinate.getLat(); + if (connectedComponents > 0 && current.next != edge) { + throw new InvalidShapeException("Shape contains more than one shared point"); + } + + // a negative id flags the edge as visited for the edges(...) method. + // since we're splitting connected components, we want the edges method to visit + // the newly separated component + final int visitID = -id; + Edge firstAppearance = visitedEdge.get(current.coordinate).v2(); + // correct the graph pointers by correcting the 'next' pointer for both the + // first appearance and this appearance of the edge + Edge temp = firstAppearance.next; + firstAppearance.next = current.next; + current.next = temp; + current.component = visitID; + // backtrack until we get back to this coordinate, setting the visit id to + // a non-visited value (anything positive) + do { + prev.component = visitID; + prev = visitedEdge.get(prev.coordinate).v1(); + ++splitIndex; + } while (!current.coordinate.equals(prev.coordinate)); + ++connectedComponents; + } else { + visitedEdge.put(current.coordinate, new Tuple(prev, current)); + } + edges.add(current); + prev = current; + } + length++; + } while(connectedComponents == 0 && (current = current.next) != edge); + + return (splitIndex != 1) ? length-splitIndex: length; + } + + /** + * Compute all coordinates of a component + * @param component an arbitrary edge of the component + * @param coordinates Array of coordinates to write the result to + * @return the coordinates parameter + */ + private static Point[] coordinates(Edge component, Point[] coordinates, double[] partitionPoint) { + for (int i = 0; i < coordinates.length; i++) { + coordinates[i] = (component = component.next).coordinate; + } + // First and last coordinates must be equal + if (coordinates[0].equals(coordinates[coordinates.length - 1]) == false) { + if (partitionPoint[2] == Double.NaN) { + throw new InvalidShapeException("Self-intersection at or near point [" + + partitionPoint[0] + "," + partitionPoint[1] + "]"); + } else { + throw new InvalidShapeException("Self-intersection at or near point [" + + partitionPoint[0] + "," + partitionPoint[1] + "," + partitionPoint[2] + "]"); + } + } + return coordinates; + } + + private static List buildPoints(List> components) { + List result = new ArrayList<>(components.size()); + for (int i = 0; i < components.size(); i++) { + List component = components.get(i); + result.add(buildPolygon(component)); + } + return result; + } + + private static Polygon buildPolygon(List polygon) { + List holes; + Point[] shell = polygon.get(0); + if (polygon.size() > 1) { + holes = new ArrayList<>(polygon.size() - 1); + for (int i = 1; i < polygon.size(); ++i) { + Point[] coords = polygon.get(i); + //We do not have holes on the dateline as they get eliminated + //when breaking the polygon around it. + double[] x = new double[coords.length]; + double[] y = new double[coords.length]; + for (int c = 0; c < coords.length; ++c) { + x[c] = normalizeLon(coords[c].getLon()); + y[c] = normalizeLat(coords[c].getLat()); + } + holes.add(new org.elasticsearch.geo.geometry.LinearRing(y, x)); + } + } else { + holes = Collections.emptyList(); + } + + double[] x = new double[shell.length]; + double[] y = new double[shell.length]; + for (int i = 0; i < shell.length; ++i) { + //Lucene Tessellator treats different +180 and -180 and we should keep the sign. + //normalizeLon method excludes -180. + x[i] = Math.abs(shell[i].getLon()) > 180 ? normalizeLon(shell[i].getLon()) : shell[i].getLon(); + y[i] = normalizeLat(shell[i].getLat()); + } + + return new Polygon(new LinearRing(y, x), holes); + } + + private static Point[][] holes(Edge[] holes, int numHoles) { + if (numHoles == 0) { + return new Point[0][]; + } + final Point[][] points = new Point[numHoles][]; + + for (int i = 0; i < numHoles; i++) { + double[] partitionPoint = new double[3]; + int length = component(holes[i], -(i+1), null, partitionPoint); // mark as visited by inverting the sign + points[i] = coordinates(holes[i], new Point[length+1], partitionPoint); + } + + return points; + } +} diff --git a/server/src/main/java/org/elasticsearch/index/mapper/GeoShapeFieldMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/GeoShapeFieldMapper.java index 6449c06fbe1ad..2ce1d5328f3b9 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/GeoShapeFieldMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/GeoShapeFieldMapper.java @@ -24,8 +24,9 @@ import org.apache.lucene.geo.Polygon; import org.apache.lucene.index.IndexableField; import org.elasticsearch.common.Explicit; +import org.elasticsearch.common.geo.GeometryIndexer; +import org.elasticsearch.common.geo.GeometryParser; import org.elasticsearch.common.geo.builders.ShapeBuilder; -import org.elasticsearch.common.geo.parsers.ShapeParser; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.geo.geometry.Circle; import org.elasticsearch.geo.geometry.Geometry; @@ -91,12 +92,17 @@ public GeoShapeFieldType clone() { } } + private final GeometryParser geometryParser; + private final GeometryIndexer geometryIndexer; + public GeoShapeFieldMapper(String simpleName, MappedFieldType fieldType, MappedFieldType defaultFieldType, Explicit ignoreMalformed, Explicit coerce, Explicit ignoreZValue, Settings indexSettings, MultiFields multiFields, CopyTo copyTo) { super(simpleName, fieldType, defaultFieldType, ignoreMalformed, coerce, ignoreZValue, indexSettings, multiFields, copyTo); + geometryParser = new GeometryParser(orientation() == ShapeBuilder.Orientation.RIGHT, coerce().value(), ignoreZValue.value()); + geometryIndexer = new GeometryIndexer(true); } @Override @@ -108,13 +114,14 @@ public GeoShapeFieldType fieldType() { @Override public void parse(ParseContext context) throws IOException { try { + Object shape = context.parseExternalValue(Object.class); if (shape == null) { - ShapeBuilder shapeBuilder = ShapeParser.parse(context.parser(), this); - if (shapeBuilder == null) { + Geometry geometry = geometryParser.parse(context.parser()); + if (geometry == null) { return; } - shape = shapeBuilder.buildGeometry(); + shape = geometryIndexer.prepareForIndexing(geometry); } indexShape(context, shape); } catch (Exception e) { diff --git a/server/src/test/java/org/elasticsearch/common/geo/BaseGeoParsingTestCase.java b/server/src/test/java/org/elasticsearch/common/geo/BaseGeoParsingTestCase.java index 9548d14cca9a1..9e5d7d7c6ce09 100644 --- a/server/src/test/java/org/elasticsearch/common/geo/BaseGeoParsingTestCase.java +++ b/server/src/test/java/org/elasticsearch/common/geo/BaseGeoParsingTestCase.java @@ -31,6 +31,7 @@ import org.locationtech.spatial4j.shape.jts.JtsGeometry; import java.io.IOException; +import java.text.ParseException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -41,14 +42,14 @@ abstract class BaseGeoParsingTestCase extends ESTestCase { protected static final GeometryFactory GEOMETRY_FACTORY = SPATIAL_CONTEXT.getGeometryFactory(); - public abstract void testParsePoint() throws IOException; - public abstract void testParseMultiPoint() throws IOException; - public abstract void testParseLineString() throws IOException; - public abstract void testParseMultiLineString() throws IOException; - public abstract void testParsePolygon() throws IOException; - public abstract void testParseMultiPolygon() throws IOException; - public abstract void testParseEnvelope() throws IOException; - public abstract void testParseGeometryCollection() throws IOException; + public abstract void testParsePoint() throws IOException, ParseException; + public abstract void testParseMultiPoint() throws IOException, ParseException; + public abstract void testParseLineString() throws IOException, ParseException; + public abstract void testParseMultiLineString() throws IOException, ParseException; + public abstract void testParsePolygon() throws IOException, ParseException; + public abstract void testParseMultiPolygon() throws IOException, ParseException; + public abstract void testParseEnvelope() throws IOException, ParseException; + public abstract void testParseGeometryCollection() throws IOException, ParseException; protected void assertValidException(XContentBuilder builder, Class expectedException) throws IOException { try (XContentParser parser = createParser(builder)) { @@ -57,13 +58,16 @@ protected void assertValidException(XContentBuilder builder, Class expectedEx } } - protected void assertGeometryEquals(Object expected, XContentBuilder geoJson, boolean useJTS) throws IOException { + protected void assertGeometryEquals(Object expected, XContentBuilder geoJson, boolean useJTS) throws IOException, ParseException { try (XContentParser parser = createParser(geoJson)) { parser.nextToken(); if (useJTS) { ElasticsearchGeoAssertions.assertEquals(expected, ShapeParser.parse(parser).buildS4J()); } else { - ElasticsearchGeoAssertions.assertEquals(expected, ShapeParser.parse(parser).buildGeometry()); + GeometryParser geometryParser = new GeometryParser(true, true, true); + org.elasticsearch.geo.geometry.Geometry shape = geometryParser.parse(parser); + shape = new GeometryIndexer(true).prepareForIndexing(shape); + ElasticsearchGeoAssertions.assertEquals(expected, shape); } } } diff --git a/server/src/test/java/org/elasticsearch/common/geo/GeoJsonParserTests.java b/server/src/test/java/org/elasticsearch/common/geo/GeoJsonParserTests.java index 4146adb2d299a..ef45194146de3 100644 --- a/server/src/test/java/org/elasticsearch/common/geo/GeoJsonParserTests.java +++ b/server/src/test/java/org/elasticsearch/common/geo/GeoJsonParserTests.java @@ -39,6 +39,7 @@ import org.elasticsearch.geo.utils.GeographyValidator; import java.io.IOException; +import java.text.ParseException; import java.util.Arrays; import java.util.Collections; @@ -149,7 +150,7 @@ public void testParseMultiDimensionShapes() throws IOException { @Override public void testParseEnvelope() throws IOException { // test #1: envelope with expected coordinate order (TopLeft, BottomRight) - XContentBuilder multilinesGeoJson = XContentFactory.jsonBuilder().startObject().field("type", "envelope") + XContentBuilder multilinesGeoJson = XContentFactory.jsonBuilder().startObject().field("type", randomBoolean() ? "envelope" : "bbox") .startArray("coordinates") .startArray().value(-50).value(30).endArray() .startArray().value(50).value(-30).endArray() @@ -159,7 +160,7 @@ public void testParseEnvelope() throws IOException { assertGeometryEquals(expected, multilinesGeoJson); // test #2: envelope that spans dateline - multilinesGeoJson = XContentFactory.jsonBuilder().startObject().field("type", "envelope") + multilinesGeoJson = XContentFactory.jsonBuilder().startObject().field("type", randomBoolean() ? "envelope" : "bbox") .startArray("coordinates") .startArray().value(50).value(30).endArray() .startArray().value(-50).value(-30).endArray() @@ -170,7 +171,7 @@ public void testParseEnvelope() throws IOException { assertGeometryEquals(expected, multilinesGeoJson); // test #3: "envelope" (actually a triangle) with invalid number of coordinates (TopRight, BottomLeft, BottomRight) - multilinesGeoJson = XContentFactory.jsonBuilder().startObject().field("type", "envelope") + multilinesGeoJson = XContentFactory.jsonBuilder().startObject().field("type", randomBoolean() ? "envelope" : "bbox") .startArray("coordinates") .startArray().value(50).value(30).endArray() .startArray().value(-50).value(-30).endArray() @@ -184,7 +185,7 @@ public void testParseEnvelope() throws IOException { } // test #4: "envelope" with empty coordinates - multilinesGeoJson = XContentFactory.jsonBuilder().startObject().field("type", "envelope") + multilinesGeoJson = XContentFactory.jsonBuilder().startObject().field("type", randomBoolean() ? "envelope" : "bbox") .startArray("coordinates") .endArray() .endObject(); @@ -618,7 +619,7 @@ public void testParseGeometryCollection() throws IOException { assertGeometryEquals(geometryExpected, geometryCollectionGeoJson); } - public void testThatParserExtractsCorrectTypeAndCoordinatesFromArbitraryJson() throws IOException { + public void testThatParserExtractsCorrectTypeAndCoordinatesFromArbitraryJson() throws IOException, ParseException { XContentBuilder pointGeoJson = XContentFactory.jsonBuilder() .startObject() .startObject("crs") diff --git a/server/src/test/java/org/elasticsearch/common/geo/GeoJsonShapeParserTests.java b/server/src/test/java/org/elasticsearch/common/geo/GeoJsonShapeParserTests.java index 74024ddcada5e..eb919eaf5ef41 100644 --- a/server/src/test/java/org/elasticsearch/common/geo/GeoJsonShapeParserTests.java +++ b/server/src/test/java/org/elasticsearch/common/geo/GeoJsonShapeParserTests.java @@ -54,6 +54,7 @@ import org.locationtech.spatial4j.shape.jts.JtsPoint; import java.io.IOException; +import java.text.ParseException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -68,7 +69,7 @@ public class GeoJsonShapeParserTests extends BaseGeoParsingTestCase { @Override - public void testParsePoint() throws IOException { + public void testParsePoint() throws IOException, ParseException { XContentBuilder pointGeoJson = XContentFactory.jsonBuilder() .startObject() .field("type", "Point") @@ -80,7 +81,7 @@ public void testParsePoint() throws IOException { } @Override - public void testParseLineString() throws IOException { + public void testParseLineString() throws IOException, ParseException { XContentBuilder lineGeoJson = XContentFactory.jsonBuilder() .startObject() .field("type", "LineString") @@ -102,12 +103,12 @@ public void testParseLineString() throws IOException { try (XContentParser parser = createParser(lineGeoJson)) { parser.nextToken(); - ElasticsearchGeoAssertions.assertLineString(ShapeParser.parse(parser).buildGeometry(), false); + ElasticsearchGeoAssertions.assertLineString(parse(parser), false); } } @Override - public void testParseMultiLineString() throws IOException { + public void testParseMultiLineString() throws IOException, ParseException { XContentBuilder multilinesGeoJson = XContentFactory.jsonBuilder() .startObject() .field("type", "MultiLineString") @@ -140,7 +141,7 @@ public void testParseMultiLineString() throws IOException { multilinesGeoJson, false); } - public void testParseCircle() throws IOException { + public void testParseCircle() throws IOException, ParseException { XContentBuilder multilinesGeoJson = XContentFactory.jsonBuilder() .startObject() .field("type", "circle") @@ -182,7 +183,7 @@ public void testParseMultiDimensionShapes() throws IOException { } @Override - public void testParseEnvelope() throws IOException { + public void testParseEnvelope() throws IOException, ParseException { // test #1: envelope with expected coordinate order (TopLeft, BottomRight) XContentBuilder multilinesGeoJson = XContentFactory.jsonBuilder().startObject().field("type", "envelope") .startArray("coordinates") @@ -235,7 +236,7 @@ public void testParseEnvelope() throws IOException { } @Override - public void testParsePolygon() throws IOException { + public void testParsePolygon() throws IOException, ParseException { XContentBuilder polygonGeoJson = XContentFactory.jsonBuilder() .startObject() .field("type", "Polygon") @@ -268,7 +269,7 @@ public void testParsePolygon() throws IOException { assertGeometryEquals(p, polygonGeoJson, false); } - public void testParse3DPolygon() throws IOException { + public void testParse3DPolygon() throws IOException, ParseException { XContentBuilder polygonGeoJson = XContentFactory.jsonBuilder() .startObject() .field("type", "Polygon") @@ -485,7 +486,7 @@ public void testParseInvalidDimensionalMultiPolygon() throws IOException { } - public void testParseOGCPolygonWithoutHoles() throws IOException { + public void testParseOGCPolygonWithoutHoles() throws IOException, ParseException { // test 1: ccw poly not crossing dateline String polygonGeoJson = Strings.toString(XContentFactory.jsonBuilder().startObject().field("type", "Polygon") .startArray("coordinates") @@ -508,7 +509,7 @@ public void testParseOGCPolygonWithoutHoles() throws IOException { try (XContentParser parser = createParser(JsonXContent.jsonXContent, polygonGeoJson)) { parser.nextToken(); - ElasticsearchGeoAssertions.assertPolygon(ShapeParser.parse(parser).buildGeometry(), false); + ElasticsearchGeoAssertions.assertPolygon(parse(parser), false); } // test 2: ccw poly crossing dateline @@ -533,7 +534,7 @@ public void testParseOGCPolygonWithoutHoles() throws IOException { try (XContentParser parser = createParser(JsonXContent.jsonXContent, polygonGeoJson)) { parser.nextToken(); - ElasticsearchGeoAssertions.assertMultiPolygon(ShapeParser.parse(parser).buildGeometry(), false); + ElasticsearchGeoAssertions.assertMultiPolygon(parse(parser), false); } // test 3: cw poly not crossing dateline @@ -558,7 +559,7 @@ public void testParseOGCPolygonWithoutHoles() throws IOException { try (XContentParser parser = createParser(JsonXContent.jsonXContent, polygonGeoJson)) { parser.nextToken(); - ElasticsearchGeoAssertions.assertPolygon(ShapeParser.parse(parser).buildGeometry(), false); + ElasticsearchGeoAssertions.assertPolygon(parse(parser), false); } // test 4: cw poly crossing dateline @@ -583,11 +584,11 @@ public void testParseOGCPolygonWithoutHoles() throws IOException { try (XContentParser parser = createParser(JsonXContent.jsonXContent, polygonGeoJson)) { parser.nextToken(); - ElasticsearchGeoAssertions.assertMultiPolygon(ShapeParser.parse(parser).buildGeometry(), false); + ElasticsearchGeoAssertions.assertMultiPolygon(parse(parser), false); } } - public void testParseOGCPolygonWithHoles() throws IOException { + public void testParseOGCPolygonWithHoles() throws IOException, ParseException { // test 1: ccw poly not crossing dateline String polygonGeoJson = Strings.toString(XContentFactory.jsonBuilder().startObject().field("type", "Polygon") .startArray("coordinates") @@ -616,7 +617,7 @@ public void testParseOGCPolygonWithHoles() throws IOException { try (XContentParser parser = createParser(JsonXContent.jsonXContent, polygonGeoJson)) { parser.nextToken(); - ElasticsearchGeoAssertions.assertPolygon(ShapeParser.parse(parser).buildGeometry(), false); + ElasticsearchGeoAssertions.assertPolygon(parse(parser), false); } // test 2: ccw poly crossing dateline @@ -647,7 +648,7 @@ public void testParseOGCPolygonWithHoles() throws IOException { try (XContentParser parser = createParser(JsonXContent.jsonXContent, polygonGeoJson)) { parser.nextToken(); - ElasticsearchGeoAssertions.assertMultiPolygon(ShapeParser.parse(parser).buildGeometry(), false); + ElasticsearchGeoAssertions.assertMultiPolygon(parse(parser), false); } // test 3: cw poly not crossing dateline @@ -678,7 +679,7 @@ public void testParseOGCPolygonWithHoles() throws IOException { try (XContentParser parser = createParser(JsonXContent.jsonXContent, polygonGeoJson)) { parser.nextToken(); - ElasticsearchGeoAssertions.assertPolygon(ShapeParser.parse(parser).buildGeometry(), false); + ElasticsearchGeoAssertions.assertPolygon(parse(parser), false); } // test 4: cw poly crossing dateline @@ -709,7 +710,7 @@ public void testParseOGCPolygonWithHoles() throws IOException { try (XContentParser parser = createParser(JsonXContent.jsonXContent, polygonGeoJson)) { parser.nextToken(); - ElasticsearchGeoAssertions.assertMultiPolygon(ShapeParser.parse(parser).buildGeometry(), false); + ElasticsearchGeoAssertions.assertMultiPolygon(parse(parser), false); } } @@ -816,7 +817,7 @@ public void testParseInvalidPolygon() throws IOException { } } - public void testParsePolygonWithHole() throws IOException { + public void testParsePolygonWithHole() throws IOException, ParseException { XContentBuilder polygonGeoJson = XContentFactory.jsonBuilder() .startObject() .field("type", "Polygon") @@ -894,7 +895,7 @@ public void testParseSelfCrossingPolygon() throws IOException { } @Override - public void testParseMultiPoint() throws IOException { + public void testParseMultiPoint() throws IOException, ParseException { XContentBuilder multiPointGeoJson = XContentFactory.jsonBuilder() .startObject() .field("type", "MultiPoint") @@ -914,7 +915,7 @@ public void testParseMultiPoint() throws IOException { } @Override - public void testParseMultiPolygon() throws IOException { + public void testParseMultiPolygon() throws IOException, ParseException { // test #1: two polygons; one without hole, one with hole XContentBuilder multiPolygonGeoJson = XContentFactory.jsonBuilder() .startObject() @@ -1043,14 +1044,14 @@ public void testParseMultiPolygon() throws IOException { new org.elasticsearch.geo.geometry.LinearRing( new double[] {0.8d, 0.2d, 0.2d, 0.8d, 0.8d}, new double[] {100.8d, 100.8d, 100.2d, 100.2d, 100.8d}); - org.elasticsearch.geo.geometry.MultiPolygon lucenePolygons = new org.elasticsearch.geo.geometry.MultiPolygon( - Collections.singletonList(new org.elasticsearch.geo.geometry.Polygon(new org.elasticsearch.geo.geometry.LinearRing( - new double[] {0d, 0d, 1d, 1d, 0d}, new double[] {100d, 101d, 101d, 100d, 100d}), Collections.singletonList(luceneHole)))); + org.elasticsearch.geo.geometry.Polygon lucenePolygons = (new org.elasticsearch.geo.geometry.Polygon( + new org.elasticsearch.geo.geometry.LinearRing( + new double[] {0d, 0d, 1d, 1d, 0d}, new double[] {100d, 101d, 101d, 100d, 100d}), Collections.singletonList(luceneHole))); assertGeometryEquals(lucenePolygons, multiPolygonGeoJson, false); } @Override - public void testParseGeometryCollection() throws IOException { + public void testParseGeometryCollection() throws IOException, ParseException { XContentBuilder geometryCollectionGeoJson = XContentFactory.jsonBuilder() .startObject() .field("type", "GeometryCollection") @@ -1138,7 +1139,7 @@ public void testParseGeometryCollection() throws IOException { assertGeometryEquals(geometryExpected, geometryCollectionGeoJson, false); } - public void testThatParserExtractsCorrectTypeAndCoordinatesFromArbitraryJson() throws IOException { + public void testThatParserExtractsCorrectTypeAndCoordinatesFromArbitraryJson() throws IOException, ParseException { XContentBuilder pointGeoJson = XContentFactory.jsonBuilder() .startObject() .startObject("crs") @@ -1161,7 +1162,7 @@ public void testThatParserExtractsCorrectTypeAndCoordinatesFromArbitraryJson() t assertGeometryEquals(expectedPt, pointGeoJson, false); } - public void testParseOrientationOption() throws IOException { + public void testParseOrientationOption() throws IOException, ParseException { // test 1: valid ccw (right handed system) poly not crossing dateline (with 'right' field) XContentBuilder polygonGeoJson = XContentFactory.jsonBuilder() .startObject() @@ -1193,7 +1194,7 @@ public void testParseOrientationOption() throws IOException { try (XContentParser parser = createParser(polygonGeoJson)) { parser.nextToken(); - ElasticsearchGeoAssertions.assertPolygon(ShapeParser.parse(parser).buildGeometry(), false); + ElasticsearchGeoAssertions.assertPolygon(parse(parser), false); } // test 2: valid ccw (right handed system) poly not crossing dateline (with 'ccw' field) @@ -1227,7 +1228,7 @@ public void testParseOrientationOption() throws IOException { try (XContentParser parser = createParser(polygonGeoJson)) { parser.nextToken(); - ElasticsearchGeoAssertions.assertPolygon(ShapeParser.parse(parser).buildGeometry(), false); + ElasticsearchGeoAssertions.assertPolygon(parse(parser), false); } // test 3: valid ccw (right handed system) poly not crossing dateline (with 'counterclockwise' field) @@ -1261,7 +1262,7 @@ public void testParseOrientationOption() throws IOException { try (XContentParser parser = createParser(polygonGeoJson)) { parser.nextToken(); - ElasticsearchGeoAssertions.assertPolygon(ShapeParser.parse(parser).buildGeometry(), false); + ElasticsearchGeoAssertions.assertPolygon(parse(parser), false); } // test 4: valid cw (left handed system) poly crossing dateline (with 'left' field) @@ -1295,7 +1296,7 @@ public void testParseOrientationOption() throws IOException { try (XContentParser parser = createParser(polygonGeoJson)) { parser.nextToken(); - ElasticsearchGeoAssertions.assertMultiPolygon(ShapeParser.parse(parser).buildGeometry(), false); + ElasticsearchGeoAssertions.assertMultiPolygon(parse(parser), false); } // test 5: valid cw multipoly (left handed system) poly crossing dateline (with 'cw' field) @@ -1329,7 +1330,7 @@ public void testParseOrientationOption() throws IOException { try (XContentParser parser = createParser(polygonGeoJson)) { parser.nextToken(); - ElasticsearchGeoAssertions.assertMultiPolygon(ShapeParser.parse(parser).buildGeometry(), false); + ElasticsearchGeoAssertions.assertMultiPolygon(parse(parser), false); } // test 6: valid cw multipoly (left handed system) poly crossing dateline (with 'clockwise' field) @@ -1363,7 +1364,7 @@ public void testParseOrientationOption() throws IOException { try (XContentParser parser = createParser(polygonGeoJson)) { parser.nextToken(); - ElasticsearchGeoAssertions.assertMultiPolygon(ShapeParser.parse(parser).buildGeometry(), false); + ElasticsearchGeoAssertions.assertMultiPolygon(parse(parser), false); } } @@ -1421,4 +1422,10 @@ public void testParseInvalidGeometryCollectionShapes() throws IOException { assertNull(parser.nextToken()); // no more elements afterwards } } + + public Geometry parse(XContentParser parser) throws IOException, ParseException { + GeometryParser geometryParser = new GeometryParser(true, true, true); + GeometryIndexer indexer = new GeometryIndexer(true); + return indexer.prepareForIndexing(geometryParser.parse(parser)); + } } diff --git a/server/src/test/java/org/elasticsearch/common/geo/GeoWKTShapeParserTests.java b/server/src/test/java/org/elasticsearch/common/geo/GeoWKTShapeParserTests.java index 286e1ce6ee7c5..8610fa551c338 100644 --- a/server/src/test/java/org/elasticsearch/common/geo/GeoWKTShapeParserTests.java +++ b/server/src/test/java/org/elasticsearch/common/geo/GeoWKTShapeParserTests.java @@ -62,6 +62,7 @@ import org.locationtech.spatial4j.shape.jts.JtsPoint; import java.io.IOException; +import java.text.ParseException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -91,7 +92,7 @@ private static XContentBuilder toWKTContent(ShapeBuilder builder, boole return XContentFactory.jsonBuilder().value(wkt); } - private void assertExpected(Object expected, ShapeBuilder builder, boolean useJTS) throws IOException { + private void assertExpected(Object expected, ShapeBuilder builder, boolean useJTS) throws IOException, ParseException { XContentBuilder xContentBuilder = toWKTContent(builder, false); assertGeometryEquals(expected, xContentBuilder, useJTS); } @@ -102,7 +103,7 @@ private void assertMalformed(ShapeBuilder builder) throws IOException { } @Override - public void testParsePoint() throws IOException { + public void testParsePoint() throws IOException, ParseException { GeoPoint p = RandomShapeGenerator.randomPoint(random()); Coordinate c = new Coordinate(p.lon(), p.lat()); Point expected = GEOMETRY_FACTORY.createPoint(c); @@ -112,7 +113,7 @@ public void testParsePoint() throws IOException { } @Override - public void testParseMultiPoint() throws IOException { + public void testParseMultiPoint() throws IOException, ParseException { int numPoints = randomIntBetween(0, 100); List coordinates = new ArrayList<>(numPoints); for (int i = 0; i < numPoints; ++i) { @@ -160,7 +161,7 @@ private List randomLineStringCoords() { } @Override - public void testParseLineString() throws IOException { + public void testParseLineString() throws IOException, ParseException { List coordinates = randomLineStringCoords(); LineString expected = GEOMETRY_FACTORY.createLineString(coordinates.toArray(new Coordinate[coordinates.size()])); assertExpected(jtsGeom(expected), new LineStringBuilder(coordinates), true); @@ -175,7 +176,7 @@ public void testParseLineString() throws IOException { } @Override - public void testParseMultiLineString() throws IOException { + public void testParseMultiLineString() throws IOException, ParseException { int numLineStrings = randomIntBetween(0, 8); List lineStrings = new ArrayList<>(numLineStrings); MultiLineStringBuilder builder = new MultiLineStringBuilder(); @@ -210,7 +211,7 @@ public void testParseMultiLineString() throws IOException { } @Override - public void testParsePolygon() throws IOException { + public void testParsePolygon() throws IOException, ParseException { PolygonBuilder builder = PolygonBuilder.class.cast( RandomShapeGenerator.createShape(random(), RandomShapeGenerator.ShapeType.POLYGON)); Coordinate[] coords = builder.coordinates()[0][0]; @@ -222,7 +223,7 @@ public void testParsePolygon() throws IOException { } @Override - public void testParseMultiPolygon() throws IOException { + public void testParseMultiPolygon() throws IOException, ParseException { int numPolys = randomIntBetween(0, 8); MultiPolygonBuilder builder = new MultiPolygonBuilder(); PolygonBuilder pb; @@ -242,7 +243,7 @@ public void testParseMultiPolygon() throws IOException { assertMalformed(builder); } - public void testParsePolygonWithHole() throws IOException { + public void testParsePolygonWithHole() throws IOException, ParseException { // add 3d point to test ISSUE #10501 List shellCoordinates = new ArrayList<>(); shellCoordinates.add(new Coordinate(100, 0)); @@ -279,7 +280,7 @@ public void testParsePolygonWithHole() throws IOException { assertMalformed(polygonWithHole); } - public void testParseMixedDimensionPolyWithHole() throws IOException { + public void testParseMixedDimensionPolyWithHole() throws IOException, ParseException { List shellCoordinates = new ArrayList<>(); shellCoordinates.add(new Coordinate(100, 0)); shellCoordinates.add(new Coordinate(101, 0)); @@ -436,7 +437,7 @@ public void testMalformedWKT() throws IOException { } @Override - public void testParseEnvelope() throws IOException { + public void testParseEnvelope() throws IOException, ParseException { org.apache.lucene.geo.Rectangle r = GeoTestUtil.nextBox(); EnvelopeBuilder builder = new EnvelopeBuilder(new Coordinate(r.minLon, r.maxLat), new Coordinate(r.maxLon, r.minLat)); @@ -452,7 +453,7 @@ public void testInvalidGeometryType() throws IOException { } @Override - public void testParseGeometryCollection() throws IOException { + public void testParseGeometryCollection() throws IOException, ParseException { if (rarely()) { // assert empty shape collection GeometryCollectionBuilder builder = new GeometryCollectionBuilder(); diff --git a/server/src/test/java/org/elasticsearch/common/geo/GeometryIndexerTests.java b/server/src/test/java/org/elasticsearch/common/geo/GeometryIndexerTests.java new file mode 100644 index 0000000000000..5ab5aaff33e05 --- /dev/null +++ b/server/src/test/java/org/elasticsearch/common/geo/GeometryIndexerTests.java @@ -0,0 +1,239 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.common.geo; + +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentFactory; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.geo.geometry.Circle; +import org.elasticsearch.geo.geometry.Geometry; +import org.elasticsearch.geo.geometry.GeometryCollection; +import org.elasticsearch.geo.geometry.Line; +import org.elasticsearch.geo.geometry.LinearRing; +import org.elasticsearch.geo.geometry.MultiLine; +import org.elasticsearch.geo.geometry.MultiPoint; +import org.elasticsearch.geo.geometry.MultiPolygon; +import org.elasticsearch.geo.geometry.Point; +import org.elasticsearch.geo.geometry.Polygon; +import org.elasticsearch.geo.utils.WellKnownText; +import org.elasticsearch.test.ESTestCase; + +import java.io.IOException; +import java.text.ParseException; +import java.util.Arrays; +import java.util.Collections; + +public class GeometryIndexerTests extends ESTestCase { + + GeometryIndexer indexer = new GeometryIndexer(true); + private static final WellKnownText WKT = new WellKnownText(true, geometry -> { + }); + + + public void testCircle() { + UnsupportedOperationException ex = + expectThrows(UnsupportedOperationException.class, () -> indexer.prepareForIndexing(new Circle(1, 2, 3))); + assertEquals("CIRCLE geometry is not supported", ex.getMessage()); + } + + public void testCollection() { + assertEquals(GeometryCollection.EMPTY, indexer.prepareForIndexing(GeometryCollection.EMPTY)); + + GeometryCollection collection = new GeometryCollection<>(Collections.singletonList( + new Point(1, 2) + )); + + Geometry indexed = new Point(1, 2); + assertEquals(indexed, indexer.prepareForIndexing(collection)); + + collection = new GeometryCollection<>(Arrays.asList( + new Point(1, 2), new Point(3, 4), new Line(new double[]{10, 20}, new double[]{160, 200}) + )); + + indexed = new GeometryCollection<>(Arrays.asList( + new Point(1, 2), new Point(3, 4), + new MultiLine(Arrays.asList( + new Line(new double[]{10, 15}, new double[]{160, 180}), + new Line(new double[]{15, 20}, new double[]{180, -160})) + )) + ); + assertEquals(indexed, indexer.prepareForIndexing(collection)); + + } + + public void testLine() { + Line line = new Line(new double[]{1, 2}, new double[]{3, 4}); + Geometry indexed = line; + assertEquals(indexed, indexer.prepareForIndexing(line)); + + line = new Line(new double[]{10, 20}, new double[]{160, 200}); + indexed = new MultiLine(Arrays.asList( + new Line(new double[]{10, 15}, new double[]{160, 180}), + new Line(new double[]{15, 20}, new double[]{180, -160})) + ); + + assertEquals(indexed, indexer.prepareForIndexing(line)); + } + + public void testMultiLine() { + Line line = new Line(new double[]{1, 2}, new double[]{3, 4}); + MultiLine multiLine = new MultiLine(Collections.singletonList(line)); + Geometry indexed = line; + assertEquals(indexed, indexer.prepareForIndexing(multiLine)); + + multiLine = new MultiLine(Arrays.asList( + line, new Line(new double[]{10, 20}, new double[]{160, 200}) + )); + + indexed = new MultiLine(Arrays.asList( + line, + new Line(new double[]{10, 15}, new double[]{160, 180}), + new Line(new double[]{15, 20}, new double[]{180, -160})) + ); + + assertEquals(indexed, indexer.prepareForIndexing(multiLine)); + } + + public void testPoint() { + Point point = new Point(1, 2); + Geometry indexed = point; + assertEquals(indexed, indexer.prepareForIndexing(point)); + + point = new Point(1, 2, 3); + assertEquals(indexed, indexer.prepareForIndexing(point)); + } + + public void testMultiPoint() { + MultiPoint multiPoint = MultiPoint.EMPTY; + Geometry indexed = multiPoint; + assertEquals(indexed, indexer.prepareForIndexing(multiPoint)); + + multiPoint = new MultiPoint(Collections.singletonList(new Point(1, 2))); + indexed = new Point(1, 2); + assertEquals(indexed, indexer.prepareForIndexing(multiPoint)); + + multiPoint = new MultiPoint(Arrays.asList(new Point(1, 2), new Point(3, 4))); + indexed = multiPoint; + assertEquals(indexed, indexer.prepareForIndexing(multiPoint)); + + multiPoint = new MultiPoint(Arrays.asList(new Point(1, 2, 10), new Point(3, 4, 10))); + assertEquals(indexed, indexer.prepareForIndexing(multiPoint)); + } + + public void testPolygon() { + Polygon polygon = new Polygon(new LinearRing(new double[]{10, 10, 20, 20, 10}, new double[]{160, 200, 200, 160, 160})); + Geometry indexed = new MultiPolygon(Arrays.asList( + new Polygon(new LinearRing(new double[]{10, 20, 20, 10, 10}, new double[]{180, 180, 160, 160, 180})), + new Polygon(new LinearRing(new double[]{20, 10, 10, 20, 20}, new double[]{-180, -180, -160, -160, -180})) + )); + + assertEquals(indexed, indexer.prepareForIndexing(polygon)); + + polygon = new Polygon(new LinearRing(new double[]{10, 10, 20, 20, 10}, new double[]{160, 200, 200, 160, 160}), + Collections.singletonList( + new LinearRing(new double[]{12, 18, 18, 12, 12}, new double[]{165, 165, 195, 195, 165}))); + + indexed = new MultiPolygon(Arrays.asList( + new Polygon(new LinearRing( + new double[]{10, 12, 12, 18, 18, 20, 20, 10, 10}, + new double[]{180, 180, 165, 165, 180, 180, 160, 160, 180})), + new Polygon(new LinearRing( + new double[]{12, 10, 10, 20, 20, 18, 18, 12, 12}, + new double[]{-180, -180, -160, -160, -180, -180, -165, -165, -180})) + )); + + assertEquals(indexed, indexer.prepareForIndexing(polygon)); + } + + public void testPolygonOrientation() throws IOException, ParseException { + assertEquals(expected("POLYGON ((160 10, -160 10, -160 0, 160 0, 160 10))"), // current algorithm shifts edges to left + actual("POLYGON ((160 0, 160 10, -160 10, -160 0, 160 0))", randomBoolean())); // In WKT the orientation is ignored + + assertEquals(expected("POLYGON ((20 10, -20 10, -20 0, 20 0, 20 10)))"), + actual("POLYGON ((20 0, 20 10, -20 10, -20 0, 20 0))", randomBoolean())); + + assertEquals(expected("POLYGON ((160 10, -160 10, -160 0, 160 0, 160 10))"), + actual(polygon(null, 160, 0, 160, 10, -160, 10, -160, 0, 160, 0), true)); + + assertEquals(expected("MULTIPOLYGON (((180 0, 180 10, 160 10, 160 0, 180 0)), ((-180 10, -180 0, -160 0, -160 10, -180 10)))"), + actual(polygon(randomBoolean() ? null : false, 160, 0, 160, 10, -160, 10, -160, 0, 160, 0), false)); + + assertEquals(expected("MULTIPOLYGON (((180 0, 180 10, 160 10, 160 0, 180 0)), ((-180 10, -180 0, -160 0, -160 10, -180 10)))"), + actual(polygon(false, 160, 0, 160, 10, -160, 10, -160, 0, 160, 0), true)); + + assertEquals(expected("POLYGON ((20 10, -20 10, -20 0, 20 0, 20 10)))"), + actual(polygon(randomBoolean() ? null : randomBoolean(), 20, 0, 20, 10, -20, 10, -20, 0, 20, 0), randomBoolean())); + } + + private XContentBuilder polygon(Boolean orientation, double... val) throws IOException { + XContentBuilder pointGeoJson = XContentFactory.jsonBuilder().startObject(); + { + pointGeoJson.field("type", "polygon"); + if (orientation != null) { + pointGeoJson.field("orientation", orientation ? "right" : "left"); + } + pointGeoJson.startArray("coordinates").startArray(); + { + assertEquals(0, val.length % 2); + for (int i = 0; i < val.length; i += 2) { + pointGeoJson.startArray().value(val[i]).value(val[i + 1]).endArray(); + } + } + pointGeoJson.endArray().endArray(); + } + pointGeoJson.endObject(); + return pointGeoJson; + } + + private Geometry expected(String wkt) throws IOException, ParseException { + return parseGeometry(wkt, true); + } + + private Geometry actual(String wkt, boolean rightOrientation) throws IOException, ParseException { + Geometry shape = parseGeometry(wkt, rightOrientation); + return new GeometryIndexer(true).prepareForIndexing(shape); + } + + + private Geometry actual(XContentBuilder geoJson, boolean rightOrientation) throws IOException, ParseException { + Geometry shape = parseGeometry(geoJson, rightOrientation); + return new GeometryIndexer(true).prepareForIndexing(shape); + } + + private Geometry parseGeometry(String wkt, boolean rightOrientation) throws IOException, ParseException { + XContentBuilder json = XContentFactory.jsonBuilder().startObject().field("value", wkt).endObject(); + try (XContentParser parser = createParser(json)) { + parser.nextToken(); + parser.nextToken(); + parser.nextToken(); + GeometryParser geometryParser = new GeometryParser(rightOrientation, true, true); + return geometryParser.parse(parser); + } + } + + private Geometry parseGeometry(XContentBuilder geoJson, boolean rightOrientation) throws IOException, ParseException { + try (XContentParser parser = createParser(geoJson)) { + parser.nextToken(); + GeometryParser geometryParser = new GeometryParser(rightOrientation, true, true); + return geometryParser.parse(parser); + } + } + +} diff --git a/server/src/test/java/org/elasticsearch/common/geo/builders/LineStringBuilderTests.java b/server/src/test/java/org/elasticsearch/common/geo/builders/LineStringBuilderTests.java index b0b11afa97c62..48985ffbee3dd 100644 --- a/server/src/test/java/org/elasticsearch/common/geo/builders/LineStringBuilderTests.java +++ b/server/src/test/java/org/elasticsearch/common/geo/builders/LineStringBuilderTests.java @@ -19,10 +19,9 @@ package org.elasticsearch.common.geo.builders; -import org.locationtech.jts.geom.Coordinate; - import org.elasticsearch.test.geo.RandomShapeGenerator; import org.elasticsearch.test.geo.RandomShapeGenerator.ShapeType; +import org.locationtech.jts.geom.Coordinate; import java.io.IOException; import java.util.List; diff --git a/server/src/test/java/org/elasticsearch/index/mapper/ExternalMapper.java b/server/src/test/java/org/elasticsearch/index/mapper/ExternalMapper.java index 31864abc2e459..1e15fd666e5eb 100644 --- a/server/src/test/java/org/elasticsearch/index/mapper/ExternalMapper.java +++ b/server/src/test/java/org/elasticsearch/index/mapper/ExternalMapper.java @@ -30,6 +30,7 @@ import org.elasticsearch.common.geo.GeoPoint; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.geo.geometry.Point; import org.elasticsearch.index.query.QueryShardContext; import java.io.IOException; @@ -185,10 +186,10 @@ public void parse(ParseContext context) throws IOException { pointMapper.parse(context.createExternalValueContext(point)); // Let's add a Dummy Shape - PointBuilder pb = new PointBuilder(-100, 45); if (shapeMapper instanceof GeoShapeFieldMapper) { - shapeMapper.parse(context.createExternalValueContext(pb.buildGeometry())); + shapeMapper.parse(context.createExternalValueContext(new Point(45, -100))); } else { + PointBuilder pb = new PointBuilder(-100, 45); shapeMapper.parse(context.createExternalValueContext(pb.buildS4J())); }