[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
This commit is contained in:
Nicholas Knize 2014-12-16 12:03:05 -06:00
parent fb6c3b7c29
commit 77a7ef28b3
9 changed files with 395 additions and 38 deletions

View File

@ -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]

View File

@ -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;
}

View File

@ -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;

View File

@ -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;

View File

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

View File

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

View File

@ -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);

View File

@ -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;

View File

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