diff --git a/docs/reference/mapping/types/geo-shape-type.asciidoc b/docs/reference/mapping/types/geo-shape-type.asciidoc index 1b35ec2f067..4f342c62763 100644 --- a/docs/reference/mapping/types/geo-shape-type.asciidoc +++ b/docs/reference/mapping/types/geo-shape-type.asciidoc @@ -11,6 +11,7 @@ You can query documents using this type using or <>. +[[geo-shape-mapping-options]] [float] ==== Mapping Options @@ -46,6 +47,17 @@ via the mapping API even if you use the precision parameter. |`distance_error_pct` |Used as a hint to the PrefixTree about how precise it should be. Defaults to 0.025 (2.5%) with 0.5 as the maximum supported value. + +|`orientation` |Optionally define how to interpret vertex order for +polygons / multipolygons. This parameter defines one of two coordinate +system rules (Right-hand or Left-hand) each of which can be specified in three +different ways. 1. Right-hand rule (default): `right`, `ccw`, `counterclockwise`, +2. Left-hand rule: `left`, `cw`, `clockwise`. The default orientation +(`counterclockwise`) complies with the OGC standard which defines +outer ring vertices in counterclockwise order with inner ring(s) vertices (holes) +in clockwise order. Setting this parameter in the geo_shape mapping explicitly +sets vertex order for the coordinate list of a geo_shape field but can be +overridden in each individual GeoJSON document. |======================================================================= [float] @@ -246,7 +258,7 @@ defines the following vertex ordering: For polygons that do not cross the dateline, vertex order will not matter in Elasticsearch. For polygons that do cross the dateline, Elasticsearch requires -vertex orderinging comply with the OGC specification. Otherwise, an unintended polygon +vertex ordering to comply with the OGC specification. Otherwise, an unintended polygon may be created and unexpected query/filter results will be returned. The following provides an example of an ambiguous polygon. Elasticsearch will apply @@ -265,6 +277,24 @@ OGC standards to eliminate ambiguity resulting in a polygon that crosses the dat } -------------------------------------------------- +An `orientation` parameter can be defined when setting the geo_shape mapping (see <>). This will define vertex +order for the coordinate list on the mapped geo_shape field. It can also be overridden on each document. The following is an example for +overriding the orientation on a document: + +[source,js] +-------------------------------------------------- +{ + "location" : { + "type" : "polygon", + "orientation" : "clockwise", + "coordinates" : [ + [ [-177.0, 10.0], [176.0, 15.0], [172.0, 0.0], [176.0, -15.0], [-177.0, -10.0], [-177.0, 10.0] ], + [ [178.2, 8.2], [-178.8, 8.2], [-180.8, -8.8], [178.2, 8.8] ] + ] + } +} +-------------------------------------------------- + [float] ===== http://www.geojson.org/geojson-spec.html#id5[MultiPoint] diff --git a/src/main/java/org/elasticsearch/common/geo/builders/BasePolygonBuilder.java b/src/main/java/org/elasticsearch/common/geo/builders/BasePolygonBuilder.java index ee1b9ed18e1..126cd0e18fb 100644 --- a/src/main/java/org/elasticsearch/common/geo/builders/BasePolygonBuilder.java +++ b/src/main/java/org/elasticsearch/common/geo/builders/BasePolygonBuilder.java @@ -48,6 +48,10 @@ public abstract class BasePolygonBuilder> extend // List of linear rings defining the holes of the polygon protected final ArrayList> holes = new ArrayList<>(); + public BasePolygonBuilder(Orientation orientation) { + super(orientation); + } + @SuppressWarnings("unchecked") private E thisRef() { return (E)this; @@ -125,9 +129,9 @@ public abstract class BasePolygonBuilder> extend Edge[] edges = new Edge[numEdges]; Edge[] holeComponents = new Edge[holes.size()]; - int offset = createEdges(0, false, shell, null, edges, 0); + int offset = createEdges(0, orientation.getValue(), shell, null, edges, 0); for (int i = 0; i < holes.size(); i++) { - int length = createEdges(i+1, true, shell, this.holes.get(i), edges, offset); + int length = createEdges(i+1, orientation.getValue(), shell, this.holes.get(i), edges, offset); holeComponents[i] = edges[offset]; offset += length; } @@ -453,11 +457,14 @@ public abstract class BasePolygonBuilder> extend } } - private static int createEdges(int component, boolean direction, BaseLineStringBuilder shell, BaseLineStringBuilder hole, + private static int createEdges(int component, boolean orientation, BaseLineStringBuilder shell, + BaseLineStringBuilder hole, Edge[] edges, int offset) { + // inner rings (holes) have an opposite direction than the outer rings + boolean direction = (component != 0) ? !orientation : orientation; // set the points array accordingly (shell or hole) Coordinate[] points = (hole != null) ? hole.coordinates(false) : shell.coordinates(false); - Edge.ring(component, direction, shell, points, 0, edges, offset, points.length-1); + Edge.ring(component, direction, orientation, shell, points, 0, edges, offset, points.length-1); return points.length-1; } diff --git a/src/main/java/org/elasticsearch/common/geo/builders/EnvelopeBuilder.java b/src/main/java/org/elasticsearch/common/geo/builders/EnvelopeBuilder.java index 9de216be180..a296b3406ef 100644 --- a/src/main/java/org/elasticsearch/common/geo/builders/EnvelopeBuilder.java +++ b/src/main/java/org/elasticsearch/common/geo/builders/EnvelopeBuilder.java @@ -32,6 +32,14 @@ public class EnvelopeBuilder extends ShapeBuilder { protected Coordinate topLeft; protected Coordinate bottomRight; + public EnvelopeBuilder() { + this(Orientation.RIGHT); + } + + public EnvelopeBuilder(Orientation orientation) { + super(orientation); + } + public EnvelopeBuilder topLeft(Coordinate topLeft) { this.topLeft = topLeft; return this; diff --git a/src/main/java/org/elasticsearch/common/geo/builders/GeometryCollectionBuilder.java b/src/main/java/org/elasticsearch/common/geo/builders/GeometryCollectionBuilder.java index ff6a67cae5b..27121998833 100644 --- a/src/main/java/org/elasticsearch/common/geo/builders/GeometryCollectionBuilder.java +++ b/src/main/java/org/elasticsearch/common/geo/builders/GeometryCollectionBuilder.java @@ -33,6 +33,14 @@ public class GeometryCollectionBuilder extends ShapeBuilder { protected final ArrayList shapes = new ArrayList<>(); + public GeometryCollectionBuilder() { + this(Orientation.RIGHT); + } + + public GeometryCollectionBuilder(Orientation orientation) { + super(orientation); + } + public GeometryCollectionBuilder shape(ShapeBuilder shape) { this.shapes.add(shape); return this; diff --git a/src/main/java/org/elasticsearch/common/geo/builders/MultiPolygonBuilder.java b/src/main/java/org/elasticsearch/common/geo/builders/MultiPolygonBuilder.java index 2c57277559d..a47f0132d75 100644 --- a/src/main/java/org/elasticsearch/common/geo/builders/MultiPolygonBuilder.java +++ b/src/main/java/org/elasticsearch/common/geo/builders/MultiPolygonBuilder.java @@ -35,13 +35,25 @@ public class MultiPolygonBuilder extends ShapeBuilder { protected final ArrayList> polygons = new ArrayList<>(); + public MultiPolygonBuilder() { + this(Orientation.RIGHT); + } + + public MultiPolygonBuilder(Orientation orientation) { + super(orientation); + } + public MultiPolygonBuilder polygon(BasePolygonBuilder polygon) { this.polygons.add(polygon); return this; } public InternalPolygonBuilder polygon() { - InternalPolygonBuilder polygon = new InternalPolygonBuilder(this); + return polygon(Orientation.RIGHT); + } + + public InternalPolygonBuilder polygon(Orientation orientation) { + InternalPolygonBuilder polygon = new InternalPolygonBuilder(this, orientation); this.polygon(polygon); return polygon; } @@ -92,8 +104,8 @@ public class MultiPolygonBuilder extends ShapeBuilder { private final MultiPolygonBuilder collection; - private InternalPolygonBuilder(MultiPolygonBuilder collection) { - super(); + private InternalPolygonBuilder(MultiPolygonBuilder collection, Orientation orientation) { + super(orientation); this.collection = collection; this.shell = new Ring<>(this); } diff --git a/src/main/java/org/elasticsearch/common/geo/builders/PolygonBuilder.java b/src/main/java/org/elasticsearch/common/geo/builders/PolygonBuilder.java index 0534b58900d..d7c7fa6abd3 100644 --- a/src/main/java/org/elasticsearch/common/geo/builders/PolygonBuilder.java +++ b/src/main/java/org/elasticsearch/common/geo/builders/PolygonBuilder.java @@ -26,11 +26,15 @@ import com.vividsolutions.jts.geom.Coordinate; public class PolygonBuilder extends BasePolygonBuilder { public PolygonBuilder() { - this(new ArrayList()); + this(new ArrayList(), Orientation.RIGHT); } - protected PolygonBuilder(ArrayList points) { - super(); + public PolygonBuilder(Orientation orientation) { + this(new ArrayList(), orientation); + } + + protected PolygonBuilder(ArrayList points, Orientation orientation) { + super(orientation); this.shell = new Ring<>(this, points); } diff --git a/src/main/java/org/elasticsearch/common/geo/builders/ShapeBuilder.java b/src/main/java/org/elasticsearch/common/geo/builders/ShapeBuilder.java index 2be99d4c837..c0fd9f804e9 100644 --- a/src/main/java/org/elasticsearch/common/geo/builders/ShapeBuilder.java +++ b/src/main/java/org/elasticsearch/common/geo/builders/ShapeBuilder.java @@ -36,6 +36,7 @@ import org.elasticsearch.common.xcontent.XContent; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.common.xcontent.json.JsonXContent; +import org.elasticsearch.index.mapper.geo.GeoShapeFieldMapper; import java.io.IOException; import java.util.*; @@ -73,10 +74,16 @@ public abstract class ShapeBuilder implements ToXContent { /** @see com.spatial4j.core.shape.jts.JtsGeometry#index() */ protected final boolean autoIndexJtsGeometry = true;//may want to turn off once SpatialStrategy impls do it. + protected Orientation orientation = Orientation.RIGHT; + protected ShapeBuilder() { } + protected ShapeBuilder(Orientation orientation) { + this.orientation = orientation; + } + protected static Coordinate coordinate(double longitude, double latitude) { return new Coordinate(longitude, latitude); } @@ -143,6 +150,14 @@ public abstract class ShapeBuilder implements ToXContent { return new PolygonBuilder(); } + /** + * Create a new Polygon + * @return a new {@link PointBuilder} + */ + public static PolygonBuilder newPolygon(Orientation orientation) { + return new PolygonBuilder(orientation); + } + /** * Create a new Collection of polygons * @return a new {@link MultiPolygonBuilder} @@ -151,6 +166,14 @@ public abstract class ShapeBuilder implements ToXContent { return new MultiPolygonBuilder(); } + /** + * Create a new Collection of polygons + * @return a new {@link MultiPolygonBuilder} + */ + public static MultiPolygonBuilder newMultiPolygon(Orientation orientation) { + return new MultiPolygonBuilder(orientation); + } + /** * Create a new GeometryCollection * @return a new {@link GeometryCollectionBuilder} @@ -159,6 +182,14 @@ public abstract class ShapeBuilder implements ToXContent { return new GeometryCollectionBuilder(); } + /** + * Create a new GeometryCollection + * @return a new {@link GeometryCollectionBuilder} + */ + public static GeometryCollectionBuilder newGeometryCollection(Orientation orientation) { + return new GeometryCollectionBuilder(orientation); + } + /** * create a new Circle * @return a new {@link CircleBuilder} @@ -171,9 +202,13 @@ public abstract class ShapeBuilder implements ToXContent { * create a new rectangle * @return a new {@link EnvelopeBuilder} */ - public static EnvelopeBuilder newEnvelope() { - return new EnvelopeBuilder(); - } + public static EnvelopeBuilder newEnvelope() { return new EnvelopeBuilder(); } + + /** + * create a new rectangle + * @return a new {@link EnvelopeBuilder} + */ + public static EnvelopeBuilder newEnvelope(Orientation orientation) { return new EnvelopeBuilder(orientation); } @Override public String toString() { @@ -237,13 +272,43 @@ public abstract class ShapeBuilder implements ToXContent { * @throws IOException if the input could not be read */ public static ShapeBuilder parse(XContentParser parser) throws IOException { - return GeoShapeType.parse(parser); + 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 + * @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(); } + public static Orientation orientationFromString(String orientation) { + orientation = orientation.toLowerCase(Locale.ROOT); + switch (orientation) { + case "right": + case "counterclockwise": + case "ccw": + return Orientation.RIGHT; + case "left": + case "clockwise": + case "cw": + return Orientation.LEFT; + default: + throw new IllegalArgumentException("Unknown orientation [" + orientation + "]"); + } + } + protected static Coordinate shift(Coordinate coordinate, double dateline) { if (dateline == 0) { return coordinate; @@ -485,8 +550,8 @@ public abstract class ShapeBuilder implements ToXContent { * number of points * @return Array of edges */ - protected static Edge[] ring(int component, boolean direction, BaseLineStringBuilder shell, Coordinate[] points, int offset, - Edge[] edges, int toffset, int length) { + protected static Edge[] ring(int component, boolean direction, boolean handedness, BaseLineStringBuilder shell, + Coordinate[] points, int offset, Edge[] edges, int toffset, int length) { // calculate the direction of the points: // find the point a the top of the set and check its // neighbors orientation. So direction is equivalent @@ -508,15 +573,15 @@ public abstract class ShapeBuilder implements ToXContent { // 1. shell orientation is cw and range is greater than a hemisphere (180 degrees) but not spanning 2 hemispheres // (translation would result in a collapsed poly) // 2. the shell of the candidate hole has been translated (to preserve the coordinate system) - if (((component == 0 && orientation) && (rng > DATELINE && rng != 2*DATELINE)) - || (shell.translated && component != 0)) { + boolean incorrectOrientation = component == 0 && handedness != orientation; + if ( (incorrectOrientation && (rng > DATELINE && rng != 2*DATELINE)) || (shell.translated && component != 0)) { translate(points); // flip the translation bit if the shell is being translated if (component == 0) { shell.translated = true; } // correct the orientation post translation (ccw for shell, cw for holes) - if (component == 0 || (component != 0 && !orientation)) { + if (component == 0 || (component != 0 && handedness == orientation)) { orientation = !orientation; } } @@ -574,9 +639,35 @@ public abstract class ShapeBuilder implements ToXContent { } + public static enum Orientation { + LEFT("left", true), + CLOCKWISE("clockwise", true), + CW("cw", true), + RIGHT("right", false), + COUNTERCLOCKWISE("counterclockwise", false), + CCW("ccw", false); + + protected String name; + protected boolean orientation; + + private Orientation(String name, boolean orientation) { + this.orientation = orientation; + this.name = name; + } + + public static Orientation forName(String name) { + return Orientation.valueOf(name.toUpperCase(Locale.ROOT)); + } + + public boolean getValue() { + return orientation; + } + } + 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; @@ -613,6 +704,18 @@ public abstract class ShapeBuilder implements ToXContent { } 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 + * @throws IOException + */ + 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) { @@ -623,6 +726,7 @@ public abstract class ShapeBuilder implements ToXContent { Distance radius = null; CoordinateNode node = null; GeometryCollectionBuilder geometryCollections = null; + Orientation requestedOrientation = (shapeMapper == null) ? Orientation.RIGHT : shapeMapper.orientation(); XContentParser.Token token; while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { @@ -637,10 +741,13 @@ public abstract class ShapeBuilder implements ToXContent { node = parseCoordinates(parser); } else if (FIELD_GEOMETRIES.equals(fieldName)) { parser.nextToken(); - geometryCollections = parseGeometries(parser); + geometryCollections = parseGeometries(parser, requestedOrientation); } else if (CircleBuilder.FIELD_RADIUS.equals(fieldName)) { parser.nextToken(); radius = Distance.parseDistance(parser.text()); + } else if (FIELD_ORIENTATION.equals(fieldName)) { + parser.nextToken(); + requestedOrientation = orientationFromString(parser.text()); } else { parser.nextToken(); parser.skipChildren(); @@ -664,10 +771,10 @@ public abstract class ShapeBuilder implements ToXContent { case MULTIPOINT: return parseMultiPoint(node); case LINESTRING: return parseLineString(node); case MULTILINESTRING: return parseMultiLine(node); - case POLYGON: return parsePolygon(node); - case MULTIPOLYGON: return parseMultiPolygon(node); + case POLYGON: return parsePolygon(node, requestedOrientation); + case MULTIPOLYGON: return parseMultiPolygon(node, requestedOrientation); case CIRCLE: return parseCircle(node, radius); - case ENVELOPE: return parseEnvelope(node); + case ENVELOPE: return parseEnvelope(node, requestedOrientation); case GEOMETRYCOLLECTION: return geometryCollections; default: throw new ElasticsearchParseException("Shape type [" + shapeType + "] not included"); @@ -694,8 +801,9 @@ public abstract class ShapeBuilder implements ToXContent { return newCircleBuilder().center(coordinates.coordinate).radius(radius); } - protected static EnvelopeBuilder parseEnvelope(CoordinateNode coordinates) { - return newEnvelope().topLeft(coordinates.children.get(0).coordinate).bottomRight(coordinates.children.get(1).coordinate); + protected static EnvelopeBuilder parseEnvelope(CoordinateNode coordinates, Orientation orientation) { + return newEnvelope(orientation). + topLeft(coordinates.children.get(0).coordinate).bottomRight(coordinates.children.get(1).coordinate); } protected static void validateMultiPointNode(CoordinateNode coordinates) { @@ -766,24 +874,24 @@ public abstract class ShapeBuilder implements ToXContent { return parseLineString(coordinates); } - protected static PolygonBuilder parsePolygon(CoordinateNode coordinates) { + protected static PolygonBuilder parsePolygon(CoordinateNode coordinates, Orientation orientation) { 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)); - PolygonBuilder polygon = new PolygonBuilder(shell.points); + PolygonBuilder polygon = new PolygonBuilder(shell.points, orientation); for (int i = 1; i < coordinates.children.size(); i++) { polygon.hole(parseLinearRing(coordinates.children.get(i))); } return polygon; } - protected static MultiPolygonBuilder parseMultiPolygon(CoordinateNode coordinates) { - MultiPolygonBuilder polygons = newMultiPolygon(); + protected static MultiPolygonBuilder parseMultiPolygon(CoordinateNode coordinates, Orientation orientation) { + MultiPolygonBuilder polygons = newMultiPolygon(orientation); for (CoordinateNode node : coordinates.children) { - polygons.polygon(parsePolygon(node)); + polygons.polygon(parsePolygon(node, orientation)); } return polygons; } @@ -795,13 +903,13 @@ public abstract class ShapeBuilder implements ToXContent { * @return Geometry[] geometries of the GeometryCollection * @throws IOException Thrown if an error occurs while reading from the XContentParser */ - protected static GeometryCollectionBuilder parseGeometries(XContentParser parser) throws IOException { + protected static GeometryCollectionBuilder parseGeometries(XContentParser parser, Orientation orientation) 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 = newGeometryCollection(); + GeometryCollectionBuilder geometryCollection = newGeometryCollection(orientation); while (token != XContentParser.Token.END_ARRAY) { ShapeBuilder shapeBuilder = GeoShapeType.parse(parser); geometryCollection.shape(shapeBuilder); diff --git a/src/main/java/org/elasticsearch/index/mapper/geo/GeoShapeFieldMapper.java b/src/main/java/org/elasticsearch/index/mapper/geo/GeoShapeFieldMapper.java index 5b1f0359924..8e48726f4a0 100644 --- a/src/main/java/org/elasticsearch/index/mapper/geo/GeoShapeFieldMapper.java +++ b/src/main/java/org/elasticsearch/index/mapper/geo/GeoShapeFieldMapper.java @@ -33,6 +33,7 @@ import org.elasticsearch.common.Strings; 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.unit.DistanceUnit; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.index.codec.docvaluesformat.DocValuesFormatProvider; @@ -79,6 +80,7 @@ public class GeoShapeFieldMapper extends AbstractFieldMapper { public static final String TREE_LEVELS = "tree_levels"; public static final String TREE_PRESISION = "precision"; public static final String DISTANCE_ERROR_PCT = "distance_error_pct"; + public static final String ORIENTATION = "orientation"; public static final String STRATEGY = "strategy"; } @@ -88,6 +90,7 @@ public class GeoShapeFieldMapper extends AbstractFieldMapper { public static final int GEOHASH_LEVELS = GeoUtils.geoHashLevelsForPrecision("50m"); public static final int QUADTREE_LEVELS = GeoUtils.quadTreeLevelsForPrecision("50m"); public static final double DISTANCE_ERROR_PCT = 0.025d; + public static final Orientation ORIENTATION = Orientation.RIGHT; public static final FieldType FIELD_TYPE = new FieldType(); @@ -99,7 +102,6 @@ public class GeoShapeFieldMapper extends AbstractFieldMapper { FIELD_TYPE.setOmitNorms(true); FIELD_TYPE.freeze(); } - } public static class Builder extends AbstractFieldMapper.Builder { @@ -109,6 +111,7 @@ public class GeoShapeFieldMapper extends AbstractFieldMapper { private int treeLevels = 0; private double precisionInMeters = -1; private double distanceErrorPct = Defaults.DISTANCE_ERROR_PCT; + private Orientation orientation = Defaults.ORIENTATION; private SpatialPrefixTree prefixTree; @@ -141,6 +144,11 @@ public class GeoShapeFieldMapper extends AbstractFieldMapper { return this; } + public Builder orientation(Orientation orientation) { + this.orientation = orientation; + return this; + } + @Override public GeoShapeFieldMapper build(BuilderContext context) { @@ -153,7 +161,7 @@ public class GeoShapeFieldMapper extends AbstractFieldMapper { throw new ElasticsearchIllegalArgumentException("Unknown prefix tree type [" + tree + "]"); } - return new GeoShapeFieldMapper(names, prefixTree, strategyName, distanceErrorPct, fieldType, postingsProvider, + return new GeoShapeFieldMapper(names, prefixTree, strategyName, distanceErrorPct, orientation, fieldType, postingsProvider, docValuesProvider, multiFieldsBuilder.build(this, context), copyTo); } } @@ -189,6 +197,9 @@ public class GeoShapeFieldMapper extends AbstractFieldMapper { } else if (Names.DISTANCE_ERROR_PCT.equals(fieldName)) { builder.distanceErrorPct(Double.parseDouble(fieldNode.toString())); iterator.remove(); + } else if (Names.ORIENTATION.equals(fieldName)) { + builder.orientation(ShapeBuilder.orientationFromString(fieldNode.toString())); + iterator.remove(); } else if (Names.STRATEGY.equals(fieldName)) { builder.strategy(fieldNode.toString()); iterator.remove(); @@ -201,16 +212,18 @@ public class GeoShapeFieldMapper extends AbstractFieldMapper { private final PrefixTreeStrategy defaultStrategy; private final RecursivePrefixTreeStrategy recursiveStrategy; private final TermQueryPrefixTreeStrategy termStrategy; + private Orientation shapeOrientation; public GeoShapeFieldMapper(FieldMapper.Names names, SpatialPrefixTree tree, String defaultStrategyName, double distanceErrorPct, - FieldType fieldType, PostingsFormatProvider postingsProvider, DocValuesFormatProvider docValuesProvider, - MultiFields multiFields, CopyTo copyTo) { + Orientation shapeOrientation, FieldType fieldType, PostingsFormatProvider postingsProvider, + DocValuesFormatProvider docValuesProvider, MultiFields multiFields, CopyTo copyTo) { super(names, 1, fieldType, null, null, null, postingsProvider, docValuesProvider, null, null, null, null, multiFields, copyTo); this.recursiveStrategy = new RecursivePrefixTreeStrategy(tree, names.indexName()); this.recursiveStrategy.setDistErrPct(distanceErrorPct); this.termStrategy = new TermQueryPrefixTreeStrategy(tree, names.indexName()); this.termStrategy.setDistErrPct(distanceErrorPct); this.defaultStrategy = resolveStrategy(defaultStrategyName); + this.shapeOrientation = shapeOrientation; } @Override @@ -233,7 +246,7 @@ public class GeoShapeFieldMapper extends AbstractFieldMapper { try { Shape shape = context.parseExternalValue(Shape.class); if (shape == null) { - ShapeBuilder shapeBuilder = ShapeBuilder.parse(context.parser()); + ShapeBuilder shapeBuilder = ShapeBuilder.parse(context.parser(), this); if (shapeBuilder == null) { return; } @@ -305,6 +318,8 @@ public class GeoShapeFieldMapper extends AbstractFieldMapper { return this.termStrategy; } + public Orientation orientation() { return this.shapeOrientation; } + public PrefixTreeStrategy resolveStrategy(String strategyName) { if (SpatialStrategy.RECURSIVE.getStrategyName().equals(strategyName)) { return recursiveStrategy; diff --git a/src/test/java/org/elasticsearch/common/geo/GeoJSONShapeParserTests.java b/src/test/java/org/elasticsearch/common/geo/GeoJSONShapeParserTests.java index de1bf890609..449d7a51027 100644 --- a/src/test/java/org/elasticsearch/common/geo/GeoJSONShapeParserTests.java +++ b/src/test/java/org/elasticsearch/common/geo/GeoJSONShapeParserTests.java @@ -681,6 +681,171 @@ public class GeoJSONShapeParserTests extends ElasticsearchTestCase { assertGeometryEquals(new JtsPoint(expected, SPATIAL_CONTEXT), pointGeoJson); } + @Test + public void testParse_orientationOption() throws IOException { + // test 1: valid ccw (right handed system) poly not crossing dateline (with 'right' field) + String polygonGeoJson = XContentFactory.jsonBuilder().startObject().field("type", "Polygon") + .field("orientation", "right") + .startArray("coordinates") + .startArray() + .startArray().value(176.0).value(15.0).endArray() + .startArray().value(-177.0).value(10.0).endArray() + .startArray().value(-177.0).value(-10.0).endArray() + .startArray().value(176.0).value(-15.0).endArray() + .startArray().value(172.0).value(0.0).endArray() + .startArray().value(176.0).value(15.0).endArray() + .endArray() + .startArray() + .startArray().value(-172.0).value(8.0).endArray() + .startArray().value(174.0).value(10.0).endArray() + .startArray().value(-172.0).value(-8.0).endArray() + .startArray().value(-172.0).value(8.0).endArray() + .endArray() + .endArray() + .endObject().string(); + + XContentParser parser = JsonXContent.jsonXContent.createParser(polygonGeoJson); + parser.nextToken(); + Shape shape = ShapeBuilder.parse(parser).build(); + + ElasticsearchGeoAssertions.assertPolygon(shape); + + // test 2: valid ccw (right handed system) poly not crossing dateline (with 'ccw' field) + polygonGeoJson = XContentFactory.jsonBuilder().startObject().field("type", "Polygon") + .field("orientation", "ccw") + .startArray("coordinates") + .startArray() + .startArray().value(176.0).value(15.0).endArray() + .startArray().value(-177.0).value(10.0).endArray() + .startArray().value(-177.0).value(-10.0).endArray() + .startArray().value(176.0).value(-15.0).endArray() + .startArray().value(172.0).value(0.0).endArray() + .startArray().value(176.0).value(15.0).endArray() + .endArray() + .startArray() + .startArray().value(-172.0).value(8.0).endArray() + .startArray().value(174.0).value(10.0).endArray() + .startArray().value(-172.0).value(-8.0).endArray() + .startArray().value(-172.0).value(8.0).endArray() + .endArray() + .endArray() + .endObject().string(); + + parser = JsonXContent.jsonXContent.createParser(polygonGeoJson); + parser.nextToken(); + shape = ShapeBuilder.parse(parser).build(); + + ElasticsearchGeoAssertions.assertPolygon(shape); + + // test 3: valid ccw (right handed system) poly not crossing dateline (with 'counterclockwise' field) + polygonGeoJson = XContentFactory.jsonBuilder().startObject().field("type", "Polygon") + .field("orientation", "counterclockwise") + .startArray("coordinates") + .startArray() + .startArray().value(176.0).value(15.0).endArray() + .startArray().value(-177.0).value(10.0).endArray() + .startArray().value(-177.0).value(-10.0).endArray() + .startArray().value(176.0).value(-15.0).endArray() + .startArray().value(172.0).value(0.0).endArray() + .startArray().value(176.0).value(15.0).endArray() + .endArray() + .startArray() + .startArray().value(-172.0).value(8.0).endArray() + .startArray().value(174.0).value(10.0).endArray() + .startArray().value(-172.0).value(-8.0).endArray() + .startArray().value(-172.0).value(8.0).endArray() + .endArray() + .endArray() + .endObject().string(); + + parser = JsonXContent.jsonXContent.createParser(polygonGeoJson); + parser.nextToken(); + shape = ShapeBuilder.parse(parser).build(); + + ElasticsearchGeoAssertions.assertPolygon(shape); + + // test 4: valid cw (left handed system) poly crossing dateline (with 'left' field) + polygonGeoJson = XContentFactory.jsonBuilder().startObject().field("type", "Polygon") + .field("orientation", "left") + .startArray("coordinates") + .startArray() + .startArray().value(176.0).value(15.0).endArray() + .startArray().value(-177.0).value(10.0).endArray() + .startArray().value(-177.0).value(-10.0).endArray() + .startArray().value(176.0).value(-15.0).endArray() + .startArray().value(172.0).value(0.0).endArray() + .startArray().value(176.0).value(15.0).endArray() + .endArray() + .startArray() + .startArray().value(-178.0).value(8.0).endArray() + .startArray().value(178.0).value(8.0).endArray() + .startArray().value(180.0).value(-8.0).endArray() + .startArray().value(-178.0).value(8.0).endArray() + .endArray() + .endArray() + .endObject().string(); + + parser = JsonXContent.jsonXContent.createParser(polygonGeoJson); + parser.nextToken(); + shape = ShapeBuilder.parse(parser).build(); + + ElasticsearchGeoAssertions.assertMultiPolygon(shape); + + // test 5: valid cw multipoly (left handed system) poly crossing dateline (with 'cw' field) + polygonGeoJson = XContentFactory.jsonBuilder().startObject().field("type", "Polygon") + .field("orientation", "cw") + .startArray("coordinates") + .startArray() + .startArray().value(176.0).value(15.0).endArray() + .startArray().value(-177.0).value(10.0).endArray() + .startArray().value(-177.0).value(-10.0).endArray() + .startArray().value(176.0).value(-15.0).endArray() + .startArray().value(172.0).value(0.0).endArray() + .startArray().value(176.0).value(15.0).endArray() + .endArray() + .startArray() + .startArray().value(-178.0).value(8.0).endArray() + .startArray().value(178.0).value(8.0).endArray() + .startArray().value(180.0).value(-8.0).endArray() + .startArray().value(-178.0).value(8.0).endArray() + .endArray() + .endArray() + .endObject().string(); + + parser = JsonXContent.jsonXContent.createParser(polygonGeoJson); + parser.nextToken(); + shape = ShapeBuilder.parse(parser).build(); + + ElasticsearchGeoAssertions.assertMultiPolygon(shape); + + // test 6: valid cw multipoly (left handed system) poly crossing dateline (with 'clockwise' field) + polygonGeoJson = XContentFactory.jsonBuilder().startObject().field("type", "Polygon") + .field("orientation", "clockwise") + .startArray("coordinates") + .startArray() + .startArray().value(176.0).value(15.0).endArray() + .startArray().value(-177.0).value(10.0).endArray() + .startArray().value(-177.0).value(-10.0).endArray() + .startArray().value(176.0).value(-15.0).endArray() + .startArray().value(172.0).value(0.0).endArray() + .startArray().value(176.0).value(15.0).endArray() + .endArray() + .startArray() + .startArray().value(-178.0).value(8.0).endArray() + .startArray().value(178.0).value(8.0).endArray() + .startArray().value(180.0).value(-8.0).endArray() + .startArray().value(-178.0).value(8.0).endArray() + .endArray() + .endArray() + .endObject().string(); + + parser = JsonXContent.jsonXContent.createParser(polygonGeoJson); + parser.nextToken(); + shape = ShapeBuilder.parse(parser).build(); + + ElasticsearchGeoAssertions.assertMultiPolygon(shape); + } + private void assertGeometryEquals(Shape expected, String geoJson) throws IOException { XContentParser parser = JsonXContent.jsonXContent.createParser(geoJson); parser.nextToken();