diff --git a/libs/geo/src/main/java/org/elasticsearch/geo/geometry/Circle.java b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/Circle.java index 7140540f5c140..cb8e2c4cb33e1 100644 --- a/libs/geo/src/main/java/org/elasticsearch/geo/geometry/Circle.java +++ b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/Circle.java @@ -102,7 +102,7 @@ public int hashCode() { } @Override - public T visit(GeometryVisitor visitor) { + public T visit(GeometryVisitor visitor) throws E { return visitor.visit(this); } diff --git a/libs/geo/src/main/java/org/elasticsearch/geo/geometry/Geometry.java b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/Geometry.java index 9322193326fc5..140dd13427294 100644 --- a/libs/geo/src/main/java/org/elasticsearch/geo/geometry/Geometry.java +++ b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/Geometry.java @@ -26,7 +26,7 @@ public interface Geometry { ShapeType type(); - T visit(GeometryVisitor visitor); + T visit(GeometryVisitor visitor) throws E; boolean isEmpty(); diff --git a/libs/geo/src/main/java/org/elasticsearch/geo/geometry/GeometryCollection.java b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/GeometryCollection.java index 56e59f94983ed..cc27d7c0972dd 100644 --- a/libs/geo/src/main/java/org/elasticsearch/geo/geometry/GeometryCollection.java +++ b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/GeometryCollection.java @@ -57,7 +57,7 @@ public ShapeType type() { } @Override - public T visit(GeometryVisitor visitor) { + public T visit(GeometryVisitor visitor) throws E { return visitor.visit(this); } diff --git a/libs/geo/src/main/java/org/elasticsearch/geo/geometry/GeometryVisitor.java b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/GeometryVisitor.java index 8317b23d1feca..f4c189fbe288f 100644 --- a/libs/geo/src/main/java/org/elasticsearch/geo/geometry/GeometryVisitor.java +++ b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/GeometryVisitor.java @@ -44,26 +44,26 @@ * * @see Visitor Pattern */ -public interface GeometryVisitor { +public interface GeometryVisitor { - T visit(Circle circle); + T visit(Circle circle) throws E; - T visit(GeometryCollection collection); + T visit(GeometryCollection collection) throws E; - T visit(Line line); + T visit(Line line) throws E; - T visit(LinearRing ring); + T visit(LinearRing ring) throws E; - T visit(MultiLine multiLine); + T visit(MultiLine multiLine) throws E; - T visit(MultiPoint multiPoint); + T visit(MultiPoint multiPoint) throws E; - T visit(MultiPolygon multiPolygon); + T visit(MultiPolygon multiPolygon) throws E; - T visit(Point point); + T visit(Point point) throws E; - T visit(Polygon polygon); + T visit(Polygon polygon) throws E; - T visit(Rectangle rectangle); + T visit(Rectangle rectangle) throws E; } diff --git a/libs/geo/src/main/java/org/elasticsearch/geo/geometry/Line.java b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/Line.java index e06ccc555aa2c..c2c9cb4b83a18 100644 --- a/libs/geo/src/main/java/org/elasticsearch/geo/geometry/Line.java +++ b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/Line.java @@ -103,7 +103,7 @@ public ShapeType type() { } @Override - public T visit(GeometryVisitor visitor) { + public T visit(GeometryVisitor visitor) throws E { return visitor.visit(this); } diff --git a/libs/geo/src/main/java/org/elasticsearch/geo/geometry/LinearRing.java b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/LinearRing.java index 7d66a93ea6d57..d27e512ef34cc 100644 --- a/libs/geo/src/main/java/org/elasticsearch/geo/geometry/LinearRing.java +++ b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/LinearRing.java @@ -54,7 +54,7 @@ public ShapeType type() { } @Override - public T visit(GeometryVisitor visitor) { + public T visit(GeometryVisitor visitor) throws E { return visitor.visit(this); } } diff --git a/libs/geo/src/main/java/org/elasticsearch/geo/geometry/MultiLine.java b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/MultiLine.java index 995c43d0c1c80..ac1f956397bb0 100644 --- a/libs/geo/src/main/java/org/elasticsearch/geo/geometry/MultiLine.java +++ b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/MultiLine.java @@ -40,7 +40,7 @@ public ShapeType type() { } @Override - public T visit(GeometryVisitor visitor) { + public T visit(GeometryVisitor visitor) throws E { return visitor.visit(this); } } diff --git a/libs/geo/src/main/java/org/elasticsearch/geo/geometry/MultiPoint.java b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/MultiPoint.java index 7d57b66ca564f..748902bd9eb72 100644 --- a/libs/geo/src/main/java/org/elasticsearch/geo/geometry/MultiPoint.java +++ b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/MultiPoint.java @@ -40,7 +40,7 @@ public ShapeType type() { } @Override - public T visit(GeometryVisitor visitor) { + public T visit(GeometryVisitor visitor) throws E { return visitor.visit(this); } diff --git a/libs/geo/src/main/java/org/elasticsearch/geo/geometry/MultiPolygon.java b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/MultiPolygon.java index 01c68d6dd0b32..a843d90165b4b 100644 --- a/libs/geo/src/main/java/org/elasticsearch/geo/geometry/MultiPolygon.java +++ b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/MultiPolygon.java @@ -40,7 +40,7 @@ public ShapeType type() { } @Override - public T visit(GeometryVisitor visitor) { + public T visit(GeometryVisitor visitor) throws E { return visitor.visit(this); } } diff --git a/libs/geo/src/main/java/org/elasticsearch/geo/geometry/Point.java b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/Point.java index 189968fdd40b3..248f433b96a13 100644 --- a/libs/geo/src/main/java/org/elasticsearch/geo/geometry/Point.java +++ b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/Point.java @@ -93,7 +93,7 @@ public int hashCode() { } @Override - public T visit(GeometryVisitor visitor) { + public T visit(GeometryVisitor visitor) throws E { return visitor.visit(this); } diff --git a/libs/geo/src/main/java/org/elasticsearch/geo/geometry/Polygon.java b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/Polygon.java index 1dee1c69fc840..ec6f564774ca9 100644 --- a/libs/geo/src/main/java/org/elasticsearch/geo/geometry/Polygon.java +++ b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/Polygon.java @@ -92,7 +92,7 @@ public LinearRing getHole(int i) { } @Override - public T visit(GeometryVisitor visitor) { + public T visit(GeometryVisitor visitor) throws E { return visitor.visit(this); } diff --git a/libs/geo/src/main/java/org/elasticsearch/geo/geometry/Rectangle.java b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/Rectangle.java index 120bf9e2eb862..ca7ec2e57c98d 100644 --- a/libs/geo/src/main/java/org/elasticsearch/geo/geometry/Rectangle.java +++ b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/Rectangle.java @@ -207,7 +207,7 @@ public int hashCode() { } @Override - public T visit(GeometryVisitor visitor) { + public T visit(GeometryVisitor visitor) throws E { return visitor.visit(this); } diff --git a/libs/geo/src/main/java/org/elasticsearch/geo/geometry/ShapeType.java b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/ShapeType.java index 2272f1ad89410..48a262a8316e3 100644 --- a/libs/geo/src/main/java/org/elasticsearch/geo/geometry/ShapeType.java +++ b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/ShapeType.java @@ -19,6 +19,8 @@ package org.elasticsearch.geo.geometry; +import java.util.Locale; + /** * Shape types supported by elasticsearch */ @@ -33,4 +35,8 @@ public enum ShapeType { LINEARRING, // not serialized by itself in WKT or WKB ENVELOPE, // not part of the actual WKB spec CIRCLE; // not part of the actual WKB spec + + public static ShapeType forName(String shapeName) { + return ShapeType.valueOf(shapeName.toUpperCase(Locale.ROOT)); + } } diff --git a/libs/geo/src/main/java/org/elasticsearch/geo/utils/WellKnownText.java b/libs/geo/src/main/java/org/elasticsearch/geo/utils/WellKnownText.java index ac0b8d9abfea4..e1af54e3383e0 100644 --- a/libs/geo/src/main/java/org/elasticsearch/geo/utils/WellKnownText.java +++ b/libs/geo/src/main/java/org/elasticsearch/geo/utils/WellKnownText.java @@ -68,7 +68,7 @@ public static void toWKT(Geometry geometry, StringBuilder sb) { if (geometry.isEmpty()) { sb.append(EMPTY); } else { - geometry.visit(new GeometryVisitor() { + geometry.visit(new GeometryVisitor() { @Override public Void visit(Circle circle) { sb.append(LPAREN); @@ -543,7 +543,7 @@ private static String nextCloserOrComma(StreamTokenizer stream) throws IOExcepti } public static String getWKTName(Geometry geometry) { - return geometry.visit(new GeometryVisitor() { + return geometry.visit(new GeometryVisitor() { @Override public String visit(Circle circle) { return "circle"; diff --git a/libs/geo/src/test/java/org/elasticsearch/geo/geometry/BaseGeometryTestCase.java b/libs/geo/src/test/java/org/elasticsearch/geo/geometry/BaseGeometryTestCase.java index b3cc834faea24..cc7dcd340c734 100644 --- a/libs/geo/src/test/java/org/elasticsearch/geo/geometry/BaseGeometryTestCase.java +++ b/libs/geo/src/test/java/org/elasticsearch/geo/geometry/BaseGeometryTestCase.java @@ -67,7 +67,7 @@ public void testVisitor() { public static void testVisitor(Geometry geom) { AtomicBoolean called = new AtomicBoolean(false); - Object result = geom.visit(new GeometryVisitor() { + Object result = geom.visit(new GeometryVisitor() { private Object verify(Geometry geometry, String expectedClass) { assertFalse("Visitor should be called only once", called.getAndSet(true)); assertSame(geom, geometry); diff --git a/server/src/main/java/org/elasticsearch/common/geo/GeoJson.java b/server/src/main/java/org/elasticsearch/common/geo/GeoJson.java new file mode 100644 index 0000000000000..3489eca8b58e4 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/common/geo/GeoJson.java @@ -0,0 +1,612 @@ +/* + * 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.ElasticsearchException; +import org.elasticsearch.ElasticsearchParseException; +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.geo.parsers.ShapeParser; +import org.elasticsearch.common.unit.DistanceUnit; +import org.elasticsearch.common.xcontent.ConstructingObjectParser; +import org.elasticsearch.common.xcontent.ObjectParser; +import org.elasticsearch.common.xcontent.ToXContent; +import org.elasticsearch.common.xcontent.ToXContentObject; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.common.xcontent.XContentSubParser; +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.elasticsearch.geo.geometry.ShapeType; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg; +import static org.elasticsearch.common.xcontent.ConstructingObjectParser.optionalConstructorArg; + +/** + * Utility class for converting libs/geo shapes to and from GeoJson + */ +public final class GeoJson { + + private static final ParseField FIELD_TYPE = new ParseField("type"); + private static final ParseField FIELD_COORDINATES = new ParseField("coordinates"); + private static final ParseField FIELD_GEOMETRIES = new ParseField("geometries"); + private static final ParseField FIELD_ORIENTATION = new ParseField("orientation"); + private static final ParseField FIELD_RADIUS = new ParseField("radius"); + + private GeoJson() { + + } + + public static Geometry fromXContent(XContentParser parser, boolean rightOrientation, boolean coerce, boolean ignoreZValue) + throws IOException { + try (XContentSubParser subParser = new XContentSubParser(parser)) { + return PARSER.apply(subParser, new ParserContext(rightOrientation, coerce, ignoreZValue)); + } + } + + public static XContentBuilder toXContent(Geometry geometry, XContentBuilder builder, ToXContent.Params params) throws IOException { + builder.startObject(); + builder.field(FIELD_TYPE.getPreferredName(), getGeoJsonName(geometry)); + geometry.visit(new GeometryVisitor() { + @Override + public XContentBuilder visit(Circle circle) throws IOException { + builder.field(FIELD_RADIUS.getPreferredName(), DistanceUnit.METERS.toString(circle.getRadiusMeters())); + builder.field(ShapeParser.FIELD_COORDINATES.getPreferredName()); + return coordinatesToXContent(circle.getLat(), circle.getLon(), circle.getAlt()); + } + + @Override + public XContentBuilder visit(GeometryCollection collection) throws IOException { + builder.startArray(FIELD_GEOMETRIES.getPreferredName()); + for (Geometry g : collection) { + toXContent(g, builder, params); + } + return builder.endArray(); + } + + @Override + public XContentBuilder visit(Line line) throws IOException { + builder.field(ShapeParser.FIELD_COORDINATES.getPreferredName()); + return coordinatesToXContent(line); + } + + @Override + public XContentBuilder visit(LinearRing ring) { + throw new UnsupportedOperationException("linearRing cannot be serialized using GeoJson"); + } + + @Override + public XContentBuilder visit(MultiLine multiLine) throws IOException { + builder.field(ShapeParser.FIELD_COORDINATES.getPreferredName()); + builder.startArray(); + for (int i = 0; i < multiLine.size(); i++) { + coordinatesToXContent(multiLine.get(i)); + } + return builder.endArray(); + } + + @Override + public XContentBuilder visit(MultiPoint multiPoint) throws IOException { + builder.startArray(ShapeParser.FIELD_COORDINATES.getPreferredName()); + for (int i = 0; i < multiPoint.size(); i++) { + Point p = multiPoint.get(i); + builder.startArray().value(p.getLon()).value(p.getLat()); + if (p.hasAlt()) { + builder.value(p.getAlt()); + } + builder.endArray(); + } + return builder.endArray(); + } + + @Override + public XContentBuilder visit(MultiPolygon multiPolygon) throws IOException { + builder.startArray(ShapeParser.FIELD_COORDINATES.getPreferredName()); + for (int i = 0; i < multiPolygon.size(); i++) { + builder.startArray(); + coordinatesToXContent(multiPolygon.get(i)); + builder.endArray(); + } + return builder.endArray(); + } + + @Override + public XContentBuilder visit(Point point) throws IOException { + builder.field(ShapeParser.FIELD_COORDINATES.getPreferredName()); + return coordinatesToXContent(point.getLat(), point.getLon(), point.getAlt()); + } + + @Override + public XContentBuilder visit(Polygon polygon) throws IOException { + builder.startArray(ShapeParser.FIELD_COORDINATES.getPreferredName()); + coordinatesToXContent(polygon.getPolygon()); + for (int i = 0; i < polygon.getNumberOfHoles(); i++) { + coordinatesToXContent(polygon.getHole(i)); + } + return builder.endArray(); + } + + @Override + public XContentBuilder visit(Rectangle rectangle) throws IOException { + builder.startArray(ShapeParser.FIELD_COORDINATES.getPreferredName()); + coordinatesToXContent(rectangle.getMaxLat(), rectangle.getMinLon(), rectangle.getMinAlt()); // top left + coordinatesToXContent(rectangle.getMinLat(), rectangle.getMaxLon(), rectangle.getMaxAlt()); // bottom right + return builder.endArray(); + } + + private XContentBuilder coordinatesToXContent(double lat, double lon, double alt) throws IOException { + builder.startArray().value(lon).value(lat); + if (Double.isNaN(alt) == false) { + builder.value(alt); + } + return builder.endArray(); + } + + private XContentBuilder coordinatesToXContent(Line line) throws IOException { + builder.startArray(); + for (int i = 0; i < line.length(); i++) { + builder.startArray().value(line.getLon(i)).value(line.getLat(i)); + if (line.hasAlt()) { + builder.value(line.getAlt(i)); + } + builder.endArray(); + } + return builder.endArray(); + } + + private XContentBuilder coordinatesToXContent(Polygon polygon) throws IOException { + coordinatesToXContent(polygon.getPolygon()); + for (int i = 0; i < polygon.getNumberOfHoles(); i++) { + coordinatesToXContent(polygon.getHole(i)); + } + return builder; + } + + }); + return builder.endObject(); + } + + private static class ParserContext { + public final boolean defaultOrientation; + public final boolean coerce; + public final boolean ignoreZValue; + + ParserContext(boolean defaultOrientation, boolean coerce, boolean ignoreZValue) { + this.defaultOrientation = defaultOrientation; + this.coerce = coerce; + this.ignoreZValue = ignoreZValue; + } + } + + private static ConstructingObjectParser PARSER = + new ConstructingObjectParser<>("geojson", true, (a, c) -> { + String type = (String) a[0]; + CoordinateNode coordinates = (CoordinateNode) a[1]; + @SuppressWarnings("unchecked") List geometries = (List) a[2]; + Boolean orientation = orientationFromString((String) a[3]); + DistanceUnit.Distance radius = (DistanceUnit.Distance) a[4]; + return createGeometry(type, geometries, coordinates, orientation, c.defaultOrientation, c.coerce, radius); + }); + + static { + PARSER.declareString(constructorArg(), FIELD_TYPE); + PARSER.declareField(optionalConstructorArg(), (p, c) -> parseCoordinates(p, c.ignoreZValue), FIELD_COORDINATES, + ObjectParser.ValueType.VALUE_ARRAY); + PARSER.declareObjectArray(optionalConstructorArg(), PARSER, FIELD_GEOMETRIES); + PARSER.declareString(optionalConstructorArg(), FIELD_ORIENTATION); + PARSER.declareField(optionalConstructorArg(), p -> DistanceUnit.Distance.parseDistance(p.text()), FIELD_RADIUS, + ObjectParser.ValueType.STRING); + } + + private static Geometry createGeometry(String type, List geometries, CoordinateNode coordinates, Boolean orientation, + boolean defaultOrientation, boolean coerce, DistanceUnit.Distance radius) { + + ShapeType shapeType = ShapeType.forName(type); + if (shapeType == ShapeType.GEOMETRYCOLLECTION) { + if (geometries == null) { + throw new ElasticsearchParseException("geometries not included"); + } + if (coordinates != null) { + throw new ElasticsearchParseException("parameter coordinates is not supported for type " + type); + } + verifyNulls(type, null, orientation, radius); + return new GeometryCollection<>(geometries); + } + + // We expect to have coordinates for all the rest + if (coordinates == null) { + throw new ElasticsearchParseException("coordinates not included"); + } + + switch (shapeType) { + case CIRCLE: + if (radius == null) { + throw new ElasticsearchParseException("radius is not specified"); + } + verifyNulls(type, geometries, orientation, null); + Point point = coordinates.asPoint(); + return new Circle(point.getLat(), point.getLon(), point.getAlt(), radius.convert(DistanceUnit.METERS).value); + case POINT: + verifyNulls(type, geometries, orientation, radius); + return coordinates.asPoint(); + case MULTIPOINT: + verifyNulls(type, geometries, orientation, radius); + return coordinates.asMultiPoint(); + case LINESTRING: + verifyNulls(type, geometries, orientation, radius); + return coordinates.asLineString(coerce); + case MULTILINESTRING: + verifyNulls(type, geometries, orientation, radius); + return coordinates.asMultiLineString(coerce); + case POLYGON: + verifyNulls(type, geometries, null, radius); + // handle possible null in orientation + return coordinates.asPolygon(orientation != null ? orientation : defaultOrientation, coerce); + case MULTIPOLYGON: + verifyNulls(type, geometries, null, radius); + // handle possible null in orientation + return coordinates.asMultiPolygon(orientation != null ? orientation : defaultOrientation, coerce); + case ENVELOPE: + verifyNulls(type, geometries, orientation, radius); + return coordinates.asRectangle(); + default: + throw new ElasticsearchParseException("unsuppoted shape type " + type); + } + } + + /** + * Checks that all passed parameters except type are null, generates corresponding error messages if they are not + */ + private static void verifyNulls(String type, List geometries, Boolean orientation, DistanceUnit.Distance radius) { + if (geometries != null) { + throw new ElasticsearchParseException("parameter geometries is not supported for type " + type); + } + if (orientation != null) { + throw new ElasticsearchParseException("parameter orientation is not supported for type " + type); + } + if (radius != null) { + throw new ElasticsearchParseException("parameter radius is not supported for type " + type); + } + } + + /** + * Recursive method which parses the arrays of coordinates used to define + * Shapes + */ + private static CoordinateNode parseCoordinates(XContentParser parser, boolean ignoreZValue) throws IOException { + XContentParser.Token token = parser.nextToken(); + // Base cases + if (token != XContentParser.Token.START_ARRAY && + token != XContentParser.Token.END_ARRAY && + token != XContentParser.Token.VALUE_NULL) { + return new CoordinateNode(parseCoordinate(parser, ignoreZValue)); + } else if (token == XContentParser.Token.VALUE_NULL) { + throw new IllegalArgumentException("coordinates cannot contain NULL values)"); + } + + List nodes = new ArrayList<>(); + while (token != XContentParser.Token.END_ARRAY) { + CoordinateNode node = parseCoordinates(parser, ignoreZValue); + if (nodes.isEmpty() == false && nodes.get(0).numDimensions() != node.numDimensions()) { + throw new ElasticsearchParseException("Exception parsing coordinates: number of dimensions do not match"); + } + nodes.add(node); + token = parser.nextToken(); + } + + return new CoordinateNode(nodes); + } + + /** + * Parser a singe set of 2 or 3 coordinates + */ + private static Point parseCoordinate(XContentParser parser, boolean ignoreZValue) throws IOException { + // Add support for coerce here + if (parser.currentToken() != XContentParser.Token.VALUE_NUMBER) { + throw new ElasticsearchParseException("geo coordinates must be numbers"); + } + double lon = parser.doubleValue(); + if (parser.nextToken() != XContentParser.Token.VALUE_NUMBER) { + throw new ElasticsearchParseException("geo coordinates must be numbers"); + } + double lat = parser.doubleValue(); + XContentParser.Token token = parser.nextToken(); + // alt (for storing purposes only - future use includes 3d shapes) + double alt = Double.NaN; + if (token == XContentParser.Token.VALUE_NUMBER) { + alt = GeoPoint.assertZValue(ignoreZValue, parser.doubleValue()); + parser.nextToken(); + } + // do not support > 3 dimensions + if (parser.currentToken() == XContentParser.Token.VALUE_NUMBER) { + throw new ElasticsearchParseException("geo coordinates greater than 3 dimensions are not supported"); + } + return new Point(lat, lon, alt); + } + + /** + * Returns true for right orientation and false for left + */ + private static Boolean orientationFromString(String orientation) { + if (orientation == null) { + return null; + } + orientation = orientation.toLowerCase(Locale.ROOT); + switch (orientation) { + case "right": + case "counterclockwise": + case "ccw": + return true; + case "left": + case "clockwise": + case "cw": + return false; + default: + throw new IllegalArgumentException("Unknown orientation [" + orientation + "]"); + } + } + + public static String getGeoJsonName(Geometry geometry) { + return geometry.visit(new GeometryVisitor<>() { + @Override + public String visit(Circle circle) { + return "Circle"; + } + + @Override + public String visit(GeometryCollection collection) { + return "GeometryCollection"; + } + + @Override + public String visit(Line line) { + return "LineString"; + } + + @Override + public String visit(LinearRing ring) { + throw new UnsupportedOperationException("line ring cannot be serialized using GeoJson"); + } + + @Override + public String visit(MultiLine multiLine) { + return "MultiLineString"; + } + + @Override + public String visit(MultiPoint multiPoint) { + return "MultiPoint"; + } + + @Override + public String visit(MultiPolygon multiPolygon) { + return "MultiPolygon"; + } + + @Override + public String visit(Point point) { + return "Point"; + } + + @Override + public String visit(Polygon polygon) { + return "Polygon"; + } + + @Override + public String visit(Rectangle rectangle) { + return "Envelope"; + } + }); + } + + private static class CoordinateNode implements ToXContentObject { + public final Point coordinate; + public final List children; + + /** + * Creates a new leaf CoordinateNode + * + * @param coordinate Coordinate for the Node + */ + CoordinateNode(Point coordinate) { + this.coordinate = coordinate; + this.children = null; + } + + /** + * Creates a new parent CoordinateNode + * + * @param children Children of the Node + */ + CoordinateNode(List children) { + this.children = children; + this.coordinate = null; + } + + public boolean isEmpty() { + return (coordinate == null && (children == null || children.isEmpty())); + } + + protected int numDimensions() { + if (isEmpty()) { + throw new ElasticsearchException("attempting to get number of dimensions on an empty coordinate node"); + } + if (coordinate != null) { + return coordinate.hasAlt() ? 3 : 2; + } + return children.get(0).numDimensions(); + } + + public Point asPoint() { + if (children != null) { + throw new ElasticsearchException("expected a single points but got a list"); + } + return coordinate; + } + + public MultiPoint asMultiPoint() { + if (coordinate != null) { + throw new ElasticsearchException("expected a list of points but got a point"); + } + List points = new ArrayList<>(); + for (CoordinateNode node : children) { + points.add(node.asPoint()); + } + return new MultiPoint(points); + } + + private double[][] asLineComponents(boolean orientation, boolean coerce) { + if (coordinate != null) { + throw new ElasticsearchException("expected a list of points but got a point"); + } + + if (children.size() < 2) { + throw new ElasticsearchException("not enough points to build a line"); + } + + boolean needsClosing; + int resultSize; + if (coerce && children.get(0).asPoint().equals(children.get(children.size() - 1).asPoint()) == false) { + needsClosing = true; + resultSize = children.size() + 1; + } else { + needsClosing = false; + resultSize = children.size(); + } + + double[] lats = new double[resultSize]; + double[] lons = new double[resultSize]; + double[] alts = numDimensions() == 3 ? new double[resultSize] : null; + int i = orientation ? 0 : lats.length - 1; + for (CoordinateNode node : children) { + Point point = node.asPoint(); + lats[i] = point.getLat(); + lons[i] = point.getLon(); + if (alts != null) { + alts[i] = point.getAlt(); + } + i = orientation ? i + 1 : i - 1; + } + if (needsClosing) { + lats[resultSize - 1] = lats[0]; + lons[resultSize - 1] = lons[0]; + if (alts != null) { + alts[resultSize - 1] = alts[0]; + } + } + double[][] components = new double[3][]; + components[0] = lats; + components[1] = lons; + components[2] = alts; + return components; + } + + public Line asLineString(boolean coerce) { + double[][] components = asLineComponents(true, coerce); + return new Line(components[0], components[1], components[2]); + } + + public LinearRing asLinearRing(boolean orientation, boolean coerce) { + double[][] components = asLineComponents(orientation, coerce); + return new LinearRing(components[0], components[1], components[2]); + } + + public MultiLine asMultiLineString(boolean coerce) { + if (coordinate != null) { + throw new ElasticsearchException("expected a list of points but got a point"); + } + List lines = new ArrayList<>(); + for (CoordinateNode node : children) { + lines.add(node.asLineString(coerce)); + } + return new MultiLine(lines); + } + + + public Polygon asPolygon(boolean orientation, boolean coerce) { + if (coordinate != null) { + throw new ElasticsearchException("expected a list of points but got a point"); + } + List lines = new ArrayList<>(); + for (CoordinateNode node : children) { + lines.add(node.asLinearRing(orientation, coerce)); + } + if (lines.size() == 1) { + return new Polygon(lines.get(0)); + } else { + LinearRing shell = lines.remove(0); + return new Polygon(shell, lines); + } + } + + public MultiPolygon asMultiPolygon(boolean orientation, boolean coerce) { + if (coordinate != null) { + throw new ElasticsearchException("expected a list of points but got a point"); + } + List polygons = new ArrayList<>(); + for (CoordinateNode node : children) { + polygons.add(node.asPolygon(orientation, coerce)); + } + return new MultiPolygon(polygons); + } + + public Rectangle asRectangle() { + if (children.size() != 2) { + throw new ElasticsearchParseException( + "invalid number of points [{}] provided for geo_shape [{}] when expecting an array of 2 coordinates", + children.size(), ShapeType.ENVELOPE); + } + // verify coordinate bounds, correct if necessary + Point uL = children.get(0).coordinate; + Point lR = children.get(1).coordinate; + return new Rectangle(lR.getLat(), uL.getLat(), uL.getLon(), lR.getLon()); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + if (children == null) { + builder.startArray().value(coordinate.getLon()).value(coordinate.getLat()).endArray(); + } else { + builder.startArray(); + for (CoordinateNode child : children) { + child.toXContent(builder, params); + } + builder.endArray(); + } + return builder; + } + } + +} diff --git a/server/src/main/java/org/elasticsearch/common/geo/GeometryParser.java b/server/src/main/java/org/elasticsearch/common/geo/GeometryParser.java new file mode 100644 index 0000000000000..8e1db18ccdd97 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/common/geo/GeometryParser.java @@ -0,0 +1,54 @@ +/* + * 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.ElasticsearchParseException; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.geo.geometry.Geometry; +import org.elasticsearch.geo.utils.WellKnownText; + +import java.io.IOException; +import java.text.ParseException; + +/** + * An utility class with a geometry parser methods supporting different shape representation formats + */ +public final class GeometryParser { + + private GeometryParser() { + + } + + /** + * Parses supplied XContent into Geometry + */ + public static Geometry parse(XContentParser parser, boolean orientation, boolean coerce, boolean ignoreZValue) throws IOException, + ParseException { + if (parser.currentToken() == XContentParser.Token.VALUE_NULL) { + return null; + } else if (parser.currentToken() == XContentParser.Token.START_OBJECT) { + return GeoJson.fromXContent(parser, orientation, coerce, ignoreZValue); + } else if (parser.currentToken() == XContentParser.Token.VALUE_STRING) { + // TODO: Add support for ignoreZValue and coerce to WKT + return WellKnownText.fromWKT(parser.text()); + } + throw new ElasticsearchParseException("shape must be an object consisting of type and coordinates"); + } +} 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 72b3e68fa025e..6449c06fbe1ad 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/GeoShapeFieldMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/GeoShapeFieldMapper.java @@ -134,7 +134,7 @@ private void indexShape(ParseContext context, Object luceneShape) { } } - private class LuceneGeometryIndexer implements GeometryVisitor { + private class LuceneGeometryIndexer implements GeometryVisitor { private ParseContext context; private LuceneGeometryIndexer(ParseContext context) { diff --git a/server/src/main/java/org/elasticsearch/index/query/GeoShapeQueryBuilder.java b/server/src/main/java/org/elasticsearch/index/query/GeoShapeQueryBuilder.java index b13666296b55d..b651a26d7e280 100644 --- a/server/src/main/java/org/elasticsearch/index/query/GeoShapeQueryBuilder.java +++ b/server/src/main/java/org/elasticsearch/index/query/GeoShapeQueryBuilder.java @@ -450,7 +450,7 @@ private Query getVectorQuery(QueryShardContext context, ShapeBuilder queryShapeB } private Query getVectorQueryFromShape(QueryShardContext context, Geometry queryShape) { - return queryShape.visit(new GeometryVisitor() { + return queryShape.visit(new GeometryVisitor() { @Override public Query visit(Circle circle) { throw new QueryShardException(context, "Field [" + fieldName + "] found and unknown shape Circle"); 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 9359101128883..7f6c56855ec70 100644 --- a/server/src/test/java/org/elasticsearch/common/geo/BaseGeoParsingTestCase.java +++ b/server/src/test/java/org/elasticsearch/common/geo/BaseGeoParsingTestCase.java @@ -67,6 +67,13 @@ protected void assertGeometryEquals(Object expected, XContentBuilder geoJson, bo } } + protected void assertGeometryEquals(org.elasticsearch.geo.geometry.Geometry expected, XContentBuilder geoJson) throws IOException { + try (XContentParser parser = createParser(geoJson)) { + parser.nextToken(); + assertEquals(expected, GeoJson.fromXContent(parser, true, false, false)); + } + } + protected ShapeCollection shapeCollection(Shape... shapes) { return new ShapeCollection<>(Arrays.asList(shapes), SPATIAL_CONTEXT); } diff --git a/server/src/test/java/org/elasticsearch/common/geo/GeoJsonParserTests.java b/server/src/test/java/org/elasticsearch/common/geo/GeoJsonParserTests.java new file mode 100644 index 0000000000000..4e2c2e50e2cf1 --- /dev/null +++ b/server/src/test/java/org/elasticsearch/common/geo/GeoJsonParserTests.java @@ -0,0 +1,757 @@ +/* + * 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.Strings; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentFactory; +import org.elasticsearch.common.xcontent.XContentParseException; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.common.xcontent.json.JsonXContent; +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.geometry.Rectangle; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; + + +/** + * Tests for {@code GeoJSONShapeParser} + */ +public class GeoJsonParserTests extends BaseGeoParsingTestCase { + + @Override + public void testParsePoint() throws IOException { + XContentBuilder pointGeoJson = XContentFactory.jsonBuilder() + .startObject() + .field("type", "Point") + .startArray("coordinates").value(100.0).value(0.0).endArray() + .endObject(); + Point expected = new Point(0.0, 100.0); + assertGeometryEquals(expected, pointGeoJson); + } + + @Override + public void testParseLineString() throws IOException { + XContentBuilder lineGeoJson = XContentFactory.jsonBuilder() + .startObject() + .field("type", "LineString") + .startArray("coordinates") + .startArray().value(100.0).value(0.0).endArray() + .startArray().value(101.0).value(1.0).endArray() + .endArray() + .endObject(); + + Line expected = new Line(new double[] {0.0, 1.0}, new double[] { 100.0, 101.0}); + try (XContentParser parser = createParser(lineGeoJson)) { + parser.nextToken(); + assertEquals(expected, GeoJson.fromXContent(parser, false, false, true)); + } + } + + @Override + public void testParseMultiLineString() throws IOException { + XContentBuilder multilinesGeoJson = XContentFactory.jsonBuilder() + .startObject() + .field("type", "MultiLineString") + .startArray("coordinates") + .startArray() + .startArray().value(100.0).value(0.0).endArray() + .startArray().value(101.0).value(1.0).endArray() + .endArray() + .startArray() + .startArray().value(102.0).value(2.0).endArray() + .startArray().value(103.0).value(3.0).endArray() + .endArray() + .endArray() + .endObject(); + + MultiLine expected = new MultiLine(Arrays.asList( + new Line(new double[] {0.0, 1.0}, new double[] { 100.0, 101.0}), + new Line(new double[] {2.0, 3.0}, new double[] { 102.0, 103.0}) + + )); + + assertGeometryEquals(expected, multilinesGeoJson); + } + + public void testParseCircle() throws IOException { + XContentBuilder multilinesGeoJson = XContentFactory.jsonBuilder() + .startObject() + .field("type", "circle") + .startArray("coordinates").value(100.0).value(0.0).endArray() + .field("radius", "200m") + .endObject(); + + Circle expected = new Circle(0.0, 100.0, 200); + assertGeometryEquals(expected, multilinesGeoJson); + } + + public void testParseMultiDimensionShapes() throws IOException { + // multi dimension point + XContentBuilder pointGeoJson = XContentFactory.jsonBuilder() + .startObject() + .field("type", "Point") + .startArray("coordinates").value(100.0).value(0.0).value(15.0).value(18.0).endArray() + .endObject(); + + try (XContentParser parser = createParser(pointGeoJson)) { + parser.nextToken(); + expectThrows(XContentParseException.class, () -> GeoJson.fromXContent(parser, false, false, false)); + assertNull(parser.nextToken()); + } + + // multi dimension linestring + XContentBuilder lineGeoJson = XContentFactory.jsonBuilder() + .startObject() + .field("type", "LineString") + .startArray("coordinates") + .startArray().value(100.0).value(0.0).value(15.0).endArray() + .startArray().value(101.0).value(1.0).value(18.0).value(19.0).endArray() + .endArray() + .endObject(); + + try (XContentParser parser = createParser(lineGeoJson)) { + parser.nextToken(); + expectThrows(XContentParseException.class, () -> GeoJson.fromXContent(parser, false, false, false)); + assertNull(parser.nextToken()); + } + } + + @Override + public void testParseEnvelope() throws IOException { + // test #1: envelope with expected coordinate order (TopLeft, BottomRight) + XContentBuilder multilinesGeoJson = XContentFactory.jsonBuilder().startObject().field("type", "envelope") + .startArray("coordinates") + .startArray().value(-50).value(30).endArray() + .startArray().value(50).value(-30).endArray() + .endArray() + .endObject(); + Rectangle expected = new Rectangle(-30, 30, -50, 50); + assertGeometryEquals(expected, multilinesGeoJson); + + // test #2: envelope that spans dateline + multilinesGeoJson = XContentFactory.jsonBuilder().startObject().field("type", "envelope") + .startArray("coordinates") + .startArray().value(50).value(30).endArray() + .startArray().value(-50).value(-30).endArray() + .endArray() + .endObject(); + + expected = new Rectangle(-30, 30, 50, -50); + assertGeometryEquals(expected, multilinesGeoJson); + + // test #3: "envelope" (actually a triangle) with invalid number of coordinates (TopRight, BottomLeft, BottomRight) + multilinesGeoJson = XContentFactory.jsonBuilder().startObject().field("type", "envelope") + .startArray("coordinates") + .startArray().value(50).value(30).endArray() + .startArray().value(-50).value(-30).endArray() + .startArray().value(50).value(-39).endArray() + .endArray() + .endObject(); + try (XContentParser parser = createParser(multilinesGeoJson)) { + parser.nextToken(); + expectThrows(XContentParseException.class, () -> GeoJson.fromXContent(parser, false, false, false)); + assertNull(parser.nextToken()); + } + + // test #4: "envelope" with empty coordinates + multilinesGeoJson = XContentFactory.jsonBuilder().startObject().field("type", "envelope") + .startArray("coordinates") + .endArray() + .endObject(); + try (XContentParser parser = createParser(multilinesGeoJson)) { + parser.nextToken(); + expectThrows(XContentParseException.class, () -> GeoJson.fromXContent(parser, false, false, false)); + assertNull(parser.nextToken()); + } + } + + @Override + public void testParsePolygon() throws IOException { + XContentBuilder polygonGeoJson = XContentFactory.jsonBuilder() + .startObject() + .field("type", "Polygon") + .startArray("coordinates") + .startArray() + .startArray().value(100.0).value(1.0).endArray() + .startArray().value(101.0).value(1.0).endArray() + .startArray().value(101.0).value(0.0).endArray() + .startArray().value(100.0).value(0.0).endArray() + .startArray().value(100.0).value(1.0).endArray() + .endArray() + .endArray() + .endObject(); + + Polygon p = new Polygon( + new LinearRing( + new double[] {1d, 1d, 0d, 0d, 1d}, + new double[] {100d, 101d, 101d, 100d, 100d})); + assertGeometryEquals(p, polygonGeoJson); + } + + public void testParse3DPolygon() throws IOException { + XContentBuilder polygonGeoJson = XContentFactory.jsonBuilder() + .startObject() + .field("type", "Polygon") + .startArray("coordinates") + .startArray() + .startArray().value(100.0).value(1.0).value(10.0).endArray() + .startArray().value(101.0).value(1.0).value(10.0).endArray() + .startArray().value(101.0).value(0.0).value(10.0).endArray() + .startArray().value(100.0).value(0.0).value(10.0).endArray() + .startArray().value(100.0).value(1.0).value(10.0).endArray() + .endArray() + .endArray() + .endObject(); + + Polygon expected = new Polygon(new LinearRing( + new double[]{1.0, 1.0, 0.0, 0.0, 1.0}, + new double[]{100.0, 101.0, 101.0, 100.0, 100.0}, + new double[]{10.0, 10.0, 10.0, 10.0, 10.0} + )); + try (XContentParser parser = createParser(polygonGeoJson)) { + parser.nextToken(); + assertEquals(expected, GeoJson.fromXContent(parser, true, false, true)); + } + } + + public void testInvalidDimensionalPolygon() throws IOException { + XContentBuilder polygonGeoJson = XContentFactory.jsonBuilder() + .startObject() + .field("type", "Polygon") + .startArray("coordinates") + .startArray() + .startArray().value(100.0).value(1.0).value(10.0).endArray() + .startArray().value(101.0).value(1.0).endArray() + .startArray().value(101.0).value(0.0).value(10.0).endArray() + .startArray().value(100.0).value(0.0).value(10.0).endArray() + .startArray().value(100.0).value(1.0).value(10.0).endArray() + .endArray() + .endArray() + .endObject(); + try (XContentParser parser = createParser(polygonGeoJson)) { + parser.nextToken(); + expectThrows(XContentParseException.class, () -> GeoJson.fromXContent(parser, true, false, true)); + assertNull(parser.nextToken()); + } + } + + public void testParseInvalidPoint() throws IOException { + // test case 1: create an invalid point object with multipoint data format + XContentBuilder invalidPoint1 = XContentFactory.jsonBuilder() + .startObject() + .field("type", "point") + .startArray("coordinates") + .startArray().value(-74.011).value(40.753).endArray() + .endArray() + .endObject(); + try (XContentParser parser = createParser(invalidPoint1)) { + parser.nextToken(); + expectThrows(XContentParseException.class, () -> GeoJson.fromXContent(parser, true, false, false)); + assertNull(parser.nextToken()); + } + + // test case 2: create an invalid point object with an empty number of coordinates + XContentBuilder invalidPoint2 = XContentFactory.jsonBuilder() + .startObject() + .field("type", "point") + .startArray("coordinates") + .endArray() + .endObject(); + try (XContentParser parser = createParser(invalidPoint2)) { + parser.nextToken(); + expectThrows(XContentParseException.class, () -> GeoJson.fromXContent(parser, true, false, false)); + assertNull(parser.nextToken()); + } + } + + public void testParseInvalidMultipoint() throws IOException { + // test case 1: create an invalid multipoint object with single coordinate + XContentBuilder invalidMultipoint1 = XContentFactory.jsonBuilder() + .startObject() + .field("type", "multipoint") + .startArray("coordinates").value(-74.011).value(40.753).endArray() + .endObject(); + try (XContentParser parser = createParser(invalidMultipoint1)) { + parser.nextToken(); + expectThrows(XContentParseException.class, () -> GeoJson.fromXContent(parser, true, false, false)); + assertNull(parser.nextToken()); + } + + // test case 2: create an invalid multipoint object with null coordinate + XContentBuilder invalidMultipoint2 = XContentFactory.jsonBuilder() + .startObject() + .field("type", "multipoint") + .startArray("coordinates") + .endArray() + .endObject(); + try (XContentParser parser = createParser(invalidMultipoint2)) { + parser.nextToken(); + expectThrows(XContentParseException.class, () -> GeoJson.fromXContent(parser, true, false, false)); + assertNull(parser.nextToken()); + } + + // test case 3: create a valid formatted multipoint object with invalid number (0) of coordinates + XContentBuilder invalidMultipoint3 = XContentFactory.jsonBuilder() + .startObject() + .field("type", "multipoint") + .startArray("coordinates") + .startArray().endArray() + .endArray() + .endObject(); + try (XContentParser parser = createParser(invalidMultipoint3)) { + parser.nextToken(); + expectThrows(XContentParseException.class, () -> GeoJson.fromXContent(parser, true, false, false)); + assertNull(parser.nextToken()); + } + } + + public void testParseInvalidDimensionalMultiPolygon() throws IOException { + // test invalid multipolygon (an "accidental" polygon with inner rings outside outer ring) + String multiPolygonGeoJson = Strings.toString(XContentFactory.jsonBuilder() + .startObject() + .field("type", "MultiPolygon") + .startArray("coordinates") + .startArray()//first poly (without holes) + .startArray() + .startArray().value(102.0).value(2.0).endArray() + .startArray().value(103.0).value(2.0).endArray() + .startArray().value(103.0).value(3.0).endArray() + .startArray().value(102.0).value(3.0).endArray() + .startArray().value(102.0).value(2.0).endArray() + .endArray() + .endArray() + .startArray()//second poly (with hole) + .startArray() + .startArray().value(100.0).value(0.0).endArray() + .startArray().value(101.0).value(0.0).endArray() + .startArray().value(101.0).value(1.0).endArray() + .startArray().value(100.0).value(1.0).endArray() + .startArray().value(100.0).value(0.0).endArray() + .endArray() + .startArray()//hole + .startArray().value(100.2).value(0.8).endArray() + .startArray().value(100.2).value(0.2).value(10.0).endArray() + .startArray().value(100.8).value(0.2).endArray() + .startArray().value(100.8).value(0.8).endArray() + .startArray().value(100.2).value(0.8).endArray() + .endArray() + .endArray() + .endArray() + .endObject()); + + try (XContentParser parser = createParser(JsonXContent.jsonXContent, multiPolygonGeoJson)) { + parser.nextToken(); + expectThrows(XContentParseException.class, () -> GeoJson.fromXContent(parser, true, false, false)); + assertNull(parser.nextToken()); + } + } + + public void testParseInvalidPolygon() throws IOException { + /* + * The following 3 test cases ensure proper error handling of invalid polygons + * per the GeoJSON specification + */ + // test case 1: create an invalid polygon with only 2 points + String invalidPoly = Strings.toString(XContentFactory.jsonBuilder().startObject().field("type", "polygon") + .startArray("coordinates") + .startArray() + .startArray().value(-74.011).value(40.753).endArray() + .startArray().value(-75.022).value(41.783).endArray() + .endArray() + .endArray() + .endObject()); + try (XContentParser parser = createParser(JsonXContent.jsonXContent, invalidPoly)) { + parser.nextToken(); + expectThrows(XContentParseException.class, () -> GeoJson.fromXContent(parser, true, false, false)); + assertNull(parser.nextToken()); + } + + // test case 2: create an invalid polygon with only 1 point + invalidPoly = Strings.toString(XContentFactory.jsonBuilder().startObject().field("type", "polygon") + .startArray("coordinates") + .startArray() + .startArray().value(-74.011).value(40.753).endArray() + .endArray() + .endArray() + .endObject()); + + try (XContentParser parser = createParser(JsonXContent.jsonXContent, invalidPoly)) { + parser.nextToken(); + expectThrows(XContentParseException.class, () -> GeoJson.fromXContent(parser, true, false, false)); + assertNull(parser.nextToken()); + } + + // test case 3: create an invalid polygon with 0 points + invalidPoly = Strings.toString(XContentFactory.jsonBuilder().startObject().field("type", "polygon") + .startArray("coordinates") + .startArray() + .startArray().endArray() + .endArray() + .endArray() + .endObject()); + + try (XContentParser parser = createParser(JsonXContent.jsonXContent, invalidPoly)) { + parser.nextToken(); + expectThrows(XContentParseException.class, () -> GeoJson.fromXContent(parser, true, false, false)); + assertNull(parser.nextToken()); + } + + // test case 4: create an invalid polygon with null value points + invalidPoly = Strings.toString(XContentFactory.jsonBuilder().startObject().field("type", "polygon") + .startArray("coordinates") + .startArray() + .startArray().nullValue().nullValue().endArray() + .endArray() + .endArray() + .endObject()); + + try (XContentParser parser = createParser(JsonXContent.jsonXContent, invalidPoly)) { + parser.nextToken(); + expectThrows(XContentParseException.class, () -> GeoJson.fromXContent(parser, true, false, false)); + assertNull(parser.nextToken()); + } + + // test case 5: create an invalid polygon with 1 invalid LinearRing + invalidPoly = Strings.toString(XContentFactory.jsonBuilder().startObject().field("type", "polygon") + .startArray("coordinates") + .nullValue().nullValue() + .endArray() + .endObject()); + + try (XContentParser parser = createParser(JsonXContent.jsonXContent, invalidPoly)) { + parser.nextToken(); + expectThrows(XContentParseException.class, () -> GeoJson.fromXContent(parser, true, false, false)); + assertNull(parser.nextToken()); + } + + // test case 6: create an invalid polygon with 0 LinearRings + invalidPoly = Strings.toString(XContentFactory.jsonBuilder().startObject().field("type", "polygon") + .startArray("coordinates").endArray() + .endObject()); + + try (XContentParser parser = createParser(JsonXContent.jsonXContent, invalidPoly)) { + parser.nextToken(); + expectThrows(XContentParseException.class, () -> GeoJson.fromXContent(parser, true, false, false)); + assertNull(parser.nextToken()); + } + + // test case 7: create an invalid polygon with 0 LinearRings + invalidPoly = Strings.toString(XContentFactory.jsonBuilder().startObject().field("type", "polygon") + .startArray("coordinates") + .startArray().value(-74.011).value(40.753).endArray() + .endArray() + .endObject()); + + try (XContentParser parser = createParser(JsonXContent.jsonXContent, invalidPoly)) { + parser.nextToken(); + expectThrows(XContentParseException.class, () -> GeoJson.fromXContent(parser, true, false, false)); + assertNull(parser.nextToken()); + } + } + + public void testParsePolygonWithHole() throws IOException { + XContentBuilder polygonGeoJson = XContentFactory.jsonBuilder() + .startObject() + .field("type", "Polygon") + .startArray("coordinates") + .startArray() + .startArray().value(100.0).value(1.0).endArray() + .startArray().value(101.0).value(1.0).endArray() + .startArray().value(101.0).value(0.0).endArray() + .startArray().value(100.0).value(0.0).endArray() + .startArray().value(100.0).value(1.0).endArray() + .endArray() + .startArray() + .startArray().value(100.2).value(0.8).endArray() + .startArray().value(100.2).value(0.2).endArray() + .startArray().value(100.8).value(0.2).endArray() + .startArray().value(100.8).value(0.8).endArray() + .startArray().value(100.2).value(0.8).endArray() + .endArray() + .endArray() + .endObject(); + + LinearRing hole = + new LinearRing( + new double[] {0.8d, 0.2d, 0.2d, 0.8d, 0.8d}, new double[] {100.2d, 100.2d, 100.8d, 100.8d, 100.2d}); + Polygon p = + new Polygon(new LinearRing( + new double[] {1d, 1d, 0d, 0d, 1d}, new double[] {100d, 101d, 101d, 100d, 100d}), Collections.singletonList(hole)); + assertGeometryEquals(p, polygonGeoJson); + } + + @Override + public void testParseMultiPoint() throws IOException { + XContentBuilder multiPointGeoJson = XContentFactory.jsonBuilder() + .startObject() + .field("type", "MultiPoint") + .startArray("coordinates") + .startArray().value(100.0).value(0.0).endArray() + .startArray().value(101.0).value(1.0).endArray() + .endArray() + .endObject(); + + assertGeometryEquals(new MultiPoint(Arrays.asList( + new Point(0, 100), + new Point(1, 101))), multiPointGeoJson); + } + + @Override + public void testParseMultiPolygon() throws IOException { + // two polygons; one without hole, one with hole + XContentBuilder multiPolygonGeoJson = XContentFactory.jsonBuilder() + .startObject() + .field("type", "MultiPolygon") + .startArray("coordinates") + .startArray()//first poly (without holes) + .startArray() + .startArray().value(102.0).value(2.0).endArray() + .startArray().value(103.0).value(2.0).endArray() + .startArray().value(103.0).value(3.0).endArray() + .startArray().value(102.0).value(3.0).endArray() + .startArray().value(102.0).value(2.0).endArray() + .endArray() + .endArray() + .startArray()//second poly (with hole) + .startArray() + .startArray().value(100.0).value(0.0).endArray() + .startArray().value(101.0).value(0.0).endArray() + .startArray().value(101.0).value(1.0).endArray() + .startArray().value(100.0).value(1.0).endArray() + .startArray().value(100.0).value(0.0).endArray() + .endArray() + .startArray()//hole + .startArray().value(100.2).value(0.8).endArray() + .startArray().value(100.2).value(0.2).endArray() + .startArray().value(100.8).value(0.2).endArray() + .startArray().value(100.8).value(0.8).endArray() + .startArray().value(100.2).value(0.8).endArray() + .endArray() + .endArray() + .endArray() + .endObject(); + + LinearRing hole = new LinearRing( + new double[] {0.8d, 0.2d, 0.2d, 0.8d, 0.8d}, new double[] {100.2d, 100.2d, 100.8d, 100.8d, 100.2d}); + + MultiPolygon polygons = new MultiPolygon(Arrays.asList( + new Polygon(new LinearRing( + new double[] {2d, 2d, 3d, 3d, 2d}, new double[] {102d, 103d, 103d, 102d, 102d})), + new Polygon(new LinearRing( + new double[] {0d, 0d, 1d, 1d, 0d}, new double[] {100d, 101d, 101d, 100d, 100d}), + Collections.singletonList(hole)))); + + assertGeometryEquals(polygons, multiPolygonGeoJson); + } + + @Override + public void testParseGeometryCollection() throws IOException { + XContentBuilder geometryCollectionGeoJson = XContentFactory.jsonBuilder() + .startObject() + .field("type", "GeometryCollection") + .startArray("geometries") + .startObject() + .field("type", "LineString") + .startArray("coordinates") + .startArray().value(100.0).value(0.0).endArray() + .startArray().value(101.0).value(1.0).endArray() + .endArray() + .endObject() + .startObject() + .field("type", "Point") + .startArray("coordinates").value(102.0).value(2.0).endArray() + .endObject() + .startObject() + .field("type", "Polygon") + .startArray("coordinates") + .startArray() + .startArray().value(-177.0).value(10.0).endArray() + .startArray().value(176.0).value(15.0).endArray() + .startArray().value(172.0).value(0.0).endArray() + .startArray().value(176.0).value(-15.0).endArray() + .startArray().value(-177.0).value(-10.0).endArray() + .startArray().value(-177.0).value(10.0).endArray() + .endArray() + .endArray() + .endObject() + .endArray() + .endObject(); + + GeometryCollection geometryExpected = new GeometryCollection<> (Arrays.asList( + new Line(new double[] {0d, 1d}, new double[] {100d, 101d}), + new Point(2d, 102d), + new Polygon(new LinearRing( + new double[] {10, 15, 0, -15, -10, 10}, + new double[] {-177, 176, 172, 176, -177, -177} + )) + )); + assertGeometryEquals(geometryExpected, geometryCollectionGeoJson); + } + + public void testThatParserExtractsCorrectTypeAndCoordinatesFromArbitraryJson() throws IOException { + XContentBuilder pointGeoJson = XContentFactory.jsonBuilder() + .startObject() + .startObject("crs") + .field("type", "name") + .startObject("properties") + .field("name", "urn:ogc:def:crs:OGC:1.3:CRS84") + .endObject() + .endObject() + .field("bbox", "foobar") + .field("type", "point") + .field("bubu", "foobar") + .startArray("coordinates").value(100.0).value(0.0).endArray() + .startObject("nested").startArray("coordinates").value(200.0).value(0.0).endArray().endObject() + .startObject("lala").field("type", "NotAPoint").endObject() + .endObject(); + + Point expectedPt = new Point(0, 100); + assertGeometryEquals(expectedPt, pointGeoJson, false); + } + + public void testParseOrientationOption() throws IOException { + // test 1: valid ccw (right handed system) poly not crossing dateline (with 'right' field) + XContentBuilder polygonGeoJson = XContentFactory.jsonBuilder() + .startObject() + .field("type", "Polygon") + .field("orientation", randomFrom("ccw", "right")) + .startArray("coordinates") + .startArray() + .startArray().value(176.0).value(15.0).endArray() + .startArray().value(-177.0).value(10.0).endArray() + .startArray().value(-177.0).value(-10.0).endArray() + .startArray().value(176.0).value(-15.0).endArray() + .startArray().value(172.0).value(0.0).endArray() + .startArray().value(176.0).value(15.0).endArray() + .endArray() + .startArray() + .startArray().value(-172.0).value(8.0).endArray() + .startArray().value(174.0).value(10.0).endArray() + .startArray().value(-172.0).value(-8.0).endArray() + .startArray().value(-172.0).value(8.0).endArray() + .endArray() + .endArray() + .endObject(); + + Polygon expected = new Polygon( + new LinearRing(new double[]{15.0, 10.0, -10.0, -15.0, 0.0, 15.0}, new double[]{176.0, -177.0, -177.0, 176.0, 172.0, 176.0}), + Collections.singletonList( + new LinearRing(new double[]{8.0, 10.0, -8.0, 8.0}, new double[]{-172.0, 174.0, -172.0, -172.0}) + )); + assertGeometryEquals(expected, polygonGeoJson); + + // test 2: valid cw poly + polygonGeoJson = XContentFactory.jsonBuilder() + .startObject() + .field("type", "Polygon") + .field("orientation", randomFrom("cw", "left")) + .startArray("coordinates") + .startArray() + .startArray().value(176.0).value(15.0).endArray() + .startArray().value(-177.0).value(10.0).endArray() + .startArray().value(-177.0).value(-10.0).endArray() + .startArray().value(176.0).value(-15.0).endArray() + .startArray().value(172.0).value(0.0).endArray() + .startArray().value(176.0).value(15.0).endArray() + .endArray() + .startArray() + .startArray().value(-172.0).value(8.0).endArray() + .startArray().value(174.0).value(10.0).endArray() + .startArray().value(-172.0).value(-8.0).endArray() + .startArray().value(-172.0).value(8.0).endArray() + .endArray() + .endArray() + .endObject(); + + expected = new Polygon( + new LinearRing(new double[]{15.0, 0.0, -15.0, -10.0, 10.0, 15.0}, new double[]{176.0, 172.0, 176.0, -177.0, -177.0, 176.0}), + Collections.singletonList( + new LinearRing(new double[]{8.0, -8.0, 10.0, 8.0}, new double[]{-172.0, -172.0, 174.0, -172.0}) + )); + assertGeometryEquals(expected, polygonGeoJson); + } + + public void testParseInvalidShapes() throws IOException { + // single dimensions point + XContentBuilder tooLittlePointGeoJson = XContentFactory.jsonBuilder() + .startObject() + .field("type", "Point") + .startArray("coordinates").value(10.0).endArray() + .endObject(); + + try (XContentParser parser = createParser(tooLittlePointGeoJson)) { + parser.nextToken(); + expectThrows(XContentParseException.class, () -> GeoJson.fromXContent(parser, true, false, false)); + assertNull(parser.nextToken()); + } + + // zero dimensions point + XContentBuilder emptyPointGeoJson = XContentFactory.jsonBuilder() + .startObject() + .field("type", "Point") + .startObject("coordinates").field("foo", "bar").endObject() + .endObject(); + + try (XContentParser parser = createParser(emptyPointGeoJson)) { + parser.nextToken(); + expectThrows(XContentParseException.class, () -> GeoJson.fromXContent(parser, true, false, false)); + assertNull(parser.nextToken()); + } + } + + public void testParseInvalidGeometryCollectionShapes() throws IOException { + // single dimensions point + XContentBuilder invalidPoints = XContentFactory.jsonBuilder() + .startObject() + .startObject("foo") + .field("type", "geometrycollection") + .startArray("geometries") + .startObject() + .field("type", "polygon") + .startArray("coordinates") + .startArray().value("46.6022226498514").value("24.7237442867977").endArray() + .startArray().value("46.6031857243798").value("24.722968774929").endArray() + .endArray() // coordinates + .endObject() + .endArray() // geometries + .endObject() + .endObject(); + try (XContentParser parser = createParser(invalidPoints)) { + parser.nextToken(); // foo + parser.nextToken(); // start object + parser.nextToken(); // start object + expectThrows(XContentParseException.class, () -> GeoJson.fromXContent(parser, true, false, false)); + assertEquals(XContentParser.Token.END_OBJECT, parser.nextToken()); // end of the document + assertNull(parser.nextToken()); // no more elements afterwards + } + } +} diff --git a/server/src/test/java/org/elasticsearch/common/geo/GeoJsonSerializationTests.java b/server/src/test/java/org/elasticsearch/common/geo/GeoJsonSerializationTests.java new file mode 100644 index 0000000000000..ab6e3242654f5 --- /dev/null +++ b/server/src/test/java/org/elasticsearch/common/geo/GeoJsonSerializationTests.java @@ -0,0 +1,269 @@ +/* + * 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.ToXContent; +import org.elasticsearch.common.xcontent.ToXContentObject; +import org.elasticsearch.common.xcontent.XContentBuilder; +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.geometry.Rectangle; +import org.elasticsearch.test.AbstractXContentTestCase; +import org.elasticsearch.test.ESTestCase; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.function.Function; +import java.util.function.Supplier; + +public class GeoJsonSerializationTests extends ESTestCase { + + private static class GeometryWrapper implements ToXContentObject { + + private Geometry geometry; + + GeometryWrapper(Geometry geometry) { + this.geometry = geometry; + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + return GeoJson.toXContent(geometry, builder, params); + } + + public static GeometryWrapper fromXContent(XContentParser parser) throws IOException { + parser.nextToken(); + return new GeometryWrapper(GeoJson.fromXContent(parser, true, false, true)); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + GeometryWrapper that = (GeometryWrapper) o; + return Objects.equals(geometry, that.geometry); + } + + @Override + public int hashCode() { + return Objects.hash(geometry); + } + } + + + private void xContentTest(Supplier instanceSupplier) throws IOException { + AbstractXContentTestCase.xContentTester( + this::createParser, + () -> new GeometryWrapper(instanceSupplier.get()), + (geometryWrapper, xContentBuilder) -> { + geometryWrapper.toXContent(xContentBuilder, ToXContent.EMPTY_PARAMS); + }, + GeometryWrapper::fromXContent) + .supportsUnknownFields(true) + .test(); + } + + + public void testPoint() throws IOException { + xContentTest(() -> randomPoint(randomBoolean())); + } + + public void testMultiPoint() throws IOException { + xContentTest(() -> randomMultiPoint(randomBoolean())); + } + + public void testLineString() throws IOException { + xContentTest(() -> randomLine(randomBoolean())); + } + + public void testMultiLineString() throws IOException { + xContentTest(() -> randomMultiLine(randomBoolean())); + } + + public void testPolygon() throws IOException { + xContentTest(() -> randomPolygon(randomBoolean())); + } + + public void testMultiPolygon() throws IOException { + xContentTest(() -> randomMultiPolygon(randomBoolean())); + } + + public void testEnvelope() throws IOException { + xContentTest(GeoJsonSerializationTests::randomRectangle); + } + + public void testGeometryCollection() throws IOException { + xContentTest(() -> randomGeometryCollection(randomBoolean())); + } + + public void testCircle() throws IOException { + xContentTest(() -> randomCircle(randomBoolean())); + } + + public static double randomLat() { + return randomDoubleBetween(-90, 90, true); + } + + public static double randomLon() { + return randomDoubleBetween(-180, 180, true); + } + + public static Circle randomCircle(boolean hasAlt) { + if (hasAlt) { + return new Circle(randomDoubleBetween(-90, 90, true), randomDoubleBetween(-180, 180, true), randomDouble(), + randomDoubleBetween(0, 100, false)); + } else { + return new Circle(randomDoubleBetween(-90, 90, true), randomDoubleBetween(-180, 180, true), randomDoubleBetween(0, 100, false)); + } + } + + public static Line randomLine(boolean hasAlts) { + int size = randomIntBetween(2, 10); + double[] lats = new double[size]; + double[] lons = new double[size]; + double[] alts = hasAlts ? new double[size] : null; + for (int i = 0; i < size; i++) { + lats[i] = randomLat(); + lons[i] = randomLon(); + if (hasAlts) { + alts[i] = randomDouble(); + } + } + if (hasAlts) { + return new Line(lats, lons, alts); + } + return new Line(lats, lons); + } + + public static Point randomPoint(boolean hasAlt) { + if (hasAlt) { + return new Point(randomLat(), randomLon(), randomDouble()); + } else { + return new Point(randomLat(), randomLon()); + } + } + + public static MultiPoint randomMultiPoint(boolean hasAlt) { + int size = randomIntBetween(3, 10); + List points = new ArrayList<>(); + for (int i = 0; i < size; i++) { + points.add(randomPoint(hasAlt)); + } + return new MultiPoint(points); + } + + public static MultiLine randomMultiLine(boolean hasAlt) { + int size = randomIntBetween(3, 10); + List lines = new ArrayList<>(); + for (int i = 0; i < size; i++) { + lines.add(randomLine(hasAlt)); + } + return new MultiLine(lines); + } + + public static MultiPolygon randomMultiPolygon(boolean hasAlt) { + int size = randomIntBetween(3, 10); + List polygons = new ArrayList<>(); + for (int i = 0; i < size; i++) { + polygons.add(randomPolygon(hasAlt)); + } + return new MultiPolygon(polygons); + } + + public static LinearRing randomLinearRing(boolean hasAlt) { + int size = randomIntBetween(3, 10); + double[] lats = new double[size + 1]; + double[] lons = new double[size + 1]; + double[] alts; + if (hasAlt) { + alts = new double[size + 1]; + } else { + alts = null; + } + for (int i = 0; i < size; i++) { + lats[i] = randomLat(); + lons[i] = randomLon(); + if (hasAlt) { + alts[i] = randomDouble(); + } + } + lats[size] = lats[0]; + lons[size] = lons[0]; + if (hasAlt) { + alts[size] = alts[0]; + return new LinearRing(lats, lons, alts); + } else { + return new LinearRing(lats, lons); + } + } + + public static Polygon randomPolygon(boolean hasAlt) { + int size = randomIntBetween(0, 10); + List holes = new ArrayList<>(); + for (int i = 0; i < size; i++) { + holes.add(randomLinearRing(hasAlt)); + } + if (holes.size() > 0) { + return new Polygon(randomLinearRing(hasAlt), holes); + } else { + return new Polygon(randomLinearRing(hasAlt)); + } + } + + public static Rectangle randomRectangle() { + double lat1 = randomLat(); + double lat2 = randomLat(); + double minLon = randomLon(); + double maxLon = randomLon(); + return new Rectangle(Math.min(lat1, lat2), Math.max(lat1, lat2), minLon, maxLon); + } + + public static GeometryCollection randomGeometryCollection(boolean hasAlt) { + return randomGeometryCollection(0, hasAlt); + } + + private static GeometryCollection randomGeometryCollection(int level, boolean hasAlt) { + int size = randomIntBetween(1, 10); + List shapes = new ArrayList<>(); + for (int i = 0; i < size; i++) { + @SuppressWarnings("unchecked") Function geometry = randomFrom( + GeoJsonSerializationTests::randomCircle, + GeoJsonSerializationTests::randomLine, + GeoJsonSerializationTests::randomPoint, + GeoJsonSerializationTests::randomPolygon, + hasAlt ? GeoJsonSerializationTests::randomPoint : (b) -> randomRectangle(), + level < 3 ? (b) -> randomGeometryCollection(level + 1, b) : GeoJsonSerializationTests::randomPoint // don't build too deep + ); + shapes.add(geometry.apply(hasAlt)); + } + return new GeometryCollection<>(shapes); + } +} diff --git a/server/src/test/java/org/elasticsearch/common/geo/GeometryParserTests.java b/server/src/test/java/org/elasticsearch/common/geo/GeometryParserTests.java new file mode 100644 index 0000000000000..24ba7780cefd6 --- /dev/null +++ b/server/src/test/java/org/elasticsearch/common/geo/GeometryParserTests.java @@ -0,0 +1,137 @@ +/* + * 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.ElasticsearchParseException; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentFactory; +import org.elasticsearch.common.xcontent.XContentParseException; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.geo.geometry.LinearRing; +import org.elasticsearch.geo.geometry.Point; +import org.elasticsearch.geo.geometry.Polygon; +import org.elasticsearch.test.ESTestCase; + +/** + * Tests for {@link GeometryParser} + */ +public class GeometryParserTests extends ESTestCase { + + public void testGeoJsonParsing() throws Exception { + + XContentBuilder pointGeoJson = XContentFactory.jsonBuilder() + .startObject() + .field("type", "Point") + .startArray("coordinates").value(100.0).value(0.0).endArray() + .endObject(); + + try (XContentParser parser = createParser(pointGeoJson)) { + parser.nextToken(); + assertEquals(new Point(0, 100), GeometryParser.parse(parser, true, randomBoolean(), randomBoolean())); + } + + XContentBuilder pointGeoJsonWithZ = XContentFactory.jsonBuilder() + .startObject() + .field("type", "Point") + .startArray("coordinates").value(100.0).value(0.0).value(10.0).endArray() + .endObject(); + + try (XContentParser parser = createParser(pointGeoJsonWithZ)) { + parser.nextToken(); + assertEquals(new Point(0, 100, 10.0), GeometryParser.parse(parser, true, randomBoolean(), true)); + } + + + try (XContentParser parser = createParser(pointGeoJsonWithZ)) { + parser.nextToken(); + expectThrows(XContentParseException.class, () -> GeometryParser.parse(parser, true, randomBoolean(), false)); + } + + XContentBuilder polygonGeoJson = XContentFactory.jsonBuilder() + .startObject() + .field("type", "Polygon") + .startArray("coordinates") + .startArray() + .startArray().value(100.0).value(1.0).endArray() + .startArray().value(101.0).value(1.0).endArray() + .startArray().value(101.0).value(0.0).endArray() + .startArray().value(100.0).value(0.0).endArray() + .endArray() + .endArray() + .endObject(); + + Polygon p = new Polygon(new LinearRing(new double[] {1d, 1d, 0d, 0d, 1d}, new double[] {100d, 101d, 101d, 100d, 100d})); + try (XContentParser parser = createParser(polygonGeoJson)) { + parser.nextToken(); + // Coerce should automatically close the polygon + assertEquals(p, GeometryParser.parse(parser, true, true, randomBoolean())); + } + + try (XContentParser parser = createParser(polygonGeoJson)) { + parser.nextToken(); + // No coerce - the polygon parsing should fail + expectThrows(XContentParseException.class, () -> GeometryParser.parse(parser, true, false, randomBoolean())); + } + } + + public void testWKTParsing() throws Exception { + XContentBuilder pointGeoJson = XContentFactory.jsonBuilder() + .startObject() + .field("foo", "Point (100 0)") + .endObject(); + + try (XContentParser parser = createParser(pointGeoJson)) { + parser.nextToken(); // Start object + parser.nextToken(); // Field Name + parser.nextToken(); // Field Value + assertEquals(new Point(0, 100), GeometryParser.parse(parser, true, randomBoolean(), randomBoolean())); + } + } + + public void testNullParsing() throws Exception { + XContentBuilder pointGeoJson = XContentFactory.jsonBuilder() + .startObject() + .nullField("foo") + .endObject(); + + try (XContentParser parser = createParser(pointGeoJson)) { + parser.nextToken(); // Start object + parser.nextToken(); // Field Name + parser.nextToken(); // Field Value + assertNull(GeometryParser.parse(parser, true, randomBoolean(), randomBoolean())); + } + } + + public void testUnsupportedValueParsing() throws Exception { + XContentBuilder pointGeoJson = XContentFactory.jsonBuilder() + .startObject() + .field("foo", 42) + .endObject(); + + try (XContentParser parser = createParser(pointGeoJson)) { + parser.nextToken(); // Start object + parser.nextToken(); // Field Name + parser.nextToken(); // Field Value + ElasticsearchParseException ex = expectThrows(ElasticsearchParseException.class, + () -> GeometryParser.parse(parser, true, randomBoolean(), randomBoolean())); + assertEquals("shape must be an object consisting of type and coordinates", ex.getMessage()); + } + } +}