All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.elasticsearch.common.geo.GeoJson Maven / Gradle / Ivy

There is a newer version: 8.13.2
Show newest version
/*
 * 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.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 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 final boolean rightOrientation;
    private final boolean coerce;
    private final GeometryValidator validator;

    public GeoJson(boolean rightOrientation, boolean coerce, GeometryValidator validator) {
        this.rightOrientation = rightOrientation;
        this.coerce = coerce;
        this.validator = validator;
    }

    public Geometry fromXContent(XContentParser parser)
        throws IOException {
        try (XContentSubParser subParser = new XContentSubParser(parser)) {
            Geometry geometry = PARSER.apply(subParser, this);
            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(ShapeParser.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(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.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(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.getY(), point.getX(), point.getZ());
            }

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

    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("unsuppoted shape type " + type);
        }
    }

    /**
     * Checks that all passed parameters except type are null, generates corresponding error messages if they are not
     */
    private static void verifyNulls(String type, List geometries, Boolean orientation, DistanceUnit.Distance radius) {
        if (geometries != null) {
            throw new ElasticsearchParseException("parameter geometries is not supported for type " + type);
        }
        if (orientation != null) {
            throw new ElasticsearchParseException("parameter orientation is not supported for type " + type);
        }
        if (radius != null) {
            throw new ElasticsearchParseException("parameter radius is not supported for type " + type);
        }
    }

    /**
     * Recursive method which parses the arrays of coordinates used to define
     * Shapes
     */
    private static CoordinateNode parseCoordinates(XContentParser parser) 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;
        }
    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy