[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 or <<query-dsl-geo-shape-query,geo_shape
Query>>. Query>>.
[[geo-shape-mapping-options]]
[float] [float]
==== Mapping Options ==== 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 |`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 precise it should be. Defaults to 0.025 (2.5%) with 0.5 as the maximum
supported value. 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] [float]
@ -246,7 +258,7 @@ defines the following vertex ordering:
For polygons that do not cross the dateline, vertex order will not matter in For polygons that do not cross the dateline, vertex order will not matter in
Elasticsearch. For polygons that do cross the dateline, Elasticsearch requires 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. may be created and unexpected query/filter results will be returned.
The following provides an example of an ambiguous polygon. Elasticsearch will apply 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] [float]
===== http://www.geojson.org/geojson-spec.html#id5[MultiPoint] ===== 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 // List of linear rings defining the holes of the polygon
protected final ArrayList<BaseLineStringBuilder<?>> holes = new ArrayList<>(); protected final ArrayList<BaseLineStringBuilder<?>> holes = new ArrayList<>();
public BasePolygonBuilder(Orientation orientation) {
super(orientation);
}
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
private E thisRef() { private E thisRef() {
return (E)this; return (E)this;
@ -125,9 +129,9 @@ public abstract class BasePolygonBuilder<E extends BasePolygonBuilder<E>> extend
Edge[] edges = new Edge[numEdges]; Edge[] edges = new Edge[numEdges];
Edge[] holeComponents = new Edge[holes.size()]; 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++) { 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]; holeComponents[i] = edges[offset];
offset += length; 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) { 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) // set the points array accordingly (shell or hole)
Coordinate[] points = (hole != null) ? hole.coordinates(false) : shell.coordinates(false); 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; return points.length-1;
} }

View File

@ -32,6 +32,14 @@ public class EnvelopeBuilder extends ShapeBuilder {
protected Coordinate topLeft; protected Coordinate topLeft;
protected Coordinate bottomRight; protected Coordinate bottomRight;
public EnvelopeBuilder() {
this(Orientation.RIGHT);
}
public EnvelopeBuilder(Orientation orientation) {
super(orientation);
}
public EnvelopeBuilder topLeft(Coordinate topLeft) { public EnvelopeBuilder topLeft(Coordinate topLeft) {
this.topLeft = topLeft; this.topLeft = topLeft;
return this; return this;

View File

@ -33,6 +33,14 @@ public class GeometryCollectionBuilder extends ShapeBuilder {
protected final ArrayList<ShapeBuilder> shapes = new ArrayList<>(); protected final ArrayList<ShapeBuilder> shapes = new ArrayList<>();
public GeometryCollectionBuilder() {
this(Orientation.RIGHT);
}
public GeometryCollectionBuilder(Orientation orientation) {
super(orientation);
}
public GeometryCollectionBuilder shape(ShapeBuilder shape) { public GeometryCollectionBuilder shape(ShapeBuilder shape) {
this.shapes.add(shape); this.shapes.add(shape);
return this; return this;

View File

@ -35,13 +35,25 @@ public class MultiPolygonBuilder extends ShapeBuilder {
protected final ArrayList<BasePolygonBuilder<?>> polygons = new ArrayList<>(); protected final ArrayList<BasePolygonBuilder<?>> polygons = new ArrayList<>();
public MultiPolygonBuilder() {
this(Orientation.RIGHT);
}
public MultiPolygonBuilder(Orientation orientation) {
super(orientation);
}
public MultiPolygonBuilder polygon(BasePolygonBuilder<?> polygon) { public MultiPolygonBuilder polygon(BasePolygonBuilder<?> polygon) {
this.polygons.add(polygon); this.polygons.add(polygon);
return this; return this;
} }
public InternalPolygonBuilder polygon() { 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); this.polygon(polygon);
return polygon; return polygon;
} }
@ -92,8 +104,8 @@ public class MultiPolygonBuilder extends ShapeBuilder {
private final MultiPolygonBuilder collection; private final MultiPolygonBuilder collection;
private InternalPolygonBuilder(MultiPolygonBuilder collection) { private InternalPolygonBuilder(MultiPolygonBuilder collection, Orientation orientation) {
super(); super(orientation);
this.collection = collection; this.collection = collection;
this.shell = new Ring<>(this); this.shell = new Ring<>(this);
} }

View File

@ -26,11 +26,15 @@ import com.vividsolutions.jts.geom.Coordinate;
public class PolygonBuilder extends BasePolygonBuilder<PolygonBuilder> { public class PolygonBuilder extends BasePolygonBuilder<PolygonBuilder> {
public PolygonBuilder() { public PolygonBuilder() {
this(new ArrayList<Coordinate>()); this(new ArrayList<Coordinate>(), Orientation.RIGHT);
} }
protected PolygonBuilder(ArrayList<Coordinate> points) { public PolygonBuilder(Orientation orientation) {
super(); this(new ArrayList<Coordinate>(), orientation);
}
protected PolygonBuilder(ArrayList<Coordinate> points, Orientation orientation) {
super(orientation);
this.shell = new Ring<>(this, points); 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.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.common.xcontent.json.JsonXContent; import org.elasticsearch.common.xcontent.json.JsonXContent;
import org.elasticsearch.index.mapper.geo.GeoShapeFieldMapper;
import java.io.IOException; import java.io.IOException;
import java.util.*; import java.util.*;
@ -73,10 +74,16 @@ public abstract class ShapeBuilder implements ToXContent {
/** @see com.spatial4j.core.shape.jts.JtsGeometry#index() */ /** @see com.spatial4j.core.shape.jts.JtsGeometry#index() */
protected final boolean autoIndexJtsGeometry = true;//may want to turn off once SpatialStrategy impls do it. protected final boolean autoIndexJtsGeometry = true;//may want to turn off once SpatialStrategy impls do it.
protected Orientation orientation = Orientation.RIGHT;
protected ShapeBuilder() { protected ShapeBuilder() {
} }
protected ShapeBuilder(Orientation orientation) {
this.orientation = orientation;
}
protected static Coordinate coordinate(double longitude, double latitude) { protected static Coordinate coordinate(double longitude, double latitude) {
return new Coordinate(longitude, latitude); return new Coordinate(longitude, latitude);
} }
@ -143,6 +150,14 @@ public abstract class ShapeBuilder implements ToXContent {
return new PolygonBuilder(); 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 * Create a new Collection of polygons
* @return a new {@link MultiPolygonBuilder} * @return a new {@link MultiPolygonBuilder}
@ -151,6 +166,14 @@ public abstract class ShapeBuilder implements ToXContent {
return new MultiPolygonBuilder(); 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 * Create a new GeometryCollection
* @return a new {@link GeometryCollectionBuilder} * @return a new {@link GeometryCollectionBuilder}
@ -159,6 +182,14 @@ public abstract class ShapeBuilder implements ToXContent {
return new GeometryCollectionBuilder(); 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 * create a new Circle
* @return a new {@link CircleBuilder} * @return a new {@link CircleBuilder}
@ -171,9 +202,13 @@ public abstract class ShapeBuilder implements ToXContent {
* create a new rectangle * create a new rectangle
* @return a new {@link EnvelopeBuilder} * @return a new {@link EnvelopeBuilder}
*/ */
public static EnvelopeBuilder newEnvelope() { public static EnvelopeBuilder newEnvelope() { return new EnvelopeBuilder(); }
return new EnvelopeBuilder();
} /**
* create a new rectangle
* @return a new {@link EnvelopeBuilder}
*/
public static EnvelopeBuilder newEnvelope(Orientation orientation) { return new EnvelopeBuilder(orientation); }
@Override @Override
public String toString() { public String toString() {
@ -237,13 +272,43 @@ public abstract class ShapeBuilder implements ToXContent {
* @throws IOException if the input could not be read * @throws IOException if the input could not be read
*/ */
public static ShapeBuilder parse(XContentParser parser) throws IOException { 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 { protected static XContentBuilder toXContent(XContentBuilder builder, Coordinate coordinate) throws IOException {
return builder.startArray().value(coordinate.x).value(coordinate.y).endArray(); 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) { protected static Coordinate shift(Coordinate coordinate, double dateline) {
if (dateline == 0) { if (dateline == 0) {
return coordinate; return coordinate;
@ -485,8 +550,8 @@ public abstract class ShapeBuilder implements ToXContent {
* number of points * number of points
* @return Array of edges * @return Array of edges
*/ */
protected static Edge[] ring(int component, boolean direction, BaseLineStringBuilder<?> shell, Coordinate[] points, int offset, protected static Edge[] ring(int component, boolean direction, boolean handedness, BaseLineStringBuilder<?> shell,
Edge[] edges, int toffset, int length) { Coordinate[] points, int offset, Edge[] edges, int toffset, int length) {
// calculate the direction of the points: // calculate the direction of the points:
// find the point a the top of the set and check its // find the point a the top of the set and check its
// neighbors orientation. So direction is equivalent // 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 // 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) // (translation would result in a collapsed poly)
// 2. the shell of the candidate hole has been translated (to preserve the coordinate system) // 2. the shell of the candidate hole has been translated (to preserve the coordinate system)
if (((component == 0 && orientation) && (rng > DATELINE && rng != 2*DATELINE)) boolean incorrectOrientation = component == 0 && handedness != orientation;
|| (shell.translated && component != 0)) { if ( (incorrectOrientation && (rng > DATELINE && rng != 2*DATELINE)) || (shell.translated && component != 0)) {
translate(points); translate(points);
// flip the translation bit if the shell is being translated // flip the translation bit if the shell is being translated
if (component == 0) { if (component == 0) {
shell.translated = true; shell.translated = true;
} }
// correct the orientation post translation (ccw for shell, cw for holes) // 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; 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_TYPE = "type";
public static final String FIELD_COORDINATES = "coordinates"; public static final String FIELD_COORDINATES = "coordinates";
public static final String FIELD_GEOMETRIES = "geometries"; public static final String FIELD_GEOMETRIES = "geometries";
public static final String FIELD_ORIENTATION = "orientation";
protected static final boolean debugEnabled() { protected static final boolean debugEnabled() {
return LOGGER.isDebugEnabled() || DEBUG; return LOGGER.isDebugEnabled() || DEBUG;
@ -613,6 +704,18 @@ public abstract class ShapeBuilder implements ToXContent {
} }
public static ShapeBuilder parse(XContentParser parser) throws IOException { 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) { if (parser.currentToken() == XContentParser.Token.VALUE_NULL) {
return null; return null;
} else if (parser.currentToken() != XContentParser.Token.START_OBJECT) { } else if (parser.currentToken() != XContentParser.Token.START_OBJECT) {
@ -623,6 +726,7 @@ public abstract class ShapeBuilder implements ToXContent {
Distance radius = null; Distance radius = null;
CoordinateNode node = null; CoordinateNode node = null;
GeometryCollectionBuilder geometryCollections = null; GeometryCollectionBuilder geometryCollections = null;
Orientation requestedOrientation = (shapeMapper == null) ? Orientation.RIGHT : shapeMapper.orientation();
XContentParser.Token token; XContentParser.Token token;
while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
@ -637,10 +741,13 @@ public abstract class ShapeBuilder implements ToXContent {
node = parseCoordinates(parser); node = parseCoordinates(parser);
} else if (FIELD_GEOMETRIES.equals(fieldName)) { } else if (FIELD_GEOMETRIES.equals(fieldName)) {
parser.nextToken(); parser.nextToken();
geometryCollections = parseGeometries(parser); geometryCollections = parseGeometries(parser, requestedOrientation);
} else if (CircleBuilder.FIELD_RADIUS.equals(fieldName)) { } else if (CircleBuilder.FIELD_RADIUS.equals(fieldName)) {
parser.nextToken(); parser.nextToken();
radius = Distance.parseDistance(parser.text()); radius = Distance.parseDistance(parser.text());
} else if (FIELD_ORIENTATION.equals(fieldName)) {
parser.nextToken();
requestedOrientation = orientationFromString(parser.text());
} else { } else {
parser.nextToken(); parser.nextToken();
parser.skipChildren(); parser.skipChildren();
@ -664,10 +771,10 @@ public abstract class ShapeBuilder implements ToXContent {
case MULTIPOINT: return parseMultiPoint(node); case MULTIPOINT: return parseMultiPoint(node);
case LINESTRING: return parseLineString(node); case LINESTRING: return parseLineString(node);
case MULTILINESTRING: return parseMultiLine(node); case MULTILINESTRING: return parseMultiLine(node);
case POLYGON: return parsePolygon(node); case POLYGON: return parsePolygon(node, requestedOrientation);
case MULTIPOLYGON: return parseMultiPolygon(node); case MULTIPOLYGON: return parseMultiPolygon(node, requestedOrientation);
case CIRCLE: return parseCircle(node, radius); case CIRCLE: return parseCircle(node, radius);
case ENVELOPE: return parseEnvelope(node); case ENVELOPE: return parseEnvelope(node, requestedOrientation);
case GEOMETRYCOLLECTION: return geometryCollections; case GEOMETRYCOLLECTION: return geometryCollections;
default: default:
throw new ElasticsearchParseException("Shape type [" + shapeType + "] not included"); 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); return newCircleBuilder().center(coordinates.coordinate).radius(radius);
} }
protected static EnvelopeBuilder parseEnvelope(CoordinateNode coordinates) { protected static EnvelopeBuilder parseEnvelope(CoordinateNode coordinates, Orientation orientation) {
return newEnvelope().topLeft(coordinates.children.get(0).coordinate).bottomRight(coordinates.children.get(1).coordinate); return newEnvelope(orientation).
topLeft(coordinates.children.get(0).coordinate).bottomRight(coordinates.children.get(1).coordinate);
} }
protected static void validateMultiPointNode(CoordinateNode coordinates) { protected static void validateMultiPointNode(CoordinateNode coordinates) {
@ -766,24 +874,24 @@ public abstract class ShapeBuilder implements ToXContent {
return parseLineString(coordinates); return parseLineString(coordinates);
} }
protected static PolygonBuilder parsePolygon(CoordinateNode coordinates) { protected static PolygonBuilder parsePolygon(CoordinateNode coordinates, Orientation orientation) {
if (coordinates.children == null || coordinates.children.isEmpty()) { if (coordinates.children == null || coordinates.children.isEmpty()) {
throw new ElasticsearchParseException("Invalid LinearRing provided for type polygon. Linear ring must be an array of " + throw new ElasticsearchParseException("Invalid LinearRing provided for type polygon. Linear ring must be an array of " +
"coordinates"); "coordinates");
} }
LineStringBuilder shell = parseLinearRing(coordinates.children.get(0)); 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++) { for (int i = 1; i < coordinates.children.size(); i++) {
polygon.hole(parseLinearRing(coordinates.children.get(i))); polygon.hole(parseLinearRing(coordinates.children.get(i)));
} }
return polygon; return polygon;
} }
protected static MultiPolygonBuilder parseMultiPolygon(CoordinateNode coordinates) { protected static MultiPolygonBuilder parseMultiPolygon(CoordinateNode coordinates, Orientation orientation) {
MultiPolygonBuilder polygons = newMultiPolygon(); MultiPolygonBuilder polygons = newMultiPolygon(orientation);
for (CoordinateNode node : coordinates.children) { for (CoordinateNode node : coordinates.children) {
polygons.polygon(parsePolygon(node)); polygons.polygon(parsePolygon(node, orientation));
} }
return polygons; return polygons;
} }
@ -795,13 +903,13 @@ public abstract class ShapeBuilder implements ToXContent {
* @return Geometry[] geometries of the GeometryCollection * @return Geometry[] geometries of the GeometryCollection
* @throws IOException Thrown if an error occurs while reading from the XContentParser * @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) { if (parser.currentToken() != XContentParser.Token.START_ARRAY) {
throw new ElasticsearchParseException("Geometries must be an array of geojson objects"); throw new ElasticsearchParseException("Geometries must be an array of geojson objects");
} }
XContentParser.Token token = parser.nextToken(); XContentParser.Token token = parser.nextToken();
GeometryCollectionBuilder geometryCollection = newGeometryCollection(); GeometryCollectionBuilder geometryCollection = newGeometryCollection(orientation);
while (token != XContentParser.Token.END_ARRAY) { while (token != XContentParser.Token.END_ARRAY) {
ShapeBuilder shapeBuilder = GeoShapeType.parse(parser); ShapeBuilder shapeBuilder = GeoShapeType.parse(parser);
geometryCollection.shape(shapeBuilder); 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.GeoUtils;
import org.elasticsearch.common.geo.SpatialStrategy; import org.elasticsearch.common.geo.SpatialStrategy;
import org.elasticsearch.common.geo.builders.ShapeBuilder; 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.unit.DistanceUnit;
import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.index.codec.docvaluesformat.DocValuesFormatProvider; 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_LEVELS = "tree_levels";
public static final String TREE_PRESISION = "precision"; public static final String TREE_PRESISION = "precision";
public static final String DISTANCE_ERROR_PCT = "distance_error_pct"; public static final String DISTANCE_ERROR_PCT = "distance_error_pct";
public static final String ORIENTATION = "orientation";
public static final String STRATEGY = "strategy"; 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 GEOHASH_LEVELS = GeoUtils.geoHashLevelsForPrecision("50m");
public static final int QUADTREE_LEVELS = GeoUtils.quadTreeLevelsForPrecision("50m"); public static final int QUADTREE_LEVELS = GeoUtils.quadTreeLevelsForPrecision("50m");
public static final double DISTANCE_ERROR_PCT = 0.025d; public static final double DISTANCE_ERROR_PCT = 0.025d;
public static final Orientation ORIENTATION = Orientation.RIGHT;
public static final FieldType FIELD_TYPE = new FieldType(); public static final FieldType FIELD_TYPE = new FieldType();
@ -99,7 +102,6 @@ public class GeoShapeFieldMapper extends AbstractFieldMapper<String> {
FIELD_TYPE.setOmitNorms(true); FIELD_TYPE.setOmitNorms(true);
FIELD_TYPE.freeze(); FIELD_TYPE.freeze();
} }
} }
public static class Builder extends AbstractFieldMapper.Builder<Builder, GeoShapeFieldMapper> { public static class Builder extends AbstractFieldMapper.Builder<Builder, GeoShapeFieldMapper> {
@ -109,6 +111,7 @@ public class GeoShapeFieldMapper extends AbstractFieldMapper<String> {
private int treeLevels = 0; private int treeLevels = 0;
private double precisionInMeters = -1; private double precisionInMeters = -1;
private double distanceErrorPct = Defaults.DISTANCE_ERROR_PCT; private double distanceErrorPct = Defaults.DISTANCE_ERROR_PCT;
private Orientation orientation = Defaults.ORIENTATION;
private SpatialPrefixTree prefixTree; private SpatialPrefixTree prefixTree;
@ -141,6 +144,11 @@ public class GeoShapeFieldMapper extends AbstractFieldMapper<String> {
return this; return this;
} }
public Builder orientation(Orientation orientation) {
this.orientation = orientation;
return this;
}
@Override @Override
public GeoShapeFieldMapper build(BuilderContext context) { public GeoShapeFieldMapper build(BuilderContext context) {
@ -153,7 +161,7 @@ public class GeoShapeFieldMapper extends AbstractFieldMapper<String> {
throw new ElasticsearchIllegalArgumentException("Unknown prefix tree type [" + tree + "]"); 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); docValuesProvider, multiFieldsBuilder.build(this, context), copyTo);
} }
} }
@ -189,6 +197,9 @@ public class GeoShapeFieldMapper extends AbstractFieldMapper<String> {
} else if (Names.DISTANCE_ERROR_PCT.equals(fieldName)) { } else if (Names.DISTANCE_ERROR_PCT.equals(fieldName)) {
builder.distanceErrorPct(Double.parseDouble(fieldNode.toString())); builder.distanceErrorPct(Double.parseDouble(fieldNode.toString()));
iterator.remove(); iterator.remove();
} else if (Names.ORIENTATION.equals(fieldName)) {
builder.orientation(ShapeBuilder.orientationFromString(fieldNode.toString()));
iterator.remove();
} else if (Names.STRATEGY.equals(fieldName)) { } else if (Names.STRATEGY.equals(fieldName)) {
builder.strategy(fieldNode.toString()); builder.strategy(fieldNode.toString());
iterator.remove(); iterator.remove();
@ -201,16 +212,18 @@ public class GeoShapeFieldMapper extends AbstractFieldMapper<String> {
private final PrefixTreeStrategy defaultStrategy; private final PrefixTreeStrategy defaultStrategy;
private final RecursivePrefixTreeStrategy recursiveStrategy; private final RecursivePrefixTreeStrategy recursiveStrategy;
private final TermQueryPrefixTreeStrategy termStrategy; private final TermQueryPrefixTreeStrategy termStrategy;
private Orientation shapeOrientation;
public GeoShapeFieldMapper(FieldMapper.Names names, SpatialPrefixTree tree, String defaultStrategyName, double distanceErrorPct, public GeoShapeFieldMapper(FieldMapper.Names names, SpatialPrefixTree tree, String defaultStrategyName, double distanceErrorPct,
FieldType fieldType, PostingsFormatProvider postingsProvider, DocValuesFormatProvider docValuesProvider, Orientation shapeOrientation, FieldType fieldType, PostingsFormatProvider postingsProvider,
MultiFields multiFields, CopyTo copyTo) { DocValuesFormatProvider docValuesProvider, MultiFields multiFields, CopyTo copyTo) {
super(names, 1, fieldType, null, null, null, postingsProvider, docValuesProvider, null, null, null, null, multiFields, 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 = new RecursivePrefixTreeStrategy(tree, names.indexName());
this.recursiveStrategy.setDistErrPct(distanceErrorPct); this.recursiveStrategy.setDistErrPct(distanceErrorPct);
this.termStrategy = new TermQueryPrefixTreeStrategy(tree, names.indexName()); this.termStrategy = new TermQueryPrefixTreeStrategy(tree, names.indexName());
this.termStrategy.setDistErrPct(distanceErrorPct); this.termStrategy.setDistErrPct(distanceErrorPct);
this.defaultStrategy = resolveStrategy(defaultStrategyName); this.defaultStrategy = resolveStrategy(defaultStrategyName);
this.shapeOrientation = shapeOrientation;
} }
@Override @Override
@ -233,7 +246,7 @@ public class GeoShapeFieldMapper extends AbstractFieldMapper<String> {
try { try {
Shape shape = context.parseExternalValue(Shape.class); Shape shape = context.parseExternalValue(Shape.class);
if (shape == null) { if (shape == null) {
ShapeBuilder shapeBuilder = ShapeBuilder.parse(context.parser()); ShapeBuilder shapeBuilder = ShapeBuilder.parse(context.parser(), this);
if (shapeBuilder == null) { if (shapeBuilder == null) {
return; return;
} }
@ -305,6 +318,8 @@ public class GeoShapeFieldMapper extends AbstractFieldMapper<String> {
return this.termStrategy; return this.termStrategy;
} }
public Orientation orientation() { return this.shapeOrientation; }
public PrefixTreeStrategy resolveStrategy(String strategyName) { public PrefixTreeStrategy resolveStrategy(String strategyName) {
if (SpatialStrategy.RECURSIVE.getStrategyName().equals(strategyName)) { if (SpatialStrategy.RECURSIVE.getStrategyName().equals(strategyName)) {
return recursiveStrategy; return recursiveStrategy;

View File

@ -681,6 +681,171 @@ public class GeoJSONShapeParserTests extends ElasticsearchTestCase {
assertGeometryEquals(new JtsPoint(expected, SPATIAL_CONTEXT), pointGeoJson); 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 { private void assertGeometryEquals(Shape expected, String geoJson) throws IOException {
XContentParser parser = JsonXContent.jsonXContent.createParser(geoJson); XContentParser parser = JsonXContent.jsonXContent.createParser(geoJson);
parser.nextToken(); parser.nextToken();