diff --git a/src/main/java/org/elasticsearch/common/geo/GeoJSONShapeParser.java b/src/main/java/org/elasticsearch/common/geo/GeoJSONShapeParser.java deleted file mode 100644 index d457c821625..00000000000 --- a/src/main/java/org/elasticsearch/common/geo/GeoJSONShapeParser.java +++ /dev/null @@ -1,230 +0,0 @@ -/* - * Licensed to ElasticSearch and Shay Banon 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 com.spatial4j.core.shape.Shape; -import com.spatial4j.core.shape.impl.RectangleImpl; -import com.spatial4j.core.shape.jts.JtsGeometry; -import com.spatial4j.core.shape.jts.JtsPoint; -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.GeometryFactory; -import com.vividsolutions.jts.geom.LinearRing; -import com.vividsolutions.jts.geom.Polygon; -import org.elasticsearch.ElasticSearchParseException; -import org.elasticsearch.common.xcontent.XContentParser; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; -import java.util.Locale; - -/** - * Parsers which supports reading {@link Shape}s in GeoJSON format from a given - * {@link XContentParser}. - *

- * An example of the format used for polygons: - *

- * { - * "type": "Polygon", - * "coordinates": [ - * [ [100.0, 0.0], [101.0, 0.0], [101.0, 1.0], - * [100.0, 1.0], [100.0, 0.0] ] - * ] - * } - *

- * Note, currently MultiPolygon and GeometryCollections are not supported - */ -public class GeoJSONShapeParser { - - private static final GeometryFactory GEOMETRY_FACTORY = new GeometryFactory(); - - private GeoJSONShapeParser() { - } - - /** - * Parses the current object from the given {@link XContentParser}, creating - * the {@link Shape} representation - * - * @param parser Parser that will be read from - * @return Shape representation of the geojson defined Shape - * @throws IOException Thrown if an error occurs while reading from the XContentParser - */ - public static Shape parse(XContentParser parser) throws IOException { - if (parser.currentToken() != XContentParser.Token.START_OBJECT) { - throw new ElasticSearchParseException("Shape must be an object consisting of type and coordinates"); - } - - String shapeType = null; - CoordinateNode node = null; - - XContentParser.Token token; - while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { - if (token == XContentParser.Token.FIELD_NAME) { - String fieldName = parser.currentName(); - - if ("type".equals(fieldName)) { - parser.nextToken(); - shapeType = parser.text().toLowerCase(Locale.ENGLISH); - if (shapeType == null) { - throw new ElasticSearchParseException("Unknown Shape type [" + parser.text() + "]"); - } - } else if ("coordinates".equals(fieldName)) { - parser.nextToken(); - node = parseCoordinates(parser); - } else { - parser.nextToken(); - parser.skipChildren(); - } - } - } - - if (shapeType == null) { - throw new ElasticSearchParseException("Shape type not included"); - } else if (node == null) { - throw new ElasticSearchParseException("Coordinates not included"); - } - - return buildShape(shapeType, node); - } - - /** - * Recursive method which parses the arrays of coordinates used to define Shapes - * - * @param parser Parser that will be read from - * @return CoordinateNode representing the start of the coordinate tree - * @throws IOException Thrown if an error occurs while reading from the XContentParser - */ - private static CoordinateNode parseCoordinates(XContentParser parser) throws IOException { - XContentParser.Token token = parser.nextToken(); - - // Base case - if (token != XContentParser.Token.START_ARRAY) { - double lon = parser.doubleValue(); - token = parser.nextToken(); - double lat = parser.doubleValue(); - token = parser.nextToken(); - return new CoordinateNode(new Coordinate(lon, lat)); - } - - List nodes = new ArrayList(); - while (token != XContentParser.Token.END_ARRAY) { - nodes.add(parseCoordinates(parser)); - token = parser.nextToken(); - } - - return new CoordinateNode(nodes); - } - - /** - * Builds the actual {@link Shape} with the given shape type from the tree - * of coordinates - * - * @param shapeType Type of Shape to be built - * @param node Root node of the coordinate tree - * @return Shape built from the coordinates - */ - private static Shape buildShape(String shapeType, CoordinateNode node) { - if ("point".equals(shapeType)) { - return new JtsPoint(GEOMETRY_FACTORY.createPoint(node.coordinate), GeoShapeConstants.SPATIAL_CONTEXT); - } else if ("linestring".equals(shapeType)) { - return new JtsGeometry(GEOMETRY_FACTORY.createLineString(toCoordinates(node)), GeoShapeConstants.SPATIAL_CONTEXT, true); - } else if ("polygon".equals(shapeType)) { - return new JtsGeometry(buildPolygon(node), GeoShapeConstants.SPATIAL_CONTEXT, true); - } else if ("multipoint".equals(shapeType)) { - return new JtsGeometry(GEOMETRY_FACTORY.createMultiPoint(toCoordinates(node)), GeoShapeConstants.SPATIAL_CONTEXT, true); - } else if ("envelope".equals(shapeType)) { - Coordinate[] coordinates = toCoordinates(node); - return new RectangleImpl(coordinates[0].x, coordinates[1].x, coordinates[1].y, coordinates[0].y, GeoShapeConstants.SPATIAL_CONTEXT); - } else if ("multipolygon".equals(shapeType)) { - Polygon[] polygons = new Polygon[node.children.size()]; - for (int i = 0; i < node.children.size(); i++) { - polygons[i] = buildPolygon(node.children.get(i)); - } - return new JtsGeometry( - GEOMETRY_FACTORY.createMultiPolygon(polygons), - GeoShapeConstants.SPATIAL_CONTEXT, - true); - } - - throw new UnsupportedOperationException("ShapeType [" + shapeType + "] not supported"); - } - - /** - * Builds a {@link Polygon} from the given CoordinateNode - * - * @param node CoordinateNode that the Polygon will be built from - * @return Polygon consisting of the coordinates in the CoordinateNode - */ - private static Polygon buildPolygon(CoordinateNode node) { - LinearRing shell = GEOMETRY_FACTORY.createLinearRing(toCoordinates(node.children.get(0))); - LinearRing[] holes = null; - if (node.children.size() > 1) { - holes = new LinearRing[node.children.size() - 1]; - for (int i = 0; i < node.children.size() - 1; i++) { - holes[i] = GEOMETRY_FACTORY.createLinearRing(toCoordinates(node.children.get(i + 1))); - } - } - return GEOMETRY_FACTORY.createPolygon(shell, holes); - } - - /** - * Converts the children of the given CoordinateNode into an array of - * {@link Coordinate}. - * - * @param node CoordinateNode whose children will be converted - * @return Coordinate array with the values taken from the children of the Node - */ - private static Coordinate[] toCoordinates(CoordinateNode node) { - Coordinate[] coordinates = new Coordinate[node.children.size()]; - for (int i = 0; i < node.children.size(); i++) { - coordinates[i] = node.children.get(i).coordinate; - } - return coordinates; - } - - /** - * Node used to represent a tree of coordinates. - *

- * Can either be a leaf node consisting of a Coordinate, or a parent with children - */ - private static class CoordinateNode { - - private Coordinate coordinate; - private List children; - - /** - * Creates a new leaf CoordinateNode - * - * @param coordinate Coordinate for the Node - */ - private CoordinateNode(Coordinate coordinate) { - this.coordinate = coordinate; - } - - /** - * Creates a new parent CoordinateNode - * - * @param children Children of the Node - */ - private CoordinateNode(List children) { - this.children = children; - } - } -} diff --git a/src/main/java/org/elasticsearch/common/geo/GeoJSONShapeSerializer.java b/src/main/java/org/elasticsearch/common/geo/GeoJSONShapeSerializer.java deleted file mode 100644 index abfa74d0721..00000000000 --- a/src/main/java/org/elasticsearch/common/geo/GeoJSONShapeSerializer.java +++ /dev/null @@ -1,232 +0,0 @@ -/* - * Licensed to ElasticSearch and Shay Banon 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 com.spatial4j.core.shape.Rectangle; -import com.spatial4j.core.shape.Shape; -import com.spatial4j.core.shape.jts.JtsGeometry; -import com.vividsolutions.jts.geom.*; -import org.elasticsearch.ElasticSearchIllegalArgumentException; -import org.elasticsearch.common.xcontent.XContentBuilder; - -import java.io.IOException; - -/** - * Serializes {@link Shape} instances into GeoJSON format - *

- * Example of the format used for points: - *

- * { "type": "Point", "coordinates": [100.0, 0.0] } - */ -public class GeoJSONShapeSerializer { - - private GeoJSONShapeSerializer() { - } - - /** - * Serializes the given {@link Shape} as GeoJSON format into the given - * {@link XContentBuilder} - * - * @param shape Shape that will be serialized - * @param builder XContentBuilder it will be serialized to - * @throws IOException Thrown if an error occurs while writing to the XContentBuilder - */ - public static void serialize(Shape shape, XContentBuilder builder) throws IOException { - if (shape instanceof JtsGeometry) { - Geometry geometry = ((JtsGeometry) shape).getGeom(); - if (geometry instanceof Point) { - serializePoint((Point) geometry, builder); - } else if (geometry instanceof LineString) { - serializeLineString((LineString) geometry, builder); - } else if (geometry instanceof Polygon) { - serializePolygon((Polygon) geometry, builder); - } else if (geometry instanceof MultiPoint) { - serializeMultiPoint((MultiPoint) geometry, builder); - } else if (geometry instanceof MultiPolygon) { - serializeMulitPolygon((MultiPolygon) geometry, builder); - } else { - throw new ElasticSearchIllegalArgumentException("Geometry type [" + geometry.getGeometryType() + "] not supported"); - } - } else if (shape instanceof com.spatial4j.core.shape.Point) { - serializePoint((com.spatial4j.core.shape.Point) shape, builder); - } else if (shape instanceof Rectangle) { - serializeRectangle((Rectangle) shape, builder); - } else { - throw new ElasticSearchIllegalArgumentException("Shape type [" + shape.getClass().getSimpleName() + "] not supported"); - } - } - - /** - * Serializes the given {@link Rectangle} - * - * @param rectangle Rectangle that will be serialized - * @param builder XContentBuilder it will be serialized to - * @throws IOException Thrown if an error occurs while writing to the XContentBuilder - */ - private static void serializeRectangle(Rectangle rectangle, XContentBuilder builder) throws IOException { - builder.field("type", "Envelope") - .startArray("coordinates") - .startArray().value(rectangle.getMinX()).value(rectangle.getMaxY()).endArray() - .startArray().value(rectangle.getMaxX()).value(rectangle.getMinY()).endArray() - .endArray(); - } - - /** - * Serializes the given {@link Point} - * - * @param point Point that will be serialized - * @param builder XContentBuilder it will be serialized to - * @throws IOException Thrown if an error occurs while writing to the XContentBuilder - */ - private static void serializePoint(Point point, XContentBuilder builder) throws IOException { - builder.field("type", "Point") - .startArray("coordinates") - .value(point.getX()).value(point.getY()) - .endArray(); - } - - /** - * Serializes the given {@link com.spatial4j.core.shape.Point} - * - * @param point Point that will be serialized - * @param builder XContentBuilder it will be serialized to - * @throws IOException Thrown if an error occurs while writing to the XContentBuilder - */ - private static void serializePoint(com.spatial4j.core.shape.Point point, XContentBuilder builder) throws IOException { - builder.field("type", "Point") - .startArray("coordinates") - .value(point.getX()).value(point.getY()) - .endArray(); - } - - /** - * Serializes the given {@link LineString} - * - * @param lineString LineString that will be serialized - * @param builder XContentBuilder it will be serialized to - * @throws IOException Thrown if an error occurs while writing to the XContentBuilder - */ - private static void serializeLineString(LineString lineString, XContentBuilder builder) throws IOException { - builder.field("type", "LineString") - .startArray("coordinates"); - - for (Coordinate coordinate : lineString.getCoordinates()) { - serializeCoordinate(coordinate, builder); - } - - builder.endArray(); - } - - /** - * Serializes the given {@link Polygon} - * - * @param polygon Polygon that will be serialized - * @param builder XContentBuilder it will be serialized to - * @throws IOException Thrown if an error occurs while writing to the XContentBuilder - */ - private static void serializePolygon(Polygon polygon, XContentBuilder builder) throws IOException { - builder.field("type", "Polygon") - .startArray("coordinates"); - - serializePolygonCoordinates(polygon, builder); - - builder.endArray(); - } - - /** - * Serializes the actual coordinates of the given {@link Polygon} - * - * @param polygon Polygon whose coordinates will be serialized - * @param builder XContentBuilder it will be serialized to - * @throws IOException Thrown if an error occurs while writing to the XContentBuilder - */ - private static void serializePolygonCoordinates(Polygon polygon, XContentBuilder builder) throws IOException { - builder.startArray(); // start outer ring - - for (Coordinate coordinate : polygon.getExteriorRing().getCoordinates()) { - serializeCoordinate(coordinate, builder); - } - - builder.endArray(); // end outer ring - - for (int i = 0; i < polygon.getNumInteriorRing(); i++) { - LineString interiorRing = polygon.getInteriorRingN(i); - - builder.startArray(); - - for (Coordinate coordinate : interiorRing.getCoordinates()) { - serializeCoordinate(coordinate, builder); - } - - builder.endArray(); - } - } - - /** - * Serializes the given {@link MultiPolygon} - * - * @param multiPolygon MultiPolygon that will be serialized - * @param builder XContentBuilder it will be serialized to - * @throws IOException Thrown if an error occurs while writing to the XContentBuilder - */ - private static void serializeMulitPolygon(MultiPolygon multiPolygon, XContentBuilder builder) throws IOException { - builder.field("type", "MultiPolygon") - .startArray("coordinates"); - - for (int i = 0; i < multiPolygon.getNumGeometries(); i++) { - builder.startArray(); - - serializePolygonCoordinates((Polygon) multiPolygon.getGeometryN(i), builder); - - builder.endArray(); - } - - builder.endArray(); - } - - /** - * Serializes the given {@link MultiPoint} - * - * @param multiPoint MultiPoint that will be serialized - * @param builder XContentBuilder it will be serialized to - * @throws IOException Thrown if an error occurs while writing to the XContentBuilder - */ - private static void serializeMultiPoint(MultiPoint multiPoint, XContentBuilder builder) throws IOException { - builder.field("type", "MultiPoint") - .startArray("coordinates"); - - for (Coordinate coordinate : multiPoint.getCoordinates()) { - serializeCoordinate(coordinate, builder); - } - - builder.endArray(); - } - - /** - * Serializes the given {@link Coordinate} - * - * @param coordinate Coordinate that will be serialized - * @param builder XContentBuilder it will be serialized to - * @throws IOException Thrown if an error occurs while writing to the XContentBuilder - */ - private static void serializeCoordinate(Coordinate coordinate, XContentBuilder builder) throws IOException { - builder.startArray().value(coordinate.x).value(coordinate.y).endArray(); - } -} diff --git a/src/main/java/org/elasticsearch/common/geo/ShapeBuilder.java b/src/main/java/org/elasticsearch/common/geo/ShapeBuilder.java deleted file mode 100644 index ade3fbd613f..00000000000 --- a/src/main/java/org/elasticsearch/common/geo/ShapeBuilder.java +++ /dev/null @@ -1,424 +0,0 @@ -/* - * Licensed to ElasticSearch and Shay Banon 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.ElasticSearchIllegalArgumentException; -import org.elasticsearch.common.xcontent.XContentBuilder; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; - -import com.spatial4j.core.shape.Point; -import com.spatial4j.core.shape.Rectangle; -import com.spatial4j.core.shape.Shape; -import com.spatial4j.core.shape.impl.PointImpl; -import com.spatial4j.core.shape.impl.RectangleImpl; -import com.spatial4j.core.shape.jts.JtsGeometry; -import com.spatial4j.core.shape.jts.JtsPoint; -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.Geometry; -import com.vividsolutions.jts.geom.GeometryFactory; -import com.vividsolutions.jts.geom.LinearRing; -import com.vividsolutions.jts.geom.MultiPolygon; -import com.vividsolutions.jts.geom.Polygon; - -/** - * Utility class for building {@link Shape} instances like {@link Point}, - * {@link Rectangle} and Polygons. - */ -public class ShapeBuilder { - - private static final GeometryFactory GEOMETRY_FACTORY = new GeometryFactory(); - - private ShapeBuilder() { - } - - /** - * Creates a new {@link Point} - * - * @param lon Longitude of point - * @param lat Latitude of point - * @return Point with the latitude and longitude - */ - public static Point newPoint(double lon, double lat) { - return new PointImpl(lon, lat, GeoShapeConstants.SPATIAL_CONTEXT); - } - - /** - * Creates a new {@link RectangleBuilder} to build a {@link Rectangle} - * - * @return RectangleBuilder instance - */ - public static RectangleBuilder newRectangle() { - return new RectangleBuilder(); - } - - /** - * Creates a new {@link PolygonBuilder} to build a Polygon - * - * @return PolygonBuilder instance - */ - public static PolygonBuilder newPolygon() { - return new PolygonBuilder(); - } - - /** - * Creates a new {@link MultiPolygonBuilder} to build a MultiPolygon - * - * @return MultiPolygonBuilder instance - */ - public static MultiPolygonBuilder newMultiPolygon() { - return new MultiPolygonBuilder(); - } - - /** - * Converts the given Shape into the JTS {@link Geometry} representation. - * If the Shape already uses a Geometry, that is returned. - * - * @param shape Shape to convert - * @return Geometry representation of the Shape - */ - public static Geometry toJTSGeometry(Shape shape) { - if (shape instanceof JtsGeometry) { - return ((JtsGeometry) shape).getGeom(); - } else if (shape instanceof JtsPoint) { - return ((JtsPoint) shape).getGeom(); - } else if (shape instanceof Rectangle) { - Rectangle rectangle = (Rectangle) shape; - - if (rectangle.getCrossesDateLine()) { - throw new IllegalArgumentException("Cannot convert Rectangles that cross the dateline into JTS Geometrys"); - } - - return newPolygon().point(rectangle.getMinX(), rectangle.getMaxY()) - .point(rectangle.getMaxX(), rectangle.getMaxY()) - .point(rectangle.getMaxX(), rectangle.getMinY()) - .point(rectangle.getMinX(), rectangle.getMinY()) - .point(rectangle.getMinX(), rectangle.getMaxY()).toPolygon(); - } else if (shape instanceof Point) { - Point point = (Point) shape; - return GEOMETRY_FACTORY.createPoint(new Coordinate(point.getX(), point.getY())); - } - - throw new IllegalArgumentException("Shape type [" + shape.getClass().getSimpleName() + "] not supported"); - } - - /** - * Builder for creating a {@link Rectangle} instance - */ - public static class RectangleBuilder { - - private Point topLeft; - private Point bottomRight; - - /** - * Sets the top left point of the Rectangle - * - * @param lon Longitude of the top left point - * @param lat Latitude of the top left point - * @return this - */ - public RectangleBuilder topLeft(double lon, double lat) { - this.topLeft = new PointImpl(lon, lat, GeoShapeConstants.SPATIAL_CONTEXT); - return this; - } - - /** - * Sets the bottom right point of the Rectangle - * - * @param lon Longitude of the bottom right point - * @param lat Latitude of the bottom right point - * @return this - */ - public RectangleBuilder bottomRight(double lon, double lat) { - this.bottomRight = new PointImpl(lon, lat, GeoShapeConstants.SPATIAL_CONTEXT); - return this; - } - - /** - * Builds the {@link Rectangle} instance - * - * @return Built Rectangle - */ - public Rectangle build() { - return new RectangleImpl(topLeft.getX(), bottomRight.getX(), bottomRight.getY(), topLeft.getY(), GeoShapeConstants.SPATIAL_CONTEXT); - } - } - - /** - * Builder for creating a {@link Shape} instance of a MultiPolygon - */ - public static class MultiPolygonBuilder { - private final ArrayList> polygons = new ArrayList>(); - - /** - * Add a new polygon to the multipolygon - * - * @return builder for the new polygon - */ - public EmbededPolygonBuilder polygon() { - EmbededPolygonBuilder builder = new EmbededPolygonBuilder(this); - polygons.add(builder); - return builder; - } - - public Shape build() { - return new JtsGeometry(toMultiPolygon(), GeoShapeConstants.SPATIAL_CONTEXT, true); - } - - public MultiPolygon toMultiPolygon() { - Polygon[] polygons = new Polygon[this.polygons.size()]; - for (int i = 0; i polygon : polygons) { - polygon.emdedXContent(null, xcontent); - } - xcontent.endArray(); - } - - } - - /** - * Builder for creating a {@link Shape} instance of a single Polygon - */ - public static class PolygonBuilder extends EmbededPolygonBuilder { - - private PolygonBuilder() { - super(null); - } - - @Override - public PolygonBuilder close() { - super.close(); - return this; - } - } - - /** - * Builder for creating a {@link Shape} instance of a Polygon - */ - public static class EmbededPolygonBuilder { - - private final E parent; - private final LinearRingBuilder> ring = new LinearRingBuilder>(this); - private final ArrayList>> holes = new ArrayList>>(); - - private EmbededPolygonBuilder(E parent) { - super(); - this.parent = parent; - } - - /** - * Adds a point to the Polygon - * - * @param lon Longitude of the point - * @param lat Latitude of the point - * @return this - */ - public EmbededPolygonBuilder point(double lon, double lat) { - ring.point(lon, lat); - return this; - } - - /** - * Start creating a new hole within the polygon - * @return a builder for holes - */ - public LinearRingBuilder> hole() { - LinearRingBuilder> builder = new LinearRingBuilder>(this); - this.holes.add(builder); - return builder; - } - - /** - * Builds a {@link Shape} instance representing the {@link Polygon} - * - * @return Built LinearRing - */ - public Shape build() { - return new JtsGeometry(toPolygon(), GeoShapeConstants.SPATIAL_CONTEXT, true); - } - - /** - * Creates the raw {@link Polygon} - * - * @return Built polygon - */ - public Polygon toPolygon() { - this.ring.close(); - LinearRing ring = this.ring.toLinearRing(); - LinearRing[] rings = new LinearRing[holes.size()]; - for (int i = 0; i < rings.length; i++) { - rings[i] = this.holes.get(i).toLinearRing(); - } - return GEOMETRY_FACTORY.createPolygon(ring, rings); - } - - /** - * Close the linestring by copying the first point if necessary - * @return parent object - */ - public E close() { - this.ring.close(); - return parent; - } - - public XContentBuilder toXContent(String name, XContentBuilder xcontent) throws IOException { - if(name != null) { - xcontent.startObject(name); - } else { - xcontent.startObject(); - } - xcontent.field("type", "polygon"); - emdedXContent("coordinates", xcontent); - xcontent.endObject(); - return xcontent; - } - - protected void emdedXContent(String name, XContentBuilder xcontent) throws IOException { - if(name != null) { - xcontent.startArray(name); - } else { - xcontent.startArray(); - } - ring.emdedXContent(null, xcontent); - for (LinearRingBuilder ring : holes) { - ring.emdedXContent(null, xcontent); - } - xcontent.endArray(); - } - - } - - /** - * Builder for creating a {@link Shape} instance of a Polygon - */ - public static class LinearRingBuilder { - - private final E parent; - private final List points = new ArrayList(); - - private LinearRingBuilder(E parent) { - super(); - this.parent = parent; - } - - /** - * Adds a point to the Ring - * - * @param lon Longitude of the point - * @param lat Latitude of the point - * @return this - */ - public LinearRingBuilder point(double lon, double lat) { - points.add(new PointImpl(lon, lat, GeoShapeConstants.SPATIAL_CONTEXT)); - return this; - } - - /** - * Builds a {@link Shape} instance representing the ring - * - * @return Built LinearRing - */ - protected Shape build() { - return new JtsGeometry(toLinearRing(), GeoShapeConstants.SPATIAL_CONTEXT, true); - } - - /** - * Creates the raw {@link Polygon} - * - * @return Built LinearRing - */ - protected LinearRing toLinearRing() { - this.close(); - Coordinate[] coordinates = new Coordinate[points.size()]; - for (int i = 0; i < coordinates.length; i++) { - coordinates[i] = new Coordinate(points.get(i).getX(), points.get(i).getY()); - } - - return GEOMETRY_FACTORY.createLinearRing(coordinates); - } - - /** - * Close the linestring by copying the first point if necessary - * @return parent object - */ - public E close() { - Point first = points.get(0); - Point last = points.get(points.size()-1); - if(first.getX() != last.getX() || first.getY() != last.getY()) { - points.add(first); - } - - if(points.size()<4) { - throw new ElasticSearchIllegalArgumentException("A linear ring is defined by a least four points"); - } - - return parent; - } - - public XContentBuilder toXContent(String name, XContentBuilder xcontent) throws IOException { - if(name != null) { - xcontent.startObject(name); - } else { - xcontent.startObject(); - } - xcontent.field("type", "linestring"); - emdedXContent("coordinates", xcontent); - xcontent.endObject(); - return xcontent; - } - - protected void emdedXContent(String name, XContentBuilder xcontent) throws IOException { - if(name != null) { - xcontent.startArray(name); - } else { - xcontent.startArray(); - } - for(Point point : points) { - xcontent.startArray().value(point.getY()).value(point.getX()).endArray(); - } - xcontent.endArray(); - } - } -} diff --git a/src/main/java/org/elasticsearch/common/geo/builders/BaseLineStringBuilder.java b/src/main/java/org/elasticsearch/common/geo/builders/BaseLineStringBuilder.java new file mode 100644 index 00000000000..ff973356324 --- /dev/null +++ b/src/main/java/org/elasticsearch/common/geo/builders/BaseLineStringBuilder.java @@ -0,0 +1,128 @@ +/* + * Licensed to ElasticSearch and Shay Banon 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.builders; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; + +import org.elasticsearch.common.xcontent.XContentBuilder; + +import com.spatial4j.core.shape.Shape; +import com.spatial4j.core.shape.jts.JtsGeometry; +import com.vividsolutions.jts.geom.Coordinate; +import com.vividsolutions.jts.geom.Geometry; +import com.vividsolutions.jts.geom.GeometryFactory; +import com.vividsolutions.jts.geom.LineString; + +public abstract class BaseLineStringBuilder> extends PointCollection { + + protected BaseLineStringBuilder() { + this(new ArrayList()); + } + + protected BaseLineStringBuilder(ArrayList points) { + super(points); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + return coordinatesToXcontent(builder, false); + } + + @Override + public Shape build() { + Coordinate[] coordinates = points.toArray(new Coordinate[points.size()]); + Geometry geometry; + if(wrapdateline) { + ArrayList strings = decompose(FACTORY, coordinates, new ArrayList()); + + if(strings.size() == 1) { + geometry = strings.get(0); + } else { + LineString[] linestrings = strings.toArray(new LineString[strings.size()]); + geometry = FACTORY.createMultiLineString(linestrings); + } + + } else { + geometry = FACTORY.createLineString(coordinates); + } + return new JtsGeometry(geometry, SPATIAL_CONTEXT, !wrapdateline); + } + + protected static ArrayList decompose(GeometryFactory factory, Coordinate[] coordinates, ArrayList strings) { + for(Coordinate[] part : decompose(+DATELINE, coordinates)) { + for(Coordinate[] line : decompose(-DATELINE, part)) { + strings.add(factory.createLineString(line)); + } + } + return strings; + } + + /** + * Decompose a linestring given as array of coordinates at a vertical line. + * + * @param dateline x-axis intercept of the vertical line + * @param coordinates coordinates forming the linestring + * @return array of linestrings given as coordinate arrays + */ + protected static Coordinate[][] decompose(double dateline, Coordinate[] coordinates) { + int offset = 0; + ArrayList parts = new ArrayList(); + + double shift = coordinates[0].x > DATELINE ? DATELINE : (coordinates[0].x < -DATELINE ? -DATELINE : 0); + + for (int i = 1; i < coordinates.length; i++) { + double t = intersection(coordinates[i-1], coordinates[i], dateline); + if(!Double.isNaN(t)) { + Coordinate[] part; + if(t<1) { + part = Arrays.copyOfRange(coordinates, offset, i+1); + part[part.length-1] = Edge.position(coordinates[i-1], coordinates[i], t); + coordinates[offset+i-1] = Edge.position(coordinates[i-1], coordinates[i], t); + shift(shift, part); + offset = i-1; + shift = coordinates[i].x > DATELINE ? DATELINE : (coordinates[i].x < -DATELINE ? -DATELINE : 0); + } else { + part = shift(shift, Arrays.copyOfRange(coordinates, offset, i+1)); + offset = i; + } + parts.add(part); + } + } + + if(offset == 0) { + parts.add(shift(shift, coordinates)); + } else if(offset < coordinates.length-1) { + Coordinate[] part = Arrays.copyOfRange(coordinates, offset, coordinates.length); + parts.add(shift(shift, part)); + } + return parts.toArray(new Coordinate[parts.size()][]); + } + + private static Coordinate[] shift(double shift, Coordinate...coordinates) { + if(shift != 0) { + for (int j = 0; j < coordinates.length; j++) { + coordinates[j] = new Coordinate(coordinates[j].x - 2 * shift, coordinates[j].y); + } + } + return coordinates; + } +} diff --git a/src/main/java/org/elasticsearch/common/geo/builders/BasePolygonBuilder.java b/src/main/java/org/elasticsearch/common/geo/builders/BasePolygonBuilder.java new file mode 100644 index 00000000000..97302888abe --- /dev/null +++ b/src/main/java/org/elasticsearch/common/geo/builders/BasePolygonBuilder.java @@ -0,0 +1,475 @@ +/* + * Licensed to ElasticSearch and Shay Banon 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.builders; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; + +import org.elasticsearch.common.xcontent.XContentBuilder; + +import com.spatial4j.core.shape.Shape; +import com.spatial4j.core.shape.jts.JtsGeometry; +import com.vividsolutions.jts.geom.Coordinate; +import com.vividsolutions.jts.geom.Geometry; +import com.vividsolutions.jts.geom.GeometryFactory; +import com.vividsolutions.jts.geom.LinearRing; +import com.vividsolutions.jts.geom.MultiPolygon; +import com.vividsolutions.jts.geom.Polygon; + +/** + * The {@link BasePolygonBuilder} implements the groundwork to create polygons. This contains + * Methods to wrap polygons at the dateline and building shapes from the data held by the + * builder. + * Since this Builder can be embedded to other builders (i.e. {@link MultiPolygonBuilder}) + * the class of the embedding builder is given by the generic argument E + + * @param type of the embedding class + */ +public abstract class BasePolygonBuilder> extends ShapeBuilder { + + public static final GeoShapeType TYPE = GeoShapeType.POLYGON; + + // Linear ring defining the shell of the polygon + protected Ring shell; + + // List of linear rings defining the holes of the polygon + protected final ArrayList> holes = new ArrayList>(); + + @SuppressWarnings("unchecked") + private E thisRef() { + return (E)this; + } + + public E point(double longitude, double latitude) { + shell.point(longitude, latitude); + return thisRef(); + } + + /** + * Add a point to the shell of the polygon + * @param coordinate coordinate of the new point + * @return this + */ + public E point(Coordinate coordinate) { + shell.point(coordinate); + return thisRef(); + } + + /** + * Add a array of points to the shell of the polygon + * @param coordinates coordinates of the new points to add + * @return this + */ + public E points(Coordinate...coordinates) { + shell.points(coordinates); + return thisRef(); + } + + /** + * Add a new hole to the polygon + * @param hole linear ring defining the hole + * @return this + */ + public E hole(BaseLineStringBuilder hole) { + holes.add(hole); + return thisRef(); + } + + /** + * build new hole to the polygon + * @param hole linear ring defining the hole + * @return this + */ + public Ring hole() { + Ring hole = new Ring(thisRef()); + this.holes.add(hole); + return hole; + } + + /** + * Close the shell of the polygon + * @return parent + */ + public ShapeBuilder close() { + return shell.close(); + } + + /** + * The coordinates setup by the builder will be assembled to a polygon. The result will consist of + * a set of polygons. Each of these components holds a list of linestrings defining the polygon: the + * first set of coordinates will be used as the shell of the polygon. The others are defined to holes + * within the polygon. + * This Method also wraps the polygons at the dateline. In order to this fact the result may + * contains more polygons and less holes than defined in the builder it self. + * + * @return coordinates of the polygon + */ + public Coordinate[][][] coordinates() { + int numEdges = shell.points.size()-1; // Last point is repeated + for (int i = 0; i < holes.size(); i++) { + numEdges += holes.get(i).points.size()-1; + } + + Edge[] edges = new Edge[numEdges]; + Edge[] holeComponents = new Edge[holes.size()]; + + int offset = createEdges(0, true, shell, edges, 0); + for (int i = 0; i < holes.size(); i++) { + int length = createEdges(i+1, false, this.holes.get(i), edges, offset); + holeComponents[i] = edges[offset]; + offset += length; + } + + int numHoles = holeComponents.length; + + numHoles = merge(edges, 0, intersections(+DATELINE, edges), holeComponents, numHoles); + numHoles = merge(edges, 0, intersections(-DATELINE, edges), holeComponents, numHoles); + + return compose(edges, holeComponents, numHoles); + } + + @Override + public Shape build() { + Geometry geometry = buildGeometry(FACTORY, wrapdateline); + return new JtsGeometry(geometry, SPATIAL_CONTEXT, !wrapdateline); + } + + protected XContentBuilder coordinatesArray(XContentBuilder builder, Params params) throws IOException { + shell.coordinatesToXcontent(builder, true); + for(BaseLineStringBuilder hole : holes) { + hole.coordinatesToXcontent(builder, true); + } + return builder; + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + builder.field(FIELD_TYPE, TYPE.shapename); + builder.startArray(FIELD_COORDINATES); + coordinatesArray(builder, params); + builder.endArray(); + builder.endObject(); + return builder; + } + + public Geometry buildGeometry(GeometryFactory factory, boolean fixDateline) { + if(fixDateline) { + Coordinate[][][] polygons = coordinates(); + return polygons.length == 1 + ? polygon(factory, polygons[0]) + : multipolygon(factory, polygons); + } else { + return toPolygon(factory); + } + } + + public Polygon toPolygon() { + return toPolygon(FACTORY); + } + + protected Polygon toPolygon(GeometryFactory factory) { + final LinearRing shell = linearRing(factory, this.shell.points); + final LinearRing[] holes = new LinearRing[this.holes.size()]; + Iterator> iterator = this.holes.iterator(); + for (int i = 0; iterator.hasNext(); i++) { + holes[i] = linearRing(factory, iterator.next().points); + } + return factory.createPolygon(shell, holes); + } + + protected static LinearRing linearRing(GeometryFactory factory, ArrayList coordinates) { + return factory.createLinearRing(coordinates.toArray(new Coordinate[coordinates.size()])); + } + + @Override + public GeoShapeType type() { + return TYPE; + } + + protected static Polygon polygon(GeometryFactory factory, Coordinate[][] polygon) { + LinearRing shell = factory.createLinearRing(polygon[0]); + LinearRing[] holes; + + if(polygon.length > 1) { + holes = new LinearRing[polygon.length-1]; + for (int i = 0; i < holes.length; i++) { + holes[i] = factory.createLinearRing(polygon[i+1]); + } + } else { + holes = null; + } + return factory.createPolygon(shell, holes); + } + + /** + * Create a Multipolygon from a set of coordinates. Each primary array contains a polygon which + * in turn contains an array of linestrings. These line Strings are represented as an array of + * coordinates. The first linestring will be the shell of the polygon the others define holes + * within the polygon. + * + * @param factory {@link GeometryFactory} to use + * @param polygons definition of polygons + * @return a new Multipolygon + */ + protected static MultiPolygon multipolygon(GeometryFactory factory, Coordinate[][][] polygons) { + Polygon[] polygonSet = new Polygon[polygons.length]; + for (int i = 0; i < polygonSet.length; i++) { + polygonSet[i] = polygon(factory, polygons[i]); + } + return factory.createMultiPolygon(polygonSet); + } + + /** + * This method sets the component id of all edges in a ring to a given id and shifts the + * coordinates of this component according to the dateline + * + * @param edge An arbitrary edge of the component + * @param id id to apply to the component + * @param edges a list of edges to which all edges of the component will be added (could be null) + * @return number of edges that belong to this component + */ + private static int component(final Edge edge, final int id, final ArrayList edges) { + // find a coordinate that is not part of the dateline + Edge any = edge; + while(any.coordinate.x == +DATELINE || any.coordinate.x == -DATELINE) { + if((any = any.next) == edge) { + break; + } + } + + double shift = any.coordinate.x > DATELINE ? DATELINE : (any.coordinate.x < -DATELINE ? -DATELINE : 0); + if (debugEnabled()) { + LOGGER.debug("shift: {[]}", shift); + } + + // run along the border of the component, collect the + // edges, shift them according to the dateline and + // update the component id + int length = 0; + Edge current = edge; + do { + + current.coordinate = shift(current.coordinate, shift); + current.component = id; + if(edges != null) { + edges.add(current); + } + + length++; + } while((current = current.next) != edge); + + return length; + } + + /** + * Compute all coordinates of a component + * @param component an arbitrary edge of the component + * @param coordinates Array of coordinates to write the result to + * @return the coordinates parameter + */ + private static Coordinate[] coordinates(Edge component, Coordinate[] coordinates) { + for (int i = 0; i < coordinates.length; i++) { + coordinates[i] = (component = component.next).coordinate; + } + return coordinates; + } + + private static Coordinate[][][] buildCoordinates(ArrayList> components) { + Coordinate[][][] result = new Coordinate[components.size()][][]; + for (int i = 0; i < result.length; i++) { + ArrayList component = components.get(i); + result[i] = component.toArray(new Coordinate[component.size()][]); + } + + if(debugEnabled()) { + for (int i = 0; i < result.length; i++) { + LOGGER.debug("Component {[]}:", i); + for (int j = 0; j < result[i].length; j++) { + LOGGER.debug("\t" + Arrays.toString(result[i][j])); + } + } + } + + return result; + } + + private static final Coordinate[][] EMPTY = new Coordinate[0][]; + + private static Coordinate[][] holes(Edge[] holes, int numHoles) { + if (numHoles == 0) { + return EMPTY; + } + final Coordinate[][] points = new Coordinate[numHoles][]; + + for (int i = 0; i < numHoles; i++) { + int length = component(holes[i], -(i+1), null); // mark as visited by inverting the sign + points[i] = coordinates(holes[i], new Coordinate[length+1]); + } + + return points; + } + + private static Edge[] edges(Edge[] edges, int numHoles, ArrayList> components) { + ArrayList mainEdges = new ArrayList(edges.length); + + for (int i = 0; i < edges.length; i++) { + if (edges[i].component >= 0) { + int length = component(edges[i], -(components.size()+numHoles+1), mainEdges); + ArrayList component = new ArrayList(); + component.add(coordinates(edges[i], new Coordinate[length+1])); + components.add(component); + } + } + + return mainEdges.toArray(new Edge[mainEdges.size()]); + } + + private static Coordinate[][][] compose(Edge[] edges, Edge[] holes, int numHoles) { + final ArrayList> components = new ArrayList>(); + assign(holes, holes(holes, numHoles), numHoles, edges(edges, numHoles, components), components); + return buildCoordinates(components); + } + + private static void assign(Edge[] holes, Coordinate[][] points, int numHoles, Edge[] edges, ArrayList> components) { + // Assign Hole to related components + // To find the new component the hole belongs to all intersections of the + // polygon edges with a vertical line are calculated. This vertical line + // is an arbitrary point of the hole. The polygon edge next to this point + // is part of the polygon the hole belongs to. + if (debugEnabled()) { + LOGGER.debug("Holes: " + Arrays.toString(holes)); + } + for (int i = 0; i < numHoles; i++) { + final Edge current = holes[i]; + final int intersections = intersections(current.coordinate.x, edges); + final int pos = Arrays.binarySearch(edges, 0, intersections, current, INTERSECTION_ORDER); + assert pos < 0 : "illegal state: two edges cross the datum at the same position"; + final int index = -(pos+2); + final int component = -edges[index].component - numHoles - 1; + + if(debugEnabled()) { + LOGGER.debug("\tposition ("+index+") of edge "+current+": " + edges[index]); + LOGGER.debug("\tComponent: " + component); + LOGGER.debug("\tHole intersections ("+current.coordinate.x+"): " + Arrays.toString(edges)); + } + + components.get(component).add(points[i]); + } + } + + private static int merge(Edge[] intersections, int offset, int length, Edge[] holes, int numHoles) { + // Intersections appear pairwise. On the first edge the inner of + // of the polygon is entered. On the second edge the outer face + // is entered. Other kinds of intersections are discard by the + // intersection function + + for (int i = 0; i < length; i += 2) { + Edge e1 = intersections[offset + i + 0]; + Edge e2 = intersections[offset + i + 1]; + + // If two segments are connected maybe a hole must be deleted + // Since Edges of components appear pairwise we need to check + // the second edge only (the first edge is either polygon or + // already handled) + if (e2.component > 0) { + //TODO: Check if we could save the set null step + numHoles--; + holes[e2.component-1] = holes[numHoles]; + holes[numHoles] = null; + } + connect(e1, e2); + } + return numHoles; + } + + private static void connect(Edge in, Edge out) { + assert in != null && out != null; + assert in != out; + // Connecting two Edges by inserting the point at + // dateline intersection and connect these by adding + // two edges between this points. One per direction + if(in.intersect != in.next.coordinate) { + // NOTE: the order of the object creation is crucial here! Don't change it! + // first edge has no point on dateline + Edge e1 = new Edge(in.intersect, in.next); + + if(out.intersect != out.next.coordinate) { + // second edge has no point on dateline + Edge e2 = new Edge(out.intersect, out.next); + in.next = new Edge(in.intersect, e2, in.intersect); + } else { + // second edge intersects with dateline + in.next = new Edge(in.intersect, out.next, in.intersect); + } + out.next = new Edge(out.intersect, e1, out.intersect); + } else { + // first edge intersects with dateline + Edge e2 = new Edge(out.intersect, in.next, out.intersect); + + if(out.intersect != out.next.coordinate) { + // second edge has no point on dateline + Edge e1 = new Edge(out.intersect, out.next); + in.next = new Edge(in.intersect, e1, in.intersect); + + } else { + // second edge intersects with dateline + in.next = new Edge(in.intersect, out.next, in.intersect); + } + out.next = e2; + } + } + + private static int createEdges(int component, boolean direction, BaseLineStringBuilder line, Edge[] edges, int offset) { + Coordinate[] points = line.coordinates(false); // last point is repeated + Edge.ring(component, direction, points, 0, edges, offset, points.length-1); + return points.length-1; + } + + public static class Ring

extends BaseLineStringBuilder> { + + private final P parent; + + protected Ring(P parent) { + this(parent, new ArrayList()); + } + + protected Ring(P parent, ArrayList points) { + super(points); + this.parent = parent; + } + + public P close() { + Coordinate start = points.get(0); + Coordinate end = points.get(points.size()-1); + if(start.x != end.x || start.y != end.y) { + points.add(start); + } + return parent; + } + + @Override + public GeoShapeType type() { + return null; + } + } +} diff --git a/src/main/java/org/elasticsearch/common/geo/builders/CircleBuilder.java b/src/main/java/org/elasticsearch/common/geo/builders/CircleBuilder.java new file mode 100644 index 00000000000..ffcb6741c97 --- /dev/null +++ b/src/main/java/org/elasticsearch/common/geo/builders/CircleBuilder.java @@ -0,0 +1,120 @@ +/* + * Licensed to ElasticSearch and Shay Banon 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.builders; + +import java.io.IOException; + +import org.elasticsearch.common.unit.DistanceUnit; +import org.elasticsearch.common.unit.DistanceUnit.Distance; +import org.elasticsearch.common.xcontent.XContentBuilder; + +import com.spatial4j.core.shape.Circle; +import com.vividsolutions.jts.geom.Coordinate; + +public class CircleBuilder extends ShapeBuilder { + + public static final String FIELD_RADIUS = "radius"; + public static final GeoShapeType TYPE = GeoShapeType.CIRCLE; + + private DistanceUnit unit; + private double radius; + private Coordinate center; + + /** + * Set the center of the circle + * + * @param center coordinate of the circles center + * @return this + */ + public CircleBuilder center(Coordinate center) { + this.center = center; + return this; + } + + /** + * set the center of the circle + * @param lon longitude of the center + * @param lat latitude of the center + * @return this + */ + public CircleBuilder center(double lon, double lat) { + return center(new Coordinate(lon, lat)); + } + + /** + * Set the radius of the circle. The String value will be parsed by {@link DistanceUnit} + * @param radius Value and unit of the circle combined in a string + * @return this + */ + public CircleBuilder radius(String radius) { + return radius(DistanceUnit.Distance.parseDistance(radius, DistanceUnit.METERS)); + } + + /** + * Set the radius of the circle + * @param radius radius of the circle (see {@link DistanceUnit.Distance}) + * @return this + */ + public CircleBuilder radius(Distance radius) { + return radius(radius.value, radius.unit); + } + + /** + * Set the radius of the circle + * @param radius value of the circles radius + * @param unit unit name of the radius value (see {@link DistanceUnit}) + * @return this + */ + public CircleBuilder radius(double radius, String unit) { + return radius(radius, DistanceUnit.fromString(unit)); + } + + /** + * Set the radius of the circle + * @param radius value of the circles radius + * @param unit unit of the radius value (see {@link DistanceUnit}) + * @return this + */ + public CircleBuilder radius(double radius, DistanceUnit unit) { + this.unit = unit; + this.radius = radius; + return this; + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + builder.field(FIELD_TYPE, TYPE.shapename); + builder.field(FIELD_RADIUS, unit.toString(radius)); + builder.field(FIELD_COORDINATES); + toXContent(builder, center); + return builder.endObject(); + } + + @Override + public Circle build() { + return SPATIAL_CONTEXT.makeCircle(center.x, center.y, 180 * radius / unit.getEarthCircumference()); + } + + @Override + public GeoShapeType type() { + return TYPE; + } +} diff --git a/src/main/java/org/elasticsearch/common/geo/builders/EnvelopeBuilder.java b/src/main/java/org/elasticsearch/common/geo/builders/EnvelopeBuilder.java new file mode 100644 index 00000000000..3bff5dd537f --- /dev/null +++ b/src/main/java/org/elasticsearch/common/geo/builders/EnvelopeBuilder.java @@ -0,0 +1,76 @@ +/* + * Licensed to ElasticSearch and Shay Banon 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.builders; + +import java.io.IOException; + +import org.elasticsearch.common.xcontent.XContentBuilder; + +import com.spatial4j.core.shape.Rectangle; +import com.vividsolutions.jts.geom.Coordinate; + +public class EnvelopeBuilder extends ShapeBuilder { + + public static final GeoShapeType TYPE = GeoShapeType.ENVELOPE; + + protected Coordinate northEast; + protected Coordinate southWest; + + public EnvelopeBuilder topLeft(Coordinate northEast) { + this.northEast = northEast; + return this; + } + + public EnvelopeBuilder topLeft(double longitude, double latitude) { + return topLeft(coordinate(longitude, latitude)); + } + + public EnvelopeBuilder bottomRight(Coordinate southWest) { + this.southWest = southWest; + return this; + } + + public EnvelopeBuilder bottomRight(double longitude, double latitude) { + return bottomRight(coordinate(longitude, latitude)); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + builder.field(FIELD_TYPE, TYPE.shapename); + builder.startArray(FIELD_COORDINATES); + toXContent(builder, northEast); + toXContent(builder, southWest); + builder.endArray(); + return builder.endObject(); + } + + @Override + public Rectangle build() { + return SPATIAL_CONTEXT.makeRectangle( + northEast.x, southWest.x, + southWest.y, northEast.y); + } + + @Override + public GeoShapeType type() { + return TYPE; + } +} diff --git a/src/main/java/org/elasticsearch/common/geo/builders/LineStringBuilder.java b/src/main/java/org/elasticsearch/common/geo/builders/LineStringBuilder.java new file mode 100644 index 00000000000..afc69ced56e --- /dev/null +++ b/src/main/java/org/elasticsearch/common/geo/builders/LineStringBuilder.java @@ -0,0 +1,45 @@ +/* + * Licensed to ElasticSearch and Shay Banon 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.builders; + +import org.elasticsearch.common.xcontent.XContentBuilder; + +import java.io.IOException; + +public class LineStringBuilder extends BaseLineStringBuilder { + + public static final GeoShapeType TYPE = GeoShapeType.LINESTRING; + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + builder.field(FIELD_TYPE, TYPE.shapename); + builder.field(FIELD_COORDINATES); + coordinatesToXcontent(builder, false); + builder.endObject(); + return builder; + } + + @Override + public GeoShapeType type() { + return TYPE; + } + +} diff --git a/src/main/java/org/elasticsearch/common/geo/builders/MultiLineStringBuilder.java b/src/main/java/org/elasticsearch/common/geo/builders/MultiLineStringBuilder.java new file mode 100644 index 00000000000..9e3ccd45f85 --- /dev/null +++ b/src/main/java/org/elasticsearch/common/geo/builders/MultiLineStringBuilder.java @@ -0,0 +1,125 @@ +/* + * Licensed to ElasticSearch and Shay Banon 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.builders; + +import org.elasticsearch.common.xcontent.XContentBuilder; + +import com.spatial4j.core.shape.Shape; +import com.spatial4j.core.shape.jts.JtsGeometry; +import com.vividsolutions.jts.geom.Coordinate; +import com.vividsolutions.jts.geom.Geometry; +import com.vividsolutions.jts.geom.LineString; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Iterator; + +public class MultiLineStringBuilder extends ShapeBuilder { + + public static final GeoShapeType TYPE = GeoShapeType.MULTILINESTRING; + + private final ArrayList> lines = new ArrayList>(); + + public InternalLineStringBuilder linestring() { + InternalLineStringBuilder line = new InternalLineStringBuilder(this); + this.lines.add(line); + return line; + } + + public MultiLineStringBuilder linestring(BaseLineStringBuilder line) { + this.lines.add(line); + return this; + } + + public Coordinate[][] coordinates() { + Coordinate[][] result = new Coordinate[lines.size()][]; + for (int i = 0; i < result.length; i++) { + result[i] = lines.get(i).coordinates(false); + } + return result; + } + + @Override + public GeoShapeType type() { + return TYPE; + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + builder.field(FIELD_TYPE, TYPE.shapename); + builder.field(FIELD_COORDINATES); + builder.startArray(); + for(BaseLineStringBuilder line : lines) { + line.coordinatesToXcontent(builder, false); + } + builder.endArray(); + builder.endObject(); + return builder; + } + + @Override + public Shape build() { + final Geometry geometry; + if(wrapdateline) { + ArrayList parts = new ArrayList(); + for (BaseLineStringBuilder line : lines) { + BaseLineStringBuilder.decompose(FACTORY, line.coordinates(false), parts); + } + if(parts.size() == 1) { + geometry = parts.get(0); + } else { + LineString[] lineStrings = parts.toArray(new LineString[parts.size()]); + geometry = FACTORY.createMultiLineString(lineStrings); + } + } else { + LineString[] lineStrings = new LineString[lines.size()]; + Iterator> iterator = lines.iterator(); + for (int i = 0; iterator.hasNext(); i++) { + lineStrings[i] = FACTORY.createLineString(iterator.next().coordinates(false)); + } + geometry = FACTORY.createMultiLineString(lineStrings); + } + return new JtsGeometry(geometry, SPATIAL_CONTEXT, true); + } + + public static class InternalLineStringBuilder extends BaseLineStringBuilder { + + private final MultiLineStringBuilder collection; + + public InternalLineStringBuilder(MultiLineStringBuilder collection) { + super(); + this.collection = collection; + } + + public MultiLineStringBuilder end() { + return collection; + } + + public Coordinate[] coordinates() { + return super.coordinates(false); + } + + @Override + public GeoShapeType type() { + return null; + } + } +} diff --git a/src/main/java/org/elasticsearch/common/geo/builders/MultiPointBuilder.java b/src/main/java/org/elasticsearch/common/geo/builders/MultiPointBuilder.java new file mode 100644 index 00000000000..5f43e953407 --- /dev/null +++ b/src/main/java/org/elasticsearch/common/geo/builders/MultiPointBuilder.java @@ -0,0 +1,54 @@ +/* + * Licensed to ElasticSearch and Shay Banon 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.builders; + +import com.spatial4j.core.shape.Shape; +import com.spatial4j.core.shape.jts.JtsGeometry; +import com.vividsolutions.jts.geom.Coordinate; +import com.vividsolutions.jts.geom.MultiPoint; +import org.elasticsearch.common.xcontent.XContentBuilder; + +import java.io.IOException; + +public class MultiPointBuilder extends PointCollection { + + public static final GeoShapeType TYPE = GeoShapeType.MULTIPOINT; + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + builder.field(FIELD_TYPE, TYPE.shapename); + builder.field(FIELD_COORDINATES); + super.coordinatesToXcontent(builder, false); + builder.endObject(); + return builder; + } + + @Override + public Shape build() { + MultiPoint geometry = FACTORY.createMultiPoint(points.toArray(new Coordinate[points.size()])); + return new JtsGeometry(geometry, SPATIAL_CONTEXT, true); + } + + @Override + public GeoShapeType type() { + return TYPE; + } +} diff --git a/src/main/java/org/elasticsearch/common/geo/builders/MultiPolygonBuilder.java b/src/main/java/org/elasticsearch/common/geo/builders/MultiPolygonBuilder.java new file mode 100644 index 00000000000..0a7852a9ae1 --- /dev/null +++ b/src/main/java/org/elasticsearch/common/geo/builders/MultiPolygonBuilder.java @@ -0,0 +1,115 @@ +/* + * Licensed to ElasticSearch and Shay Banon 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.builders; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Iterator; + +import org.elasticsearch.common.xcontent.XContentBuilder; + +import com.spatial4j.core.shape.Shape; +import com.spatial4j.core.shape.jts.JtsGeometry; +import com.vividsolutions.jts.geom.Coordinate; +import com.vividsolutions.jts.geom.Geometry; +import com.vividsolutions.jts.geom.Polygon; + +public class MultiPolygonBuilder extends ShapeBuilder { + + public static final GeoShapeType TYPE = GeoShapeType.MULTIPOLYGON; + + protected final ArrayList> polygons = new ArrayList>(); + + public MultiPolygonBuilder polygon(BasePolygonBuilder polygon) { + this.polygons.add(polygon); + return this; + } + + public InternalPolygonBuilder polygon() { + InternalPolygonBuilder polygon = new InternalPolygonBuilder(this); + this.polygon(polygon); + return polygon; + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + builder.field(FIELD_TYPE, TYPE.shapename); + builder.startArray(FIELD_COORDINATES); + for(BasePolygonBuilder polygon : polygons) { + builder.startArray(); + polygon.coordinatesArray(builder, params); + builder.endArray(); + } + builder.endArray(); + return builder.endObject(); + } + + @Override + public GeoShapeType type() { + return TYPE; + } + + @Override + public Shape build() { + + Polygon[] polygons; + + if(wrapdateline) { + ArrayList polygonSet = new ArrayList(this.polygons.size()); + for (BasePolygonBuilder polygon : this.polygons) { + for(Coordinate[][] part : polygon.coordinates()) { + polygonSet.add(PolygonBuilder.polygon(FACTORY, part)); + } + } + + polygons = polygonSet.toArray(new Polygon[polygonSet.size()]); + } else { + polygons = new Polygon[this.polygons.size()]; + Iterator> iterator = this.polygons.iterator(); + for (int i = 0; iterator.hasNext(); i++) { + polygons[i] = iterator.next().toPolygon(FACTORY); + } + } + + Geometry geometry = polygons.length == 1 + ? polygons[0] + : FACTORY.createMultiPolygon(polygons); + + return new JtsGeometry(geometry, SPATIAL_CONTEXT, !wrapdateline); + } + + public static class InternalPolygonBuilder extends BasePolygonBuilder { + + private final MultiPolygonBuilder collection; + + private InternalPolygonBuilder(MultiPolygonBuilder collection) { + super(); + this.collection = collection; + this.shell = new Ring(this); + } + + @Override + public MultiPolygonBuilder close() { + super.close(); + return collection; + } + } +} diff --git a/src/main/java/org/elasticsearch/common/geo/builders/PointBuilder.java b/src/main/java/org/elasticsearch/common/geo/builders/PointBuilder.java new file mode 100644 index 00000000000..f37edd3934a --- /dev/null +++ b/src/main/java/org/elasticsearch/common/geo/builders/PointBuilder.java @@ -0,0 +1,66 @@ +/* + * Licensed to ElasticSearch and Shay Banon 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.builders; + +import java.io.IOException; + +import org.elasticsearch.common.xcontent.XContentBuilder; + +import com.spatial4j.core.shape.Point; +import com.vividsolutions.jts.geom.Coordinate; + +public class PointBuilder extends ShapeBuilder { + + public static final GeoShapeType TYPE = GeoShapeType.POINT; + + private Coordinate coordinate; + + public PointBuilder coordinate(Coordinate coordinate) { + this.coordinate = coordinate; + return this; + } + + public double longitude() { + return coordinate.x; + } + + public double latitude() { + return coordinate.y; + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + builder.field(FIELD_TYPE, TYPE.shapename); + builder.field(FIELD_COORDINATES); + toXContent(builder, coordinate); + return builder.endObject(); + } + + @Override + public Point build() { + return SPATIAL_CONTEXT.makePoint(coordinate.x, coordinate.y); + } + + @Override + public GeoShapeType type() { + return TYPE; + } +} diff --git a/src/main/java/org/elasticsearch/common/geo/builders/PointCollection.java b/src/main/java/org/elasticsearch/common/geo/builders/PointCollection.java new file mode 100644 index 00000000000..7eb02e41ecf --- /dev/null +++ b/src/main/java/org/elasticsearch/common/geo/builders/PointCollection.java @@ -0,0 +1,129 @@ +/* + * Licensed to ElasticSearch and Shay Banon 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.builders; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; + +import org.elasticsearch.common.xcontent.XContentBuilder; + +import com.vividsolutions.jts.geom.Coordinate; + +/** + * The {@link PointCollection} is an abstract base implementation for all GeoShapes. It simply handles a set of points. + */ +public abstract class PointCollection> extends ShapeBuilder { + + protected final ArrayList points; + + protected PointCollection() { + this(new ArrayList()); + } + + protected PointCollection(ArrayList points) { + this.points = points; + } + + @SuppressWarnings("unchecked") + private E thisRef() { + return (E)this; + } + + /** + * Add a new point to the collection + * @param longitude longitude of the coordinate + * @param latitude latitude of the coordinate + * @return this + */ + public E point(double longitude, double latitude) { + return this.point(coordinate(longitude, latitude)); + } + + /** + * Add a new point to the collection + * @param coordinate coordinate of the point + * @return this + */ + public E point(Coordinate coordinate) { + this.points.add(coordinate); + return thisRef(); + } + + /** + * Add a array of points to the collection + * + * @param coordinates array of {@link Coordinate}s to add + * @return this + */ + public E points(Coordinate...coordinates) { + return this.points(Arrays.asList(coordinates)); + } + + /** + * Add a collection of points to the collection + * + * @param coordinates array of {@link Coordinate}s to add + * @return this + */ + public E points(Collection coordinates) { + this.points.addAll(coordinates); + return thisRef(); + } + + /** + * Copy all points to a new Array + * + * @param closed if set to true the first point of the array is repeated as last element + * @return Array of coordinates + */ + protected Coordinate[] coordinates(boolean closed) { + Coordinate[] result = points.toArray(new Coordinate[points.size() + (closed?1:0)]); + if(closed) { + result[result.length-1] = result[0]; + } + return result; + } + + /** + * builds an array of coordinates to a {@link XContentBuilder} + * + * @param builder builder to use + * @param closed repeat the first point at the end of the array if it's not already defines as last element of the array + * @return the builder + * @throws IOException + */ + protected XContentBuilder coordinatesToXcontent(XContentBuilder builder, boolean closed) throws IOException { + builder.startArray(); + for(Coordinate point : points) { + toXContent(builder, point); + } + if(closed) { + Coordinate start = points.get(0); + Coordinate end = points.get(points.size()-1); + if(start.x != end.x || start.y != end.y) { + toXContent(builder, points.get(0)); + } + } + builder.endArray(); + return builder; + } +} diff --git a/src/main/java/org/elasticsearch/common/geo/GeoShapeConstants.java b/src/main/java/org/elasticsearch/common/geo/builders/PolygonBuilder.java similarity index 60% rename from src/main/java/org/elasticsearch/common/geo/GeoShapeConstants.java rename to src/main/java/org/elasticsearch/common/geo/builders/PolygonBuilder.java index 355ffa4af50..5d53c4dfc2a 100644 --- a/src/main/java/org/elasticsearch/common/geo/GeoShapeConstants.java +++ b/src/main/java/org/elasticsearch/common/geo/builders/PolygonBuilder.java @@ -17,14 +17,26 @@ * under the License. */ -package org.elasticsearch.common.geo; +package org.elasticsearch.common.geo.builders; -import com.spatial4j.core.context.jts.JtsSpatialContext; +import java.util.ArrayList; -/** - * Common constants through the GeoShape codebase - */ -public interface GeoShapeConstants { +import com.vividsolutions.jts.geom.Coordinate; - public static final JtsSpatialContext SPATIAL_CONTEXT = new JtsSpatialContext(true); +public class PolygonBuilder extends BasePolygonBuilder { + + public PolygonBuilder() { + this(new ArrayList()); + } + + protected PolygonBuilder(ArrayList points) { + super(); + this.shell = new Ring(this, points); + } + + @Override + public PolygonBuilder close() { + super.close(); + return this; + } } diff --git a/src/main/java/org/elasticsearch/common/geo/builders/ShapeBuilder.java b/src/main/java/org/elasticsearch/common/geo/builders/ShapeBuilder.java new file mode 100644 index 00000000000..012d6fd8606 --- /dev/null +++ b/src/main/java/org/elasticsearch/common/geo/builders/ShapeBuilder.java @@ -0,0 +1,628 @@ +/* + * Licensed to ElasticSearch and Shay Banon 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.builders; + +import java.io.IOException; +import java.util.*; + +import org.elasticsearch.ElasticSearchIllegalArgumentException; +import org.elasticsearch.ElasticSearchParseException; +import org.elasticsearch.common.logging.ESLogger; +import org.elasticsearch.common.logging.ESLoggerFactory; +import org.elasticsearch.common.unit.DistanceUnit; +import org.elasticsearch.common.unit.DistanceUnit.Distance; +import org.elasticsearch.common.xcontent.ToXContent; +import org.elasticsearch.common.xcontent.XContent; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.common.xcontent.json.JsonXContent; + +import com.spatial4j.core.context.jts.JtsSpatialContext; +import com.spatial4j.core.shape.Shape; +import com.vividsolutions.jts.geom.Coordinate; +import com.vividsolutions.jts.geom.GeometryFactory; + +public abstract class ShapeBuilder implements ToXContent { + + protected static final ESLogger LOGGER = ESLoggerFactory.getLogger(ShapeBuilder.class.getName()); + + private static final boolean DEBUG; + static { + // if asserts are enabled we run the debug statements even if they are not logged + // to prevent exceptions only present if debug enabled + boolean debug = false; + assert debug = true; + DEBUG = debug; + } + + public static final double DATELINE = 180; + public static final GeometryFactory FACTORY = new GeometryFactory(); + public static final JtsSpatialContext SPATIAL_CONTEXT = new JtsSpatialContext(true); + + protected final boolean wrapdateline = true; + + protected ShapeBuilder() { + + } + + protected static Coordinate coordinate(double longitude, double latitude) { + return new Coordinate(longitude, latitude); + } + + /** + * Create a new point + * + * @param longitude longitude of the point + * @param latitude latitude of the point + * @return a new {@link PointBuilder} + */ + public static PointBuilder newPoint(double longitude, double latitude) { + return newPoint(new Coordinate(longitude, latitude)); + } + + /** + * Create a new {@link PointBuilder} from a {@link Coordinate} + * @param coordinate coordinate defining the position of the point + * @return a new {@link PointBuilder} + */ + public static PointBuilder newPoint(Coordinate coordinate) { + return new PointBuilder().coordinate(coordinate); + } + + /** + * Create a new set of points + * @return new {@link MultiPointBuilder} + */ + public static MultiPointBuilder newMultiPoint() { + return new MultiPointBuilder(); + } + + /** + * Create a new lineString + * @return a new {@link LineStringBuilder} + */ + public static LineStringBuilder newLineString() { + return new LineStringBuilder(); + } + + /** + * Create a new Collection of lineStrings + * @return a new {@link MultiLineStringBuilder} + */ + public static MultiLineStringBuilder newMultiLinestring() { + return new MultiLineStringBuilder(); + } + + /** + * Create a new Polygon + * @return a new {@link PointBuilder} + */ + public static PolygonBuilder newPolygon() { + return new PolygonBuilder(); + } + + /** + * Create a new Collection of polygons + * @return a new {@link MultiPolygonBuilder} + */ + public static MultiPolygonBuilder newMultiPolygon() { + return new MultiPolygonBuilder(); + } + + /** + * create a new Circle + * @return a new {@link CircleBuilder} + */ + public static CircleBuilder newCircleBuilder() { + return new CircleBuilder(); + } + + /** + * create a new rectangle + * @return a new {@link EnvelopeBuilder} + */ + public static EnvelopeBuilder newEnvelope() { + return new EnvelopeBuilder(); + } + + @Override + public String toString() { + try { + XContentBuilder xcontent = JsonXContent.contentBuilder(); + return toXContent(xcontent, EMPTY_PARAMS).prettyPrint().string(); + } catch (IOException e) { + return super.toString(); + } + } + + /** + * Create a new Shape from this builder. Since calling this method could change the + * defined shape. (by inserting new coordinates or change the position of points) + * the builder looses its validity. So this method should only be called once on a builder + * @return new {@link Shape} defined by the builder + */ + public abstract Shape build(); + + /** + * Recursive method which parses the arrays of coordinates used to define + * Shapes + * + * @param parser + * Parser that will be read from + * @return CoordinateNode representing the start of the coordinate tree + * @throws IOException + * Thrown if an error occurs while reading from the + * XContentParser + */ + private static CoordinateNode parseCoordinates(XContentParser parser) throws IOException { + XContentParser.Token token = parser.nextToken(); + + // Base case + if (token != XContentParser.Token.START_ARRAY) { + double lon = parser.doubleValue(); + token = parser.nextToken(); + double lat = parser.doubleValue(); + token = parser.nextToken(); + return new CoordinateNode(new Coordinate(lon, lat)); + } + + List nodes = new ArrayList(); + while (token != XContentParser.Token.END_ARRAY) { + nodes.add(parseCoordinates(parser)); + token = parser.nextToken(); + } + + return new CoordinateNode(nodes); + } + + /** + * Create a new {@link ShapeBuilder} from {@link XContent} + * @param parser parser to read the GeoShape from + * @return {@link ShapeBuilder} read from the parser or null + * if the parsers current token has been + * @throws IOException if the input could not be read + */ + public static ShapeBuilder parse(XContentParser parser) throws IOException { + return GeoShapeType.parse(parser); + } + + protected static XContentBuilder toXContent(XContentBuilder builder, Coordinate coordinate) throws IOException { + return builder.startArray().value(coordinate.x).value(coordinate.y).endArray(); + } + + protected static Coordinate shift(Coordinate coordinate, double dateline) { + if (dateline == 0) { + return coordinate; + } else { + return new Coordinate(-2 * dateline + coordinate.x, coordinate.y); + } + } + + /** + * get the shapes type + * @return type of the shape + */ + public abstract GeoShapeType type(); + + /** + * Calculate the intersection of a line segment and a vertical dateline. + * + * @param p1 + * start-point of the line segment + * @param p2 + * end-point of the line segment + * @param dateline + * x-coordinate of the vertical dateline + * @return position of the intersection in the open range (0..1] if the line + * segment intersects with the line segment. Otherwise this method + * returns {@link Double#NaN} + */ + protected static final double intersection(Coordinate p1, Coordinate p2, double dateline) { + if (p1.x == p2.x) { + return Double.NaN; + } else { + final double t = (dateline - p1.x) / (p2.x - p1.x); + if (t > 1 || t <= 0) { + return Double.NaN; + } else { + return t; + } + } + } + + /** + * Calculate all intersections of line segments and a vertical line. The + * Array of edges will be ordered asc by the y-coordinate of the + * intersections of edges. + * + * @param dateline + * x-coordinate of the dateline + * @param edges + * set of edges that may intersect with the dateline + * @return number of intersecting edges + */ + protected static int intersections(double dateline, Edge[] edges) { + int numIntersections = 0; + assert !Double.isNaN(dateline); + for (int i = 0; i < edges.length; i++) { + Coordinate p1 = edges[i].coordinate; + Coordinate p2 = edges[i].next.coordinate; + assert !Double.isNaN(p2.x) && !Double.isNaN(p1.x); + edges[i].intersect = IntersectionOrder.SENTINEL; + + double position = intersection(p1, p2, dateline); + if (!Double.isNaN(position)) { + if (position == 1) { + if (Double.compare(p1.x, dateline) == Double.compare(edges[i].next.next.coordinate.x, dateline)) { + // Ignore the ear + continue; + } else if (p2.x == dateline) { + // Ignore Linesegment on dateline + continue; + } + } + edges[i].intersection(position); + numIntersections++; + } + } + Arrays.sort(edges, INTERSECTION_ORDER); + return numIntersections; + } + + /** + * Node used to represent a tree of coordinates. + *

+ * Can either be a leaf node consisting of a Coordinate, or a parent with + * children + */ + protected static class CoordinateNode implements ToXContent { + + protected final Coordinate coordinate; + protected final List children; + + /** + * Creates a new leaf CoordinateNode + * + * @param coordinate + * Coordinate for the Node + */ + protected CoordinateNode(Coordinate coordinate) { + this.coordinate = coordinate; + this.children = null; + } + + /** + * Creates a new parent CoordinateNode + * + * @param children + * Children of the Node + */ + protected CoordinateNode(List children) { + this.children = children; + this.coordinate = null; + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + if (children == null) { + builder.startArray().value(coordinate.x).value(coordinate.y).endArray(); + } else { + builder.startArray(); + for (CoordinateNode child : children) { + child.toXContent(builder, params); + } + builder.endArray(); + } + return builder; + } + } + + /** + * This helper class implements a linked list for {@link Coordinate}. It contains + * fields for a dateline intersection and component id + */ + protected static final class Edge { + Coordinate coordinate; // coordinate of the start point + Edge next; // next segment + Coordinate intersect; // potential intersection with dateline + int component = -1; // id of the component this edge belongs to + + protected Edge(Coordinate coordinate, Edge next, Coordinate intersection) { + this.coordinate = coordinate; + this.next = next; + this.intersect = intersection; + if (next != null) { + this.component = next.component; + } + } + + protected Edge(Coordinate coordinate, Edge next) { + this(coordinate, next, IntersectionOrder.SENTINEL); + } + + private static final int top(Coordinate[] points, int offset, int length) { + int top = 0; // we start at 1 here since top points to 0 + for (int i = 1; i < length; i++) { + if (points[offset + i].y < points[offset + top].y) { + top = i; + } else if (points[offset + i].y == points[offset + top].y) { + if (points[offset + i].x < points[offset + top].x) { + top = i; + } + } + } + return top; + } + + /** + * Concatenate a set of points to a polygon + * + * @param component + * component id of the polygon + * @param direction + * direction of the ring + * @param points + * list of points to concatenate + * @param pointOffset + * index of the first point + * @param edges + * Array of edges to write the result to + * @param edgeOffset + * index of the first edge in the result + * @param length + * number of points to use + * @return the edges creates + */ + private static Edge[] concat(int component, boolean direction, Coordinate[] points, final int pointOffset, Edge[] edges, final int edgeOffset, + int length) { + assert edges.length >= length+edgeOffset; + assert points.length >= length+pointOffset; + edges[edgeOffset] = new Edge(points[pointOffset], null); + for (int i = 1; i < length; i++) { + if (direction) { + edges[edgeOffset + i] = new Edge(points[pointOffset + i], edges[edgeOffset + i - 1]); + edges[edgeOffset + i].component = component; + } else { + edges[edgeOffset + i - 1].next = edges[edgeOffset + i] = new Edge(points[pointOffset + i], null); + edges[edgeOffset + i - 1].component = component; + } + } + + if (direction) { + edges[edgeOffset].next = edges[edgeOffset + length - 1]; + edges[edgeOffset].component = component; + } else { + edges[edgeOffset + length - 1].next = edges[edgeOffset]; + edges[edgeOffset + length - 1].component = component; + } + + return edges; + } + + /** + * Create a connected list of a list of coordinates + * + * @param points + * array of point + * @param offset + * index of the first point + * @param length + * number of points + * @return Array of edges + */ + protected static Edge[] ring(int component, boolean direction, Coordinate[] points, int offset, Edge[] edges, int toffset, + int length) { + // calculate the direction of the points: + // find the point a the top of the set and check its + // neighbors orientation. So direction is equivalent + // to clockwise/counterclockwise + final int top = top(points, offset, length); + final int prev = (offset + ((top + length - 1) % length)); + final int next = (offset + ((top + 1) % length)); + final boolean orientation = points[offset + prev].x > points[offset + next].x; + return concat(component, direction ^ orientation, points, offset, edges, toffset, length); + } + + /** + * Set the intersection of this line segment to the given position + * + * @param position + * position of the intersection [0..1] + * @return the {@link Coordinate} of the intersection + */ + protected Coordinate intersection(double position) { + return intersect = position(coordinate, next.coordinate, position); + } + + public static Coordinate position(Coordinate p1, Coordinate p2, double position) { + if (position == 0) { + return p1; + } else if (position == 1) { + return p2; + } else { + final double x = p1.x + position * (p2.x - p1.x); + final double y = p1.y + position * (p2.y - p1.y); + return new Coordinate(x, y); + } + } + + @Override + public String toString() { + return "Edge[Component=" + component + "; start=" + coordinate + " " + "; intersection=" + intersect + "]"; + } + } + + protected static final IntersectionOrder INTERSECTION_ORDER = new IntersectionOrder(); + + private static final class IntersectionOrder implements Comparator { + private static final Coordinate SENTINEL = new Coordinate(Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY); + + @Override + public int compare(Edge o1, Edge o2) { + return Double.compare(o1.intersect.y, o2.intersect.y); + } + + } + + public static final String FIELD_TYPE = "type"; + public static final String FIELD_COORDINATES = "coordinates"; + + protected static final boolean debugEnabled() { + return LOGGER.isDebugEnabled() || DEBUG; + } + + /** + * Enumeration that lists all {@link GeoShapeType}s that can be handled + */ + public static enum GeoShapeType { + POINT("point"), + MULTIPOINT("multipoint"), + LINESTRING("linestring"), + MULTILINESTRING("multilinestring"), + POLYGON("polygon"), + MULTIPOLYGON("multipolygon"), + ENVELOPE("envelope"), + CIRCLE("circle"); + + protected final String shapename; + + private GeoShapeType(String shapename) { + this.shapename = shapename; + } + + public static GeoShapeType forName(String geoshapename) { + String typename = geoshapename.toLowerCase(Locale.ROOT); + for (GeoShapeType type : values()) { + if(type.shapename.equals(typename)) { + return type; + } + } + throw new ElasticSearchIllegalArgumentException("unknown geo_shape ["+geoshapename+"]"); + } + + public static ShapeBuilder parse(XContentParser parser) throws IOException { + if (parser.currentToken() == XContentParser.Token.VALUE_NULL) { + return null; + } else if (parser.currentToken() != XContentParser.Token.START_OBJECT) { + throw new ElasticSearchParseException("Shape must be an object consisting of type and coordinates"); + } + + GeoShapeType shapeType = null; + Distance radius = null; + CoordinateNode node = null; + + XContentParser.Token token; + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + if (token == XContentParser.Token.FIELD_NAME) { + String fieldName = parser.currentName(); + + if (FIELD_TYPE.equals(fieldName)) { + parser.nextToken(); + shapeType = GeoShapeType.forName(parser.text()); + } else if (FIELD_COORDINATES.equals(fieldName)) { + parser.nextToken(); + node = parseCoordinates(parser); + } else if (CircleBuilder.FIELD_RADIUS.equals(fieldName)) { + parser.nextToken(); + radius = Distance.parseDistance(parser.text(), DistanceUnit.METERS); + } else { + parser.nextToken(); + parser.skipChildren(); + } + } + } + + if (shapeType == null) { + throw new ElasticSearchParseException("Shape type not included"); + } else if (node == null) { + throw new ElasticSearchParseException("Coordinates not included"); + } else if (radius != null && GeoShapeType.CIRCLE != shapeType) { + throw new ElasticSearchParseException("Field [" + CircleBuilder.FIELD_RADIUS + "] is supported for [" + CircleBuilder.TYPE + + "] only"); + } + + switch (shapeType) { + case POINT: return parsePoint(node); + case MULTIPOINT: return parseMultiPoint(node); + case LINESTRING: return parseLineString(node); + case MULTILINESTRING: return parseMultiLine(node); + case POLYGON: return parsePolygon(node); + case MULTIPOLYGON: return parseMultiPolygon(node); + case CIRCLE: return parseCircle(node, radius); + case ENVELOPE: return parseEnvelope(node); + default: + throw new ElasticSearchParseException("Shape type [" + shapeType + "] not included"); + } + } + + protected static PointBuilder parsePoint(CoordinateNode node) { + return newPoint(node.coordinate); + } + + protected static CircleBuilder parseCircle(CoordinateNode coordinates, Distance radius) { + return newCircleBuilder().center(coordinates.coordinate).radius(radius); + } + + protected static EnvelopeBuilder parseEnvelope(CoordinateNode coordinates) { + return newEnvelope().topLeft(coordinates.children.get(0).coordinate).bottomRight(coordinates.children.get(1).coordinate); + } + + protected static MultiPointBuilder parseMultiPoint(CoordinateNode coordinates) { + MultiPointBuilder points = new MultiPointBuilder(); + for (CoordinateNode node : coordinates.children) { + points.point(node.coordinate); + } + return points; + } + + protected static LineStringBuilder parseLineString(CoordinateNode coordinates) { + LineStringBuilder line = newLineString(); + for (CoordinateNode node : coordinates.children) { + line.point(node.coordinate); + } + return line; + } + + protected static MultiLineStringBuilder parseMultiLine(CoordinateNode coordinates) { + MultiLineStringBuilder multiline = newMultiLinestring(); + for (CoordinateNode node : coordinates.children) { + multiline.linestring(parseLineString(node)); + } + return multiline; + } + + protected static PolygonBuilder parsePolygon(CoordinateNode coordinates) { + LineStringBuilder shell = parseLineString(coordinates.children.get(0)); + PolygonBuilder polygon = new PolygonBuilder(shell.points); + for (int i = 1; i < coordinates.children.size(); i++) { + polygon.hole(parseLineString(coordinates.children.get(i))); + } + return polygon; + } + + protected static MultiPolygonBuilder parseMultiPolygon(CoordinateNode coordinates) { + MultiPolygonBuilder polygons = newMultiPolygon(); + for (CoordinateNode node : coordinates.children) { + polygons.polygon(parsePolygon(node)); + } + return polygons; + } + } +} diff --git a/src/main/java/org/elasticsearch/index/mapper/geo/GeoShapeFieldMapper.java b/src/main/java/org/elasticsearch/index/mapper/geo/GeoShapeFieldMapper.java index 42778106b5e..34ed79984df 100644 --- a/src/main/java/org/elasticsearch/index/mapper/geo/GeoShapeFieldMapper.java +++ b/src/main/java/org/elasticsearch/index/mapper/geo/GeoShapeFieldMapper.java @@ -29,10 +29,9 @@ import org.apache.lucene.spatial.prefix.tree.QuadPrefixTree; import org.apache.lucene.spatial.prefix.tree.SpatialPrefixTree; import org.elasticsearch.ElasticSearchIllegalArgumentException; import org.elasticsearch.common.Strings; -import org.elasticsearch.common.geo.GeoJSONShapeParser; -import org.elasticsearch.common.geo.GeoShapeConstants; import org.elasticsearch.common.geo.GeoUtils; import org.elasticsearch.common.geo.SpatialStrategy; +import org.elasticsearch.common.geo.builders.ShapeBuilder; import org.elasticsearch.common.unit.DistanceUnit; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.index.codec.postingsformat.PostingsFormatProvider; @@ -43,8 +42,6 @@ import org.elasticsearch.index.mapper.MapperParsingException; import org.elasticsearch.index.mapper.ParseContext; import org.elasticsearch.index.mapper.core.AbstractFieldMapper; -import com.spatial4j.core.shape.Shape; - import java.io.IOException; import java.util.Map; @@ -143,9 +140,9 @@ public class GeoShapeFieldMapper extends AbstractFieldMapper { final FieldMapper.Names names = buildNames(context); if (Names.TREE_GEOHASH.equals(tree)) { - prefixTree = new GeohashPrefixTree(GeoShapeConstants.SPATIAL_CONTEXT, getLevels(treeLevels, precisionInMeters, Defaults.GEOHASH_LEVELS, true)); + prefixTree = new GeohashPrefixTree(ShapeBuilder.SPATIAL_CONTEXT, getLevels(treeLevels, precisionInMeters, Defaults.GEOHASH_LEVELS, true)); } else if (Names.TREE_QUADTREE.equals(tree)) { - prefixTree = new QuadPrefixTree(GeoShapeConstants.SPATIAL_CONTEXT, getLevels(treeLevels, precisionInMeters, Defaults.QUADTREE_LEVELS, false)); + prefixTree = new QuadPrefixTree(ShapeBuilder.SPATIAL_CONTEXT, getLevels(treeLevels, precisionInMeters, Defaults.QUADTREE_LEVELS, false)); } else { throw new ElasticSearchIllegalArgumentException("Unknown prefix tree type [" + tree + "]"); } @@ -215,8 +212,8 @@ public class GeoShapeFieldMapper extends AbstractFieldMapper { @Override public void parse(ParseContext context) throws IOException { try { - Shape shape = GeoJSONShapeParser.parse(context.parser()); - Field[] fields = defaultStrategy.createIndexableFields(shape); + ShapeBuilder shape = ShapeBuilder.parse(context.parser()); + Field[] fields = defaultStrategy.createIndexableFields(shape.build()); if (fields == null || fields.length == 0) { return; } diff --git a/src/main/java/org/elasticsearch/index/query/FilterBuilders.java b/src/main/java/org/elasticsearch/index/query/FilterBuilders.java index 6a8e197ed2b..186f676d8cf 100644 --- a/src/main/java/org/elasticsearch/index/query/FilterBuilders.java +++ b/src/main/java/org/elasticsearch/index/query/FilterBuilders.java @@ -19,10 +19,10 @@ package org.elasticsearch.index.query; -import com.spatial4j.core.shape.Shape; import org.elasticsearch.common.Nullable; import org.elasticsearch.common.geo.GeoPoint; import org.elasticsearch.common.geo.ShapeRelation; +import org.elasticsearch.common.geo.builders.ShapeBuilder; /** * A static factory for simple "import static" usage. @@ -414,7 +414,7 @@ public abstract class FilterBuilders { * @param shape Shape to use in the filter * @param relation relation of the shapes */ - public static GeoShapeFilterBuilder geoShapeFilter(String name, Shape shape, ShapeRelation relation) { + public static GeoShapeFilterBuilder geoShapeFilter(String name, ShapeBuilder shape, ShapeRelation relation) { return new GeoShapeFilterBuilder(name, shape, relation); } @@ -428,7 +428,7 @@ public abstract class FilterBuilders { * @param name The shape field name * @param shape Shape to use in the filter */ - public static GeoShapeFilterBuilder geoIntersectionFilter(String name, Shape shape) { + public static GeoShapeFilterBuilder geoIntersectionFilter(String name, ShapeBuilder shape) { return geoShapeFilter(name, shape, ShapeRelation.INTERSECTS); } @@ -442,7 +442,7 @@ public abstract class FilterBuilders { * @param name The shape field name * @param shape Shape to use in the filter */ - public static GeoShapeFilterBuilder geoWithinFilter(String name, Shape shape) { + public static GeoShapeFilterBuilder geoWithinFilter(String name, ShapeBuilder shape) { return geoShapeFilter(name, shape, ShapeRelation.WITHIN); } @@ -456,7 +456,7 @@ public abstract class FilterBuilders { * @param name The shape field name * @param shape Shape to use in the filter */ - public static GeoShapeFilterBuilder geoDisjointFilter(String name, Shape shape) { + public static GeoShapeFilterBuilder geoDisjointFilter(String name, ShapeBuilder shape) { return geoShapeFilter(name, shape, ShapeRelation.DISJOINT); } diff --git a/src/main/java/org/elasticsearch/index/query/GeoShapeFilterBuilder.java b/src/main/java/org/elasticsearch/index/query/GeoShapeFilterBuilder.java index e0807624446..2b57bb6f34d 100644 --- a/src/main/java/org/elasticsearch/index/query/GeoShapeFilterBuilder.java +++ b/src/main/java/org/elasticsearch/index/query/GeoShapeFilterBuilder.java @@ -19,14 +19,13 @@ package org.elasticsearch.index.query; -import com.spatial4j.core.shape.Shape; -import org.elasticsearch.common.geo.GeoJSONShapeSerializer; +import java.io.IOException; + import org.elasticsearch.common.geo.ShapeRelation; import org.elasticsearch.common.geo.SpatialStrategy; +import org.elasticsearch.common.geo.builders.ShapeBuilder; import org.elasticsearch.common.xcontent.XContentBuilder; -import java.io.IOException; - /** * {@link FilterBuilder} that builds a GeoShape Filter */ @@ -34,7 +33,7 @@ public class GeoShapeFilterBuilder extends BaseFilterBuilder { private final String name; - private final Shape shape; + private final ShapeBuilder shape; private SpatialStrategy strategy = null; @@ -58,7 +57,7 @@ public class GeoShapeFilterBuilder extends BaseFilterBuilder { * @param name Name of the field that will be filtered * @param shape Shape used in the filter */ - public GeoShapeFilterBuilder(String name, Shape shape) { + public GeoShapeFilterBuilder(String name, ShapeBuilder shape) { this(name, shape, null, null, null); } @@ -70,7 +69,7 @@ public class GeoShapeFilterBuilder extends BaseFilterBuilder { * @param relation {@link ShapeRelation} of query and indexed shape * @param shape Shape used in the filter */ - public GeoShapeFilterBuilder(String name, Shape shape, ShapeRelation relation) { + public GeoShapeFilterBuilder(String name, ShapeBuilder shape, ShapeRelation relation) { this(name, shape, null, null, relation); } @@ -86,7 +85,7 @@ public class GeoShapeFilterBuilder extends BaseFilterBuilder { this(name, null, indexedShapeId, indexedShapeType, relation); } - private GeoShapeFilterBuilder(String name, Shape shape, String indexedShapeId, String indexedShapeType, ShapeRelation relation) { + private GeoShapeFilterBuilder(String name, ShapeBuilder shape, String indexedShapeId, String indexedShapeType, ShapeRelation relation) { this.name = name; this.shape = shape; this.indexedShapeId = indexedShapeId; @@ -183,9 +182,7 @@ public class GeoShapeFilterBuilder extends BaseFilterBuilder { } if (shape != null) { - builder.startObject("shape"); - GeoJSONShapeSerializer.serialize(shape, builder); - builder.endObject(); + builder.field("shape", shape); } else { builder.startObject("indexed_shape") .field("id", indexedShapeId) diff --git a/src/main/java/org/elasticsearch/index/query/GeoShapeFilterParser.java b/src/main/java/org/elasticsearch/index/query/GeoShapeFilterParser.java index 210368f4fe9..cc76c003162 100644 --- a/src/main/java/org/elasticsearch/index/query/GeoShapeFilterParser.java +++ b/src/main/java/org/elasticsearch/index/query/GeoShapeFilterParser.java @@ -22,8 +22,8 @@ package org.elasticsearch.index.query; import com.spatial4j.core.shape.Shape; import org.apache.lucene.search.Filter; import org.apache.lucene.spatial.prefix.PrefixTreeStrategy; -import org.elasticsearch.common.geo.GeoJSONShapeParser; import org.elasticsearch.common.geo.ShapeRelation; +import org.elasticsearch.common.geo.builders.ShapeBuilder; import org.elasticsearch.common.inject.Inject; import org.elasticsearch.common.inject.internal.Nullable; import org.elasticsearch.common.xcontent.XContentParser; @@ -80,7 +80,7 @@ public class GeoShapeFilterParser implements FilterParser { String fieldName = null; ShapeRelation shapeRelation = ShapeRelation.INTERSECTS; String strategyName = null; - Shape shape = null; + ShapeBuilder shape = null; boolean cache = false; CacheKeyFilter.Key cacheKey = null; String filterName = null; @@ -105,7 +105,7 @@ public class GeoShapeFilterParser implements FilterParser { token = parser.nextToken(); if ("shape".equals(currentFieldName)) { - shape = GeoJSONShapeParser.parse(parser); + shape = ShapeBuilder.parse(parser); } else if ("relation".equals(currentFieldName)) { shapeRelation = ShapeRelation.getRelationByName(parser.text()); if (shapeRelation == null) { diff --git a/src/main/java/org/elasticsearch/index/query/GeoShapeQueryBuilder.java b/src/main/java/org/elasticsearch/index/query/GeoShapeQueryBuilder.java index a47294d4548..c8b60ded8a2 100644 --- a/src/main/java/org/elasticsearch/index/query/GeoShapeQueryBuilder.java +++ b/src/main/java/org/elasticsearch/index/query/GeoShapeQueryBuilder.java @@ -19,13 +19,12 @@ package org.elasticsearch.index.query; -import com.spatial4j.core.shape.Shape; -import org.elasticsearch.common.geo.GeoJSONShapeSerializer; -import org.elasticsearch.common.geo.SpatialStrategy; -import org.elasticsearch.common.xcontent.XContentBuilder; - import java.io.IOException; +import org.elasticsearch.common.geo.SpatialStrategy; +import org.elasticsearch.common.geo.builders.ShapeBuilder; +import org.elasticsearch.common.xcontent.XContentBuilder; + /** * {@link QueryBuilder} that builds a GeoShape Query */ @@ -35,7 +34,7 @@ public class GeoShapeQueryBuilder extends BaseQueryBuilder implements BoostableQ private SpatialStrategy strategy = null; - private final Shape shape; + private final ShapeBuilder shape; private float boost = -1; @@ -52,7 +51,7 @@ public class GeoShapeQueryBuilder extends BaseQueryBuilder implements BoostableQ * @param name Name of the field that will be queried * @param shape Shape used in the query */ - public GeoShapeQueryBuilder(String name, Shape shape) { + public GeoShapeQueryBuilder(String name, ShapeBuilder shape) { this(name, shape, null, null); } @@ -68,7 +67,7 @@ public class GeoShapeQueryBuilder extends BaseQueryBuilder implements BoostableQ this(name, null, indexedShapeId, indexedShapeType); } - private GeoShapeQueryBuilder(String name, Shape shape, String indexedShapeId, String indexedShapeType) { + private GeoShapeQueryBuilder(String name, ShapeBuilder shape, String indexedShapeId, String indexedShapeType) { this.name = name; this.shape = shape; this.indexedShapeId = indexedShapeId; @@ -129,9 +128,7 @@ public class GeoShapeQueryBuilder extends BaseQueryBuilder implements BoostableQ } if (shape != null) { - builder.startObject("shape"); - GeoJSONShapeSerializer.serialize(shape, builder); - builder.endObject(); + builder.field("shape", shape); } else { builder.startObject("indexed_shape") .field("id", indexedShapeId) diff --git a/src/main/java/org/elasticsearch/index/query/GeoShapeQueryParser.java b/src/main/java/org/elasticsearch/index/query/GeoShapeQueryParser.java index 25ec5643d18..b9735fbd281 100644 --- a/src/main/java/org/elasticsearch/index/query/GeoShapeQueryParser.java +++ b/src/main/java/org/elasticsearch/index/query/GeoShapeQueryParser.java @@ -19,7 +19,6 @@ package org.elasticsearch.index.query; -import com.spatial4j.core.shape.Shape; import org.apache.lucene.search.Query; import org.apache.lucene.spatial.prefix.PrefixTreeStrategy; import org.apache.lucene.spatial.query.SpatialArgs; @@ -27,8 +26,8 @@ import org.apache.lucene.spatial.query.SpatialOperation; import org.elasticsearch.ElasticSearchIllegalArgumentException; import org.elasticsearch.common.Nullable; import org.elasticsearch.common.Strings; -import org.elasticsearch.common.geo.GeoJSONShapeParser; import org.elasticsearch.common.geo.ShapeRelation; +import org.elasticsearch.common.geo.builders.ShapeBuilder; import org.elasticsearch.common.inject.Inject; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.index.mapper.FieldMapper; @@ -61,7 +60,7 @@ public class GeoShapeQueryParser implements QueryParser { String fieldName = null; ShapeRelation shapeRelation = ShapeRelation.INTERSECTS; String strategyName = null; - Shape shape = null; + ShapeBuilder shape = null; String id = null; String type = null; @@ -83,7 +82,7 @@ public class GeoShapeQueryParser implements QueryParser { currentFieldName = parser.currentName(); token = parser.nextToken(); if ("shape".equals(currentFieldName)) { - shape = GeoJSONShapeParser.parse(parser); + shape = ShapeBuilder.parse(parser); } else if ("strategy".equals(currentFieldName)) { strategyName = parser.text(); } else if ("relation".equals(currentFieldName)) { @@ -156,14 +155,14 @@ public class GeoShapeQueryParser implements QueryParser { this.fetchService = fetchService; } - public static SpatialArgs getArgs(Shape shape, ShapeRelation relation) { + public static SpatialArgs getArgs(ShapeBuilder shape, ShapeRelation relation) { switch(relation) { case DISJOINT: - return new SpatialArgs(SpatialOperation.IsDisjointTo, shape); + return new SpatialArgs(SpatialOperation.IsDisjointTo, shape.build()); case INTERSECTS: - return new SpatialArgs(SpatialOperation.Intersects, shape); + return new SpatialArgs(SpatialOperation.Intersects, shape.build()); case WITHIN: - return new SpatialArgs(SpatialOperation.IsWithin, shape); + return new SpatialArgs(SpatialOperation.IsWithin, shape.build()); default: throw new ElasticSearchIllegalArgumentException(""); diff --git a/src/main/java/org/elasticsearch/index/query/QueryBuilders.java b/src/main/java/org/elasticsearch/index/query/QueryBuilders.java index 8413e3ef1ae..56d1abc2e06 100644 --- a/src/main/java/org/elasticsearch/index/query/QueryBuilders.java +++ b/src/main/java/org/elasticsearch/index/query/QueryBuilders.java @@ -19,11 +19,11 @@ package org.elasticsearch.index.query; -import com.spatial4j.core.shape.Shape; -import org.elasticsearch.common.Nullable; - import java.util.Collection; +import org.elasticsearch.common.Nullable; +import org.elasticsearch.common.geo.builders.ShapeBuilder; + /** * A static factory for simple "import static" usage. */ @@ -693,7 +693,7 @@ public abstract class QueryBuilders { * @param name The field name * @param values The terms */ - public static TermsQueryBuilder termsQuery(String name, Collection values) { + public static TermsQueryBuilder termsQuery(String name, Collection values) { return new TermsQueryBuilder(name, values); } @@ -763,7 +763,7 @@ public abstract class QueryBuilders { * @param name The field name * @param values The terms */ - public static TermsQueryBuilder inQuery(String name, Collection values) { + public static TermsQueryBuilder inQuery(String name, Collection values) { return new TermsQueryBuilder(name, values); } @@ -796,7 +796,7 @@ public abstract class QueryBuilders { * @param name The shape field name * @param shape Shape to use in the Query */ - public static GeoShapeQueryBuilder geoShapeQuery(String name, Shape shape) { + public static GeoShapeQueryBuilder geoShapeQuery(String name, ShapeBuilder shape) { return new GeoShapeQueryBuilder(name, shape); } diff --git a/src/main/java/org/elasticsearch/index/search/shape/ShapeFetchService.java b/src/main/java/org/elasticsearch/index/search/shape/ShapeFetchService.java index 58a3c192d61..e0a5993febd 100644 --- a/src/main/java/org/elasticsearch/index/search/shape/ShapeFetchService.java +++ b/src/main/java/org/elasticsearch/index/search/shape/ShapeFetchService.java @@ -19,14 +19,13 @@ package org.elasticsearch.index.search.shape; -import com.spatial4j.core.shape.Shape; import org.elasticsearch.ElasticSearchIllegalArgumentException; import org.elasticsearch.ElasticSearchIllegalStateException; import org.elasticsearch.action.get.GetRequest; import org.elasticsearch.action.get.GetResponse; import org.elasticsearch.client.Client; import org.elasticsearch.common.component.AbstractComponent; -import org.elasticsearch.common.geo.GeoJSONShapeParser; +import org.elasticsearch.common.geo.builders.ShapeBuilder; import org.elasticsearch.common.inject.Inject; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.xcontent.XContentHelper; @@ -57,7 +56,7 @@ public class ShapeFetchService extends AbstractComponent { * @return Shape with the given ID * @throws IOException Can be thrown while parsing the Shape Document and extracting the Shape */ - public Shape fetch(String id, String type, String index, String shapeField) throws IOException { + public ShapeBuilder fetch(String id, String type, String index, String shapeField) throws IOException { GetResponse response = client.get(new GetRequest(index, type, id).preference("_local").operationThreaded(false)).actionGet(); if (!response.isExists()) { throw new ElasticSearchIllegalArgumentException("Shape with ID [" + id + "] in type [" + type + "] not found"); @@ -71,7 +70,7 @@ public class ShapeFetchService extends AbstractComponent { if (currentToken == XContentParser.Token.FIELD_NAME) { if (shapeField.equals(parser.currentName())) { parser.nextToken(); - return GeoJSONShapeParser.parse(parser); + return ShapeBuilder.parse(parser); } else { parser.nextToken(); parser.skipChildren(); diff --git a/src/test/java/org/elasticsearch/test/hamcrest/ElasticsearchGeoAssertions.java b/src/test/java/org/elasticsearch/test/hamcrest/ElasticsearchGeoAssertions.java new file mode 100644 index 00000000000..967eba4b14e --- /dev/null +++ b/src/test/java/org/elasticsearch/test/hamcrest/ElasticsearchGeoAssertions.java @@ -0,0 +1,205 @@ +/* + * Licensed to ElasticSearch and Shay Banon 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.test.hamcrest; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import com.spatial4j.core.shape.Shape; +import com.spatial4j.core.shape.jts.JtsGeometry; +import com.spatial4j.core.shape.jts.JtsPoint; +import com.vividsolutions.jts.geom.Coordinate; +import com.vividsolutions.jts.geom.Geometry; +import com.vividsolutions.jts.geom.LineString; +import com.vividsolutions.jts.geom.MultiLineString; +import com.vividsolutions.jts.geom.MultiPoint; +import com.vividsolutions.jts.geom.MultiPolygon; +import com.vividsolutions.jts.geom.Polygon; + +public class ElasticsearchGeoAssertions { + + private static int top(Coordinate...points) { + int top = 0; + for (int i = 1; i < points.length; i++) { + if(points[i].y < points[top].y) { + top = i; + } else if(points[i].y == points[top].y) { + if(points[i].x <= points[top].x) { + top = i; + } + } + } + return top; + } + + private static int prev(int top, Coordinate...points) { + for (int i = 1; i < points.length; i++) { + int p = (top + points.length - i) % points.length; + if((points[p].x != points[top].x) || (points[p].y != points[top].y)) { + return p; + } + } + return -1; + } + + private static int next(int top, Coordinate...points) { + for (int i = 1; i < points.length; i++) { + int n = (top + i) % points.length; + if((points[n].x != points[top].x) || (points[n].y != points[top].y)) { + return n; + } + } + return -1; + } + + private static Coordinate[] fixedOrderedRing(List coordinates, boolean direction) { + return fixedOrderedRing(coordinates.toArray(new Coordinate[coordinates.size()]), direction); + } + + private static Coordinate[] fixedOrderedRing(Coordinate[] points, boolean direction) { + + final int top = top(points); + final int next = next(top, points); + final int prev = prev(top, points); + final boolean orientation = points[next].x < points[prev].x; + + if(orientation != direction) { + List asList = Arrays.asList(points); + Collections.reverse(asList); + return fixedOrderedRing(asList, direction); + } else { + if(top>0) { + Coordinate[] aligned = new Coordinate[points.length]; + System.arraycopy(points, top, aligned, 0, points.length-top-1); + System.arraycopy(points, 0, aligned, points.length-top-1, top); + aligned[aligned.length-1] = aligned[0]; + return aligned; + } else { + return points; + } + } + + } + + public static void assertEquals(Coordinate c1, Coordinate c2) { + assert (c1.x == c2.x && c1.y == c2.y): "expected coordinate " + c1 + " but found " + c2; + } + + private static boolean isRing(Coordinate[] c) { + return (c[0].x == c[c.length-1].x) && (c[0].y == c[c.length-1].y); + } + + public static void assertEquals(Coordinate[] c1, Coordinate[] c2) { + assert (c1.length == c1.length) : "expected " + c1.length + " coordinates but found " + c2.length; + + if(isRing(c1) && isRing(c2)) { + c1 = fixedOrderedRing(c1, true); + c2 = fixedOrderedRing(c2, true); + } + + for (int i = 0; i < c2.length; i++) { + assertEquals(c1[i], c2[i]); + } + } + + public static void assertEquals(LineString l1, LineString l2) { + assertEquals(l1.getCoordinates(), l2.getCoordinates()); + } + + public static void assertEquals(Polygon p1, Polygon p2) { + assert (p1.getNumInteriorRing() == p2.getNumInteriorRing()) : "expect " + p1.getNumInteriorRing() + " interior ring but found " + p2.getNumInteriorRing(); + + assertEquals(p1.getExteriorRing(), p2.getExteriorRing()); + + // TODO: This test do not check all permutations of linestrings. So the test + // fails if the holes of the polygons are not ordered the same way + for (int i = 0; i < p1.getNumInteriorRing(); i++) { + assertEquals(p1.getInteriorRingN(i), p2.getInteriorRingN(i)); + } + } + + public static void assertEquals(MultiPolygon p1, MultiPolygon p2) { + assert p1.getNumGeometries() == p2.getNumGeometries(): "expected " + p1.getNumGeometries() + " geometries but found " + p2.getNumGeometries(); + + // TODO: This test do not check all permutations. So the Test fails + // if the inner polygons are not ordered the same way in both Multipolygons + for (int i = 0; i < p1.getNumGeometries(); i++) { + Geometry a = p1.getGeometryN(i); + Geometry b = p2.getGeometryN(i); + assertEquals(a, b); + } + } + + public static void assertEquals(Geometry s1, Geometry s2) { + if(s1 instanceof LineString && s2 instanceof LineString) { + assertEquals((LineString) s1, (LineString) s2); + + } else if (s1 instanceof Polygon && s2 instanceof Polygon) { + assertEquals((Polygon) s1, (Polygon) s2); + + } else if (s1 instanceof MultiPoint && s2 instanceof MultiPoint) { + assert s1.equals(s2): "Expected " + s1 + " but found " + s2; + + } else if (s1 instanceof MultiPolygon && s2 instanceof MultiPolygon) { + assertEquals((MultiPolygon) s1, (MultiPolygon) s2); + + } else { + throw new RuntimeException("equality of shape types not supported [" + s1.getClass().getName() + " and " + s2.getClass().getName() + "]"); + } + } + + public static void assertEquals(JtsGeometry g1, JtsGeometry g2) { + assertEquals(g1.getGeom(), g2.getGeom()); + } + + public static void assertEquals(Shape s1, Shape s2) { + if(s1 instanceof JtsGeometry && s2 instanceof JtsGeometry) { + assertEquals((JtsGeometry) s1, (JtsGeometry) s2); + } else if(s1 instanceof JtsPoint && s2 instanceof JtsPoint) { + JtsPoint p1 = (JtsPoint) s1; + JtsPoint p2 = (JtsPoint) s2; + assert p1.equals(p1): "expected " + p1 + " but found " + p2; + } else { + throw new RuntimeException("equality of shape types not supported [" + s1.getClass().getName() + " and " + s2.getClass().getName() + "]"); + } + } + + private static Geometry unwrap(Shape shape) { + assert (shape instanceof JtsGeometry): "shape is not a JTSGeometry"; + return ((JtsGeometry)shape).getGeom(); + } + + public static void assertMultiPolygon(Shape shape) { + assert(unwrap(shape) instanceof MultiPolygon): "expected MultiPolygon but found " + unwrap(shape).getClass().getName(); + } + + public static void assertPolygon(Shape shape) { + assert(unwrap(shape) instanceof Polygon): "expected Polygon but found " + unwrap(shape).getClass().getName(); + } + + public static void assertLineString(Shape shape) { + assert(unwrap(shape) instanceof LineString): "expected LineString but found " + unwrap(shape).getClass().getName(); + } + + public static void assertMultiLineString(Shape shape) { + assert(unwrap(shape) instanceof MultiLineString): "expected MultiLineString but found " + unwrap(shape).getClass().getName(); + } +} diff --git a/src/test/java/org/elasticsearch/test/integration/search/geo/GeoFilterTests.java b/src/test/java/org/elasticsearch/test/integration/search/geo/GeoFilterTests.java index 78b4122c71d..15b30418386 100644 --- a/src/test/java/org/elasticsearch/test/integration/search/geo/GeoFilterTests.java +++ b/src/test/java/org/elasticsearch/test/integration/search/geo/GeoFilterTests.java @@ -37,9 +37,9 @@ import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.geo.GeoHashUtils; import org.elasticsearch.common.geo.GeoPoint; import org.elasticsearch.common.geo.GeoUtils; -import org.elasticsearch.common.geo.ShapeBuilder; -import org.elasticsearch.common.geo.ShapeBuilder.MultiPolygonBuilder; -import org.elasticsearch.common.geo.ShapeBuilder.PolygonBuilder; +import org.elasticsearch.common.geo.builders.ShapeBuilder; +import org.elasticsearch.common.geo.builders.MultiPolygonBuilder; +import org.elasticsearch.common.geo.builders.PolygonBuilder; import org.elasticsearch.common.io.Streams; import org.elasticsearch.common.xcontent.XContentFactory; import org.elasticsearch.index.query.FilterBuilders; @@ -105,27 +105,27 @@ public class GeoFilterTests extends AbstractSharedClusterTest { try { // self intersection polygon ShapeBuilder.newPolygon() - .point(-10, -10) - .point(10, 10) - .point(-10, 10) - .point(10, -10) - .close().build(); + .point(-10, -10) + .point(10, 10) + .point(-10, 10) + .point(10, -10) + .close().build(); assert false : "Self intersection not detected"; } catch (InvalidShapeException e) { } // polygon with hole ShapeBuilder.newPolygon() - .point(-10, -10).point(-10, 10).point(10, 10).point(10, -10) - .hole() + .point(-10, -10).point(-10, 10).point(10, 10).point(10, -10) + .hole() .point(-5, -5).point(-5, 5).point(5, 5).point(5, -5) .close().close().build(); try { // polygon with overlapping hole ShapeBuilder.newPolygon() - .point(-10, -10).point(-10, 10).point(10, 10).point(10, -10) - .hole() + .point(-10, -10).point(-10, 10).point(10, 10).point(10, -10) + .hole() .point(-5, -5).point(-5, 11).point(5, 11).point(5, -5) .close().close().build(); @@ -136,8 +136,8 @@ public class GeoFilterTests extends AbstractSharedClusterTest { try { // polygon with intersection holes ShapeBuilder.newPolygon() - .point(-10, -10).point(-10, 10).point(10, 10).point(10, -10) - .hole() + .point(-10, -10).point(-10, 10).point(10, 10).point(10, -10) + .hole() .point(-5, -5).point(-5, 5).point(5, 5).point(5, -5) .close() .hole() @@ -151,14 +151,14 @@ public class GeoFilterTests extends AbstractSharedClusterTest { try { // Common line in polygon ShapeBuilder.newPolygon() - .point(-10, -10) - .point(-10, 10) - .point(-5, 10) - .point(-5, -5) - .point(-5, 20) - .point(10, 20) - .point(10, -10) - .close().build(); + .point(-10, -10) + .point(-10, 10) + .point(-5, 10) + .point(-5, -5) + .point(-5, 20) + .point(10, 20) + .point(10, -10) + .close().build(); assert false : "Self intersection not detected"; } catch (InvalidShapeException e) { } @@ -181,7 +181,7 @@ public class GeoFilterTests extends AbstractSharedClusterTest { // Multipolygon: polygon with hole and polygon within the whole ShapeBuilder.newMultiPolygon() - .polygon() + .polygon() .point(-10, -10).point(-10, 10).point(10, 10).point(10, -10) .hole() .point(-5, -5).point(-5, 5).point(5, 5).point(5, -5) @@ -240,9 +240,9 @@ public class GeoFilterTests extends AbstractSharedClusterTest { // with a hole of size 5x5 equidistant from all sides. This hole in turn contains // the second polygon of size 4x4 equidistant from all sites MultiPolygonBuilder polygon = ShapeBuilder.newMultiPolygon() - .polygon() - .point(-10, -10).point(-10, 10).point(10, 10).point(10, -10) - .hole() + .polygon() + .point(-10, -10).point(-10, 10).point(10, 10).point(10, -10) + .hole() .point(-5, -5).point(-5, 5).point(5, 5).point(5, -5) .close() .close() @@ -250,7 +250,8 @@ public class GeoFilterTests extends AbstractSharedClusterTest { .point(-4, -4).point(-4, 4).point(4, 4).point(4, -4) .close(); - BytesReference data = polygon.toXContent("area", jsonBuilder().startObject()).endObject().bytes(); + BytesReference data = jsonBuilder().startObject().field("area", polygon).endObject().bytes(); + client().prepareIndex("shapes", "polygon", "1").setSource(data).execute().actionGet(); client().admin().indices().prepareRefresh().execute().actionGet(); @@ -308,13 +309,13 @@ public class GeoFilterTests extends AbstractSharedClusterTest { // Create a polygon that fills the empty area of the polygon defined above PolygonBuilder inverse = ShapeBuilder.newPolygon() - .point(-5, -5).point(-5, 5).point(5, 5).point(5, -5) - .hole() + .point(-5, -5).point(-5, 5).point(5, 5).point(5, -5) + .hole() .point(-4, -4).point(-4, 4).point(4, 4).point(4, -4) .close() .close(); - data = inverse.toXContent("area", jsonBuilder().startObject()).endObject().bytes(); + data = jsonBuilder().startObject().field("area", inverse).endObject().bytes(); client().prepareIndex("shapes", "polygon", "2").setSource(data).execute().actionGet(); client().admin().indices().prepareRefresh().execute().actionGet(); @@ -341,28 +342,53 @@ public class GeoFilterTests extends AbstractSharedClusterTest { result = client().prepareSearch() .setQuery(matchAllQuery()) - .setFilter(FilterBuilders.geoWithinFilter("area", builder.build())) + .setFilter(FilterBuilders.geoWithinFilter("area", builder)) .execute().actionGet(); assertHitCount(result, 2); } -/* TODO: fix Polygon builder! It is not possible to cross the lats -180 and 180. - * A simple solution is following the path that is currently set up. When - * it's crossing the 180° lat set the new point to the intersection of line- - * segment and longitude and start building a new Polygon on the other side - * of the latitude. When crossing the latitude again continue drawing the - * first polygon. This approach can also applied to the holes because the - * commonline of hole and polygon will not be recognized as intersection. - */ + // Create a polygon crossing longitude 180. + builder = ShapeBuilder.newPolygon() + .point(170, -10).point(190, -10).point(190, 10).point(170, 10) + .close(); -// // Create a polygon crossing longitude 180. -// builder = ShapeBuilder.newPolygon() -// .point(170, -10).point(180, 10).point(170, -10).point(10, -10) -// .close(); -// -// data = builder.toXContent("area", jsonBuilder().startObject()).endObject().bytes(); -// client().prepareIndex("shapes", "polygon", "1").setSource(data).execute().actionGet(); -// client().admin().indices().prepareRefresh().execute().actionGet(); + data = jsonBuilder().startObject().field("area", builder).endObject().bytes(); + client().prepareIndex("shapes", "polygon", "1").setSource(data).execute().actionGet(); + client().admin().indices().prepareRefresh().execute().actionGet(); + + // Create a polygon crossing longitude 180 with hole. + builder = ShapeBuilder.newPolygon() + .point(170, -10).point(190, -10).point(190, 10).point(170, 10) + .hole().point(175, -5).point(185,-5).point(185,5).point(175,5).close() + .close(); + + data = jsonBuilder().startObject().field("area", builder).endObject().bytes(); + client().prepareIndex("shapes", "polygon", "1").setSource(data).execute().actionGet(); + client().admin().indices().prepareRefresh().execute().actionGet(); + + result = client().prepareSearch() + .setQuery(matchAllQuery()) + .setFilter(FilterBuilders.geoIntersectionFilter("area", ShapeBuilder.newPoint(174, -4))) + .execute().actionGet(); + assertHitCount(result, 1); + + result = client().prepareSearch() + .setQuery(matchAllQuery()) + .setFilter(FilterBuilders.geoIntersectionFilter("area", ShapeBuilder.newPoint(-174, -4))) + .execute().actionGet(); + assertHitCount(result, 1); + + result = client().prepareSearch() + .setQuery(matchAllQuery()) + .setFilter(FilterBuilders.geoIntersectionFilter("area", ShapeBuilder.newPoint(180, -4))) + .execute().actionGet(); + assertHitCount(result, 0); + + result = client().prepareSearch() + .setQuery(matchAllQuery()) + .setFilter(FilterBuilders.geoIntersectionFilter("area", ShapeBuilder.newPoint(180, -6))) + .execute().actionGet(); + assertHitCount(result, 1); } @Test diff --git a/src/test/java/org/elasticsearch/test/integration/search/geo/GeoShapeIntegrationTests.java b/src/test/java/org/elasticsearch/test/integration/search/geo/GeoShapeIntegrationTests.java index 24b72202a5f..8186bf945f4 100644 --- a/src/test/java/org/elasticsearch/test/integration/search/geo/GeoShapeIntegrationTests.java +++ b/src/test/java/org/elasticsearch/test/integration/search/geo/GeoShapeIntegrationTests.java @@ -19,7 +19,6 @@ package org.elasticsearch.test.integration.search.geo; -import static org.elasticsearch.common.geo.ShapeBuilder.newRectangle; import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; import static org.elasticsearch.index.query.FilterBuilders.geoIntersectionFilter; import static org.elasticsearch.index.query.QueryBuilders.filteredQuery; @@ -35,15 +34,13 @@ import java.util.List; import java.util.Map; import org.elasticsearch.action.search.SearchResponse; -import org.elasticsearch.common.geo.GeoJSONShapeSerializer; +import org.elasticsearch.common.geo.builders.ShapeBuilder; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentFactory; import org.elasticsearch.index.query.QueryBuilders; import org.elasticsearch.test.integration.AbstractSharedClusterTest; import org.testng.annotations.Test; -import com.spatial4j.core.shape.Shape; - public class GeoShapeIntegrationTests extends AbstractSharedClusterTest { @Test @@ -74,7 +71,9 @@ public class GeoShapeIntegrationTests extends AbstractSharedClusterTest { .endObject()).execute().actionGet(); refresh(); - Shape shape = newRectangle().topLeft(-45, 45).bottomRight(45, -45).build(); + client().admin().indices().prepareRefresh().execute().actionGet(); + + ShapeBuilder shape = ShapeBuilder.newEnvelope().topLeft(-45, 45).bottomRight(45, -45); SearchResponse searchResponse = client().prepareSearch() .setQuery(filteredQuery(matchAllQuery(), @@ -121,7 +120,7 @@ public class GeoShapeIntegrationTests extends AbstractSharedClusterTest { client().admin().indices().prepareRefresh().execute().actionGet(); - Shape query = newRectangle().topLeft(-122.88, 48.62).bottomRight(-122.82, 48.54).build(); + ShapeBuilder query = ShapeBuilder.newEnvelope().topLeft(-122.88, 48.62).bottomRight(-122.82, 48.54); // This search would fail if both geoshape indexing and geoshape filtering // used the bottom-level optimization in SpatialPrefixTree#recursiveGetNodes. @@ -156,10 +155,9 @@ public class GeoShapeIntegrationTests extends AbstractSharedClusterTest { refresh(); - Shape shape = newRectangle().topLeft(-45, 45).bottomRight(45, -45).build(); + ShapeBuilder shape = ShapeBuilder.newEnvelope().topLeft(-45, 45).bottomRight(45, -45); XContentBuilder shapeContent = jsonBuilder().startObject() - .startObject("shape"); - GeoJSONShapeSerializer.serialize(shape, shapeContent); + .field("shape", shape); shapeContent.endObject(); createIndex("shapes"); ensureGreen(); @@ -184,6 +182,26 @@ public class GeoShapeIntegrationTests extends AbstractSharedClusterTest { assertThat(searchResponse.getHits().getAt(0).id(), equalTo("1")); } + @Test + public void testReusableBuilder() throws IOException { + ShapeBuilder polygon = ShapeBuilder.newPolygon() + .point(170, -10).point(190, -10).point(190, 10).point(170, 10) + .hole().point(175, -5).point(185,-5).point(185,5).point(175,5).close() + .close(); + assertUnmodified(polygon); + + ShapeBuilder linestring = ShapeBuilder.newLineString() + .point(170, -10).point(190, -10).point(190, 10).point(170, 10); + assertUnmodified(linestring); + } + + private void assertUnmodified(ShapeBuilder builder) throws IOException { + String before = jsonBuilder().startObject().field("area", builder).endObject().string(); + builder.build(); + String after = jsonBuilder().startObject().field("area", builder).endObject().string(); + assertThat(before, equalTo(after)); + } + @Test public void testParsingMultipleShapes() throws IOException { String mapping = XContentFactory.jsonBuilder() diff --git a/src/test/java/org/elasticsearch/test/unit/common/geo/GeoJSONShapeParserTests.java b/src/test/java/org/elasticsearch/test/unit/common/geo/GeoJSONShapeParserTests.java index 99d1e05b197..c5f8f5ee029 100644 --- a/src/test/java/org/elasticsearch/test/unit/common/geo/GeoJSONShapeParserTests.java +++ b/src/test/java/org/elasticsearch/test/unit/common/geo/GeoJSONShapeParserTests.java @@ -1,21 +1,49 @@ +/* + * Licensed to ElasticSearch and Shay Banon 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.test.unit.common.geo; -import com.spatial4j.core.shape.Shape; -import com.spatial4j.core.shape.jts.JtsGeometry; -import com.spatial4j.core.shape.jts.JtsPoint; -import com.vividsolutions.jts.geom.*; -import org.elasticsearch.common.geo.GeoJSONShapeParser; -import org.elasticsearch.common.geo.GeoShapeConstants; -import org.elasticsearch.common.xcontent.XContentFactory; -import org.elasticsearch.common.xcontent.XContentParser; -import org.elasticsearch.common.xcontent.json.JsonXContent; -import org.testng.annotations.Test; +import static org.testng.Assert.assertEquals; import java.io.IOException; import java.util.ArrayList; import java.util.List; -import static org.testng.Assert.assertEquals; +import org.elasticsearch.common.geo.builders.ShapeBuilder; +import org.elasticsearch.common.xcontent.XContentFactory; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.common.xcontent.json.JsonXContent; +import org.testng.annotations.Test; + +import com.spatial4j.core.shape.Shape; +import com.spatial4j.core.shape.jts.JtsGeometry; +import com.spatial4j.core.shape.jts.JtsPoint; +import com.vividsolutions.jts.geom.Coordinate; +import com.vividsolutions.jts.geom.GeometryFactory; +import com.vividsolutions.jts.geom.LineString; +import com.vividsolutions.jts.geom.LinearRing; +import com.vividsolutions.jts.geom.MultiPoint; +import com.vividsolutions.jts.geom.MultiPolygon; +import com.vividsolutions.jts.geom.Point; +import com.vividsolutions.jts.geom.Polygon; + +import static org.elasticsearch.test.hamcrest.ElasticsearchGeoAssertions.assertEquals; /** * Tests for {@link GeoJSONShapeParser} @@ -31,7 +59,7 @@ public class GeoJSONShapeParserTests { .endObject().string(); Point expected = GEOMETRY_FACTORY.createPoint(new Coordinate(100.0, 0.0)); - assertGeometryEquals(new JtsPoint(expected, GeoShapeConstants.SPATIAL_CONTEXT), pointGeoJson); + assertGeometryEquals(new JtsPoint(expected, ShapeBuilder.SPATIAL_CONTEXT), pointGeoJson); } @Test @@ -49,7 +77,7 @@ public class GeoJSONShapeParserTests { LineString expected = GEOMETRY_FACTORY.createLineString( lineCoordinates.toArray(new Coordinate[lineCoordinates.size()])); - assertGeometryEquals(new JtsGeometry(expected, GeoShapeConstants.SPATIAL_CONTEXT, false), lineGeoJson); + assertGeometryEquals(new JtsGeometry(expected, ShapeBuilder.SPATIAL_CONTEXT, false), lineGeoJson); } @Test @@ -57,11 +85,11 @@ public class GeoJSONShapeParserTests { String polygonGeoJson = XContentFactory.jsonBuilder().startObject().field("type", "Polygon") .startArray("coordinates") .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(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().string(); @@ -73,10 +101,9 @@ public class GeoJSONShapeParserTests { shellCoordinates.add(new Coordinate(100, 1)); shellCoordinates.add(new Coordinate(100, 0)); - LinearRing shell = GEOMETRY_FACTORY.createLinearRing( - shellCoordinates.toArray(new Coordinate[shellCoordinates.size()])); + LinearRing shell = GEOMETRY_FACTORY.createLinearRing(shellCoordinates.toArray(new Coordinate[shellCoordinates.size()])); Polygon expected = GEOMETRY_FACTORY.createPolygon(shell, null); - assertGeometryEquals(new JtsGeometry(expected, GeoShapeConstants.SPATIAL_CONTEXT, false), polygonGeoJson); + assertGeometryEquals(new JtsGeometry(expected, ShapeBuilder.SPATIAL_CONTEXT, false), polygonGeoJson); } @Test @@ -84,18 +111,18 @@ public class GeoJSONShapeParserTests { String polygonGeoJson = XContentFactory.jsonBuilder().startObject().field("type", "Polygon") .startArray("coordinates") .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(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() - .startArray().value(100.2).value(0.2).endArray() .endArray() .endArray() .endObject().string(); @@ -120,7 +147,7 @@ public class GeoJSONShapeParserTests { holes[0] = GEOMETRY_FACTORY.createLinearRing( holeCoordinates.toArray(new Coordinate[holeCoordinates.size()])); Polygon expected = GEOMETRY_FACTORY.createPolygon(shell, holes); - assertGeometryEquals(new JtsGeometry(expected, GeoShapeConstants.SPATIAL_CONTEXT, false), polygonGeoJson); + assertGeometryEquals(new JtsGeometry(expected, ShapeBuilder.SPATIAL_CONTEXT, false), polygonGeoJson); } @Test @@ -138,7 +165,7 @@ public class GeoJSONShapeParserTests { MultiPoint expected = GEOMETRY_FACTORY.createMultiPoint( multiPointCoordinates.toArray(new Coordinate[multiPointCoordinates.size()])); - assertGeometryEquals(new JtsGeometry(expected, GeoShapeConstants.SPATIAL_CONTEXT, false), multiPointGeoJson); + assertGeometryEquals(new JtsGeometry(expected, ShapeBuilder.SPATIAL_CONTEXT, false), multiPointGeoJson); } @Test @@ -163,11 +190,11 @@ public class GeoJSONShapeParserTests { .startArray().value(100.0).value(0.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() - .startArray().value(100.2).value(0.2).endArray() .endArray() .endArray() .endArray() @@ -187,27 +214,25 @@ public class GeoJSONShapeParserTests { holeCoordinates.add(new Coordinate(100.2, 0.8)); holeCoordinates.add(new Coordinate(100.2, 0.2)); - LinearRing shell = GEOMETRY_FACTORY.createLinearRing( - shellCoordinates.toArray(new Coordinate[shellCoordinates.size()])); + LinearRing shell = GEOMETRY_FACTORY.createLinearRing(shellCoordinates.toArray(new Coordinate[shellCoordinates.size()])); LinearRing[] holes = new LinearRing[1]; - holes[0] = GEOMETRY_FACTORY.createLinearRing( - holeCoordinates.toArray(new Coordinate[holeCoordinates.size()])); + holes[0] = GEOMETRY_FACTORY.createLinearRing(holeCoordinates.toArray(new Coordinate[holeCoordinates.size()])); Polygon withHoles = GEOMETRY_FACTORY.createPolygon(shell, holes); shellCoordinates = new ArrayList(); - shellCoordinates.add(new Coordinate(102, 2)); - shellCoordinates.add(new Coordinate(103, 2)); - shellCoordinates.add(new Coordinate(103, 3)); shellCoordinates.add(new Coordinate(102, 3)); + shellCoordinates.add(new Coordinate(103, 3)); + shellCoordinates.add(new Coordinate(103, 2)); shellCoordinates.add(new Coordinate(102, 2)); + shellCoordinates.add(new Coordinate(102, 3)); - shell = GEOMETRY_FACTORY.createLinearRing( - shellCoordinates.toArray(new Coordinate[shellCoordinates.size()])); + + shell = GEOMETRY_FACTORY.createLinearRing(shellCoordinates.toArray(new Coordinate[shellCoordinates.size()])); Polygon withoutHoles = GEOMETRY_FACTORY.createPolygon(shell, null); MultiPolygon expected = GEOMETRY_FACTORY.createMultiPolygon(new Polygon[] {withoutHoles, withHoles}); - assertGeometryEquals(new JtsGeometry(expected, GeoShapeConstants.SPATIAL_CONTEXT, false), multiPolygonGeoJson); + assertGeometryEquals(new JtsGeometry(expected, ShapeBuilder.SPATIAL_CONTEXT, false), multiPolygonGeoJson); } @Test @@ -228,12 +253,13 @@ public class GeoJSONShapeParserTests { .endObject().string(); Point expected = GEOMETRY_FACTORY.createPoint(new Coordinate(100.0, 0.0)); - assertGeometryEquals(new JtsPoint(expected, GeoShapeConstants.SPATIAL_CONTEXT), pointGeoJson); + assertGeometryEquals(new JtsPoint(expected, ShapeBuilder.SPATIAL_CONTEXT), pointGeoJson); } private void assertGeometryEquals(Shape expected, String geoJson) throws IOException { XContentParser parser = JsonXContent.jsonXContent.createParser(geoJson); parser.nextToken(); - assertEquals(GeoJSONShapeParser.parse(parser), expected); + assertEquals(ShapeBuilder.parse(parser).build(), expected); } + } diff --git a/src/test/java/org/elasticsearch/test/unit/common/geo/GeoJSONShapeSerializerTests.java b/src/test/java/org/elasticsearch/test/unit/common/geo/GeoJSONShapeSerializerTests.java deleted file mode 100644 index 06f32a55a49..00000000000 --- a/src/test/java/org/elasticsearch/test/unit/common/geo/GeoJSONShapeSerializerTests.java +++ /dev/null @@ -1,224 +0,0 @@ -package org.elasticsearch.test.unit.common.geo; - -import com.spatial4j.core.shape.Shape; -import com.spatial4j.core.shape.jts.JtsGeometry; -import com.spatial4j.core.shape.jts.JtsPoint; -import com.vividsolutions.jts.geom.*; -import org.elasticsearch.common.geo.GeoJSONShapeSerializer; -import org.elasticsearch.common.geo.GeoShapeConstants; -import org.elasticsearch.common.xcontent.XContentBuilder; -import org.elasticsearch.common.xcontent.XContentFactory; -import org.testng.annotations.Test; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; - -import static org.testng.Assert.assertEquals; - -/** - * Tests for {@link GeoJSONShapeSerializer} - */ -public class GeoJSONShapeSerializerTests { - - private static final GeometryFactory GEOMETRY_FACTORY = new GeometryFactory(); - - @Test - public void testSerialize_simplePoint() throws IOException { - XContentBuilder expected = XContentFactory.jsonBuilder().startObject().field("type", "Point") - .startArray("coordinates").value(100.0).value(0.0).endArray() - .endObject(); - - Point point = GEOMETRY_FACTORY.createPoint(new Coordinate(100.0, 0.0)); - assertSerializationEquals(expected, new JtsPoint(point, GeoShapeConstants.SPATIAL_CONTEXT)); - } - - @Test - public void testSerialize_lineString() throws IOException { - XContentBuilder expected = 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(); - - List lineCoordinates = new ArrayList(); - lineCoordinates.add(new Coordinate(100, 0)); - lineCoordinates.add(new Coordinate(101, 1)); - - LineString lineString = GEOMETRY_FACTORY.createLineString( - lineCoordinates.toArray(new Coordinate[lineCoordinates.size()])); - - assertSerializationEquals(expected, new JtsGeometry(lineString, GeoShapeConstants.SPATIAL_CONTEXT, false)); - } - - @Test - public void testSerialize_polygonNoHoles() throws IOException { - XContentBuilder expected = XContentFactory.jsonBuilder().startObject().field("type", "Polygon") - .startArray("coordinates") - .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() - .endArray() - .endObject(); - - List shellCoordinates = new ArrayList(); - shellCoordinates.add(new Coordinate(100, 0)); - shellCoordinates.add(new Coordinate(101, 0)); - shellCoordinates.add(new Coordinate(101, 1)); - shellCoordinates.add(new Coordinate(100, 1)); - shellCoordinates.add(new Coordinate(100, 0)); - - LinearRing shell = GEOMETRY_FACTORY.createLinearRing( - shellCoordinates.toArray(new Coordinate[shellCoordinates.size()])); - Polygon polygon = GEOMETRY_FACTORY.createPolygon(shell, null); - - assertSerializationEquals(expected, new JtsGeometry(polygon, GeoShapeConstants.SPATIAL_CONTEXT, false)); - } - - @Test - public void testSerialize_polygonWithHole() throws IOException { - XContentBuilder expected = XContentFactory.jsonBuilder().startObject().field("type", "Polygon") - .startArray("coordinates") - .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() - .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() - .startArray().value(100.2).value(0.2).endArray() - .endArray() - .endArray() - .endObject(); - - List shellCoordinates = new ArrayList(); - shellCoordinates.add(new Coordinate(100, 0)); - shellCoordinates.add(new Coordinate(101, 0)); - shellCoordinates.add(new Coordinate(101, 1)); - shellCoordinates.add(new Coordinate(100, 1)); - shellCoordinates.add(new Coordinate(100, 0)); - - List holeCoordinates = new ArrayList(); - holeCoordinates.add(new Coordinate(100.2, 0.2)); - holeCoordinates.add(new Coordinate(100.8, 0.2)); - holeCoordinates.add(new Coordinate(100.8, 0.8)); - holeCoordinates.add(new Coordinate(100.2, 0.8)); - holeCoordinates.add(new Coordinate(100.2, 0.2)); - - LinearRing shell = GEOMETRY_FACTORY.createLinearRing( - shellCoordinates.toArray(new Coordinate[shellCoordinates.size()])); - LinearRing[] holes = new LinearRing[1]; - holes[0] = GEOMETRY_FACTORY.createLinearRing( - holeCoordinates.toArray(new Coordinate[holeCoordinates.size()])); - Polygon polygon = GEOMETRY_FACTORY.createPolygon(shell, holes); - - assertSerializationEquals(expected, new JtsGeometry(polygon, GeoShapeConstants.SPATIAL_CONTEXT, false)); - } - - @Test - public void testSerialize_multiPoint() throws IOException { - XContentBuilder expected = 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(); - - List multiPointCoordinates = new ArrayList(); - multiPointCoordinates.add(new Coordinate(100, 0)); - multiPointCoordinates.add(new Coordinate(101, 1)); - - MultiPoint multiPoint = GEOMETRY_FACTORY.createMultiPoint( - multiPointCoordinates.toArray(new Coordinate[multiPointCoordinates.size()])); - - assertSerializationEquals(expected, new JtsGeometry(multiPoint, GeoShapeConstants.SPATIAL_CONTEXT, false)); - } - - @Test - public void testSerialize_multiPolygon() throws IOException { - XContentBuilder expected = XContentFactory.jsonBuilder().startObject().field("type", "MultiPolygon") - .startArray("coordinates") - .startArray() - .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() - .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() - .startArray().value(100.2).value(0.2).endArray() - .endArray() - .endArray() - .startArray() - .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() - - .endArray() - .endObject(); - - List shellCoordinates = new ArrayList(); - shellCoordinates.add(new Coordinate(100, 0)); - shellCoordinates.add(new Coordinate(101, 0)); - shellCoordinates.add(new Coordinate(101, 1)); - shellCoordinates.add(new Coordinate(100, 1)); - shellCoordinates.add(new Coordinate(100, 0)); - - List holeCoordinates = new ArrayList(); - holeCoordinates.add(new Coordinate(100.2, 0.2)); - holeCoordinates.add(new Coordinate(100.8, 0.2)); - holeCoordinates.add(new Coordinate(100.8, 0.8)); - holeCoordinates.add(new Coordinate(100.2, 0.8)); - holeCoordinates.add(new Coordinate(100.2, 0.2)); - - LinearRing shell = GEOMETRY_FACTORY.createLinearRing( - shellCoordinates.toArray(new Coordinate[shellCoordinates.size()])); - LinearRing[] holes = new LinearRing[1]; - holes[0] = GEOMETRY_FACTORY.createLinearRing( - holeCoordinates.toArray(new Coordinate[holeCoordinates.size()])); - Polygon withHoles = GEOMETRY_FACTORY.createPolygon(shell, holes); - - shellCoordinates = new ArrayList(); - shellCoordinates.add(new Coordinate(102, 2)); - shellCoordinates.add(new Coordinate(103, 2)); - shellCoordinates.add(new Coordinate(103, 3)); - shellCoordinates.add(new Coordinate(102, 3)); - shellCoordinates.add(new Coordinate(102, 2)); - - shell = GEOMETRY_FACTORY.createLinearRing( - shellCoordinates.toArray(new Coordinate[shellCoordinates.size()])); - Polygon withoutHoles = GEOMETRY_FACTORY.createPolygon(shell, null); - - MultiPolygon multiPolygon = GEOMETRY_FACTORY.createMultiPolygon(new Polygon[] {withHoles, withoutHoles}); - - assertSerializationEquals(expected, new JtsGeometry(multiPolygon, GeoShapeConstants.SPATIAL_CONTEXT, false)); - } - - private void assertSerializationEquals(XContentBuilder expected, Shape shape) throws IOException { - XContentBuilder builder = XContentFactory.jsonBuilder(); - builder.startObject(); - GeoJSONShapeSerializer.serialize(shape, builder); - builder.endObject(); - assertEquals(expected.string(), builder.string()); - } -} diff --git a/src/test/java/org/elasticsearch/test/unit/common/geo/ShapeBuilderTests.java b/src/test/java/org/elasticsearch/test/unit/common/geo/ShapeBuilderTests.java index e397bfa7288..5f41567915f 100644 --- a/src/test/java/org/elasticsearch/test/unit/common/geo/ShapeBuilderTests.java +++ b/src/test/java/org/elasticsearch/test/unit/common/geo/ShapeBuilderTests.java @@ -1,17 +1,37 @@ +/* + * Licensed to ElasticSearch and Shay Banon 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.test.unit.common.geo; +import static org.testng.Assert.assertEquals; + +import org.elasticsearch.common.geo.builders.ShapeBuilder; +import org.testng.annotations.Test; + import com.spatial4j.core.shape.Point; import com.spatial4j.core.shape.Rectangle; import com.spatial4j.core.shape.Shape; import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.Geometry; import com.vividsolutions.jts.geom.LineString; import com.vividsolutions.jts.geom.Polygon; -import org.elasticsearch.common.geo.ShapeBuilder; -import org.testng.annotations.Test; - -import static org.testng.Assert.assertEquals; +import static org.elasticsearch.test.hamcrest.ElasticsearchGeoAssertions.*; /** * Tests for {@link ShapeBuilder} */ @@ -19,14 +39,14 @@ public class ShapeBuilderTests { @Test public void testNewPoint() { - Point point = ShapeBuilder.newPoint(-100, 45); + Point point = ShapeBuilder.newPoint(-100, 45).build(); assertEquals(-100D, point.getX()); assertEquals(45D, point.getY()); } @Test public void testNewRectangle() { - Rectangle rectangle = ShapeBuilder.newRectangle().topLeft(-45, 30).bottomRight(45, -30).build(); + Rectangle rectangle = ShapeBuilder.newEnvelope().topLeft(-45, 30).bottomRight(45, -30).build(); assertEquals(-45D, rectangle.getMinX()); assertEquals(-30D, rectangle.getMinY()); assertEquals(45D, rectangle.getMaxX()); @@ -48,26 +68,130 @@ public class ShapeBuilderTests { assertEquals(exterior.getCoordinateN(2), new Coordinate(45, -30)); assertEquals(exterior.getCoordinateN(3), new Coordinate(-45, -30)); } + + @Test + public void testLineStringBuilder() { + // Building a simple LineString + ShapeBuilder.newLineString() + .point(-130.0, 55.0) + .point(-130.0, -40.0) + .point(-15.0, -40.0) + .point(-20.0, 50.0) + .point(-45.0, 50.0) + .point(-45.0, -15.0) + .point(-110.0, -15.0) + .point(-110.0, 55.0).build(); + + // Building a linestring that needs to be wrapped + ShapeBuilder.newLineString() + .point(100.0, 50.0) + .point(110.0, -40.0) + .point(240.0, -40.0) + .point(230.0, 60.0) + .point(200.0, 60.0) + .point(200.0, -30.0) + .point(130.0, -30.0) + .point(130.0, 60.0) + .build(); + + // Building a lineString on the dateline + ShapeBuilder.newLineString() + .point(-180.0, 80.0) + .point(-180.0, 40.0) + .point(-180.0, -40.0) + .point(-180.0, -80.0) + .build(); + + // Building a lineString on the dateline + ShapeBuilder.newLineString() + .point(180.0, 80.0) + .point(180.0, 40.0) + .point(180.0, -40.0) + .point(180.0, -80.0) + .build(); + } @Test - public void testToJTSGeometry() { - ShapeBuilder.PolygonBuilder polygonBuilder = ShapeBuilder.newPolygon() - .point(-45, 30) - .point(45, 30) - .point(45, -30) - .point(-45, -30) - .close(); + public void testMultiLineString() { + ShapeBuilder.newMultiLinestring() + .linestring() + .point(-100.0, 50.0) + .point(50.0, 50.0) + .point(50.0, 20.0) + .point(-100.0, 20.0) + .end() + .linestring() + .point(-100.0, 20.0) + .point(50.0, 20.0) + .point(50.0, 0.0) + .point(-100.0, 0.0) + .end() + .build(); - Shape polygon = polygonBuilder.build(); - Geometry polygonGeometry = ShapeBuilder.toJTSGeometry(polygon); - assertEquals(polygonBuilder.toPolygon(), polygonGeometry); - - Rectangle rectangle = ShapeBuilder.newRectangle().topLeft(-45, 30).bottomRight(45, -30).build(); - Geometry rectangleGeometry = ShapeBuilder.toJTSGeometry(rectangle); - assertEquals(rectangleGeometry, polygonGeometry); - - Point point = ShapeBuilder.newPoint(-45, 30); - Geometry pointGeometry = ShapeBuilder.toJTSGeometry(point); - assertEquals(pointGeometry.getCoordinate(), new Coordinate(-45, 30)); + + // LineString that needs to be wrappped + ShapeBuilder.newMultiLinestring() + .linestring() + .point(150.0, 60.0) + .point(200.0, 60.0) + .point(200.0, 40.0) + .point(150.0, 40.0) + .end() + .linestring() + .point(150.0, 20.0) + .point(200.0, 20.0) + .point(200.0, 0.0) + .point(150.0, 0.0) + .end() + .build(); } + + @Test + public void testPolygonSelfIntersection() { + try { + ShapeBuilder.newPolygon() + .point(-40.0, 50.0) + .point(40.0, 50.0) + .point(-40.0, -50.0) + .point(40.0, -50.0) + .close().build(); + assert false : "Polygon self-intersection"; + } catch (Throwable e) {} + + } + + @Test + public void testGeoCircle() { + ShapeBuilder.newCircleBuilder().center(0, 0).radius("100m").build(); + ShapeBuilder.newCircleBuilder().center(+180, 0).radius("100m").build(); + ShapeBuilder.newCircleBuilder().center(-180, 0).radius("100m").build(); + ShapeBuilder.newCircleBuilder().center(0, 90).radius("100m").build(); + ShapeBuilder.newCircleBuilder().center(0, -90).radius("100m").build(); + } + + @Test + public void testPolygonWrapping() { + Shape shape = ShapeBuilder.newPolygon() + .point(-150.0, 65.0) + .point(-250.0, 65.0) + .point(-250.0, -65.0) + .point(-150.0, -65.0) + .close().build(); + + assertMultiPolygon(shape); + } + + @Test + public void testLineStringWrapping() { + Shape shape = ShapeBuilder.newLineString() + .point(-150.0, 65.0) + .point(-250.0, 65.0) + .point(-250.0, -65.0) + .point(-150.0, -65.0) + .build(); + + assertMultiLineString(shape); + } + + }