Selaa lähdekoodia

[GEO] Add optional left/right parameter to GeoJSON

This feature adds an optional orientation parameter to the GeoJSON document and geo_shape mapping enabling users to explicitly define how they want Elasticsearch to interpret vertex ordering.  The default uses the right-hand rule (counterclockwise for outer ring, clockwise for inner ring) complying with OGC Simple Feature Access standards. The parameter can be explicitly specified for an entire index using the geo_shape mapping by adding "orientation":{"left"|"right"|"cw"|"ccw"|"clockwise"|"counterclockwise"} and/or overridden on each insert by adding the same parameter to the GeoJSON document.

closes #8764
Nicholas Knize 11 vuotta sitten
vanhempi
commit
77a7ef28b3

+ 31 - 1
docs/reference/mapping/types/geo-shape-type.asciidoc

@@ -11,6 +11,7 @@ You can query documents using this type using
 or <<query-dsl-geo-shape-query,geo_shape
 Query>>.
 
+[[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 <<geo-shape-mapping-options>>). 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]
 

+ 11 - 4
src/main/java/org/elasticsearch/common/geo/builders/BasePolygonBuilder.java

@@ -48,6 +48,10 @@ public abstract class BasePolygonBuilder<E extends BasePolygonBuilder<E>> extend
     // List of linear rings defining the holes of the polygon 
     protected final ArrayList<BaseLineStringBuilder<?>> 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<E extends BasePolygonBuilder<E>> 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<E extends BasePolygonBuilder<E>> 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;
     }
 

+ 8 - 0
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;

+ 8 - 0
src/main/java/org/elasticsearch/common/geo/builders/GeometryCollectionBuilder.java

@@ -33,6 +33,14 @@ public class GeometryCollectionBuilder extends ShapeBuilder {
 
     protected final ArrayList<ShapeBuilder> 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;

+ 15 - 3
src/main/java/org/elasticsearch/common/geo/builders/MultiPolygonBuilder.java

@@ -35,13 +35,25 @@ public class MultiPolygonBuilder extends ShapeBuilder {
 
     protected final ArrayList<BasePolygonBuilder<?>> 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);
         }

+ 7 - 3
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<PolygonBuilder> {
 
     public PolygonBuilder() {
-        this(new ArrayList<Coordinate>());
+        this(new ArrayList<Coordinate>(), Orientation.RIGHT);
     }
 
-    protected PolygonBuilder(ArrayList<Coordinate> points) {
-        super();
+    public PolygonBuilder(Orientation orientation) {
+        this(new ArrayList<Coordinate>(), orientation);
+    }
+
+    protected PolygonBuilder(ArrayList<Coordinate> points, Orientation orientation) {
+        super(orientation);
         this.shell = new Ring<>(this, points);
     }
 

+ 130 - 22
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 <code><null</code>
+     * @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);

+ 20 - 5
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<String> {
         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<String> {
         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<String> {
             FIELD_TYPE.setOmitNorms(true);
             FIELD_TYPE.freeze();
         }
-
     }
 
     public static class Builder extends AbstractFieldMapper.Builder<Builder, GeoShapeFieldMapper> {
@@ -109,6 +111,7 @@ public class GeoShapeFieldMapper extends AbstractFieldMapper<String> {
         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<String> {
             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<String> {
                 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<String> {
                 } 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<String> {
     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<String> {
         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<String> {
         return this.termStrategy;
     }
 
+    public Orientation orientation() { return this.shapeOrientation; }
+
     public PrefixTreeStrategy resolveStrategy(String strategyName) {
         if (SpatialStrategy.RECURSIVE.getStrategyName().equals(strategyName)) {
             return recursiveStrategy;

+ 165 - 0
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();