Geo: Add GeoJson parser to libs/geo classes (#41575) (#41657)

Adds GeoJson parser for Geometry classes defined in libs/geo.

Relates #40908 and #29872
This commit is contained in:
Igor Motov 2019-04-29 19:43:31 -04:00 committed by GitHub
parent 92a820bc1a
commit 10ab838106
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 1869 additions and 27 deletions

View File

@ -102,7 +102,7 @@ public class Circle implements Geometry {
}
@Override
public <T> T visit(GeometryVisitor<T> visitor) {
public <T, E extends Exception> T visit(GeometryVisitor<T, E> visitor) throws E {
return visitor.visit(this);
}

View File

@ -26,7 +26,7 @@ public interface Geometry {
ShapeType type();
<T> T visit(GeometryVisitor<T> visitor);
<T, E extends Exception> T visit(GeometryVisitor<T, E> visitor) throws E;
boolean isEmpty();

View File

@ -57,7 +57,7 @@ public class GeometryCollection<G extends Geometry> implements Geometry, Iterabl
}
@Override
public <T> T visit(GeometryVisitor<T> visitor) {
public <T, E extends Exception> T visit(GeometryVisitor<T, E> visitor) throws E {
return visitor.visit(this);
}

View File

@ -44,26 +44,26 @@ package org.elasticsearch.geo.geometry;
*
* @see <a href="https://en.wikipedia.org/wiki/Visitor_pattern">Visitor Pattern</a>
*/
public interface GeometryVisitor<T> {
public interface GeometryVisitor<T, E extends Exception> {
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;
}

View File

@ -103,7 +103,7 @@ public class Line implements Geometry {
}
@Override
public <T> T visit(GeometryVisitor<T> visitor) {
public <T, E extends Exception> T visit(GeometryVisitor<T, E> visitor) throws E {
return visitor.visit(this);
}

View File

@ -54,7 +54,7 @@ public class LinearRing extends Line {
}
@Override
public <T> T visit(GeometryVisitor<T> visitor) {
public <T, E extends Exception> T visit(GeometryVisitor<T, E> visitor) throws E {
return visitor.visit(this);
}
}

View File

@ -40,7 +40,7 @@ public class MultiLine extends GeometryCollection<Line> {
}
@Override
public <T> T visit(GeometryVisitor<T> visitor) {
public <T, E extends Exception> T visit(GeometryVisitor<T, E> visitor) throws E {
return visitor.visit(this);
}
}

View File

@ -40,7 +40,7 @@ public class MultiPoint extends GeometryCollection<Point> {
}
@Override
public <T> T visit(GeometryVisitor<T> visitor) {
public <T, E extends Exception> T visit(GeometryVisitor<T, E> visitor) throws E {
return visitor.visit(this);
}

View File

@ -40,7 +40,7 @@ public class MultiPolygon extends GeometryCollection<Polygon> {
}
@Override
public <T> T visit(GeometryVisitor<T> visitor) {
public <T, E extends Exception> T visit(GeometryVisitor<T, E> visitor) throws E {
return visitor.visit(this);
}
}

View File

@ -93,7 +93,7 @@ public class Point implements Geometry {
}
@Override
public <T> T visit(GeometryVisitor<T> visitor) {
public <T, E extends Exception> T visit(GeometryVisitor<T, E> visitor) throws E {
return visitor.visit(this);
}

View File

@ -92,7 +92,7 @@ public final class Polygon implements Geometry {
}
@Override
public <T> T visit(GeometryVisitor<T> visitor) {
public <T, E extends Exception> T visit(GeometryVisitor<T, E> visitor) throws E {
return visitor.visit(this);
}

View File

@ -207,7 +207,7 @@ public class Rectangle implements Geometry {
}
@Override
public <T> T visit(GeometryVisitor<T> visitor) {
public <T, E extends Exception> T visit(GeometryVisitor<T, E> visitor) throws E {
return visitor.visit(this);
}

View File

@ -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));
}
}

View File

@ -68,7 +68,7 @@ public class WellKnownText {
if (geometry.isEmpty()) {
sb.append(EMPTY);
} else {
geometry.visit(new GeometryVisitor<Void>() {
geometry.visit(new GeometryVisitor<Void, RuntimeException>() {
@Override
public Void visit(Circle circle) {
sb.append(LPAREN);
@ -543,7 +543,7 @@ public class WellKnownText {
}
public static String getWKTName(Geometry geometry) {
return geometry.visit(new GeometryVisitor<String>() {
return geometry.visit(new GeometryVisitor<String, RuntimeException>() {
@Override
public String visit(Circle circle) {
return "circle";

View File

@ -67,7 +67,7 @@ abstract class BaseGeometryTestCase<T extends Geometry> extends AbstractWireTest
public static void testVisitor(Geometry geom) {
AtomicBoolean called = new AtomicBoolean(false);
Object result = geom.visit(new GeometryVisitor<Object>() {
Object result = geom.visit(new GeometryVisitor<Object, RuntimeException>() {
private Object verify(Geometry geometry, String expectedClass) {
assertFalse("Visitor should be called only once", called.getAndSet(true));
assertSame(geom, geometry);

View File

@ -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<XContentBuilder, IOException>() {
@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<Geometry, ParserContext> PARSER =
new ConstructingObjectParser<>("geojson", true, (a, c) -> {
String type = (String) a[0];
CoordinateNode coordinates = (CoordinateNode) a[1];
@SuppressWarnings("unchecked") List<Geometry> geometries = (List<Geometry>) 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<Geometry> 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<Geometry> 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<CoordinateNode> 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<String, RuntimeException>() {
@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<CoordinateNode> 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<CoordinateNode> 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<Point> 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<Line> 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<LinearRing> 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<Polygon> 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;
}
}
}

View File

@ -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");
}
}

View File

@ -134,7 +134,7 @@ public class GeoShapeFieldMapper extends BaseGeoShapeFieldMapper {
}
}
private class LuceneGeometryIndexer implements GeometryVisitor<Void> {
private class LuceneGeometryIndexer implements GeometryVisitor<Void, RuntimeException> {
private ParseContext context;
private LuceneGeometryIndexer(ParseContext context) {

View File

@ -459,7 +459,7 @@ public class GeoShapeQueryBuilder extends AbstractQueryBuilder<GeoShapeQueryBuil
}
private Query getVectorQueryFromShape(QueryShardContext context, Geometry queryShape) {
return queryShape.visit(new GeometryVisitor<Query>() {
return queryShape.visit(new GeometryVisitor<Query, RuntimeException>() {
@Override
public Query visit(Circle circle) {
throw new QueryShardException(context, "Field [" + fieldName + "] found and unknown shape Circle");

View File

@ -67,6 +67,13 @@ abstract class BaseGeoParsingTestCase extends ESTestCase {
}
}
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<Shape> shapeCollection(Shape... shapes) {
return new ShapeCollection<>(Arrays.asList(shapes), SPATIAL_CONTEXT);
}

View File

@ -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<Geometry> 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
}
}
}

View File

@ -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<Geometry> 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<Point> 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<Line> 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<Polygon> 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<LinearRing> 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<Geometry> randomGeometryCollection(boolean hasAlt) {
return randomGeometryCollection(0, hasAlt);
}
private static GeometryCollection<Geometry> randomGeometryCollection(int level, boolean hasAlt) {
int size = randomIntBetween(1, 10);
List<Geometry> shapes = new ArrayList<>();
for (int i = 0; i < size; i++) {
@SuppressWarnings("unchecked") Function<Boolean, Geometry> 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);
}
}

View File

@ -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());
}
}
}