[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:
parent
fb6c3b7c29
commit
77a7ef28b3
|
@ -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]
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
|
|
Loading…
Reference in New Issue