Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
org.elasticsearch.common.geo.GeoJson Maven / Gradle / Ivy
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
package org.elasticsearch.common.geo;
import org.elasticsearch.ElasticsearchException;
import org.elasticsearch.ElasticsearchParseException;
import org.elasticsearch.common.unit.DistanceUnit;
import org.elasticsearch.geometry.Circle;
import org.elasticsearch.geometry.Geometry;
import org.elasticsearch.geometry.GeometryCollection;
import org.elasticsearch.geometry.GeometryVisitor;
import org.elasticsearch.geometry.Line;
import org.elasticsearch.geometry.LinearRing;
import org.elasticsearch.geometry.MultiLine;
import org.elasticsearch.geometry.MultiPoint;
import org.elasticsearch.geometry.MultiPolygon;
import org.elasticsearch.geometry.Point;
import org.elasticsearch.geometry.Polygon;
import org.elasticsearch.geometry.Rectangle;
import org.elasticsearch.geometry.ShapeType;
import org.elasticsearch.geometry.utils.GeometryValidator;
import org.elasticsearch.xcontent.ConstructingObjectParser;
import org.elasticsearch.xcontent.ObjectParser;
import org.elasticsearch.xcontent.ParseField;
import org.elasticsearch.xcontent.ToXContent;
import org.elasticsearch.xcontent.ToXContentObject;
import org.elasticsearch.xcontent.XContentBuilder;
import org.elasticsearch.xcontent.XContentParser;
import org.elasticsearch.xcontent.XContentSubParser;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import static org.elasticsearch.xcontent.ConstructingObjectParser.constructorArg;
import static org.elasticsearch.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 enum CONTEXT {
TT(true, true),
FF(false, false),
TF(true, false),
FT(false, true);
private final boolean rightOrientation;
private final boolean coerce;
CONTEXT(boolean coerce, boolean rightOrientation) {
this.coerce = coerce;
this.rightOrientation = rightOrientation;
}
static CONTEXT getContext(boolean coerce, boolean rightOrientation) {
if (coerce) {
return rightOrientation ? TT : TF;
} else {
return rightOrientation ? FT : FF;
}
}
}
private GeoJson() {}
public static Geometry fromXContent(GeometryValidator validator, boolean coerce, boolean rightOrientation, XContentParser parser)
throws IOException {
try (XContentSubParser subParser = new XContentSubParser(parser)) {
Geometry geometry = PARSER.apply(subParser, CONTEXT.getContext(coerce, rightOrientation));
validator.validate(geometry);
return geometry;
}
}
public static XContentBuilder toXContent(Geometry geometry, XContentBuilder builder, ToXContent.Params params) throws IOException {
builder.startObject();
builder.field(FIELD_TYPE.getPreferredName(), getGeoJsonName(geometry));
geometry.visit(new GeometryVisitor() {
@Override
public XContentBuilder visit(Circle circle) throws IOException {
builder.field(FIELD_RADIUS.getPreferredName(), DistanceUnit.METERS.toString(circle.getRadiusMeters()));
builder.field(FIELD_COORDINATES.getPreferredName());
return coordinatesToXContent(circle.getY(), circle.getX(), circle.getZ());
}
@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(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(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(FIELD_COORDINATES.getPreferredName());
for (int i = 0; i < multiPoint.size(); i++) {
Point p = multiPoint.get(i);
builder.startArray().value(p.getX()).value(p.getY());
if (p.hasZ()) {
builder.value(p.getZ());
}
builder.endArray();
}
return builder.endArray();
}
@Override
public XContentBuilder visit(MultiPolygon multiPolygon) throws IOException {
builder.startArray(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(FIELD_COORDINATES.getPreferredName());
return coordinatesToXContent(point.getY(), point.getX(), point.getZ());
}
@Override
public XContentBuilder visit(Polygon polygon) throws IOException {
builder.startArray(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(FIELD_COORDINATES.getPreferredName());
coordinatesToXContent(rectangle.getMaxY(), rectangle.getMinX(), rectangle.getMinZ()); // top left
coordinatesToXContent(rectangle.getMinY(), rectangle.getMaxX(), rectangle.getMaxZ()); // 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.getX(i)).value(line.getY(i));
if (line.hasZ()) {
builder.value(line.getZ(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();
}
/**
* Produces that same GeoJSON as toXContent only in parsed map form
*/
public static Map toMap(Geometry geometry) {
Map root = new HashMap<>();
root.put(FIELD_TYPE.getPreferredName(), getGeoJsonName(geometry));
geometry.visit(new GeometryVisitor() {
@Override
public Void visit(Circle circle) {
root.put(FIELD_RADIUS.getPreferredName(), DistanceUnit.METERS.toString(circle.getRadiusMeters()));
root.put(FIELD_COORDINATES.getPreferredName(), coordinatesToList(circle.getY(), circle.getX(), circle.getZ()));
return null;
}
@Override
public Void visit(GeometryCollection collection) {
List geometries = new ArrayList<>(collection.size());
for (Geometry g : collection) {
geometries.add(toMap(g));
}
root.put(FIELD_GEOMETRIES.getPreferredName(), geometries);
return null;
}
@Override
public Void visit(Line line) {
root.put(FIELD_COORDINATES.getPreferredName(), coordinatesToList(line));
return null;
}
@Override
public Void visit(LinearRing ring) {
throw new UnsupportedOperationException("linearRing cannot be serialized using GeoJson");
}
@Override
public Void visit(MultiLine multiLine) {
List lines = new ArrayList<>(multiLine.size());
for (int i = 0; i < multiLine.size(); i++) {
lines.add(coordinatesToList(multiLine.get(i)));
}
root.put(FIELD_COORDINATES.getPreferredName(), lines);
return null;
}
@Override
public Void visit(MultiPoint multiPoint) {
List points = new ArrayList<>(multiPoint.size());
for (int i = 0; i < multiPoint.size(); i++) {
Point p = multiPoint.get(i);
List point = new ArrayList<>();
point.add(p.getX());
point.add(p.getY());
if (p.hasZ()) {
point.add(p.getZ());
}
points.add(point);
}
root.put(FIELD_COORDINATES.getPreferredName(), points);
return null;
}
@Override
public Void visit(MultiPolygon multiPolygon) {
List polygons = new ArrayList<>();
for (int i = 0; i < multiPolygon.size(); i++) {
polygons.add(coordinatesToList(multiPolygon.get(i)));
}
root.put(FIELD_COORDINATES.getPreferredName(), polygons);
return null;
}
@Override
public Void visit(Point point) {
root.put(FIELD_COORDINATES.getPreferredName(), coordinatesToList(point.getY(), point.getX(), point.getZ()));
return null;
}
@Override
public Void visit(Polygon polygon) {
List coords = new ArrayList<>(polygon.getNumberOfHoles() + 1);
coords.add(coordinatesToList(polygon.getPolygon()));
for (int i = 0; i < polygon.getNumberOfHoles(); i++) {
coords.add(coordinatesToList(polygon.getHole(i)));
}
root.put(FIELD_COORDINATES.getPreferredName(), coords);
return null;
}
@Override
public Void visit(Rectangle rectangle) {
List coords = new ArrayList<>(2);
coords.add(coordinatesToList(rectangle.getMaxY(), rectangle.getMinX(), rectangle.getMinZ())); // top left
coords.add(coordinatesToList(rectangle.getMinY(), rectangle.getMaxX(), rectangle.getMaxZ())); // bottom right
root.put(FIELD_COORDINATES.getPreferredName(), coords);
return null;
}
private List coordinatesToList(double lat, double lon, double alt) {
List coords = new ArrayList<>(3);
coords.add(lon);
coords.add(lat);
if (Double.isNaN(alt) == false) {
coords.add(alt);
}
return coords;
}
private List coordinatesToList(Line line) {
List lines = new ArrayList<>(line.length());
for (int i = 0; i < line.length(); i++) {
List coords = new ArrayList<>(3);
coords.add(line.getX(i));
coords.add(line.getY(i));
if (line.hasZ()) {
coords.add(line.getZ(i));
}
lines.add(coords);
}
return lines;
}
private List coordinatesToList(Polygon polygon) {
List coords = new ArrayList<>(polygon.getNumberOfHoles() + 1);
coords.add(coordinatesToList(polygon.getPolygon()));
for (int i = 0; i < polygon.getNumberOfHoles(); i++) {
coords.add(coordinatesToList(polygon.getHole(i)));
}
return coords;
}
});
return root;
}
private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>("geojson", true, (a, c) -> {
String type = (String) a[0];
CoordinateNode coordinates = (CoordinateNode) a[1];
@SuppressWarnings("unchecked")
List geometries = (List) a[2];
Boolean orientation = orientationFromString((String) a[3]);
DistanceUnit.Distance radius = (DistanceUnit.Distance) a[4];
return createGeometry(type, geometries, coordinates, orientation, c.rightOrientation, c.coerce, radius);
});
static {
PARSER.declareString(constructorArg(), FIELD_TYPE);
PARSER.declareField(optionalConstructorArg(), (p, c) -> parseCoordinates(p), FIELD_COORDINATES, ObjectParser.ValueType.VALUE_ARRAY);
PARSER.declareObjectArray(optionalConstructorArg(), PARSER, FIELD_GEOMETRIES);
PARSER.declareString(optionalConstructorArg(), FIELD_ORIENTATION);
PARSER.declareField(
optionalConstructorArg(),
p -> DistanceUnit.Distance.parseDistance(p.text()),
FIELD_RADIUS,
ObjectParser.ValueType.STRING
);
}
private static Geometry createGeometry(
String type,
List geometries,
CoordinateNode coordinates,
Boolean orientation,
boolean defaultOrientation,
boolean coerce,
DistanceUnit.Distance radius
) {
ShapeType shapeType;
if ("bbox".equalsIgnoreCase(type)) {
shapeType = ShapeType.ENVELOPE;
} else {
shapeType = ShapeType.forName(type);
}
if (shapeType == ShapeType.GEOMETRYCOLLECTION) {
if (geometries == null) {
throw new ElasticsearchParseException("geometries not included");
}
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.getX(), point.getY(), point.getZ(), 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("unsupported shape type " + type);
}
}
/**
* Checks that all passed parameters except type are null, generates corresponding error messages if they are not
*/
private static void verifyNulls(String type, List geometries, Boolean orientation, DistanceUnit.Distance radius) {
if (geometries != null) {
throw new ElasticsearchParseException("parameter geometries is not supported for type " + type);
}
if (orientation != null) {
throw new ElasticsearchParseException("parameter orientation is not supported for type " + type);
}
if (radius != null) {
throw new ElasticsearchParseException("parameter radius is not supported for type " + type);
}
}
/**
* Recursive method which parses the arrays of coordinates used to define
* Shapes
*/
private static CoordinateNode parseCoordinates(XContentParser parser) 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));
} else if (token == XContentParser.Token.VALUE_NULL) {
throw new IllegalArgumentException("coordinates cannot contain NULL values)");
}
List nodes = new ArrayList<>();
while (token != XContentParser.Token.END_ARRAY) {
CoordinateNode node = parseCoordinates(parser);
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) 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 = 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(lon, lat, alt);
}
/**
* Returns true for right orientation and false for left
*/
private static Boolean orientationFromString(String orientation) {
if (orientation == null) {
return null;
}
orientation = orientation.toLowerCase(Locale.ROOT);
switch (orientation) {
case "right":
case "counterclockwise":
case "ccw":
return true;
case "left":
case "clockwise":
case "cw":
return false;
default:
throw new IllegalArgumentException("Unknown orientation [" + orientation + "]");
}
}
public static String getGeoJsonName(Geometry geometry) {
return geometry.visit(new GeometryVisitor() {
@Override
public String visit(Circle circle) {
return "Circle";
}
@Override
public String visit(GeometryCollection collection) {
return "GeometryCollection";
}
@Override
public String visit(Line line) {
return "LineString";
}
@Override
public String visit(LinearRing ring) {
throw new UnsupportedOperationException("line ring cannot be serialized using GeoJson");
}
@Override
public String visit(MultiLine multiLine) {
return "MultiLineString";
}
@Override
public String visit(MultiPoint multiPoint) {
return "MultiPoint";
}
@Override
public String visit(MultiPolygon multiPolygon) {
return "MultiPolygon";
}
@Override
public String visit(Point point) {
return "Point";
}
@Override
public String visit(Polygon polygon) {
return "Polygon";
}
@Override
public String visit(Rectangle rectangle) {
return "Envelope";
}
});
}
private static class CoordinateNode implements ToXContentObject {
public final Point coordinate;
public final List children;
/**
* Creates a new leaf CoordinateNode
*
* @param coordinate Coordinate for the Node
*/
CoordinateNode(Point coordinate) {
this.coordinate = coordinate;
this.children = null;
}
/**
* Creates a new parent CoordinateNode
*
* @param children Children of the Node
*/
CoordinateNode(List children) {
this.children = children;
this.coordinate = null;
}
public boolean isEmpty() {
return (coordinate == null && (children == null || children.isEmpty()));
}
protected int numDimensions() {
if (isEmpty()) {
throw new ElasticsearchException("attempting to get number of dimensions on an empty coordinate node");
}
if (coordinate != null) {
return coordinate.hasZ() ? 3 : 2;
}
return children.get(0).numDimensions();
}
public Point asPoint() {
if (children != null) {
throw new ElasticsearchException("expected a single points but got a list");
}
return coordinate;
}
public MultiPoint asMultiPoint() {
if (coordinate != null) {
throw new ElasticsearchException("expected a list of points but got a point");
}
List points = new ArrayList<>();
for (CoordinateNode node : children) {
points.add(node.asPoint());
}
return new MultiPoint(points);
}
private double[][] asLineComponents(boolean orientation, boolean coerce, boolean close) {
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 (close && 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.getY();
lons[i] = point.getX();
if (alts != null) {
alts[i] = point.getZ();
}
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, false);
return new Line(components[1], components[0], components[2]);
}
public LinearRing asLinearRing(boolean orientation, boolean coerce) {
double[][] components = asLineComponents(orientation, coerce, true);
return new LinearRing(components[1], components[0], components[2]);
}
public MultiLine asMultiLineString(boolean coerce) {
if (coordinate != null) {
throw new ElasticsearchException("expected a list of points but got a point");
}
List lines = new ArrayList<>();
for (CoordinateNode node : children) {
lines.add(node.asLineString(coerce));
}
return new MultiLine(lines);
}
public Polygon asPolygon(boolean orientation, boolean coerce) {
if (coordinate != null) {
throw new ElasticsearchException("expected a list of points but got a point");
}
List lines = new ArrayList<>();
for (CoordinateNode node : children) {
lines.add(node.asLinearRing(orientation, coerce));
}
if (lines.size() == 1) {
return new Polygon(lines.get(0));
} else {
LinearRing shell = lines.remove(0);
return new Polygon(shell, lines);
}
}
public MultiPolygon asMultiPolygon(boolean orientation, boolean coerce) {
if (coordinate != null) {
throw new ElasticsearchException("expected a list of points but got a point");
}
List polygons = new ArrayList<>();
for (CoordinateNode node : children) {
polygons.add(node.asPolygon(orientation, coerce));
}
return new MultiPolygon(polygons);
}
public Rectangle asRectangle() {
if (children.size() != 2) {
throw new ElasticsearchParseException(
"invalid number of points [{}] provided for geo_shape [{}] when expecting an array of 2 coordinates",
children.size(),
ShapeType.ENVELOPE
);
}
// verify coordinate bounds, correct if necessary
Point uL = children.get(0).coordinate;
Point lR = children.get(1).coordinate;
return new Rectangle(uL.getX(), lR.getX(), uL.getY(), lR.getY());
}
@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
if (children == null) {
builder.startArray().value(coordinate.getX()).value(coordinate.getY()).endArray();
} else {
builder.startArray();
for (CoordinateNode child : children) {
child.toXContent(builder, params);
}
builder.endArray();
}
return builder;
}
}
}