Adds GeoJson parser for Geometry classes defined in libs/geo. Relates #40908 and #29872
This commit is contained in:
parent
92a820bc1a
commit
10ab838106
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue