[7.x-backport] Centralize BoundingBox logic to a dedicated class (#50469)

Both geo_bounding_box query and geo_bounds aggregation have
a very similar definition of a "bounding box". A lot of this
logic (serialization, xcontent-parsing, etc) can be centralized
instead of having separated efforts to do the same things
This commit is contained in:
Tal Levy 2019-12-23 11:21:39 -08:00 committed by GitHub
parent 694b119f0a
commit bed121efaf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 434 additions and 189 deletions

View File

@ -0,0 +1,229 @@
/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.elasticsearch.common.geo;
import org.elasticsearch.ElasticsearchParseException;
import org.elasticsearch.common.ParseField;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.common.io.stream.Writeable;
import org.elasticsearch.common.xcontent.ToXContentObject;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.geometry.Geometry;
import org.elasticsearch.geometry.Rectangle;
import org.elasticsearch.geometry.ShapeType;
import org.elasticsearch.geometry.utils.StandardValidator;
import org.elasticsearch.geometry.utils.WellKnownText;
import java.io.IOException;
import java.text.ParseException;
import java.util.Objects;
/**
* A class representing a Geo-Bounding-Box for use by Geo queries and aggregations
* that deal with extents/rectangles representing rectangular areas of interest.
*/
public class GeoBoundingBox implements ToXContentObject, Writeable {
private static final WellKnownText WKT_PARSER = new WellKnownText(true, new StandardValidator(true));
static final ParseField TOP_RIGHT_FIELD = new ParseField("top_right");
static final ParseField BOTTOM_LEFT_FIELD = new ParseField("bottom_left");
static final ParseField TOP_FIELD = new ParseField("top");
static final ParseField BOTTOM_FIELD = new ParseField("bottom");
static final ParseField LEFT_FIELD = new ParseField("left");
static final ParseField RIGHT_FIELD = new ParseField("right");
static final ParseField WKT_FIELD = new ParseField("wkt");
public static final ParseField BOUNDS_FIELD = new ParseField("bounds");
public static final ParseField LAT_FIELD = new ParseField("lat");
public static final ParseField LON_FIELD = new ParseField("lon");
public static final ParseField TOP_LEFT_FIELD = new ParseField("top_left");
public static final ParseField BOTTOM_RIGHT_FIELD = new ParseField("bottom_right");
private final GeoPoint topLeft;
private final GeoPoint bottomRight;
public GeoBoundingBox(GeoPoint topLeft, GeoPoint bottomRight) {
this.topLeft = topLeft;
this.bottomRight = bottomRight;
}
public GeoBoundingBox(StreamInput input) throws IOException {
this.topLeft = input.readGeoPoint();
this.bottomRight = input.readGeoPoint();
}
public GeoPoint topLeft() {
return topLeft;
}
public GeoPoint bottomRight() {
return bottomRight;
}
public double top() {
return topLeft.lat();
}
public double bottom() {
return bottomRight.lat();
}
public double left() {
return topLeft.lon();
}
public double right() {
return bottomRight.lon();
}
@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
builder.startObject(BOUNDS_FIELD.getPreferredName());
toXContentFragment(builder, true);
builder.endObject();
return builder;
}
public XContentBuilder toXContentFragment(XContentBuilder builder, boolean buildLatLonFields) throws IOException {
if (buildLatLonFields) {
builder.startObject(TOP_LEFT_FIELD.getPreferredName());
builder.field(LAT_FIELD.getPreferredName(), topLeft.lat());
builder.field(LON_FIELD.getPreferredName(), topLeft.lon());
builder.endObject();
} else {
builder.array(TOP_LEFT_FIELD.getPreferredName(), topLeft.lon(), topLeft.lat());
}
if (buildLatLonFields) {
builder.startObject(BOTTOM_RIGHT_FIELD.getPreferredName());
builder.field(LAT_FIELD.getPreferredName(), bottomRight.lat());
builder.field(LON_FIELD.getPreferredName(), bottomRight.lon());
builder.endObject();
} else {
builder.array(BOTTOM_RIGHT_FIELD.getPreferredName(), bottomRight.lon(), bottomRight.lat());
}
return builder;
}
@Override
public void writeTo(StreamOutput out) throws IOException {
out.writeGeoPoint(topLeft);
out.writeGeoPoint(bottomRight);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
GeoBoundingBox that = (GeoBoundingBox) o;
return topLeft.equals(that.topLeft) &&
bottomRight.equals(that.bottomRight);
}
@Override
public int hashCode() {
return Objects.hash(topLeft, bottomRight);
}
@Override
public String toString() {
return "BBOX (" + topLeft.lon() + ", " + bottomRight.lon() + ", " + topLeft.lat() + ", " + bottomRight.lat() + ")";
}
/**
* Parses the bounding box and returns bottom, top, left, right coordinates
*/
public static GeoBoundingBox parseBoundingBox(XContentParser parser) throws IOException, ElasticsearchParseException {
XContentParser.Token token = parser.currentToken();
if (token != XContentParser.Token.START_OBJECT) {
throw new ElasticsearchParseException("failed to parse bounding box. Expected start object but found [{}]", token);
}
double top = Double.NaN;
double bottom = Double.NaN;
double left = Double.NaN;
double right = Double.NaN;
String currentFieldName;
GeoPoint sparse = new GeoPoint();
Rectangle envelope = null;
while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
if (token == XContentParser.Token.FIELD_NAME) {
currentFieldName = parser.currentName();
token = parser.nextToken();
if (WKT_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
try {
Geometry geometry = WKT_PARSER.fromWKT(parser.text());
if (ShapeType.ENVELOPE.equals(geometry.type()) == false) {
throw new ElasticsearchParseException("failed to parse WKT bounding box. ["
+ geometry.type() + "] found. expected [" + ShapeType.ENVELOPE + "]");
}
envelope = (Rectangle) geometry;
} catch (ParseException|IllegalArgumentException e) {
throw new ElasticsearchParseException("failed to parse WKT bounding box", e);
}
} else if (TOP_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
top = parser.doubleValue();
} else if (BOTTOM_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
bottom = parser.doubleValue();
} else if (LEFT_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
left = parser.doubleValue();
} else if (RIGHT_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
right = parser.doubleValue();
} else {
if (TOP_LEFT_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
GeoUtils.parseGeoPoint(parser, sparse, false, GeoUtils.EffectivePoint.TOP_LEFT);
top = sparse.getLat();
left = sparse.getLon();
} else if (BOTTOM_RIGHT_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
GeoUtils.parseGeoPoint(parser, sparse, false, GeoUtils.EffectivePoint.BOTTOM_RIGHT);
bottom = sparse.getLat();
right = sparse.getLon();
} else if (TOP_RIGHT_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
GeoUtils.parseGeoPoint(parser, sparse, false, GeoUtils.EffectivePoint.TOP_RIGHT);
top = sparse.getLat();
right = sparse.getLon();
} else if (BOTTOM_LEFT_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
GeoUtils.parseGeoPoint(parser, sparse, false, GeoUtils.EffectivePoint.BOTTOM_LEFT);
bottom = sparse.getLat();
left = sparse.getLon();
} else {
throw new ElasticsearchParseException("failed to parse bounding box. unexpected field [{}]", currentFieldName);
}
}
} else {
throw new ElasticsearchParseException("failed to parse bounding box. field name expected but [{}] found", token);
}
}
if (envelope != null) {
if (Double.isNaN(top) == false || Double.isNaN(bottom) == false || Double.isNaN(left) == false ||
Double.isNaN(right) == false) {
throw new ElasticsearchParseException("failed to parse bounding box. Conflicting definition found "
+ "using well-known text and explicit corners.");
}
GeoPoint topLeft = new GeoPoint(envelope.getMaxLat(), envelope.getMinLon());
GeoPoint bottomRight = new GeoPoint(envelope.getMinLat(), envelope.getMaxLon());
return new GeoBoundingBox(topLeft, bottomRight);
}
GeoPoint topLeft = new GeoPoint(top, left);
GeoPoint bottomRight = new GeoPoint(bottom, right);
return new GeoBoundingBox(topLeft, bottomRight);
}
}

View File

@ -21,7 +21,6 @@ package org.elasticsearch.index.query;
import org.apache.lucene.document.LatLonDocValuesField;
import org.apache.lucene.document.LatLonPoint;
//import org.apache.lucene.geo.Rectangle;
import org.apache.lucene.search.IndexOrDocValuesQuery;
import org.apache.lucene.search.MatchNoDocsQuery;
import org.apache.lucene.search.Query;
@ -29,11 +28,9 @@ import org.elasticsearch.ElasticsearchParseException;
import org.elasticsearch.common.Numbers;
import org.elasticsearch.common.ParseField;
import org.elasticsearch.common.ParsingException;
import org.elasticsearch.common.geo.GeoBoundingBox;
import org.elasticsearch.common.geo.GeoPoint;
import org.elasticsearch.common.geo.GeoShapeType;
import org.elasticsearch.common.geo.GeoUtils;
import org.elasticsearch.common.geo.builders.EnvelopeBuilder;
import org.elasticsearch.common.geo.parsers.GeoWKTParser;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.common.xcontent.XContentBuilder;
@ -66,24 +63,12 @@ public class GeoBoundingBoxQueryBuilder extends AbstractQueryBuilder<GeoBounding
private static final ParseField TYPE_FIELD = new ParseField("type");
private static final ParseField VALIDATION_METHOD_FIELD = new ParseField("validation_method");
private static final ParseField TOP_FIELD = new ParseField("top");
private static final ParseField BOTTOM_FIELD = new ParseField("bottom");
private static final ParseField LEFT_FIELD = new ParseField("left");
private static final ParseField RIGHT_FIELD = new ParseField("right");
private static final ParseField TOP_LEFT_FIELD = new ParseField("top_left");
private static final ParseField BOTTOM_RIGHT_FIELD = new ParseField("bottom_right");
private static final ParseField TOP_RIGHT_FIELD = new ParseField("top_right");
private static final ParseField BOTTOM_LEFT_FIELD = new ParseField("bottom_left");
private static final ParseField IGNORE_UNMAPPED_FIELD = new ParseField("ignore_unmapped");
private static final ParseField WKT_FIELD = new ParseField("wkt");
/** Name of field holding geo coordinates to compute the bounding box on.*/
private final String fieldName;
/** Top left corner coordinates of bounding box. */
private GeoPoint topLeft = new GeoPoint(Double.NaN, Double.NaN);
/** Bottom right corner coordinates of bounding box.*/
private GeoPoint bottomRight = new GeoPoint(Double.NaN, Double.NaN);
private GeoBoundingBox geoBoundingBox = new GeoBoundingBox(new GeoPoint(Double.NaN, Double.NaN), new GeoPoint(Double.NaN, Double.NaN));
/** How to deal with incorrect coordinates.*/
private GeoValidationMethod validationMethod = GeoValidationMethod.DEFAULT;
/** How the query should be run. */
@ -108,8 +93,7 @@ public class GeoBoundingBoxQueryBuilder extends AbstractQueryBuilder<GeoBounding
public GeoBoundingBoxQueryBuilder(StreamInput in) throws IOException {
super(in);
fieldName = in.readString();
topLeft = in.readGeoPoint();
bottomRight = in.readGeoPoint();
geoBoundingBox = new GeoBoundingBox(in);
type = GeoExecType.readFromStream(in);
validationMethod = GeoValidationMethod.readFromStream(in);
ignoreUnmapped = in.readBoolean();
@ -118,8 +102,7 @@ public class GeoBoundingBoxQueryBuilder extends AbstractQueryBuilder<GeoBounding
@Override
protected void doWriteTo(StreamOutput out) throws IOException {
out.writeString(fieldName);
out.writeGeoPoint(topLeft);
out.writeGeoPoint(bottomRight);
geoBoundingBox.writeTo(out);
type.writeTo(out);
validationMethod.writeTo(out);
out.writeBoolean(ignoreUnmapped);
@ -162,8 +145,8 @@ public class GeoBoundingBoxQueryBuilder extends AbstractQueryBuilder<GeoBounding
// we do not check longitudes as the query generation code can deal with flipped left/right values
}
topLeft.reset(top, left);
bottomRight.reset(bottom, right);
geoBoundingBox.topLeft().reset(top, left);
geoBoundingBox.bottomRight().reset(bottom, right);
return this;
}
@ -197,12 +180,12 @@ public class GeoBoundingBoxQueryBuilder extends AbstractQueryBuilder<GeoBounding
/** Returns the top left corner of the bounding box. */
public GeoPoint topLeft() {
return topLeft;
return geoBoundingBox.topLeft();
}
/** Returns the bottom right corner of the bounding box. */
public GeoPoint bottomRight() {
return bottomRight;
return geoBoundingBox.bottomRight();
}
/**
@ -295,6 +278,9 @@ public class GeoBoundingBoxQueryBuilder extends AbstractQueryBuilder<GeoBounding
return null;
}
GeoPoint topLeft = geoBoundingBox.topLeft();
GeoPoint bottomRight = geoBoundingBox.bottomRight();
QueryValidationException validationException = null;
// For everything post 2.0 validate latitude and longitude unless validation was explicitly turned off
if (GeoUtils.isValidLatitude(topLeft.getLat()) == false) {
@ -335,8 +321,8 @@ public class GeoBoundingBoxQueryBuilder extends AbstractQueryBuilder<GeoBounding
throw new QueryShardException(context, "couldn't validate latitude/ longitude values", exception);
}
GeoPoint luceneTopLeft = new GeoPoint(topLeft);
GeoPoint luceneBottomRight = new GeoPoint(bottomRight);
GeoPoint luceneTopLeft = new GeoPoint(geoBoundingBox.topLeft());
GeoPoint luceneBottomRight = new GeoPoint(geoBoundingBox.bottomRight());
if (GeoValidationMethod.isCoerce(validationMethod)) {
// Special case: if the difference between the left and right is 360 and the right is greater than the left, we are asking for
// the complete longitude range so need to set longitude to the complete longitude range
@ -368,8 +354,7 @@ public class GeoBoundingBoxQueryBuilder extends AbstractQueryBuilder<GeoBounding
builder.startObject(NAME);
builder.startObject(fieldName);
builder.array(TOP_LEFT_FIELD.getPreferredName(), topLeft.getLon(), topLeft.getLat());
builder.array(BOTTOM_RIGHT_FIELD.getPreferredName(), bottomRight.getLon(), bottomRight.getLat());
geoBoundingBox.toXContentFragment(builder, false);
builder.endObject();
builder.field(VALIDATION_METHOD_FIELD.getPreferredName(), validationMethod);
builder.field(TYPE_FIELD.getPreferredName(), type);
@ -391,7 +376,7 @@ public class GeoBoundingBoxQueryBuilder extends AbstractQueryBuilder<GeoBounding
boolean ignoreUnmapped = DEFAULT_IGNORE_UNMAPPED;
// bottom (minLat), top (maxLat), left (minLon), right (maxLon)
double[] bbox = null;
GeoBoundingBox bbox = null;
String type = "memory";
while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
@ -399,7 +384,7 @@ public class GeoBoundingBoxQueryBuilder extends AbstractQueryBuilder<GeoBounding
currentFieldName = parser.currentName();
} else if (token == XContentParser.Token.START_OBJECT) {
try {
bbox = parseBoundingBox(parser);
bbox = GeoBoundingBox.parseBoundingBox(parser);
fieldName = currentFieldName;
} catch (Exception e) {
throw new ElasticsearchParseException("failed to parse [{}] query. [{}]", NAME, e.getMessage());
@ -426,11 +411,8 @@ public class GeoBoundingBoxQueryBuilder extends AbstractQueryBuilder<GeoBounding
throw new ElasticsearchParseException("failed to parse [{}] query. bounding box not provided", NAME);
}
final GeoPoint topLeft = new GeoPoint(bbox[1], bbox[2]);
final GeoPoint bottomRight = new GeoPoint(bbox[0], bbox[3]);
GeoBoundingBoxQueryBuilder builder = new GeoBoundingBoxQueryBuilder(fieldName);
builder.setCorners(topLeft, bottomRight);
builder.setCorners(bbox.topLeft(), bbox.bottomRight());
builder.queryName(queryName);
builder.boost(boost);
builder.type(GeoExecType.fromString(type));
@ -444,8 +426,7 @@ public class GeoBoundingBoxQueryBuilder extends AbstractQueryBuilder<GeoBounding
@Override
protected boolean doEquals(GeoBoundingBoxQueryBuilder other) {
return Objects.equals(topLeft, other.topLeft) &&
Objects.equals(bottomRight, other.bottomRight) &&
return Objects.equals(geoBoundingBox, other.geoBoundingBox) &&
Objects.equals(type, other.type) &&
Objects.equals(validationMethod, other.validationMethod) &&
Objects.equals(fieldName, other.fieldName) &&
@ -454,7 +435,7 @@ public class GeoBoundingBoxQueryBuilder extends AbstractQueryBuilder<GeoBounding
@Override
protected int doHashCode() {
return Objects.hash(topLeft, bottomRight, type, validationMethod, fieldName, ignoreUnmapped);
return Objects.hash(geoBoundingBox, type, validationMethod, fieldName, ignoreUnmapped);
}
@Override
@ -462,72 +443,4 @@ public class GeoBoundingBoxQueryBuilder extends AbstractQueryBuilder<GeoBounding
return NAME;
}
/**
* Parses the bounding box and returns bottom, top, left, right coordinates
*/
public static double[] parseBoundingBox(XContentParser parser) throws IOException, ElasticsearchParseException {
XContentParser.Token token = parser.currentToken();
if (token != XContentParser.Token.START_OBJECT) {
throw new ElasticsearchParseException("failed to parse bounding box. Expected start object but found [{}]", token);
}
double top = Double.NaN;
double bottom = Double.NaN;
double left = Double.NaN;
double right = Double.NaN;
String currentFieldName;
GeoPoint sparse = new GeoPoint();
EnvelopeBuilder envelope = null;
while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
if (token == XContentParser.Token.FIELD_NAME) {
currentFieldName = parser.currentName();
token = parser.nextToken();
if (WKT_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
envelope = (EnvelopeBuilder)(GeoWKTParser.parseExpectedType(parser, GeoShapeType.ENVELOPE));
} else if (TOP_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
top = parser.doubleValue();
} else if (BOTTOM_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
bottom = parser.doubleValue();
} else if (LEFT_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
left = parser.doubleValue();
} else if (RIGHT_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
right = parser.doubleValue();
} else {
if (TOP_LEFT_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
GeoUtils.parseGeoPoint(parser, sparse, false, GeoUtils.EffectivePoint.TOP_LEFT);
top = sparse.getLat();
left = sparse.getLon();
} else if (BOTTOM_RIGHT_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
GeoUtils.parseGeoPoint(parser, sparse, false, GeoUtils.EffectivePoint.BOTTOM_RIGHT);
bottom = sparse.getLat();
right = sparse.getLon();
} else if (TOP_RIGHT_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
GeoUtils.parseGeoPoint(parser, sparse, false, GeoUtils.EffectivePoint.TOP_RIGHT);
top = sparse.getLat();
right = sparse.getLon();
} else if (BOTTOM_LEFT_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
GeoUtils.parseGeoPoint(parser, sparse, false, GeoUtils.EffectivePoint.BOTTOM_LEFT);
bottom = sparse.getLat();
left = sparse.getLon();
} else {
throw new ElasticsearchParseException("failed to parse bounding box. unexpected field [{}]", currentFieldName);
}
}
} else {
throw new ElasticsearchParseException("failed to parse bounding box. field name expected but [{}] found", token);
}
}
if (envelope != null) {
if (Double.isNaN(top) == false || Double.isNaN(bottom) == false || Double.isNaN(left) == false ||
Double.isNaN(right) == false) {
throw new ElasticsearchParseException("failed to parse bounding box. Conflicting definition found "
+ "using well-known text and explicit corners.");
}
org.locationtech.spatial4j.shape.Rectangle r = envelope.buildS4J();
return new double[]{r.getMinY(), r.getMaxY(), r.getMinX(), r.getMaxX()};
}
return new double[]{bottom, top, left, right};
}
}

View File

@ -19,7 +19,7 @@
package org.elasticsearch.search.aggregations.metrics;
import org.elasticsearch.common.ParseField;
import org.elasticsearch.common.geo.GeoBoundingBox;
import org.elasticsearch.common.geo.GeoPoint;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
@ -33,14 +33,6 @@ import java.util.Map;
import java.util.Objects;
public class InternalGeoBounds extends InternalAggregation implements GeoBounds {
static final ParseField BOUNDS_FIELD = new ParseField("bounds");
static final ParseField TOP_LEFT_FIELD = new ParseField("top_left");
static final ParseField BOTTOM_RIGHT_FIELD = new ParseField("bottom_right");
static final ParseField LAT_FIELD = new ParseField("lat");
static final ParseField LON_FIELD = new ParseField("lon");
final double top;
final double bottom;
final double posLeft;
@ -132,30 +124,30 @@ public class InternalGeoBounds extends InternalAggregation implements GeoBounds
if (path.isEmpty()) {
return this;
} else if (path.size() == 1) {
BoundingBox boundingBox = resolveBoundingBox();
GeoBoundingBox geoBoundingBox = resolveGeoBoundingBox();
String bBoxSide = path.get(0);
switch (bBoxSide) {
case "top":
return boundingBox.topLeft.lat();
return geoBoundingBox.top();
case "left":
return boundingBox.topLeft.lon();
return geoBoundingBox.left();
case "bottom":
return boundingBox.bottomRight.lat();
return geoBoundingBox.bottom();
case "right":
return boundingBox.bottomRight.lon();
return geoBoundingBox.right();
default:
throw new IllegalArgumentException("Found unknown path element [" + bBoxSide + "] in [" + getName() + "]");
}
} else if (path.size() == 2) {
BoundingBox boundingBox = resolveBoundingBox();
GeoBoundingBox geoBoundingBox = resolveGeoBoundingBox();
GeoPoint cornerPoint = null;
String cornerString = path.get(0);
switch (cornerString) {
case "top_left":
cornerPoint = boundingBox.topLeft;
cornerPoint = geoBoundingBox.topLeft();
break;
case "bottom_right":
cornerPoint = boundingBox.bottomRight;
cornerPoint = geoBoundingBox.bottomRight();
break;
default:
throw new IllegalArgumentException("Found unknown path element [" + cornerString + "] in [" + getName() + "]");
@ -176,78 +168,50 @@ public class InternalGeoBounds extends InternalAggregation implements GeoBounds
@Override
public XContentBuilder doXContentBody(XContentBuilder builder, Params params) throws IOException {
GeoPoint topLeft = topLeft();
GeoPoint bottomRight = bottomRight();
if (topLeft != null) {
builder.startObject(BOUNDS_FIELD.getPreferredName());
builder.startObject(TOP_LEFT_FIELD.getPreferredName());
builder.field(LAT_FIELD.getPreferredName(), topLeft.lat());
builder.field(LON_FIELD.getPreferredName(), topLeft.lon());
builder.endObject();
builder.startObject(BOTTOM_RIGHT_FIELD.getPreferredName());
builder.field(LAT_FIELD.getPreferredName(), bottomRight.lat());
builder.field(LON_FIELD.getPreferredName(), bottomRight.lon());
builder.endObject();
builder.endObject();
GeoBoundingBox bbox = resolveGeoBoundingBox();
if (bbox != null) {
bbox.toXContent(builder, params);
}
return builder;
}
private static class BoundingBox {
private final GeoPoint topLeft;
private final GeoPoint bottomRight;
BoundingBox(GeoPoint topLeft, GeoPoint bottomRight) {
this.topLeft = topLeft;
this.bottomRight = bottomRight;
}
public GeoPoint topLeft() {
return topLeft;
}
public GeoPoint bottomRight() {
return bottomRight;
}
}
private BoundingBox resolveBoundingBox() {
private GeoBoundingBox resolveGeoBoundingBox() {
if (Double.isInfinite(top)) {
return null;
} else if (Double.isInfinite(posLeft)) {
return new BoundingBox(new GeoPoint(top, negLeft), new GeoPoint(bottom, negRight));
return new GeoBoundingBox(new GeoPoint(top, negLeft), new GeoPoint(bottom, negRight));
} else if (Double.isInfinite(negLeft)) {
return new BoundingBox(new GeoPoint(top, posLeft), new GeoPoint(bottom, posRight));
return new GeoBoundingBox(new GeoPoint(top, posLeft), new GeoPoint(bottom, posRight));
} else if (wrapLongitude) {
double unwrappedWidth = posRight - negLeft;
double wrappedWidth = (180 - posLeft) - (-180 - negRight);
if (unwrappedWidth <= wrappedWidth) {
return new BoundingBox(new GeoPoint(top, negLeft), new GeoPoint(bottom, posRight));
return new GeoBoundingBox(new GeoPoint(top, negLeft), new GeoPoint(bottom, posRight));
} else {
return new BoundingBox(new GeoPoint(top, posLeft), new GeoPoint(bottom, negRight));
return new GeoBoundingBox(new GeoPoint(top, posLeft), new GeoPoint(bottom, negRight));
}
} else {
return new BoundingBox(new GeoPoint(top, negLeft), new GeoPoint(bottom, posRight));
return new GeoBoundingBox(new GeoPoint(top, negLeft), new GeoPoint(bottom, posRight));
}
}
@Override
public GeoPoint topLeft() {
BoundingBox boundingBox = resolveBoundingBox();
if (boundingBox == null) {
GeoBoundingBox geoBoundingBox = resolveGeoBoundingBox();
if (geoBoundingBox == null) {
return null;
} else {
return boundingBox.topLeft();
return geoBoundingBox.topLeft();
}
}
@Override
public GeoPoint bottomRight() {
BoundingBox boundingBox = resolveBoundingBox();
if (boundingBox == null) {
GeoBoundingBox geoBoundingBox = resolveGeoBoundingBox();
if (geoBoundingBox == null) {
return null;
} else {
return boundingBox.bottomRight();
return geoBoundingBox.bottomRight();
}
}

View File

@ -20,6 +20,7 @@
package org.elasticsearch.search.aggregations.metrics;
import org.elasticsearch.common.collect.Tuple;
import org.elasticsearch.common.geo.GeoBoundingBox;
import org.elasticsearch.common.geo.GeoPoint;
import org.elasticsearch.common.xcontent.ConstructingObjectParser;
import org.elasticsearch.common.xcontent.ObjectParser;
@ -30,15 +31,14 @@ import org.elasticsearch.search.aggregations.ParsedAggregation;
import java.io.IOException;
import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg;
import static org.elasticsearch.search.aggregations.metrics.InternalGeoBounds.BOTTOM_RIGHT_FIELD;
import static org.elasticsearch.search.aggregations.metrics.InternalGeoBounds.BOUNDS_FIELD;
import static org.elasticsearch.search.aggregations.metrics.InternalGeoBounds.LAT_FIELD;
import static org.elasticsearch.search.aggregations.metrics.InternalGeoBounds.LON_FIELD;
import static org.elasticsearch.search.aggregations.metrics.InternalGeoBounds.TOP_LEFT_FIELD;
import static org.elasticsearch.common.geo.GeoBoundingBox.BOTTOM_RIGHT_FIELD;
import static org.elasticsearch.common.geo.GeoBoundingBox.BOUNDS_FIELD;
import static org.elasticsearch.common.geo.GeoBoundingBox.LAT_FIELD;
import static org.elasticsearch.common.geo.GeoBoundingBox.LON_FIELD;
import static org.elasticsearch.common.geo.GeoBoundingBox.TOP_LEFT_FIELD;
public class ParsedGeoBounds extends ParsedAggregation implements GeoBounds {
private GeoPoint topLeft;
private GeoPoint bottomRight;
private GeoBoundingBox geoBoundingBox;
@Override
public String getType() {
@ -47,29 +47,20 @@ public class ParsedGeoBounds extends ParsedAggregation implements GeoBounds {
@Override
public XContentBuilder doXContentBody(XContentBuilder builder, Params params) throws IOException {
if (topLeft != null) {
builder.startObject(BOUNDS_FIELD.getPreferredName());
builder.startObject(TOP_LEFT_FIELD.getPreferredName());
builder.field(LAT_FIELD.getPreferredName(), topLeft.getLat());
builder.field(LON_FIELD.getPreferredName(), topLeft.getLon());
builder.endObject();
builder.startObject(BOTTOM_RIGHT_FIELD.getPreferredName());
builder.field(LAT_FIELD.getPreferredName(), bottomRight.getLat());
builder.field(LON_FIELD.getPreferredName(), bottomRight.getLon());
builder.endObject();
builder.endObject();
if (geoBoundingBox != null) {
geoBoundingBox.toXContent(builder, params);
}
return builder;
}
@Override
public GeoPoint topLeft() {
return topLeft;
return geoBoundingBox.topLeft();
}
@Override
public GeoPoint bottomRight() {
return bottomRight;
return geoBoundingBox.bottomRight();
}
private static final ObjectParser<ParsedGeoBounds, Void> PARSER = new ObjectParser<>(ParsedGeoBounds.class.getSimpleName(), true,
@ -85,8 +76,7 @@ public class ParsedGeoBounds extends ParsedAggregation implements GeoBounds {
static {
declareAggregationFields(PARSER);
PARSER.declareObject((agg, bbox) -> {
agg.topLeft = bbox.v1();
agg.bottomRight = bbox.v2();
agg.geoBoundingBox = new GeoBoundingBox(bbox.v1(), bbox.v2());
}, BOUNDS_PARSER, BOUNDS_FIELD);
BOUNDS_PARSER.declareObject(constructorArg(), GEO_POINT_PARSER, TOP_LEFT_FIELD);

View File

@ -0,0 +1,149 @@
/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.elasticsearch.common.geo;
import org.elasticsearch.ElasticsearchParseException;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentFactory;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.geo.GeometryTestUtils;
import org.elasticsearch.test.ESTestCase;
import java.io.IOException;
import java.util.Arrays;
import static org.hamcrest.Matchers.equalTo;
/**
* Tests for {@link GeoBoundingBox}
*/
public class GeoBoundingBoxTests extends ESTestCase {
public void testInvalidParseInvalidWKT() throws IOException {
XContentBuilder bboxBuilder = XContentFactory.jsonBuilder()
.startObject()
.field("wkt", "invalid")
.endObject();
XContentParser parser = createParser(bboxBuilder);
parser.nextToken();
ElasticsearchParseException e = expectThrows(ElasticsearchParseException.class, () -> GeoBoundingBox.parseBoundingBox(parser));
assertThat(e.getMessage(), equalTo("failed to parse WKT bounding box"));
}
public void testInvalidParsePoint() throws IOException {
XContentBuilder bboxBuilder = XContentFactory.jsonBuilder()
.startObject()
.field("wkt", "POINT (100.0 100.0)")
.endObject();
XContentParser parser = createParser(bboxBuilder);
parser.nextToken();
ElasticsearchParseException e = expectThrows(ElasticsearchParseException.class, () -> GeoBoundingBox.parseBoundingBox(parser));
assertThat(e.getMessage(), equalTo("failed to parse WKT bounding box. [POINT] found. expected [ENVELOPE]"));
}
public void testWKT() throws IOException {
GeoBoundingBox geoBoundingBox = randomBBox();
assertBBox(geoBoundingBox,
XContentFactory.jsonBuilder()
.startObject()
.field("wkt", geoBoundingBox.toString())
.endObject()
);
}
public void testTopBottomLeftRight() throws Exception {
GeoBoundingBox geoBoundingBox = randomBBox();
assertBBox(geoBoundingBox,
XContentFactory.jsonBuilder()
.startObject()
.field("top", geoBoundingBox.top())
.field("bottom", geoBoundingBox.bottom())
.field("left", geoBoundingBox.left())
.field("right", geoBoundingBox.right())
.endObject()
);
}
public void testTopLeftBottomRight() throws Exception {
GeoBoundingBox geoBoundingBox = randomBBox();
assertBBox(geoBoundingBox,
XContentFactory.jsonBuilder()
.startObject()
.field("top_left", geoBoundingBox.topLeft())
.field("bottom_right", geoBoundingBox.bottomRight())
.endObject()
);
}
public void testTopRightBottomLeft() throws Exception {
GeoBoundingBox geoBoundingBox = randomBBox();
assertBBox(geoBoundingBox,
XContentFactory.jsonBuilder()
.startObject()
.field("top_right", new GeoPoint(geoBoundingBox.top(), geoBoundingBox.right()))
.field("bottom_left", new GeoPoint(geoBoundingBox.bottom(), geoBoundingBox.left()))
.endObject()
);
}
// test that no exception is thrown. BBOX parsing is not validated
public void testNullTopBottomLeftRight() throws Exception {
GeoBoundingBox geoBoundingBox = randomBBox();
XContentBuilder builder = XContentFactory.jsonBuilder().startObject();
for (String field : randomSubsetOf(Arrays.asList("top", "bottom", "left", "right"))) {
switch (field) {
case "top":
builder.field("top", geoBoundingBox.top());
break;
case "bottom":
builder.field("bottom", geoBoundingBox.bottom());
break;
case "left":
builder.field("left", geoBoundingBox.left());
break;
case "right":
builder.field("right", geoBoundingBox.right());
break;
default:
throw new IllegalStateException("unexpected branching");
}
}
builder.endObject();
try (XContentParser parser = createParser(builder)) {
parser.nextToken();
GeoBoundingBox.parseBoundingBox(parser);
}
}
private void assertBBox(GeoBoundingBox expected, XContentBuilder builder) throws IOException {
try (XContentParser parser = createParser(builder)) {
parser.nextToken();
assertThat(GeoBoundingBox.parseBoundingBox(parser), equalTo(expected));
}
}
private GeoBoundingBox randomBBox() {
double topLat = GeometryTestUtils.randomLat();
double bottomLat = randomDoubleBetween(GeoUtils.MIN_LAT, topLat, true);
return new GeoBoundingBox(new GeoPoint(topLat, GeometryTestUtils.randomLon()),
new GeoPoint(bottomLat, GeometryTestUtils.randomLon()));
}
}

View File

@ -39,7 +39,7 @@ import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcke
import static org.hamcrest.Matchers.anyOf;
import static org.hamcrest.Matchers.equalTo;
public class GeoBoundingBoxIT extends ESIntegTestCase {
public class GeoBoundingBoxQueryIT extends ESIntegTestCase {
@Override
protected boolean forbidPrivateIndexSettings() {