From 8904fc821015d951944555259d1247fd7fdbe757 Mon Sep 17 00:00:00 2001 From: Nicholas Knize Date: Wed, 1 Nov 2017 12:29:52 -0500 Subject: [PATCH] [Geo] Decouple geojson parse logic from ShapeBuilders This is the first step to supporting WKT (and other future) format(s). The ShapeBuilders are quite messy and can be simplified by decoupling the parse logic from the build logic. This commit refactors the parsing logic into its own package separate from the Shape builders. It also decouples the GeoShapeType into a standalone enumerator that is responsible for validating the parsed data and providing the appropriate builder. This future-proofs the code making it easier to maintain and add new shape types. --- .../QueryDSLDocumentationTests.java | 4 +- .../common/geo/GeoShapeType.java | 316 ++++++++++ .../common/geo/builders/CircleBuilder.java | 13 +- .../geo/builders/CoordinateCollection.java | 155 ----- .../common/geo/builders/EnvelopeBuilder.java | 8 +- .../builders/GeometryCollectionBuilder.java | 6 +- .../geo/builders/LineStringBuilder.java | 30 +- .../geo/builders/MultiLineStringBuilder.java | 18 +- .../geo/builders/MultiPointBuilder.java | 27 +- .../geo/builders/MultiPolygonBuilder.java | 8 +- .../common/geo/builders/PointBuilder.java | 70 +-- .../common/geo/builders/PolygonBuilder.java | 13 +- .../common/geo/builders/ShapeBuilder.java | 544 +++++------------- .../common/geo/builders/ShapeBuilders.java | 153 ----- .../common/geo/parsers/CoordinateNode.java | 81 +++ .../common/geo/parsers/GeoJsonParser.java | 194 +++++++ .../common/geo/parsers/ShapeParser.java | 68 +++ .../index/mapper/GeoShapeFieldMapper.java | 5 +- .../index/query/GeoShapeQueryBuilder.java | 5 +- .../elasticsearch/search/SearchModule.java | 5 +- .../common/geo/BaseGeoParsingTestCase.java | 40 ++ ...ests.java => GeoJsonShapeParserTests.java} | 48 +- .../common/geo/ShapeBuilderTests.java | 79 +-- .../AbstractShapeBuilderTestCase.java | 15 +- .../geo/builders/LineStringBuilderTests.java | 4 +- .../builders/MultiLineStringBuilderTests.java | 2 +- .../geo/builders/MultiPointBuilderTests.java | 2 +- .../index/mapper/ExternalMapper.java | 6 +- .../ExternalValuesMapperIntegrationIT.java | 4 +- .../query/GeoShapeQueryBuilderTests.java | 3 +- .../elasticsearch/search/geo/GeoFilterIT.java | 49 +- .../search/geo/GeoShapeQueryTests.java | 41 +- .../test/geo/RandomShapeGenerator.java | 3 +- .../hamcrest/ElasticsearchGeoAssertions.java | 4 +- 34 files changed, 1058 insertions(+), 965 deletions(-) create mode 100644 core/src/main/java/org/elasticsearch/common/geo/GeoShapeType.java delete mode 100644 core/src/main/java/org/elasticsearch/common/geo/builders/CoordinateCollection.java delete mode 100644 core/src/main/java/org/elasticsearch/common/geo/builders/ShapeBuilders.java create mode 100644 core/src/main/java/org/elasticsearch/common/geo/parsers/CoordinateNode.java create mode 100644 core/src/main/java/org/elasticsearch/common/geo/parsers/GeoJsonParser.java create mode 100644 core/src/main/java/org/elasticsearch/common/geo/parsers/ShapeParser.java create mode 100644 core/src/test/java/org/elasticsearch/common/geo/BaseGeoParsingTestCase.java rename core/src/test/java/org/elasticsearch/common/geo/{GeoJSONShapeParserTests.java => GeoJsonShapeParserTests.java} (97%) diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/QueryDSLDocumentationTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/QueryDSLDocumentationTests.java index 4e5d024f74a..7e84b266f7f 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/QueryDSLDocumentationTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/QueryDSLDocumentationTests.java @@ -23,7 +23,7 @@ import org.apache.lucene.search.join.ScoreMode; import org.elasticsearch.common.geo.GeoPoint; import org.elasticsearch.common.geo.ShapeRelation; import org.elasticsearch.common.geo.builders.CoordinatesBuilder; -import org.elasticsearch.common.geo.builders.ShapeBuilders; +import org.elasticsearch.common.geo.builders.MultiPointBuilder; import org.elasticsearch.common.unit.DistanceUnit; import org.elasticsearch.index.query.GeoShapeQueryBuilder; import org.elasticsearch.index.query.functionscore.FunctionScoreQueryBuilder; @@ -189,7 +189,7 @@ public class QueryDSLDocumentationTests extends ESTestCase { // tag::geo_shape GeoShapeQueryBuilder qb = geoShapeQuery( "pin.location", // <1> - ShapeBuilders.newMultiPoint( // <2> + new MultiPointBuilder( // <2> new CoordinatesBuilder() .coordinate(0, 0) .coordinate(0, 10) diff --git a/core/src/main/java/org/elasticsearch/common/geo/GeoShapeType.java b/core/src/main/java/org/elasticsearch/common/geo/GeoShapeType.java new file mode 100644 index 00000000000..f8030296940 --- /dev/null +++ b/core/src/main/java/org/elasticsearch/common/geo/GeoShapeType.java @@ -0,0 +1,316 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.elasticsearch.common.geo; + +import com.vividsolutions.jts.geom.Coordinate; +import org.elasticsearch.ElasticsearchParseException; +import org.elasticsearch.common.geo.builders.CircleBuilder; +import org.elasticsearch.common.geo.builders.CoordinatesBuilder; +import org.elasticsearch.common.geo.builders.EnvelopeBuilder; +import org.elasticsearch.common.geo.builders.GeometryCollectionBuilder; +import org.elasticsearch.common.geo.builders.LineStringBuilder; +import org.elasticsearch.common.geo.builders.MultiLineStringBuilder; +import org.elasticsearch.common.geo.builders.MultiPointBuilder; +import org.elasticsearch.common.geo.builders.MultiPolygonBuilder; +import org.elasticsearch.common.geo.builders.PointBuilder; +import org.elasticsearch.common.geo.builders.PolygonBuilder; +import org.elasticsearch.common.geo.builders.ShapeBuilder; +import org.elasticsearch.common.geo.builders.ShapeBuilder.Orientation; +import org.elasticsearch.common.geo.parsers.CoordinateNode; +import org.elasticsearch.common.io.stream.NamedWriteableRegistry.Entry; +import org.elasticsearch.common.unit.DistanceUnit; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +/** + * Enumeration that lists all {@link GeoShapeType}s that can be parsed and indexed + */ +public enum GeoShapeType { + POINT("point") { + @Override + public ShapeBuilder getBuilder(CoordinateNode coordinates, DistanceUnit.Distance radius, + Orientation orientation, boolean coerce) { + return new PointBuilder().coordinate(validate(coordinates, coerce).coordinate); + } + + @Override + CoordinateNode validate(CoordinateNode coordinates, boolean coerce) { + if (coordinates.isEmpty()) { + throw new ElasticsearchParseException( + "invalid number of points (0) provided when expecting a single coordinate ([lat, lng])"); + } else if (coordinates.children != null) { + throw new ElasticsearchParseException("multipoint data provided when single point data expected."); + } + return coordinates; + } + }, + MULTIPOINT("multipoint") { + @Override + public ShapeBuilder getBuilder(CoordinateNode coordinates, DistanceUnit.Distance radius, + Orientation orientation, boolean coerce) { + validate(coordinates, coerce); + CoordinatesBuilder coordinatesBuilder = new CoordinatesBuilder(); + for (CoordinateNode node : coordinates.children) { + coordinatesBuilder.coordinate(node.coordinate); + } + return new MultiPointBuilder(coordinatesBuilder.build()); + } + + @Override + CoordinateNode validate(CoordinateNode coordinates, boolean coerce) { + if (coordinates.children == null || coordinates.children.isEmpty()) { + if (coordinates.coordinate != null) { + throw new ElasticsearchParseException("single coordinate found when expecting an array of " + + "coordinates. change type to point or change data to an array of >0 coordinates"); + } + throw new ElasticsearchParseException("no data provided for multipoint object when expecting " + + ">0 points (e.g., [[lat, lng]] or [[lat, lng], ...])"); + } else { + for (CoordinateNode point : coordinates.children) { + POINT.validate(point, coerce); + } + } + return coordinates; + } + + }, + LINESTRING("linestring") { + @Override + public ShapeBuilder getBuilder(CoordinateNode coordinates, DistanceUnit.Distance radius, + Orientation orientation, boolean coerce) { + validate(coordinates, coerce); + CoordinatesBuilder line = new CoordinatesBuilder(); + for (CoordinateNode node : coordinates.children) { + line.coordinate(node.coordinate); + } + return new LineStringBuilder(line); + } + + @Override + CoordinateNode validate(CoordinateNode coordinates, boolean coerce) { + if (coordinates.children.size() < 2) { + throw new ElasticsearchParseException("invalid number of points in LineString (found [{}] - must be >= 2)", + coordinates.children.size()); + } + return coordinates; + } + }, + MULTILINESTRING("multilinestring") { + @Override + public ShapeBuilder getBuilder(CoordinateNode coordinates, DistanceUnit.Distance radius, + Orientation orientation, boolean coerce) { + validate(coordinates, coerce); + MultiLineStringBuilder multiline = new MultiLineStringBuilder(); + for (CoordinateNode node : coordinates.children) { + multiline.linestring(LineStringBuilder.class.cast(LINESTRING.getBuilder(node, radius, orientation, coerce))); + } + return multiline; + } + + @Override + CoordinateNode validate(CoordinateNode coordinates, boolean coerce) { + if (coordinates.children.size() < 1) { + throw new ElasticsearchParseException("invalid number of lines in MultiLineString (found [{}] - must be >= 1)", + coordinates.children.size()); + } + return coordinates; + } + }, + POLYGON("polygon") { + @Override + public ShapeBuilder getBuilder(CoordinateNode coordinates, DistanceUnit.Distance radius, + Orientation orientation, boolean coerce) { + validate(coordinates, coerce); + // build shell + LineStringBuilder shell = LineStringBuilder.class.cast(LINESTRING.getBuilder(coordinates.children.get(0), + radius, orientation, coerce)); + // build polygon with shell and holes + PolygonBuilder polygon = new PolygonBuilder(shell, orientation); + for (int i = 1; i < coordinates.children.size(); ++i) { + CoordinateNode child = coordinates.children.get(i); + LineStringBuilder hole = LineStringBuilder.class.cast(LINESTRING.getBuilder(child, radius, orientation, coerce)); + polygon.hole(hole); + } + return polygon; + } + + void validateLinearRing(CoordinateNode coordinates, boolean coerce) { + if (coordinates.children == null || coordinates.children.isEmpty()) { + String error = "Invalid LinearRing found."; + error += (coordinates.coordinate == null) ? + " No coordinate array provided" : " Found a single coordinate when expecting a coordinate array"; + throw new ElasticsearchParseException(error); + } + + int numValidPts = coerce ? 3 : 4; + if (coordinates.children.size() < numValidPts) { + throw new ElasticsearchParseException("invalid number of points in LinearRing (found [{}] - must be >= [{}])", + coordinates.children.size(), numValidPts); + } + // close linear ring iff coerce is set and ring is open, otherwise throw parse exception + if (!coordinates.children.get(0).coordinate.equals( + coordinates.children.get(coordinates.children.size() - 1).coordinate)) { + if (coerce == true) { + coordinates.children.add(coordinates.children.get(0)); + } else { + throw new ElasticsearchParseException("invalid LinearRing found (coordinates are not closed)"); + } + } + } + + @Override + CoordinateNode validate(CoordinateNode coordinates, boolean coerce) { + /** + * Per GeoJSON spec (http://geojson.org/geojson-spec.html#linestring) + * A LinearRing is closed LineString with 4 or more positions. The first and last positions + * are equivalent (they represent equivalent points). Though a LinearRing is not explicitly + * represented as a GeoJSON geometry type, it is referred to in the Polygon geometry type definition. + */ + if (coordinates.children == null || coordinates.children.isEmpty()) { + throw new ElasticsearchParseException( + "invalid LinearRing provided for type polygon. Linear ring must be an array of coordinates"); + } + for (CoordinateNode ring : coordinates.children) { + validateLinearRing(ring, coerce); + } + + return coordinates; + } + }, + MULTIPOLYGON("multipolygon") { + @Override + public ShapeBuilder getBuilder(CoordinateNode coordinates, DistanceUnit.Distance radius, + Orientation orientation, boolean coerce) { + validate(coordinates, coerce); + MultiPolygonBuilder polygons = new MultiPolygonBuilder(orientation); + for (CoordinateNode node : coordinates.children) { + polygons.polygon(PolygonBuilder.class.cast(POLYGON.getBuilder(node, radius, orientation, coerce))); + } + return polygons; + } + + @Override + CoordinateNode validate(CoordinateNode coordinates, boolean coerce) { + // noop; todo validate at least 1 polygon to ensure valid multipolygon + return coordinates; + } + }, + ENVELOPE("envelope") { + @Override + public ShapeBuilder getBuilder(CoordinateNode coordinates, DistanceUnit.Distance radius, + Orientation orientation, boolean coerce) { + validate(coordinates, coerce); + // verify coordinate bounds, correct if necessary + Coordinate uL = coordinates.children.get(0).coordinate; + Coordinate lR = coordinates.children.get(1).coordinate; + if (((lR.x < uL.x) || (uL.y < lR.y))) { + Coordinate uLtmp = uL; + uL = new Coordinate(Math.min(uL.x, lR.x), Math.max(uL.y, lR.y)); + lR = new Coordinate(Math.max(uLtmp.x, lR.x), Math.min(uLtmp.y, lR.y)); + } + return new EnvelopeBuilder(uL, lR); + } + + @Override + CoordinateNode validate(CoordinateNode coordinates, boolean coerce) { + // validate the coordinate array for envelope type + if (coordinates.children.size() != 2) { + throw new ElasticsearchParseException( + "invalid number of points [{}] provided for geo_shape [{}] when expecting an array of 2 coordinates", + coordinates.children.size(), GeoShapeType.ENVELOPE.shapename); + } + return coordinates; + } + }, + CIRCLE("circle") { + @Override + public ShapeBuilder getBuilder(CoordinateNode coordinates, DistanceUnit.Distance radius, + Orientation orientation, boolean coerce) { + return new CircleBuilder().center(coordinates.coordinate).radius(radius); + + } + + @Override + CoordinateNode validate(CoordinateNode coordinates, boolean coerce) { + // noop + return coordinates; + } + }, + GEOMETRYCOLLECTION("geometrycollection") { + @Override + public ShapeBuilder getBuilder(CoordinateNode coordinates, DistanceUnit.Distance radius, + Orientation orientation, boolean coerce) { + // noop, handled in parser + return null; + } + + @Override + CoordinateNode validate(CoordinateNode coordinates, boolean coerce) { + // noop + return null; + } + }; + + private final String shapename; + private static Map shapeTypeMap = new HashMap<>(); + + static { + for (GeoShapeType type : values()) { + shapeTypeMap.put(type.shapename, type); + } + } + + GeoShapeType(String shapename) { + this.shapename = shapename; + } + + public String shapeName() { + return shapename; + } + + public static GeoShapeType forName(String geoshapename) { + String typename = geoshapename.toLowerCase(Locale.ROOT); + if (shapeTypeMap.containsKey(typename)) { + return shapeTypeMap.get(typename); + } + throw new IllegalArgumentException("unknown geo_shape ["+geoshapename+"]"); + } + + public abstract ShapeBuilder getBuilder(CoordinateNode coordinates, DistanceUnit.Distance radius, + ShapeBuilder.Orientation orientation, boolean coerce); + abstract CoordinateNode validate(CoordinateNode coordinates, boolean coerce); + + public static List getShapeWriteables() { + List namedWriteables = new ArrayList<>(); + namedWriteables.add(new Entry(ShapeBuilder.class, PointBuilder.TYPE.shapeName(), PointBuilder::new)); + namedWriteables.add(new Entry(ShapeBuilder.class, CircleBuilder.TYPE.shapeName(), CircleBuilder::new)); + namedWriteables.add(new Entry(ShapeBuilder.class, EnvelopeBuilder.TYPE.shapeName(), EnvelopeBuilder::new)); + namedWriteables.add(new Entry(ShapeBuilder.class, MultiPointBuilder.TYPE.shapeName(), MultiPointBuilder::new)); + namedWriteables.add(new Entry(ShapeBuilder.class, LineStringBuilder.TYPE.shapeName(), LineStringBuilder::new)); + namedWriteables.add(new Entry(ShapeBuilder.class, MultiLineStringBuilder.TYPE.shapeName(), MultiLineStringBuilder::new)); + namedWriteables.add(new Entry(ShapeBuilder.class, PolygonBuilder.TYPE.shapeName(), PolygonBuilder::new)); + namedWriteables.add(new Entry(ShapeBuilder.class, MultiPolygonBuilder.TYPE.shapeName(), MultiPolygonBuilder::new)); + namedWriteables.add(new Entry(ShapeBuilder.class, GeometryCollectionBuilder.TYPE.shapeName(), GeometryCollectionBuilder::new)); + return namedWriteables; + } +} diff --git a/core/src/main/java/org/elasticsearch/common/geo/builders/CircleBuilder.java b/core/src/main/java/org/elasticsearch/common/geo/builders/CircleBuilder.java index 658d8ed84c1..108e66d9150 100644 --- a/core/src/main/java/org/elasticsearch/common/geo/builders/CircleBuilder.java +++ b/core/src/main/java/org/elasticsearch/common/geo/builders/CircleBuilder.java @@ -19,6 +19,9 @@ package org.elasticsearch.common.geo.builders; +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.geo.GeoShapeType; +import org.elasticsearch.common.geo.parsers.ShapeParser; import org.locationtech.spatial4j.shape.Circle; import com.vividsolutions.jts.geom.Coordinate; @@ -31,9 +34,9 @@ import org.elasticsearch.common.xcontent.XContentBuilder; import java.io.IOException; import java.util.Objects; -public class CircleBuilder extends ShapeBuilder { +public class CircleBuilder extends ShapeBuilder { - public static final String FIELD_RADIUS = "radius"; + public static final ParseField FIELD_RADIUS = new ParseField("radius"); public static final GeoShapeType TYPE = GeoShapeType.CIRCLE; private DistanceUnit unit = DistanceUnit.DEFAULT; @@ -148,9 +151,9 @@ public class CircleBuilder extends ShapeBuilder { @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); + builder.field(ShapeParser.FIELD_TYPE.getPreferredName(), TYPE.shapeName()); + builder.field(FIELD_RADIUS.getPreferredName(), unit.toString(radius)); + builder.field(ShapeParser.FIELD_COORDINATES.getPreferredName()); toXContent(builder, center); return builder.endObject(); } diff --git a/core/src/main/java/org/elasticsearch/common/geo/builders/CoordinateCollection.java b/core/src/main/java/org/elasticsearch/common/geo/builders/CoordinateCollection.java deleted file mode 100644 index b6b9df45d04..00000000000 --- a/core/src/main/java/org/elasticsearch/common/geo/builders/CoordinateCollection.java +++ /dev/null @@ -1,155 +0,0 @@ -/* - * Licensed to Elasticsearch under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -package org.elasticsearch.common.geo.builders; - -import com.vividsolutions.jts.geom.Coordinate; - -import org.elasticsearch.common.io.stream.StreamInput; -import org.elasticsearch.common.io.stream.StreamOutput; -import org.elasticsearch.common.xcontent.XContentBuilder; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.List; - -/** - * The {@link CoordinateCollection} is an abstract base implementation for {@link LineStringBuilder} and {@link MultiPointBuilder}. - * It holds a common list of {@link Coordinate}, provides setters for adding elements to the list and can render this to XContent. - */ -public abstract class CoordinateCollection> extends ShapeBuilder { - - protected final List coordinates; - - /** - * Construct a new collection of coordinates. - * @param coordinates an initial list of coordinates - * @throws IllegalArgumentException if coordinates is null or empty - */ - protected CoordinateCollection(List coordinates) { - if (coordinates == null || coordinates.size() == 0) { - throw new IllegalArgumentException("cannot create point collection with empty set of points"); - } - this.coordinates = coordinates; - } - - /** - * Read from a stream. - */ - protected CoordinateCollection(StreamInput in) throws IOException { - int size = in.readVInt(); - coordinates = new ArrayList<>(size); - for (int i=0; i < size; i++) { - coordinates.add(readFromStream(in)); - } - } - - @Override - public void writeTo(StreamOutput out) throws IOException { - out.writeVInt(coordinates.size()); - for (Coordinate point : coordinates) { - writeCoordinateTo(point, out); - } - } - - @SuppressWarnings("unchecked") - private E thisRef() { - return (E)this; - } - - /** - * Add a new coordinate to the collection - * @param longitude longitude of the coordinate - * @param latitude latitude of the coordinate - * @return this - */ - public E coordinate(double longitude, double latitude) { - return this.coordinate(new Coordinate(longitude, latitude)); - } - - /** - * Add a new coordinate to the collection - * @param coordinate coordinate of the point - * @return this - */ - public E coordinate(Coordinate coordinate) { - this.coordinates.add(coordinate); - return thisRef(); - } - - /** - * Add a array of coordinates to the collection - * - * @param coordinates array of {@link Coordinate}s to add - * @return this - */ - public E coordinates(Coordinate...coordinates) { - return this.coordinates(Arrays.asList(coordinates)); - } - - /** - * Add a collection of coordinates to the collection - * - * @param coordinates array of {@link Coordinate}s to add - * @return this - */ - public E coordinates(Collection coordinates) { - this.coordinates.addAll(coordinates); - return thisRef(); - } - - /** - * Copy all coordinate 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 = coordinates.toArray(new Coordinate[coordinates.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 - */ - protected XContentBuilder coordinatesToXcontent(XContentBuilder builder, boolean closed) throws IOException { - builder.startArray(); - for(Coordinate coord : coordinates) { - toXContent(builder, coord); - } - if(closed) { - Coordinate start = coordinates.get(0); - Coordinate end = coordinates.get(coordinates.size()-1); - if(start.x != end.x || start.y != end.y) { - toXContent(builder, coordinates.get(0)); - } - } - builder.endArray(); - return builder; - } -} diff --git a/core/src/main/java/org/elasticsearch/common/geo/builders/EnvelopeBuilder.java b/core/src/main/java/org/elasticsearch/common/geo/builders/EnvelopeBuilder.java index 5b80ceeeeea..b352aa1d924 100644 --- a/core/src/main/java/org/elasticsearch/common/geo/builders/EnvelopeBuilder.java +++ b/core/src/main/java/org/elasticsearch/common/geo/builders/EnvelopeBuilder.java @@ -19,6 +19,8 @@ package org.elasticsearch.common.geo.builders; +import org.elasticsearch.common.geo.GeoShapeType; +import org.elasticsearch.common.geo.parsers.ShapeParser; import org.locationtech.spatial4j.shape.Rectangle; import com.vividsolutions.jts.geom.Coordinate; @@ -29,7 +31,7 @@ import org.elasticsearch.common.xcontent.XContentBuilder; import java.io.IOException; import java.util.Objects; -public class EnvelopeBuilder extends ShapeBuilder { +public class EnvelopeBuilder extends ShapeBuilder { public static final GeoShapeType TYPE = GeoShapeType.ENVELOPE; @@ -71,8 +73,8 @@ public class EnvelopeBuilder extends ShapeBuilder { @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { builder.startObject(); - builder.field(FIELD_TYPE, TYPE.shapeName()); - builder.startArray(FIELD_COORDINATES); + builder.field(ShapeParser.FIELD_TYPE.getPreferredName(), TYPE.shapeName()); + builder.startArray(ShapeParser.FIELD_COORDINATES.getPreferredName()); toXContent(builder, topLeft); toXContent(builder, bottomRight); builder.endArray(); diff --git a/core/src/main/java/org/elasticsearch/common/geo/builders/GeometryCollectionBuilder.java b/core/src/main/java/org/elasticsearch/common/geo/builders/GeometryCollectionBuilder.java index de6ed35ff90..3ea422265a7 100644 --- a/core/src/main/java/org/elasticsearch/common/geo/builders/GeometryCollectionBuilder.java +++ b/core/src/main/java/org/elasticsearch/common/geo/builders/GeometryCollectionBuilder.java @@ -19,6 +19,8 @@ package org.elasticsearch.common.geo.builders; +import org.elasticsearch.common.geo.GeoShapeType; +import org.elasticsearch.common.geo.parsers.ShapeParser; import org.locationtech.spatial4j.shape.Shape; import org.elasticsearch.ElasticsearchException; @@ -125,8 +127,8 @@ public class GeometryCollectionBuilder extends ShapeBuilder { @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { builder.startObject(); - builder.field(FIELD_TYPE, TYPE.shapeName()); - builder.startArray(FIELD_GEOMETRIES); + builder.field(ShapeParser.FIELD_TYPE.getPreferredName(), TYPE.shapeName()); + builder.startArray(ShapeParser.FIELD_GEOMETRIES.getPreferredName()); for (ShapeBuilder shape : shapes) { shape.toXContent(builder, params); } diff --git a/core/src/main/java/org/elasticsearch/common/geo/builders/LineStringBuilder.java b/core/src/main/java/org/elasticsearch/common/geo/builders/LineStringBuilder.java index e79578d9ab2..c595c126f7a 100644 --- a/core/src/main/java/org/elasticsearch/common/geo/builders/LineStringBuilder.java +++ b/core/src/main/java/org/elasticsearch/common/geo/builders/LineStringBuilder.java @@ -24,17 +24,18 @@ import com.vividsolutions.jts.geom.Geometry; import com.vividsolutions.jts.geom.GeometryFactory; import com.vividsolutions.jts.geom.LineString; +import org.elasticsearch.common.geo.GeoShapeType; +import org.elasticsearch.common.geo.parsers.ShapeParser; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.xcontent.XContentBuilder; -import org.locationtech.spatial4j.shape.Shape; +import org.locationtech.spatial4j.shape.jts.JtsGeometry; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import java.util.Objects; -public class LineStringBuilder extends CoordinateCollection { +public class LineStringBuilder extends ShapeBuilder { public static final GeoShapeType TYPE = GeoShapeType.LINESTRING; /** @@ -65,8 +66,8 @@ public class LineStringBuilder extends CoordinateCollection { @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { builder.startObject(); - builder.field(FIELD_TYPE, TYPE.shapeName()); - builder.field(FIELD_COORDINATES); + builder.field(ShapeParser.FIELD_TYPE.getPreferredName(), TYPE.shapeName()); + builder.field(ShapeParser.FIELD_COORDINATES.getPreferredName()); coordinatesToXcontent(builder, false); builder.endObject(); return builder; @@ -91,7 +92,7 @@ public class LineStringBuilder extends CoordinateCollection { } @Override - public Shape build() { + public JtsGeometry build() { Coordinate[] coordinates = this.coordinates.toArray(new Coordinate[this.coordinates.size()]); Geometry geometry; if(wrapdateline) { @@ -168,21 +169,4 @@ public class LineStringBuilder extends CoordinateCollection { } return coordinates; } - - @Override - public int hashCode() { - return Objects.hash(coordinates); - } - - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (obj == null || getClass() != obj.getClass()) { - return false; - } - LineStringBuilder other = (LineStringBuilder) obj; - return Objects.equals(coordinates, other.coordinates); - } } diff --git a/core/src/main/java/org/elasticsearch/common/geo/builders/MultiLineStringBuilder.java b/core/src/main/java/org/elasticsearch/common/geo/builders/MultiLineStringBuilder.java index 04e25862c8b..1a4f71da2d4 100644 --- a/core/src/main/java/org/elasticsearch/common/geo/builders/MultiLineStringBuilder.java +++ b/core/src/main/java/org/elasticsearch/common/geo/builders/MultiLineStringBuilder.java @@ -19,6 +19,8 @@ package org.elasticsearch.common.geo.builders; +import org.elasticsearch.common.geo.GeoShapeType; +import org.elasticsearch.common.geo.parsers.ShapeParser; import org.locationtech.spatial4j.shape.Shape; import com.vividsolutions.jts.geom.Coordinate; import com.vividsolutions.jts.geom.Geometry; @@ -27,21 +29,19 @@ import com.vividsolutions.jts.geom.LineString; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.xcontent.XContentBuilder; +import org.locationtech.spatial4j.shape.jts.JtsGeometry; import java.io.IOException; import java.util.ArrayList; import java.util.Iterator; import java.util.Objects; -public class MultiLineStringBuilder extends ShapeBuilder { +public class MultiLineStringBuilder extends ShapeBuilder { public static final GeoShapeType TYPE = GeoShapeType.MULTILINESTRING; private final ArrayList lines = new ArrayList<>(); - public MultiLineStringBuilder() { - } - /** * Read from a stream. */ @@ -52,6 +52,10 @@ public class MultiLineStringBuilder extends ShapeBuilder { } } + public MultiLineStringBuilder() { + super(); + } + @Override public void writeTo(StreamOutput out) throws IOException { out.writeVInt(lines.size()); @@ -81,8 +85,8 @@ public class MultiLineStringBuilder extends ShapeBuilder { @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { builder.startObject(); - builder.field(FIELD_TYPE, TYPE.shapeName()); - builder.field(FIELD_COORDINATES); + builder.field(ShapeParser.FIELD_TYPE.getPreferredName(), TYPE.shapeName()); + builder.field(ShapeParser.FIELD_COORDINATES.getPreferredName()); builder.startArray(); for(LineStringBuilder line : lines) { line.coordinatesToXcontent(builder, false); @@ -93,7 +97,7 @@ public class MultiLineStringBuilder extends ShapeBuilder { } @Override - public Shape build() { + public JtsGeometry build() { final Geometry geometry; if(wrapdateline) { ArrayList parts = new ArrayList<>(); diff --git a/core/src/main/java/org/elasticsearch/common/geo/builders/MultiPointBuilder.java b/core/src/main/java/org/elasticsearch/common/geo/builders/MultiPointBuilder.java index f8a06244362..ae38126f87b 100644 --- a/core/src/main/java/org/elasticsearch/common/geo/builders/MultiPointBuilder.java +++ b/core/src/main/java/org/elasticsearch/common/geo/builders/MultiPointBuilder.java @@ -21,7 +21,9 @@ package org.elasticsearch.common.geo.builders; import com.vividsolutions.jts.geom.Coordinate; +import org.elasticsearch.common.geo.GeoShapeType; import org.elasticsearch.common.geo.XShapeCollection; +import org.elasticsearch.common.geo.parsers.ShapeParser; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.xcontent.XContentBuilder; import org.locationtech.spatial4j.shape.Point; @@ -32,7 +34,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Objects; -public class MultiPointBuilder extends CoordinateCollection { +public class MultiPointBuilder extends ShapeBuilder, MultiPointBuilder> { public static final GeoShapeType TYPE = GeoShapeType.MULTIPOINT; @@ -54,15 +56,15 @@ public class MultiPointBuilder extends CoordinateCollection { @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { builder.startObject(); - builder.field(FIELD_TYPE, TYPE.shapeName()); - builder.field(FIELD_COORDINATES); + builder.field(ShapeParser.FIELD_TYPE.getPreferredName(), TYPE.shapeName()); + builder.field(ShapeParser.FIELD_COORDINATES.getPreferredName()); super.coordinatesToXcontent(builder, false); builder.endObject(); return builder; } @Override - public Shape build() { + public XShapeCollection build() { //Could wrap JtsGeometry but probably slower due to conversions to/from JTS in relate() //MultiPoint geometry = FACTORY.createMultiPoint(points.toArray(new Coordinate[points.size()])); List shapes = new ArrayList<>(coordinates.size()); @@ -78,21 +80,4 @@ public class MultiPointBuilder extends CoordinateCollection { public GeoShapeType type() { return TYPE; } - - @Override - public int hashCode() { - return Objects.hash(coordinates); - } - - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (obj == null || getClass() != obj.getClass()) { - return false; - } - MultiPointBuilder other = (MultiPointBuilder) obj; - return Objects.equals(coordinates, other.coordinates); - } } diff --git a/core/src/main/java/org/elasticsearch/common/geo/builders/MultiPolygonBuilder.java b/core/src/main/java/org/elasticsearch/common/geo/builders/MultiPolygonBuilder.java index f5e5bca5051..3c002631b8d 100644 --- a/core/src/main/java/org/elasticsearch/common/geo/builders/MultiPolygonBuilder.java +++ b/core/src/main/java/org/elasticsearch/common/geo/builders/MultiPolygonBuilder.java @@ -19,6 +19,8 @@ package org.elasticsearch.common.geo.builders; +import org.elasticsearch.common.geo.GeoShapeType; +import org.elasticsearch.common.geo.parsers.ShapeParser; import org.locationtech.spatial4j.shape.Shape; import com.vividsolutions.jts.geom.Coordinate; @@ -102,9 +104,9 @@ public class MultiPolygonBuilder extends ShapeBuilder { @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { builder.startObject(); - builder.field(FIELD_TYPE, TYPE.shapeName()); - builder.field(FIELD_ORIENTATION, orientation.name().toLowerCase(Locale.ROOT)); - builder.startArray(FIELD_COORDINATES); + builder.field(ShapeParser.FIELD_TYPE.getPreferredName(), TYPE.shapeName()); + builder.field(ShapeParser.FIELD_ORIENTATION.getPreferredName(), orientation.name().toLowerCase(Locale.ROOT)); + builder.startArray(ShapeParser.FIELD_COORDINATES.getPreferredName()); for(PolygonBuilder polygon : polygons) { builder.startArray(); polygon.coordinatesArray(builder, params); diff --git a/core/src/main/java/org/elasticsearch/common/geo/builders/PointBuilder.java b/core/src/main/java/org/elasticsearch/common/geo/builders/PointBuilder.java index fdd9826410a..029ac14955a 100644 --- a/core/src/main/java/org/elasticsearch/common/geo/builders/PointBuilder.java +++ b/core/src/main/java/org/elasticsearch/common/geo/builders/PointBuilder.java @@ -19,86 +19,78 @@ package org.elasticsearch.common.geo.builders; +import org.elasticsearch.common.geo.GeoShapeType; +import org.elasticsearch.common.geo.parsers.ShapeParser; import org.locationtech.spatial4j.shape.Point; import com.vividsolutions.jts.geom.Coordinate; import org.elasticsearch.common.io.stream.StreamInput; -import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.xcontent.XContentBuilder; import java.io.IOException; -import java.util.Objects; +import java.util.ArrayList; -public class PointBuilder extends ShapeBuilder { +public class PointBuilder extends ShapeBuilder { public static final GeoShapeType TYPE = GeoShapeType.POINT; - private Coordinate coordinate; - /** * Create a point at [0.0,0.0] */ public PointBuilder() { - this.coordinate = ZERO_ZERO; + super(); + this.coordinates.add(ZERO_ZERO); + } + + public PointBuilder(double lon, double lat) { + //super(new ArrayList<>(1)); + super(); + this.coordinates.add(new Coordinate(lon, lat)); } - /** - * Read from a stream. - */ public PointBuilder(StreamInput in) throws IOException { - coordinate = readFromStream(in); - } - - @Override - public void writeTo(StreamOutput out) throws IOException { - writeCoordinateTo(coordinate, out); + super(in); } public PointBuilder coordinate(Coordinate coordinate) { - this.coordinate = coordinate; + this.coordinates.set(0, coordinate); return this; } public double longitude() { - return coordinate.x; + return coordinates.get(0).x; } public double latitude() { - return coordinate.y; + return coordinates.get(0).y; + } + + /** + * 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 new PointBuilder().coordinate(new Coordinate(longitude, latitude)); } @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); + builder.field(ShapeParser.FIELD_TYPE.getPreferredName(), TYPE.shapeName()); + builder.field(ShapeParser.FIELD_COORDINATES.getPreferredName()); + toXContent(builder, coordinates.get(0)); return builder.endObject(); } @Override public Point build() { - return SPATIAL_CONTEXT.makePoint(coordinate.x, coordinate.y); + return SPATIAL_CONTEXT.makePoint(coordinates.get(0).x, coordinates.get(0).y); } @Override public GeoShapeType type() { return TYPE; } - - @Override - public int hashCode() { - return Objects.hash(coordinate); - } - - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (obj == null || getClass() != obj.getClass()) { - return false; - } - PointBuilder other = (PointBuilder) obj; - return Objects.equals(coordinate, other.coordinate); - } } diff --git a/core/src/main/java/org/elasticsearch/common/geo/builders/PolygonBuilder.java b/core/src/main/java/org/elasticsearch/common/geo/builders/PolygonBuilder.java index f88a246dd0b..919aae37c73 100644 --- a/core/src/main/java/org/elasticsearch/common/geo/builders/PolygonBuilder.java +++ b/core/src/main/java/org/elasticsearch/common/geo/builders/PolygonBuilder.java @@ -26,12 +26,15 @@ import com.vividsolutions.jts.geom.LinearRing; import com.vividsolutions.jts.geom.MultiPolygon; import com.vividsolutions.jts.geom.Polygon; import org.elasticsearch.common.collect.Tuple; +import org.elasticsearch.common.geo.GeoShapeType; +import org.elasticsearch.common.geo.parsers.ShapeParser; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.util.set.Sets; import org.elasticsearch.common.xcontent.XContentBuilder; import org.locationtech.spatial4j.exception.InvalidShapeException; import org.locationtech.spatial4j.shape.Shape; +import org.locationtech.spatial4j.shape.jts.JtsGeometry; import java.io.IOException; import java.util.ArrayList; @@ -49,7 +52,7 @@ import java.util.concurrent.atomic.AtomicBoolean; * Methods to wrap polygons at the dateline and building shapes from the data held by the * builder. */ -public class PolygonBuilder extends ShapeBuilder { +public class PolygonBuilder extends ShapeBuilder { public static final GeoShapeType TYPE = GeoShapeType.POLYGON; @@ -222,7 +225,7 @@ public class PolygonBuilder extends ShapeBuilder { } @Override - public Shape build() { + public JtsGeometry build() { return jtsGeometry(buildGeometry(FACTORY, wrapdateline)); } @@ -237,9 +240,9 @@ public class PolygonBuilder extends ShapeBuilder { @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { builder.startObject(); - builder.field(FIELD_TYPE, TYPE.shapeName()); - builder.field(FIELD_ORIENTATION, orientation.name().toLowerCase(Locale.ROOT)); - builder.startArray(FIELD_COORDINATES); + builder.field(ShapeParser.FIELD_TYPE.getPreferredName(), TYPE.shapeName()); + builder.field(ShapeParser.FIELD_ORIENTATION.getPreferredName(), orientation.name().toLowerCase(Locale.ROOT)); + builder.startArray(ShapeParser.FIELD_COORDINATES.getPreferredName()); coordinatesArray(builder, params); builder.endArray(); builder.endObject(); diff --git a/core/src/main/java/org/elasticsearch/common/geo/builders/ShapeBuilder.java b/core/src/main/java/org/elasticsearch/common/geo/builders/ShapeBuilder.java index ea3c001949a..ef50a667faa 100644 --- a/core/src/main/java/org/elasticsearch/common/geo/builders/ShapeBuilder.java +++ b/core/src/main/java/org/elasticsearch/common/geo/builders/ShapeBuilder.java @@ -25,18 +25,14 @@ import com.vividsolutions.jts.geom.GeometryFactory; import org.apache.logging.log4j.Logger; import org.elasticsearch.Assertions; -import org.elasticsearch.ElasticsearchParseException; import org.elasticsearch.common.Strings; +import org.elasticsearch.common.geo.GeoShapeType; import org.elasticsearch.common.io.stream.NamedWriteable; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.logging.ESLoggerFactory; -import org.elasticsearch.common.unit.DistanceUnit.Distance; import org.elasticsearch.common.xcontent.ToXContentObject; -import org.elasticsearch.common.xcontent.XContent; import org.elasticsearch.common.xcontent.XContentBuilder; -import org.elasticsearch.common.xcontent.XContentParser; -import org.elasticsearch.index.mapper.GeoShapeFieldMapper; import org.locationtech.spatial4j.context.jts.JtsSpatialContext; import org.locationtech.spatial4j.exception.InvalidShapeException; import org.locationtech.spatial4j.shape.Shape; @@ -45,14 +41,16 @@ import org.locationtech.spatial4j.shape.jts.JtsGeometry; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Comparator; import java.util.List; import java.util.Locale; +import java.util.Objects; /** * Basic class for building GeoJSON shapes like Polygons, Linestrings, etc */ -public abstract class ShapeBuilder implements NamedWriteable, ToXContentObject { +public abstract class ShapeBuilder> implements NamedWriteable, ToXContentObject { protected static final Logger LOGGER = ESLoggerFactory.getLogger(ShapeBuilder.class.getName()); @@ -63,6 +61,8 @@ public abstract class ShapeBuilder implements NamedWriteable, ToXContentObject { DEBUG = Assertions.ENABLED; } + protected final List coordinates; + public static final double DATELINE = 180; /** @@ -85,7 +85,103 @@ public abstract class ShapeBuilder implements NamedWriteable, ToXContentObject { /** @see org.locationtech.spatial4j.shape.jts.JtsGeometry#index() */ protected static final boolean AUTO_INDEX_JTS_GEOMETRY = true;//may want to turn off once SpatialStrategy impls do it. + /** default ctor */ protected ShapeBuilder() { + coordinates = new ArrayList<>(); + } + + /** ctor from list of coordinates */ + protected ShapeBuilder(List coordinates) { + if (coordinates == null || coordinates.size() == 0) { + throw new IllegalArgumentException("cannot create point collection with empty set of points"); + } + this.coordinates = coordinates; + } + + /** ctor from serialized stream input */ + protected ShapeBuilder(StreamInput in) throws IOException { + int size = in.readVInt(); + coordinates = new ArrayList<>(size); + for (int i=0; i < size; i++) { + coordinates.add(readFromStream(in)); + } + } + + protected static Coordinate readFromStream(StreamInput in) throws IOException { + return new Coordinate(in.readDouble(), in.readDouble()); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeVInt(coordinates.size()); + for (Coordinate point : coordinates) { + writeCoordinateTo(point, out); + } + } + + protected static void writeCoordinateTo(Coordinate coordinate, StreamOutput out) throws IOException { + out.writeDouble(coordinate.x); + out.writeDouble(coordinate.y); + } + + @SuppressWarnings("unchecked") + private E thisRef() { + return (E)this; + } + + /** + * Add a new coordinate to the collection + * @param longitude longitude of the coordinate + * @param latitude latitude of the coordinate + * @return this + */ + public E coordinate(double longitude, double latitude) { + return this.coordinate(new Coordinate(longitude, latitude)); + } + + /** + * Add a new coordinate to the collection + * @param coordinate coordinate of the point + * @return this + */ + public E coordinate(Coordinate coordinate) { + this.coordinates.add(coordinate); + return thisRef(); + } + + /** + * Add a array of coordinates to the collection + * + * @param coordinates array of {@link Coordinate}s to add + * @return this + */ + public E coordinates(Coordinate...coordinates) { + return this.coordinates(Arrays.asList(coordinates)); + } + + /** + * Add a collection of coordinates to the collection + * + * @param coordinates array of {@link Coordinate}s to add + * @return this + */ + public E coordinates(Collection coordinates) { + this.coordinates.addAll(coordinates); + return thisRef(); + } + + /** + * Copy all coordinate 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 = coordinates.toArray(new Coordinate[coordinates.size() + (closed?1:0)]); + if(closed) { + result[result.length-1] = result[0]; + } + return result; } protected JtsGeometry jtsGeometry(Geometry geom) { @@ -104,84 +200,7 @@ public abstract class ShapeBuilder implements NamedWriteable, ToXContentObject { * 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 cases - if (token != XContentParser.Token.START_ARRAY && - token != XContentParser.Token.END_ARRAY && - token != XContentParser.Token.VALUE_NULL) { - double lon = parser.doubleValue(); - token = parser.nextToken(); - double lat = parser.doubleValue(); - token = parser.nextToken(); - while (token == XContentParser.Token.VALUE_NUMBER) { - token = parser.nextToken(); - } - return new CoordinateNode(new Coordinate(lon, lat)); - } else if (token == XContentParser.Token.VALUE_NULL) { - throw new IllegalArgumentException("coordinates cannot contain NULL values)"); - } - - List nodes = new ArrayList<>(); - while (token != XContentParser.Token.END_ARRAY) { - 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 null - * @throws IOException if the input could not be read - */ - public static ShapeBuilder parse(XContentParser parser) throws IOException { - return GeoShapeType.parse(parser, null); - } - - /** - * Create a new {@link ShapeBuilder} from {@link XContent} - * @param parser parser to read the GeoShape from - * @param geoDocMapper document field mapper reference required for spatial parameters relevant - * to the shape construction process (e.g., orientation) - * todo: refactor to place build specific parameters in the SpatialContext - * @return {@link ShapeBuilder} read from the parser or null - * if the parsers current token has been null - * @throws IOException if the input could not be read - */ - public static ShapeBuilder parse(XContentParser parser, GeoShapeFieldMapper geoDocMapper) throws IOException { - return GeoShapeType.parse(parser, geoDocMapper); - } - - protected static XContentBuilder toXContent(XContentBuilder builder, Coordinate coordinate) throws IOException { - return builder.startArray().value(coordinate.x).value(coordinate.y).endArray(); - } - - protected static void writeCoordinateTo(Coordinate coordinate, StreamOutput out) throws IOException { - out.writeDouble(coordinate.x); - out.writeDouble(coordinate.y); - } - - protected static Coordinate readFromStream(StreamInput in) throws IOException { - return new Coordinate(in.readDouble(), in.readDouble()); - } + public abstract T build(); protected static Coordinate shift(Coordinate coordinate, double dateline) { if (dateline == 0) { @@ -255,58 +274,6 @@ public abstract class ShapeBuilder implements NamedWriteable, ToXContentObject { 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 ToXContentObject { - - 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; - } - - protected boolean isEmpty() { - return (coordinate == null && (children == null || children.isEmpty())); - } - - @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 @@ -415,293 +382,50 @@ public abstract class ShapeBuilder implements NamedWriteable, ToXContentObject { } } - public static final String FIELD_TYPE = "type"; - public static final String FIELD_COORDINATES = "coordinates"; - public static final String FIELD_GEOMETRIES = "geometries"; - public static final String FIELD_ORIENTATION = "orientation"; - protected static final boolean debugEnabled() { return LOGGER.isDebugEnabled() || DEBUG; } + protected static XContentBuilder toXContent(XContentBuilder builder, Coordinate coordinate) throws IOException { + return builder.startArray().value(coordinate.x).value(coordinate.y).endArray(); + } + /** - * Enumeration that lists all {@link GeoShapeType}s that can be handled + * 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 */ - public enum GeoShapeType { - POINT("point"), - MULTIPOINT("multipoint"), - LINESTRING("linestring"), - MULTILINESTRING("multilinestring"), - POLYGON("polygon"), - MULTIPOLYGON("multipolygon"), - GEOMETRYCOLLECTION("geometrycollection"), - ENVELOPE("envelope"), - CIRCLE("circle"); - - private final String shapename; - - GeoShapeType(String shapename) { - this.shapename = shapename; + protected XContentBuilder coordinatesToXcontent(XContentBuilder builder, boolean closed) throws IOException { + builder.startArray(); + for(Coordinate coord : coordinates) { + toXContent(builder, coord); } - - protected String shapeName() { - return 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 IllegalArgumentException("unknown geo_shape ["+geoshapename+"]"); - } - - public static ShapeBuilder parse(XContentParser parser) throws IOException { - return parse(parser, null); - } - - /** - * Parse the geometry specified by the source document and return a ShapeBuilder instance used to - * build the actual geometry - * @param parser - parse utility object including source document - * @param shapeMapper - field mapper needed for index specific parameters - * @return ShapeBuilder - a builder instance used to create the geometry - */ - public static ShapeBuilder parse(XContentParser parser, GeoShapeFieldMapper shapeMapper) 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; - GeometryCollectionBuilder geometryCollections = null; - - Orientation requestedOrientation = (shapeMapper == null) ? Orientation.RIGHT : shapeMapper.fieldType().orientation(); - boolean coerce = (shapeMapper == null) ? GeoShapeFieldMapper.Defaults.COERCE.value() : shapeMapper.coerce().value(); - - 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 (FIELD_GEOMETRIES.equals(fieldName)) { - parser.nextToken(); - geometryCollections = parseGeometries(parser, shapeMapper); - } else if (CircleBuilder.FIELD_RADIUS.equals(fieldName)) { - parser.nextToken(); - radius = Distance.parseDistance(parser.text()); - } else if (FIELD_ORIENTATION.equals(fieldName)) { - parser.nextToken(); - requestedOrientation = Orientation.fromString(parser.text()); - } else { - parser.nextToken(); - parser.skipChildren(); - } - } - } - - if (shapeType == null) { - throw new ElasticsearchParseException("shape type not included"); - } else if (node == null && GeoShapeType.GEOMETRYCOLLECTION != shapeType) { - throw new ElasticsearchParseException("coordinates not included"); - } else if (geometryCollections == null && GeoShapeType.GEOMETRYCOLLECTION == shapeType) { - throw new ElasticsearchParseException("geometries not included"); - } else if (radius != null && GeoShapeType.CIRCLE != shapeType) { - throw new ElasticsearchParseException("field [{}] is supported for [{}] only", CircleBuilder.FIELD_RADIUS, - CircleBuilder.TYPE); - } - - 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, requestedOrientation, coerce); - case MULTIPOLYGON: return parseMultiPolygon(node, requestedOrientation, coerce); - case CIRCLE: return parseCircle(node, radius); - case ENVELOPE: return parseEnvelope(node); - case GEOMETRYCOLLECTION: return geometryCollections; - default: - throw new ElasticsearchParseException("shape type [{}] not included", shapeType); + if(closed) { + Coordinate start = coordinates.get(0); + Coordinate end = coordinates.get(coordinates.size()-1); + if(start.x != end.x || start.y != end.y) { + toXContent(builder, coordinates.get(0)); } } + builder.endArray(); + return builder; + } - protected static void validatePointNode(CoordinateNode node) { - if (node.isEmpty()) { - throw new ElasticsearchParseException( - "invalid number of points (0) provided when expecting a single coordinate ([lat, lng])"); - } else if (node.coordinate == null) { - if (node.children.isEmpty() == false) { - throw new ElasticsearchParseException("multipoint data provided when single point data expected."); - } - } - } + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof ShapeBuilder)) return false; - protected static PointBuilder parsePoint(CoordinateNode node) { - validatePointNode(node); - return ShapeBuilders.newPoint(node.coordinate); - } + ShapeBuilder that = (ShapeBuilder) o; - protected static CircleBuilder parseCircle(CoordinateNode coordinates, Distance radius) { - return ShapeBuilders.newCircleBuilder().center(coordinates.coordinate).radius(radius); - } + return Objects.equals(coordinates, that.coordinates); + } - protected static EnvelopeBuilder parseEnvelope(CoordinateNode coordinates) { - // validate the coordinate array for envelope type - if (coordinates.children.size() != 2) { - throw new ElasticsearchParseException( - "invalid number of points [{}] provided for geo_shape [{}] when expecting an array of 2 coordinates", - coordinates.children.size(), GeoShapeType.ENVELOPE.shapename); - } - // verify coordinate bounds, correct if necessary - Coordinate uL = coordinates.children.get(0).coordinate; - Coordinate lR = coordinates.children.get(1).coordinate; - if (((lR.x < uL.x) || (uL.y < lR.y))) { - Coordinate uLtmp = uL; - uL = new Coordinate(Math.min(uL.x, lR.x), Math.max(uL.y, lR.y)); - lR = new Coordinate(Math.max(uLtmp.x, lR.x), Math.min(uLtmp.y, lR.y)); - } - return ShapeBuilders.newEnvelope(uL, lR); - } - - protected static void validateMultiPointNode(CoordinateNode coordinates) { - if (coordinates.children == null || coordinates.children.isEmpty()) { - if (coordinates.coordinate != null) { - throw new ElasticsearchParseException("single coordinate found when expecting an array of " + - "coordinates. change type to point or change data to an array of >0 coordinates"); - } - throw new ElasticsearchParseException("no data provided for multipoint object when expecting " + - ">0 points (e.g., [[lat, lng]] or [[lat, lng], ...])"); - } else { - for (CoordinateNode point : coordinates.children) { - validatePointNode(point); - } - } - } - - protected static MultiPointBuilder parseMultiPoint(CoordinateNode coordinates) { - validateMultiPointNode(coordinates); - CoordinatesBuilder points = new CoordinatesBuilder(); - for (CoordinateNode node : coordinates.children) { - points.coordinate(node.coordinate); - } - return new MultiPointBuilder(points.build()); - } - - protected static LineStringBuilder parseLineString(CoordinateNode coordinates) { - /** - * Per GeoJSON spec (http://geojson.org/geojson-spec.html#linestring) - * "coordinates" member must be an array of two or more positions - * LineStringBuilder should throw a graceful exception if < 2 coordinates/points are provided - */ - if (coordinates.children.size() < 2) { - throw new ElasticsearchParseException("invalid number of points in LineString (found [{}] - must be >= 2)", - coordinates.children.size()); - } - - CoordinatesBuilder line = new CoordinatesBuilder(); - for (CoordinateNode node : coordinates.children) { - line.coordinate(node.coordinate); - } - return ShapeBuilders.newLineString(line); - } - - protected static MultiLineStringBuilder parseMultiLine(CoordinateNode coordinates) { - MultiLineStringBuilder multiline = ShapeBuilders.newMultiLinestring(); - for (CoordinateNode node : coordinates.children) { - multiline.linestring(parseLineString(node)); - } - return multiline; - } - - protected static LineStringBuilder parseLinearRing(CoordinateNode coordinates, boolean coerce) { - /** - * Per GeoJSON spec (http://geojson.org/geojson-spec.html#linestring) - * A LinearRing is closed LineString with 4 or more positions. The first and last positions - * are equivalent (they represent equivalent points). Though a LinearRing is not explicitly - * represented as a GeoJSON geometry type, it is referred to in the Polygon geometry type definition. - */ - if (coordinates.children == null) { - String error = "Invalid LinearRing found."; - error += (coordinates.coordinate == null) ? - " No coordinate array provided" : " Found a single coordinate when expecting a coordinate array"; - throw new ElasticsearchParseException(error); - } - - int numValidPts = coerce ? 3 : 4; - if (coordinates.children.size() < numValidPts) { - throw new ElasticsearchParseException("invalid number of points in LinearRing (found [{}] - must be >= [{}])", - coordinates.children.size(), numValidPts); - } - - if (!coordinates.children.get(0).coordinate.equals( - coordinates.children.get(coordinates.children.size() - 1).coordinate)) { - if (coerce) { - coordinates.children.add(coordinates.children.get(0)); - } else { - throw new ElasticsearchParseException("invalid LinearRing found (coordinates are not closed)"); - } - } - return parseLineString(coordinates); - } - - protected static PolygonBuilder parsePolygon(CoordinateNode coordinates, final Orientation orientation, final boolean coerce) { - if (coordinates.children == null || coordinates.children.isEmpty()) { - throw new ElasticsearchParseException( - "invalid LinearRing provided for type polygon. Linear ring must be an array of coordinates"); - } - - LineStringBuilder shell = parseLinearRing(coordinates.children.get(0), coerce); - PolygonBuilder polygon = new PolygonBuilder(shell, orientation); - for (int i = 1; i < coordinates.children.size(); i++) { - polygon.hole(parseLinearRing(coordinates.children.get(i), coerce)); - } - return polygon; - } - - protected static MultiPolygonBuilder parseMultiPolygon(CoordinateNode coordinates, final Orientation orientation, - final boolean coerce) { - MultiPolygonBuilder polygons = ShapeBuilders.newMultiPolygon(orientation); - for (CoordinateNode node : coordinates.children) { - polygons.polygon(parsePolygon(node, orientation, coerce)); - } - return polygons; - } - - /** - * Parse the geometries array of a GeometryCollection - * - * @param parser Parser that will be read from - * @return Geometry[] geometries of the GeometryCollection - * @throws IOException Thrown if an error occurs while reading from the XContentParser - */ - protected static GeometryCollectionBuilder parseGeometries(XContentParser parser, GeoShapeFieldMapper mapper) throws - IOException { - if (parser.currentToken() != XContentParser.Token.START_ARRAY) { - throw new ElasticsearchParseException("geometries must be an array of geojson objects"); - } - - XContentParser.Token token = parser.nextToken(); - GeometryCollectionBuilder geometryCollection = ShapeBuilders.newGeometryCollection(); - while (token != XContentParser.Token.END_ARRAY) { - ShapeBuilder shapeBuilder = GeoShapeType.parse(parser); - geometryCollection.shape(shapeBuilder); - token = parser.nextToken(); - } - - return geometryCollection; - } + @Override + public int hashCode() { + return Objects.hash(coordinates); } @Override diff --git a/core/src/main/java/org/elasticsearch/common/geo/builders/ShapeBuilders.java b/core/src/main/java/org/elasticsearch/common/geo/builders/ShapeBuilders.java deleted file mode 100644 index e0afa4c20d5..00000000000 --- a/core/src/main/java/org/elasticsearch/common/geo/builders/ShapeBuilders.java +++ /dev/null @@ -1,153 +0,0 @@ -/* - * Licensed to Elasticsearch under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -package org.elasticsearch.common.geo.builders; - -import java.util.List; - -import com.vividsolutions.jts.geom.Coordinate; -import org.elasticsearch.common.io.stream.NamedWriteableRegistry.Entry; - -/** - * A collection of static methods for creating ShapeBuilders. - */ -public class ShapeBuilders { - - /** - * 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 ShapeBuilders.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(List points) { - return new MultiPointBuilder(points); - } - - /** - * Create a new lineString - * @return a new {@link LineStringBuilder} - */ - public static LineStringBuilder newLineString(List list) { - return new LineStringBuilder(list); - } - - /** - * Create a new lineString - * @return a new {@link LineStringBuilder} - */ - public static LineStringBuilder newLineString(CoordinatesBuilder coordinates) { - return new LineStringBuilder(coordinates); - } - - /** - * Create a new Collection of lineStrings - * @return a new {@link MultiLineStringBuilder} - */ - public static MultiLineStringBuilder newMultiLinestring() { - return new MultiLineStringBuilder(); - } - - /** - * Create a new PolygonBuilder - * @return a new {@link PolygonBuilder} - */ - public static PolygonBuilder newPolygon(List shell) { - return new PolygonBuilder(new CoordinatesBuilder().coordinates(shell)); - } - - /** - * Create a new PolygonBuilder - * @return a new {@link PolygonBuilder} - */ - public static PolygonBuilder newPolygon(CoordinatesBuilder shell) { - return new PolygonBuilder(shell); - } - - /** - * Create a new Collection of polygons - * @return a new {@link MultiPolygonBuilder} - */ - public static MultiPolygonBuilder newMultiPolygon() { - return new MultiPolygonBuilder(); - } - - /** - * Create a new Collection of polygons - * @return a new {@link MultiPolygonBuilder} - */ - public static MultiPolygonBuilder newMultiPolygon(ShapeBuilder.Orientation orientation) { - return new MultiPolygonBuilder(orientation); - } - - /** - * Create a new GeometryCollection - * @return a new {@link GeometryCollectionBuilder} - */ - public static GeometryCollectionBuilder newGeometryCollection() { - return new GeometryCollectionBuilder(); - } - - /** - * 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(Coordinate topLeft, Coordinate bottomRight) { - return new EnvelopeBuilder(topLeft, bottomRight); - } - - public static void register(List namedWriteables) { - namedWriteables.add(new Entry(ShapeBuilder.class, PointBuilder.TYPE.shapeName(), PointBuilder::new)); - namedWriteables.add(new Entry(ShapeBuilder.class, CircleBuilder.TYPE.shapeName(), CircleBuilder::new)); - namedWriteables.add(new Entry(ShapeBuilder.class, EnvelopeBuilder.TYPE.shapeName(), EnvelopeBuilder::new)); - namedWriteables.add(new Entry(ShapeBuilder.class, MultiPointBuilder.TYPE.shapeName(), MultiPointBuilder::new)); - namedWriteables.add(new Entry(ShapeBuilder.class, LineStringBuilder.TYPE.shapeName(), LineStringBuilder::new)); - namedWriteables.add(new Entry(ShapeBuilder.class, MultiLineStringBuilder.TYPE.shapeName(), MultiLineStringBuilder::new)); - namedWriteables.add(new Entry(ShapeBuilder.class, PolygonBuilder.TYPE.shapeName(), PolygonBuilder::new)); - namedWriteables.add(new Entry(ShapeBuilder.class, MultiPolygonBuilder.TYPE.shapeName(), MultiPolygonBuilder::new)); - namedWriteables.add(new Entry(ShapeBuilder.class, GeometryCollectionBuilder.TYPE.shapeName(), GeometryCollectionBuilder::new)); - } -} diff --git a/core/src/main/java/org/elasticsearch/common/geo/parsers/CoordinateNode.java b/core/src/main/java/org/elasticsearch/common/geo/parsers/CoordinateNode.java new file mode 100644 index 00000000000..d766d75d5ec --- /dev/null +++ b/core/src/main/java/org/elasticsearch/common/geo/parsers/CoordinateNode.java @@ -0,0 +1,81 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.elasticsearch.common.geo.parsers; + +import com.vividsolutions.jts.geom.Coordinate; +import org.elasticsearch.common.xcontent.ToXContentObject; +import org.elasticsearch.common.xcontent.XContentBuilder; + +import java.io.IOException; +import java.util.List; + +/** + * Node used to represent a tree of coordinates. + *

+ * Can either be a leaf node consisting of a Coordinate, or a parent with + * children + */ +public class CoordinateNode implements ToXContentObject { + public final Coordinate coordinate; + public 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; + } + + public boolean isEmpty() { + return (coordinate == null && (children == null || children.isEmpty())); + } + + public boolean isMultiPoint() { + return children != null && children.size() > 1; + } + + @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; + } +} diff --git a/core/src/main/java/org/elasticsearch/common/geo/parsers/GeoJsonParser.java b/core/src/main/java/org/elasticsearch/common/geo/parsers/GeoJsonParser.java new file mode 100644 index 00000000000..90145448be3 --- /dev/null +++ b/core/src/main/java/org/elasticsearch/common/geo/parsers/GeoJsonParser.java @@ -0,0 +1,194 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.elasticsearch.common.geo.parsers; + +import com.vividsolutions.jts.geom.Coordinate; +import org.elasticsearch.ElasticsearchParseException; +import org.elasticsearch.common.Explicit; +import org.elasticsearch.common.geo.GeoShapeType; +import org.elasticsearch.common.geo.builders.CircleBuilder; +import org.elasticsearch.common.geo.builders.GeometryCollectionBuilder; +import org.elasticsearch.common.geo.builders.ShapeBuilder; +import org.elasticsearch.common.unit.DistanceUnit; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.index.mapper.GeoShapeFieldMapper; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * Parses shape geometry represented in geojson + * + * complies with geojson specification: https://tools.ietf.org/html/rfc7946 + */ +abstract class GeoJsonParser { + protected static ShapeBuilder parse(XContentParser parser, GeoShapeFieldMapper shapeMapper) + throws IOException { + GeoShapeType shapeType = null; + DistanceUnit.Distance radius = null; + CoordinateNode coordinateNode = null; + GeometryCollectionBuilder geometryCollections = null; + + ShapeBuilder.Orientation requestedOrientation = + (shapeMapper == null) ? ShapeBuilder.Orientation.RIGHT : shapeMapper.fieldType().orientation(); + Explicit coerce = (shapeMapper == null) ? GeoShapeFieldMapper.Defaults.COERCE : shapeMapper.coerce(); + + String malformedException = null; + + XContentParser.Token token; + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + if (token == XContentParser.Token.FIELD_NAME) { + String fieldName = parser.currentName(); + + if (ShapeParser.FIELD_TYPE.match(fieldName)) { + parser.nextToken(); + final GeoShapeType type = GeoShapeType.forName(parser.text()); + if (shapeType != null && shapeType.equals(type) == false) { + malformedException = ShapeParser.FIELD_TYPE + " already parsed as [" + + shapeType + "] cannot redefine as [" + type + "]"; + } else { + shapeType = type; + } + } else if (ShapeParser.FIELD_COORDINATES.match(fieldName)) { + parser.nextToken(); + coordinateNode = parseCoordinates(parser); + } else if (ShapeParser.FIELD_GEOMETRIES.match(fieldName)) { + if (shapeType == null) { + shapeType = GeoShapeType.GEOMETRYCOLLECTION; + } else if (shapeType.equals(GeoShapeType.GEOMETRYCOLLECTION) == false) { + malformedException = "cannot have [" + ShapeParser.FIELD_GEOMETRIES + "] with type set to [" + + shapeType + "]"; + } + parser.nextToken(); + geometryCollections = parseGeometries(parser, shapeMapper); + } else if (CircleBuilder.FIELD_RADIUS.match(fieldName)) { + if (shapeType == null) { + shapeType = GeoShapeType.CIRCLE; + } else if (shapeType != null && shapeType.equals(GeoShapeType.CIRCLE) == false) { + malformedException = "cannot have [" + CircleBuilder.FIELD_RADIUS + "] with type set to [" + + shapeType + "]"; + } + parser.nextToken(); + radius = DistanceUnit.Distance.parseDistance(parser.text()); + } else if (ShapeParser.FIELD_ORIENTATION.match(fieldName)) { + if (shapeType != null + && (shapeType.equals(GeoShapeType.POLYGON) || shapeType.equals(GeoShapeType.MULTIPOLYGON)) == false) { + malformedException = "cannot have [" + ShapeParser.FIELD_ORIENTATION + "] with type set to [" + shapeType + "]"; + } + parser.nextToken(); + requestedOrientation = ShapeBuilder.Orientation.fromString(parser.text()); + } else { + parser.nextToken(); + parser.skipChildren(); + } + } + } + + if (malformedException != null) { + throw new ElasticsearchParseException(malformedException); + } else if (shapeType == null) { + throw new ElasticsearchParseException("shape type not included"); + } else if (coordinateNode == null && GeoShapeType.GEOMETRYCOLLECTION != shapeType) { + throw new ElasticsearchParseException("coordinates not included"); + } else if (geometryCollections == null && GeoShapeType.GEOMETRYCOLLECTION == shapeType) { + throw new ElasticsearchParseException("geometries not included"); + } else if (radius != null && GeoShapeType.CIRCLE != shapeType) { + throw new ElasticsearchParseException("field [{}] is supported for [{}] only", CircleBuilder.FIELD_RADIUS, + CircleBuilder.TYPE); + } + + if (shapeType == null) { + throw new ElasticsearchParseException("shape type [{}] not included", shapeType); + } + + if (shapeType.equals(GeoShapeType.GEOMETRYCOLLECTION)) { + return geometryCollections; + } + + return shapeType.getBuilder(coordinateNode, radius, requestedOrientation, coerce.value()); + } + + /** + * 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 cases + if (token != XContentParser.Token.START_ARRAY && + token != XContentParser.Token.END_ARRAY && + token != XContentParser.Token.VALUE_NULL) { + return new CoordinateNode(parseCoordinate(parser)); + } else if (token == XContentParser.Token.VALUE_NULL) { + throw new IllegalArgumentException("coordinates cannot contain NULL values)"); + } + + List nodes = new ArrayList<>(); + while (token != XContentParser.Token.END_ARRAY) { + nodes.add(parseCoordinates(parser)); + token = parser.nextToken(); + } + + return new CoordinateNode(nodes); + } + + private static Coordinate parseCoordinate(XContentParser parser) throws IOException { + double lon = parser.doubleValue(); + parser.nextToken(); + double lat = parser.doubleValue(); + XContentParser.Token token = parser.nextToken(); + while (token == XContentParser.Token.VALUE_NUMBER) { + token = parser.nextToken(); + } + // todo support z/alt + return new Coordinate(lon, lat); + } + + /** + * Parse the geometries array of a GeometryCollection + * + * @param parser Parser that will be read from + * @return Geometry[] geometries of the GeometryCollection + * @throws IOException Thrown if an error occurs while reading from the XContentParser + */ + static GeometryCollectionBuilder parseGeometries(XContentParser parser, GeoShapeFieldMapper mapper) throws + IOException { + if (parser.currentToken() != XContentParser.Token.START_ARRAY) { + throw new ElasticsearchParseException("geometries must be an array of geojson objects"); + } + + XContentParser.Token token = parser.nextToken(); + GeometryCollectionBuilder geometryCollection = new GeometryCollectionBuilder(); + while (token != XContentParser.Token.END_ARRAY) { + ShapeBuilder shapeBuilder = ShapeParser.parse(parser); + geometryCollection.shape(shapeBuilder); + token = parser.nextToken(); + } + + return geometryCollection; + } +} diff --git a/core/src/main/java/org/elasticsearch/common/geo/parsers/ShapeParser.java b/core/src/main/java/org/elasticsearch/common/geo/parsers/ShapeParser.java new file mode 100644 index 00000000000..39540f902fe --- /dev/null +++ b/core/src/main/java/org/elasticsearch/common/geo/parsers/ShapeParser.java @@ -0,0 +1,68 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.elasticsearch.common.geo.parsers; + +import org.elasticsearch.ElasticsearchParseException; +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.geo.builders.ShapeBuilder; +import org.elasticsearch.common.xcontent.XContent; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.index.mapper.GeoShapeFieldMapper; + +import java.io.IOException; + +/** + * first point of entry for a shape parser + */ +public interface ShapeParser { + ParseField FIELD_TYPE = new ParseField("type"); + ParseField FIELD_COORDINATES = new ParseField("coordinates"); + ParseField FIELD_GEOMETRIES = new ParseField("geometries"); + ParseField FIELD_ORIENTATION = new ParseField("orientation"); + + /** + * Create a new {@link ShapeBuilder} from {@link XContent} + * @param parser parser to read the GeoShape from + * @param shapeMapper document field mapper reference required for spatial parameters relevant + * to the shape construction process (e.g., orientation) + * todo: refactor to place build specific parameters in the SpatialContext + * @return {@link ShapeBuilder} read from the parser or null + * if the parsers current token has been null + * @throws IOException if the input could not be read + */ + static ShapeBuilder parse(XContentParser parser, GeoShapeFieldMapper shapeMapper) throws IOException { + if (parser.currentToken() == XContentParser.Token.VALUE_NULL) { + return null; + } if (parser.currentToken() == XContentParser.Token.START_OBJECT) { + return GeoJsonParser.parse(parser, shapeMapper); + } + throw new ElasticsearchParseException("shape must be an object consisting of type and coordinates"); + } + + /** + * 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 null + * @throws IOException if the input could not be read + */ + static ShapeBuilder parse(XContentParser parser) throws IOException { + return parse(parser, null); + } +} diff --git a/core/src/main/java/org/elasticsearch/index/mapper/GeoShapeFieldMapper.java b/core/src/main/java/org/elasticsearch/index/mapper/GeoShapeFieldMapper.java index cd08cbc8e01..495bdf49cb9 100644 --- a/core/src/main/java/org/elasticsearch/index/mapper/GeoShapeFieldMapper.java +++ b/core/src/main/java/org/elasticsearch/index/mapper/GeoShapeFieldMapper.java @@ -36,6 +36,7 @@ import org.elasticsearch.common.geo.GeoUtils; import org.elasticsearch.common.geo.SpatialStrategy; import org.elasticsearch.common.geo.builders.ShapeBuilder; import org.elasticsearch.common.geo.builders.ShapeBuilder.Orientation; +import org.elasticsearch.common.geo.parsers.ShapeParser; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.unit.DistanceUnit; import org.elasticsearch.common.xcontent.XContentBuilder; @@ -468,7 +469,7 @@ public class GeoShapeFieldMapper extends FieldMapper { try { Shape shape = context.parseExternalValue(Shape.class); if (shape == null) { - ShapeBuilder shapeBuilder = ShapeBuilder.parse(context.parser(), this); + ShapeBuilder shapeBuilder = ShapeParser.parse(context.parser(), this); if (shapeBuilder == null) { return null; } @@ -476,7 +477,7 @@ public class GeoShapeFieldMapper extends FieldMapper { } if (fieldType().pointsOnly() && !(shape instanceof Point)) { throw new MapperParsingException("[{" + fieldType().name() + "}] is configured for points only but a " + - ((shape instanceof JtsGeometry) ? ((JtsGeometry)shape).getGeom().getGeometryType() : shape.getClass()) + " was found"); + ((shape instanceof JtsGeometry) ? ((JtsGeometry) shape).getGeom().getGeometryType() : shape.getClass()) + " was found"); } List fields = new ArrayList<>(Arrays.asList(fieldType().defaultStrategy().createIndexableFields(shape))); createFieldNamesField(context, fields); diff --git a/core/src/main/java/org/elasticsearch/index/query/GeoShapeQueryBuilder.java b/core/src/main/java/org/elasticsearch/index/query/GeoShapeQueryBuilder.java index 0aef7a3528e..0424cf6f14b 100644 --- a/core/src/main/java/org/elasticsearch/index/query/GeoShapeQueryBuilder.java +++ b/core/src/main/java/org/elasticsearch/index/query/GeoShapeQueryBuilder.java @@ -39,6 +39,7 @@ import org.elasticsearch.common.geo.ShapeRelation; import org.elasticsearch.common.geo.ShapesAvailability; import org.elasticsearch.common.geo.SpatialStrategy; import org.elasticsearch.common.geo.builders.ShapeBuilder; +import org.elasticsearch.common.geo.parsers.ShapeParser; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.xcontent.NamedXContentRegistry; @@ -410,7 +411,7 @@ public class GeoShapeQueryBuilder extends AbstractQueryBuilder shapeCollection(Shape... shapes) { diff --git a/core/src/test/java/org/elasticsearch/common/geo/ShapeBuilderTests.java b/core/src/test/java/org/elasticsearch/common/geo/ShapeBuilderTests.java index dc01a39cb81..d1f7d5601a6 100644 --- a/core/src/test/java/org/elasticsearch/common/geo/ShapeBuilderTests.java +++ b/core/src/test/java/org/elasticsearch/common/geo/ShapeBuilderTests.java @@ -24,10 +24,13 @@ import com.vividsolutions.jts.geom.LineString; import com.vividsolutions.jts.geom.Polygon; import org.elasticsearch.common.geo.builders.CoordinatesBuilder; +import org.elasticsearch.common.geo.builders.CircleBuilder; +import org.elasticsearch.common.geo.builders.EnvelopeBuilder; import org.elasticsearch.common.geo.builders.LineStringBuilder; +import org.elasticsearch.common.geo.builders.MultiLineStringBuilder; +import org.elasticsearch.common.geo.builders.PointBuilder; import org.elasticsearch.common.geo.builders.PolygonBuilder; import org.elasticsearch.common.geo.builders.ShapeBuilder; -import org.elasticsearch.common.geo.builders.ShapeBuilders; import org.elasticsearch.test.ESTestCase; import org.locationtech.spatial4j.exception.InvalidShapeException; import org.locationtech.spatial4j.shape.Circle; @@ -46,13 +49,13 @@ import static org.hamcrest.Matchers.containsString; public class ShapeBuilderTests extends ESTestCase { public void testNewPoint() { - Point point = ShapeBuilders.newPoint(-100, 45).build(); + Point point = new PointBuilder().coordinate(-100, 45).build(); assertEquals(-100D, point.getX(), 0.0d); assertEquals(45D, point.getY(), 0.0d); } public void testNewRectangle() { - Rectangle rectangle = ShapeBuilders.newEnvelope(new Coordinate(-45, 30), new Coordinate(45, -30)).build(); + Rectangle rectangle = new EnvelopeBuilder(new Coordinate(-45, 30), new Coordinate(45, -30)).build(); assertEquals(-45D, rectangle.getMinX(), 0.0d); assertEquals(-30D, rectangle.getMinY(), 0.0d); assertEquals(45D, rectangle.getMaxX(), 0.0d); @@ -60,7 +63,7 @@ public class ShapeBuilderTests extends ESTestCase { } public void testNewPolygon() { - Polygon polygon = ShapeBuilders.newPolygon(new CoordinatesBuilder() + Polygon polygon = new PolygonBuilder(new CoordinatesBuilder() .coordinate(-45, 30) .coordinate(45, 30) .coordinate(45, -30) @@ -75,7 +78,7 @@ public class ShapeBuilderTests extends ESTestCase { } public void testNewPolygon_coordinate() { - Polygon polygon = ShapeBuilders.newPolygon(new CoordinatesBuilder() + Polygon polygon = new PolygonBuilder(new CoordinatesBuilder() .coordinate(new Coordinate(-45, 30)) .coordinate(new Coordinate(45, 30)) .coordinate(new Coordinate(45, -30)) @@ -90,7 +93,7 @@ public class ShapeBuilderTests extends ESTestCase { } public void testNewPolygon_coordinates() { - Polygon polygon = ShapeBuilders.newPolygon(new CoordinatesBuilder() + Polygon polygon = new PolygonBuilder(new CoordinatesBuilder() .coordinates(new Coordinate(-45, 30), new Coordinate(45, 30), new Coordinate(45, -30), new Coordinate(-45, -30), new Coordinate(-45, 30)) ).toPolygon(); @@ -103,7 +106,7 @@ public class ShapeBuilderTests extends ESTestCase { public void testLineStringBuilder() { // Building a simple LineString - ShapeBuilders.newLineString(new CoordinatesBuilder() + new LineStringBuilder(new CoordinatesBuilder() .coordinate(-130.0, 55.0) .coordinate(-130.0, -40.0) .coordinate(-15.0, -40.0) @@ -114,7 +117,7 @@ public class ShapeBuilderTests extends ESTestCase { .coordinate(-110.0, 55.0)).build(); // Building a linestring that needs to be wrapped - ShapeBuilders.newLineString(new CoordinatesBuilder() + new LineStringBuilder(new CoordinatesBuilder() .coordinate(100.0, 50.0) .coordinate(110.0, -40.0) .coordinate(240.0, -40.0) @@ -127,7 +130,7 @@ public class ShapeBuilderTests extends ESTestCase { .build(); // Building a lineString on the dateline - ShapeBuilders.newLineString(new CoordinatesBuilder() + new LineStringBuilder(new CoordinatesBuilder() .coordinate(-180.0, 80.0) .coordinate(-180.0, 40.0) .coordinate(-180.0, -40.0) @@ -136,7 +139,7 @@ public class ShapeBuilderTests extends ESTestCase { .build(); // Building a lineString on the dateline - ShapeBuilders.newLineString(new CoordinatesBuilder() + new LineStringBuilder(new CoordinatesBuilder() .coordinate(180.0, 80.0) .coordinate(180.0, 40.0) .coordinate(180.0, -40.0) @@ -146,7 +149,7 @@ public class ShapeBuilderTests extends ESTestCase { } public void testMultiLineString() { - ShapeBuilders.newMultiLinestring() + new MultiLineStringBuilder() .linestring(new LineStringBuilder(new CoordinatesBuilder() .coordinate(-100.0, 50.0) .coordinate(50.0, 50.0) @@ -164,7 +167,7 @@ public class ShapeBuilderTests extends ESTestCase { .build(); // LineString that needs to be wrapped - ShapeBuilders.newMultiLinestring() + new MultiLineStringBuilder() .linestring(new LineStringBuilder(new CoordinatesBuilder() .coordinate(150.0, 60.0) .coordinate(200.0, 60.0) @@ -183,7 +186,7 @@ public class ShapeBuilderTests extends ESTestCase { } public void testPolygonSelfIntersection() { - PolygonBuilder newPolygon = ShapeBuilders.newPolygon(new CoordinatesBuilder() + PolygonBuilder newPolygon = new PolygonBuilder(new CoordinatesBuilder() .coordinate(-40.0, 50.0) .coordinate(40.0, 50.0) .coordinate(-40.0, -50.0) @@ -194,31 +197,31 @@ public class ShapeBuilderTests extends ESTestCase { public void testGeoCircle() { double earthCircumference = 40075016.69; - Circle circle = ShapeBuilders.newCircleBuilder().center(0, 0).radius("100m").build(); + Circle circle = new CircleBuilder().center(0, 0).radius("100m").build(); assertEquals((360 * 100) / earthCircumference, circle.getRadius(), 0.00000001); assertEquals(new PointImpl(0, 0, ShapeBuilder.SPATIAL_CONTEXT), circle.getCenter()); - circle = ShapeBuilders.newCircleBuilder().center(+180, 0).radius("100m").build(); + circle = new CircleBuilder().center(+180, 0).radius("100m").build(); assertEquals((360 * 100) / earthCircumference, circle.getRadius(), 0.00000001); assertEquals(new PointImpl(180, 0, ShapeBuilder.SPATIAL_CONTEXT), circle.getCenter()); - circle = ShapeBuilders.newCircleBuilder().center(-180, 0).radius("100m").build(); + circle = new CircleBuilder().center(-180, 0).radius("100m").build(); assertEquals((360 * 100) / earthCircumference, circle.getRadius(), 0.00000001); assertEquals(new PointImpl(-180, 0, ShapeBuilder.SPATIAL_CONTEXT), circle.getCenter()); - circle = ShapeBuilders.newCircleBuilder().center(0, 90).radius("100m").build(); + circle = new CircleBuilder().center(0, 90).radius("100m").build(); assertEquals((360 * 100) / earthCircumference, circle.getRadius(), 0.00000001); assertEquals(new PointImpl(0, 90, ShapeBuilder.SPATIAL_CONTEXT), circle.getCenter()); - circle = ShapeBuilders.newCircleBuilder().center(0, -90).radius("100m").build(); + circle = new CircleBuilder().center(0, -90).radius("100m").build(); assertEquals((360 * 100) / earthCircumference, circle.getRadius(), 0.00000001); assertEquals(new PointImpl(0, -90, ShapeBuilder.SPATIAL_CONTEXT), circle.getCenter()); double randomLat = (randomDouble() * 180) - 90; double randomLon = (randomDouble() * 360) - 180; double randomRadius = randomIntBetween(1, (int) earthCircumference / 4); - circle = ShapeBuilders.newCircleBuilder().center(randomLon, randomLat).radius(randomRadius + "m").build(); + circle = new CircleBuilder().center(randomLon, randomLat).radius(randomRadius + "m").build(); assertEquals((360 * randomRadius) / earthCircumference, circle.getRadius(), 0.00000001); assertEquals(new PointImpl(randomLon, randomLat, ShapeBuilder.SPATIAL_CONTEXT), circle.getCenter()); } public void testPolygonWrapping() { - Shape shape = ShapeBuilders.newPolygon(new CoordinatesBuilder() + Shape shape = new PolygonBuilder(new CoordinatesBuilder() .coordinate(-150.0, 65.0) .coordinate(-250.0, 65.0) .coordinate(-250.0, -65.0) @@ -231,7 +234,7 @@ public class ShapeBuilderTests extends ESTestCase { } public void testLineStringWrapping() { - Shape shape = ShapeBuilders.newLineString(new CoordinatesBuilder() + Shape shape = new LineStringBuilder(new CoordinatesBuilder() .coordinate(-150.0, 65.0) .coordinate(-250.0, 65.0) .coordinate(-250.0, -65.0) @@ -248,7 +251,7 @@ public class ShapeBuilderTests extends ESTestCase { // expected results: 3 polygons, 1 with a hole // a giant c shape - PolygonBuilder builder = ShapeBuilders.newPolygon(new CoordinatesBuilder() + PolygonBuilder builder = new PolygonBuilder(new CoordinatesBuilder() .coordinate(174,0) .coordinate(-176,0) .coordinate(-176,3) @@ -292,7 +295,7 @@ public class ShapeBuilderTests extends ESTestCase { // expected results: 3 polygons, 1 with a hole // a giant c shape - PolygonBuilder builder = ShapeBuilders.newPolygon(new CoordinatesBuilder() + PolygonBuilder builder = new PolygonBuilder(new CoordinatesBuilder() .coordinate(-186,0) .coordinate(-176,0) .coordinate(-176,3) @@ -331,7 +334,7 @@ public class ShapeBuilderTests extends ESTestCase { } public void testComplexShapeWithHole() { - PolygonBuilder builder = ShapeBuilders.newPolygon(new CoordinatesBuilder() + PolygonBuilder builder = new PolygonBuilder(new CoordinatesBuilder() .coordinate(-85.0018514,37.1311314) .coordinate(-85.0016645,37.1315293) .coordinate(-85.0016246,37.1317069) @@ -407,7 +410,7 @@ public class ShapeBuilderTests extends ESTestCase { } public void testShapeWithHoleAtEdgeEndPoints() { - PolygonBuilder builder = ShapeBuilders.newPolygon(new CoordinatesBuilder() + PolygonBuilder builder = new PolygonBuilder(new CoordinatesBuilder() .coordinate(-4, 2) .coordinate(4, 2) .coordinate(6, 0) @@ -430,7 +433,7 @@ public class ShapeBuilderTests extends ESTestCase { } public void testShapeWithPointOnDateline() { - PolygonBuilder builder = ShapeBuilders.newPolygon(new CoordinatesBuilder() + PolygonBuilder builder = new PolygonBuilder(new CoordinatesBuilder() .coordinate(180, 0) .coordinate(176, 4) .coordinate(176, -4) @@ -443,7 +446,7 @@ public class ShapeBuilderTests extends ESTestCase { public void testShapeWithEdgeAlongDateline() { // test case 1: test the positive side of the dateline - PolygonBuilder builder = ShapeBuilders.newPolygon(new CoordinatesBuilder() + PolygonBuilder builder = new PolygonBuilder(new CoordinatesBuilder() .coordinate(180, 0) .coordinate(176, 4) .coordinate(180, -4) @@ -454,7 +457,7 @@ public class ShapeBuilderTests extends ESTestCase { assertPolygon(shape); // test case 2: test the negative side of the dateline - builder = ShapeBuilders.newPolygon(new CoordinatesBuilder() + builder = new PolygonBuilder(new CoordinatesBuilder() .coordinate(-176, 4) .coordinate(-180, 0) .coordinate(-180, -4) @@ -467,7 +470,7 @@ public class ShapeBuilderTests extends ESTestCase { public void testShapeWithBoundaryHoles() { // test case 1: test the positive side of the dateline - PolygonBuilder builder = ShapeBuilders.newPolygon(new CoordinatesBuilder() + PolygonBuilder builder = new PolygonBuilder(new CoordinatesBuilder() .coordinate(-177, 10) .coordinate(176, 15) .coordinate(172, 0) @@ -486,7 +489,7 @@ public class ShapeBuilderTests extends ESTestCase { assertMultiPolygon(shape); // test case 2: test the negative side of the dateline - builder = ShapeBuilders.newPolygon( + builder = new PolygonBuilder( new CoordinatesBuilder() .coordinate(-176, 15) .coordinate(179, 10) @@ -510,7 +513,7 @@ public class ShapeBuilderTests extends ESTestCase { public void testShapeWithTangentialHole() { // test a shape with one tangential (shared) vertex (should pass) - PolygonBuilder builder = ShapeBuilders.newPolygon(new CoordinatesBuilder() + PolygonBuilder builder = new PolygonBuilder(new CoordinatesBuilder() .coordinate(179, 10) .coordinate(168, 15) .coordinate(164, 0) @@ -531,7 +534,7 @@ public class ShapeBuilderTests extends ESTestCase { public void testShapeWithInvalidTangentialHole() { // test a shape with one invalid tangential (shared) vertex (should throw exception) - PolygonBuilder builder = ShapeBuilders.newPolygon(new CoordinatesBuilder() + PolygonBuilder builder = new PolygonBuilder(new CoordinatesBuilder() .coordinate(179, 10) .coordinate(168, 15) .coordinate(164, 0) @@ -552,7 +555,7 @@ public class ShapeBuilderTests extends ESTestCase { public void testBoundaryShapeWithTangentialHole() { // test a shape with one tangential (shared) vertex for each hole (should pass) - PolygonBuilder builder = ShapeBuilders.newPolygon(new CoordinatesBuilder() + PolygonBuilder builder = new PolygonBuilder(new CoordinatesBuilder() .coordinate(-177, 10) .coordinate(176, 15) .coordinate(172, 0) @@ -579,7 +582,7 @@ public class ShapeBuilderTests extends ESTestCase { public void testBoundaryShapeWithInvalidTangentialHole() { // test shape with two tangential (shared) vertices (should throw exception) - PolygonBuilder builder = ShapeBuilders.newPolygon(new CoordinatesBuilder() + PolygonBuilder builder = new PolygonBuilder(new CoordinatesBuilder() .coordinate(-177, 10) .coordinate(176, 15) .coordinate(172, 0) @@ -602,7 +605,7 @@ public class ShapeBuilderTests extends ESTestCase { * Test an enveloping polygon around the max mercator bounds */ public void testBoundaryShape() { - PolygonBuilder builder = ShapeBuilders.newPolygon(new CoordinatesBuilder() + PolygonBuilder builder = new PolygonBuilder(new CoordinatesBuilder() .coordinate(-180, 90) .coordinate(180, 90) .coordinate(180, -90) @@ -616,7 +619,7 @@ public class ShapeBuilderTests extends ESTestCase { public void testShapeWithAlternateOrientation() { // cw: should produce a multi polygon spanning hemispheres - PolygonBuilder builder = ShapeBuilders.newPolygon(new CoordinatesBuilder() + PolygonBuilder builder = new PolygonBuilder(new CoordinatesBuilder() .coordinate(180, 0) .coordinate(176, 4) .coordinate(-176, 4) @@ -627,7 +630,7 @@ public class ShapeBuilderTests extends ESTestCase { assertPolygon(shape); // cw: geo core will convert to ccw across the dateline - builder = ShapeBuilders.newPolygon(new CoordinatesBuilder() + builder = new PolygonBuilder(new CoordinatesBuilder() .coordinate(180, 0) .coordinate(-176, 4) .coordinate(176, 4) @@ -640,7 +643,7 @@ public class ShapeBuilderTests extends ESTestCase { } public void testInvalidShapeWithConsecutiveDuplicatePoints() { - PolygonBuilder builder = ShapeBuilders.newPolygon(new CoordinatesBuilder() + PolygonBuilder builder = new PolygonBuilder(new CoordinatesBuilder() .coordinate(180, 0) .coordinate(176, 4) .coordinate(176, 4) diff --git a/core/src/test/java/org/elasticsearch/common/geo/builders/AbstractShapeBuilderTestCase.java b/core/src/test/java/org/elasticsearch/common/geo/builders/AbstractShapeBuilderTestCase.java index c3e4781b3d2..5ac55832959 100644 --- a/core/src/test/java/org/elasticsearch/common/geo/builders/AbstractShapeBuilderTestCase.java +++ b/core/src/test/java/org/elasticsearch/common/geo/builders/AbstractShapeBuilderTestCase.java @@ -19,13 +19,14 @@ package org.elasticsearch.common.geo.builders; +import org.elasticsearch.common.geo.GeoShapeType; +import org.elasticsearch.common.geo.parsers.ShapeParser; import org.elasticsearch.common.io.stream.NamedWriteable; import org.elasticsearch.common.io.stream.NamedWriteableRegistry; import org.elasticsearch.common.io.stream.Writeable.Reader; import org.elasticsearch.common.xcontent.ToXContent; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentFactory; -import org.elasticsearch.common.xcontent.XContentHelper; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.test.ESTestCase; @@ -33,8 +34,6 @@ import org.junit.AfterClass; import org.junit.BeforeClass; import java.io.IOException; -import java.util.ArrayList; -import java.util.List; import static org.elasticsearch.test.EqualsHashCodeTestUtils.checkEqualsAndHashCode; @@ -49,9 +48,7 @@ public abstract class AbstractShapeBuilderTestCase exte @BeforeClass public static void init() { if (namedWriteableRegistry == null) { - List shapes = new ArrayList<>(); - ShapeBuilders.register(shapes); - namedWriteableRegistry = new NamedWriteableRegistry(shapes); + namedWriteableRegistry = new NamedWriteableRegistry(GeoShapeType.getShapeWriteables()); } } @@ -82,9 +79,9 @@ public abstract class AbstractShapeBuilderTestCase exte } XContentBuilder builder = testShape.toXContent(contentBuilder, ToXContent.EMPTY_PARAMS); XContentBuilder shuffled = shuffleXContent(builder); - XContentParser shapeParser = createParser(shuffled); - shapeParser.nextToken(); - ShapeBuilder parsedShape = ShapeBuilder.parse(shapeParser); + XContentParser shapeContentParser = createParser(shuffled); + shapeContentParser.nextToken(); + ShapeBuilder parsedShape = ShapeParser.parse(shapeContentParser); assertNotSame(testShape, parsedShape); assertEquals(testShape, parsedShape); assertEquals(testShape.hashCode(), parsedShape.hashCode()); diff --git a/core/src/test/java/org/elasticsearch/common/geo/builders/LineStringBuilderTests.java b/core/src/test/java/org/elasticsearch/common/geo/builders/LineStringBuilderTests.java index be7772b0da6..3b5f2662316 100644 --- a/core/src/test/java/org/elasticsearch/common/geo/builders/LineStringBuilderTests.java +++ b/core/src/test/java/org/elasticsearch/common/geo/builders/LineStringBuilderTests.java @@ -49,7 +49,7 @@ public class LineStringBuilderTests extends AbstractShapeBuilderTestCase