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 4508d389358..247e5d86ceb 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 @@ public final class GeoJson { 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 final class GeoJson { 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 @@ public final class GeoJson { 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 @@ public final class GeoJson { } 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 00000000000..8351ef7805e --- /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 6449c06fbe1..2ce1d5328f3 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.Line; 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 class GeoShapeFieldMapper extends BaseGeoShapeFieldMapper { } } + 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 class GeoShapeFieldMapper extends BaseGeoShapeFieldMapper { @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 9548d14cca9..9e5d7d7c6ce 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.ShapeCollection; 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 @@ import static org.elasticsearch.common.geo.builders.ShapeBuilder.SPATIAL_CONTEXT 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 @@ abstract class BaseGeoParsingTestCase extends ESTestCase { } } - 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 4146adb2d29..ef45194146d 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.geometry.Rectangle; 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 class GeoJsonParserTests extends BaseGeoParsingTestCase { @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 class GeoJsonParserTests extends BaseGeoParsingTestCase { 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 class GeoJsonParserTests extends BaseGeoParsingTestCase { 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 class GeoJsonParserTests extends BaseGeoParsingTestCase { } // 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 class GeoJsonParserTests extends BaseGeoParsingTestCase { 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 74024ddcada..eb919eaf5ef 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.ShapeCollection; 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 @@ import static org.elasticsearch.common.geo.builders.ShapeBuilder.SPATIAL_CONTEXT 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 class GeoJsonShapeParserTests extends BaseGeoParsingTestCase { } @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 class GeoJsonShapeParserTests extends BaseGeoParsingTestCase { 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 class GeoJsonShapeParserTests extends BaseGeoParsingTestCase { 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 class GeoJsonShapeParserTests extends BaseGeoParsingTestCase { } @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 class GeoJsonShapeParserTests extends BaseGeoParsingTestCase { } @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 class GeoJsonShapeParserTests extends BaseGeoParsingTestCase { 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 class GeoJsonShapeParserTests extends BaseGeoParsingTestCase { } - 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 class GeoJsonShapeParserTests extends BaseGeoParsingTestCase { 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 class GeoJsonShapeParserTests extends BaseGeoParsingTestCase { 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 class GeoJsonShapeParserTests extends BaseGeoParsingTestCase { 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 class GeoJsonShapeParserTests extends BaseGeoParsingTestCase { 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 class GeoJsonShapeParserTests extends BaseGeoParsingTestCase { 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 class GeoJsonShapeParserTests extends BaseGeoParsingTestCase { 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 class GeoJsonShapeParserTests extends BaseGeoParsingTestCase { 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 class GeoJsonShapeParserTests extends BaseGeoParsingTestCase { 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 class GeoJsonShapeParserTests extends BaseGeoParsingTestCase { } } - public void testParsePolygonWithHole() throws IOException { + public void testParsePolygonWithHole() throws IOException, ParseException { XContentBuilder polygonGeoJson = XContentFactory.jsonBuilder() .startObject() .field("type", "Polygon") @@ -894,7 +895,7 @@ public class GeoJsonShapeParserTests extends BaseGeoParsingTestCase { } @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 class GeoJsonShapeParserTests extends BaseGeoParsingTestCase { } @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 class GeoJsonShapeParserTests extends BaseGeoParsingTestCase { 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 class GeoJsonShapeParserTests extends BaseGeoParsingTestCase { 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 class GeoJsonShapeParserTests extends BaseGeoParsingTestCase { 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 class GeoJsonShapeParserTests extends BaseGeoParsingTestCase { 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 class GeoJsonShapeParserTests extends BaseGeoParsingTestCase { 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 class GeoJsonShapeParserTests extends BaseGeoParsingTestCase { 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 class GeoJsonShapeParserTests extends BaseGeoParsingTestCase { 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 class GeoJsonShapeParserTests extends BaseGeoParsingTestCase { 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 class GeoJsonShapeParserTests extends BaseGeoParsingTestCase { try (XContentParser parser = createParser(polygonGeoJson)) { parser.nextToken(); - ElasticsearchGeoAssertions.assertMultiPolygon(ShapeParser.parse(parser).buildGeometry(), false); + ElasticsearchGeoAssertions.assertMultiPolygon(parse(parser), false); } } @@ -1421,4 +1422,10 @@ public class GeoJsonShapeParserTests extends BaseGeoParsingTestCase { 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 286e1ce6ee7..8610fa551c3 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.ShapeCollection; 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 @@ public class GeoWKTShapeParserTests extends BaseGeoParsingTestCase { 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 @@ public class GeoWKTShapeParserTests extends BaseGeoParsingTestCase { } @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 class GeoWKTShapeParserTests extends BaseGeoParsingTestCase { } @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 @@ public class GeoWKTShapeParserTests extends BaseGeoParsingTestCase { } @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 class GeoWKTShapeParserTests extends BaseGeoParsingTestCase { } @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 class GeoWKTShapeParserTests extends BaseGeoParsingTestCase { } @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 class GeoWKTShapeParserTests extends BaseGeoParsingTestCase { } @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 class GeoWKTShapeParserTests extends BaseGeoParsingTestCase { 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 class GeoWKTShapeParserTests extends BaseGeoParsingTestCase { 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 class GeoWKTShapeParserTests extends BaseGeoParsingTestCase { } @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 class GeoWKTShapeParserTests extends BaseGeoParsingTestCase { } @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 00000000000..5ab5aaff33e --- /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 b0b11afa97c..48985ffbee3 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 31864abc2e4..1e15fd666e5 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.collect.Iterators; 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 class ExternalMapper extends FieldMapper { 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())); }