[GEO] Add WKT Support to GeoBoundingBoxQueryBuilder
Add WKT BBOX parsing support to GeoBoundingBoxQueryBuilder.
This commit is contained in:
parent
18463e7e9f
commit
5ed25f1e12
|
@ -180,6 +180,31 @@ GET /_search
|
|||
--------------------------------------------------
|
||||
// CONSOLE
|
||||
|
||||
[float]
|
||||
===== Bounding Box as Well-Known Text (WKT)
|
||||
|
||||
[source,js]
|
||||
--------------------------------------------------
|
||||
GET /_search
|
||||
{
|
||||
"query": {
|
||||
"bool" : {
|
||||
"must" : {
|
||||
"match_all" : {}
|
||||
},
|
||||
"filter" : {
|
||||
"geo_bounding_box" : {
|
||||
"pin.location" : {
|
||||
"wkt" : "BBOX (-74.1, -71.12, 40.73, 40.01)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
--------------------------------------------------
|
||||
// CONSOLE
|
||||
|
||||
[float]
|
||||
===== Geohash
|
||||
|
||||
|
|
|
@ -63,6 +63,12 @@ public class GeoWKTParser {
|
|||
|
||||
public static ShapeBuilder parse(XContentParser parser)
|
||||
throws IOException, ElasticsearchParseException {
|
||||
return parseExpectedType(parser, null);
|
||||
}
|
||||
|
||||
/** throws an exception if the parsed geometry type does not match the expected shape type */
|
||||
public static ShapeBuilder parseExpectedType(XContentParser parser, final GeoShapeType shapeType)
|
||||
throws IOException, ElasticsearchParseException {
|
||||
FastStringReader reader = new FastStringReader(parser.text());
|
||||
try {
|
||||
// setup the tokenizer; configured to read words w/o numbers
|
||||
|
@ -77,7 +83,7 @@ public class GeoWKTParser {
|
|||
tokenizer.wordChars('.', '.');
|
||||
tokenizer.whitespaceChars(0, ' ');
|
||||
tokenizer.commentChar('#');
|
||||
ShapeBuilder builder = parseGeometry(tokenizer);
|
||||
ShapeBuilder builder = parseGeometry(tokenizer, shapeType);
|
||||
checkEOF(tokenizer);
|
||||
return builder;
|
||||
} finally {
|
||||
|
@ -86,8 +92,14 @@ public class GeoWKTParser {
|
|||
}
|
||||
|
||||
/** parse geometry from the stream tokenizer */
|
||||
private static ShapeBuilder parseGeometry(StreamTokenizer stream) throws IOException, ElasticsearchParseException {
|
||||
private static ShapeBuilder parseGeometry(StreamTokenizer stream, GeoShapeType shapeType)
|
||||
throws IOException, ElasticsearchParseException {
|
||||
final GeoShapeType type = GeoShapeType.forName(nextWord(stream));
|
||||
if (shapeType != null && shapeType != GeoShapeType.GEOMETRYCOLLECTION) {
|
||||
if (type.wktName().equals(shapeType.wktName()) == false) {
|
||||
throw new ElasticsearchParseException("Expected geometry type [{}] but found [{}]", shapeType, type);
|
||||
}
|
||||
}
|
||||
switch (type) {
|
||||
case POINT:
|
||||
return parsePoint(stream);
|
||||
|
@ -228,9 +240,10 @@ public class GeoWKTParser {
|
|||
if (nextEmptyOrOpen(stream).equals(EMPTY)) {
|
||||
return null;
|
||||
}
|
||||
GeometryCollectionBuilder builder = new GeometryCollectionBuilder().shape(parseGeometry(stream));
|
||||
GeometryCollectionBuilder builder = new GeometryCollectionBuilder().shape(
|
||||
parseGeometry(stream, GeoShapeType.GEOMETRYCOLLECTION));
|
||||
while (nextCloserOrComma(stream).equals(COMMA)) {
|
||||
builder.shape(parseGeometry(stream));
|
||||
builder.shape(parseGeometry(stream, null));
|
||||
}
|
||||
return builder;
|
||||
}
|
||||
|
|
|
@ -31,7 +31,10 @@ import org.elasticsearch.common.ParseField;
|
|||
import org.elasticsearch.common.ParsingException;
|
||||
import org.elasticsearch.common.geo.GeoHashUtils;
|
||||
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;
|
||||
|
@ -62,7 +65,6 @@ 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 FIELD_FIELD = new ParseField("field");
|
||||
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");
|
||||
|
@ -72,6 +74,8 @@ public class GeoBoundingBoxQueryBuilder extends AbstractQueryBuilder<GeoBounding
|
|||
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;
|
||||
|
@ -378,11 +382,6 @@ public class GeoBoundingBoxQueryBuilder extends AbstractQueryBuilder<GeoBounding
|
|||
public static GeoBoundingBoxQueryBuilder fromXContent(XContentParser parser) throws IOException {
|
||||
String fieldName = null;
|
||||
|
||||
double top = Double.NaN;
|
||||
double bottom = Double.NaN;
|
||||
double left = Double.NaN;
|
||||
double right = Double.NaN;
|
||||
|
||||
float boost = AbstractQueryBuilder.DEFAULT_BOOST;
|
||||
String queryName = null;
|
||||
String currentFieldName = null;
|
||||
|
@ -390,56 +389,18 @@ public class GeoBoundingBoxQueryBuilder extends AbstractQueryBuilder<GeoBounding
|
|||
GeoValidationMethod validationMethod = null;
|
||||
boolean ignoreUnmapped = DEFAULT_IGNORE_UNMAPPED;
|
||||
|
||||
GeoPoint sparse = new GeoPoint();
|
||||
|
||||
Rectangle bbox = null;
|
||||
String type = "memory";
|
||||
|
||||
while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
|
||||
if (token == XContentParser.Token.FIELD_NAME) {
|
||||
currentFieldName = parser.currentName();
|
||||
} else if (token == XContentParser.Token.START_OBJECT) {
|
||||
fieldName = currentFieldName;
|
||||
|
||||
while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
|
||||
if (token == XContentParser.Token.FIELD_NAME) {
|
||||
currentFieldName = parser.currentName();
|
||||
token = parser.nextToken();
|
||||
if (FIELD_FIELD.match(currentFieldName)) {
|
||||
fieldName = parser.text();
|
||||
} else if (TOP_FIELD.match(currentFieldName)) {
|
||||
top = parser.doubleValue();
|
||||
} else if (BOTTOM_FIELD.match(currentFieldName)) {
|
||||
bottom = parser.doubleValue();
|
||||
} else if (LEFT_FIELD.match(currentFieldName)) {
|
||||
left = parser.doubleValue();
|
||||
} else if (RIGHT_FIELD.match(currentFieldName)) {
|
||||
right = parser.doubleValue();
|
||||
} else {
|
||||
if (TOP_LEFT_FIELD.match(currentFieldName)) {
|
||||
GeoUtils.parseGeoPoint(parser, sparse);
|
||||
top = sparse.getLat();
|
||||
left = sparse.getLon();
|
||||
} else if (BOTTOM_RIGHT_FIELD.match(currentFieldName)) {
|
||||
GeoUtils.parseGeoPoint(parser, sparse);
|
||||
bottom = sparse.getLat();
|
||||
right = sparse.getLon();
|
||||
} else if (TOP_RIGHT_FIELD.match(currentFieldName)) {
|
||||
GeoUtils.parseGeoPoint(parser, sparse);
|
||||
top = sparse.getLat();
|
||||
right = sparse.getLon();
|
||||
} else if (BOTTOM_LEFT_FIELD.match(currentFieldName)) {
|
||||
GeoUtils.parseGeoPoint(parser, sparse);
|
||||
bottom = sparse.getLat();
|
||||
left = sparse.getLon();
|
||||
} else {
|
||||
throw new ElasticsearchParseException("failed to parse [{}] query. unexpected field [{}]",
|
||||
NAME, currentFieldName);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw new ElasticsearchParseException("failed to parse [{}] query. field name expected but [{}] found",
|
||||
NAME, token);
|
||||
}
|
||||
try {
|
||||
bbox = parseBoundingBox(parser);
|
||||
fieldName = currentFieldName;
|
||||
} catch (Exception e) {
|
||||
throw new ElasticsearchParseException("failed to parse [{}] query. [{}]", NAME, e.getMessage());
|
||||
}
|
||||
} else if (token.isValue()) {
|
||||
if (AbstractQueryBuilder.NAME_FIELD.match(currentFieldName)) {
|
||||
|
@ -459,8 +420,13 @@ public class GeoBoundingBoxQueryBuilder extends AbstractQueryBuilder<GeoBounding
|
|||
}
|
||||
}
|
||||
|
||||
final GeoPoint topLeft = sparse.reset(top, left); //just keep the object
|
||||
final GeoPoint bottomRight = new GeoPoint(bottom, right);
|
||||
if (bbox == null) {
|
||||
throw new ElasticsearchParseException("failed to parse [{}] query. bounding box not provided", NAME);
|
||||
}
|
||||
|
||||
final GeoPoint topLeft = new GeoPoint(bbox.maxLat, bbox.minLon); //just keep the object
|
||||
final GeoPoint bottomRight = new GeoPoint(bbox.minLat, bbox.maxLon);
|
||||
|
||||
GeoBoundingBoxQueryBuilder builder = new GeoBoundingBoxQueryBuilder(fieldName);
|
||||
builder.setCorners(topLeft, bottomRight);
|
||||
builder.queryName(queryName);
|
||||
|
@ -493,4 +459,69 @@ public class GeoBoundingBoxQueryBuilder extends AbstractQueryBuilder<GeoBounding
|
|||
public String getWriteableName() {
|
||||
return NAME;
|
||||
}
|
||||
|
||||
public static Rectangle 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)) {
|
||||
envelope = (EnvelopeBuilder)(GeoWKTParser.parseExpectedType(parser, GeoShapeType.ENVELOPE));
|
||||
} else if (TOP_FIELD.match(currentFieldName)) {
|
||||
top = parser.doubleValue();
|
||||
} else if (BOTTOM_FIELD.match(currentFieldName)) {
|
||||
bottom = parser.doubleValue();
|
||||
} else if (LEFT_FIELD.match(currentFieldName)) {
|
||||
left = parser.doubleValue();
|
||||
} else if (RIGHT_FIELD.match(currentFieldName)) {
|
||||
right = parser.doubleValue();
|
||||
} else {
|
||||
if (TOP_LEFT_FIELD.match(currentFieldName)) {
|
||||
GeoUtils.parseGeoPoint(parser, sparse);
|
||||
top = sparse.getLat();
|
||||
left = sparse.getLon();
|
||||
} else if (BOTTOM_RIGHT_FIELD.match(currentFieldName)) {
|
||||
GeoUtils.parseGeoPoint(parser, sparse);
|
||||
bottom = sparse.getLat();
|
||||
right = sparse.getLon();
|
||||
} else if (TOP_RIGHT_FIELD.match(currentFieldName)) {
|
||||
GeoUtils.parseGeoPoint(parser, sparse);
|
||||
top = sparse.getLat();
|
||||
right = sparse.getLon();
|
||||
} else if (BOTTOM_LEFT_FIELD.match(currentFieldName)) {
|
||||
GeoUtils.parseGeoPoint(parser, sparse);
|
||||
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) || Double.isNaN(bottom) || Double.isNaN(left) || 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.build();
|
||||
return new Rectangle(r.getMinY(), r.getMaxY(), r.getMinX(), r.getMaxX());
|
||||
}
|
||||
return new Rectangle(bottom, top, left, right);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -39,6 +39,7 @@ import org.elasticsearch.common.geo.builders.ShapeBuilder;
|
|||
import org.elasticsearch.common.geo.parsers.GeoWKTParser;
|
||||
import org.elasticsearch.common.xcontent.XContentBuilder;
|
||||
import org.elasticsearch.common.xcontent.XContentFactory;
|
||||
import org.elasticsearch.common.xcontent.XContentParser;
|
||||
import org.elasticsearch.test.geo.RandomShapeGenerator;
|
||||
import org.locationtech.spatial4j.exception.InvalidShapeException;
|
||||
import org.locationtech.spatial4j.shape.Rectangle;
|
||||
|
@ -51,6 +52,8 @@ import java.util.ArrayList;
|
|||
import java.util.List;
|
||||
|
||||
import static org.elasticsearch.common.geo.builders.ShapeBuilder.SPATIAL_CONTEXT;
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.hamcrest.Matchers.hasToString;
|
||||
|
||||
/**
|
||||
* Tests for {@code GeoWKTShapeParser}
|
||||
|
@ -252,4 +255,13 @@ public class GeoWKTShapeParserTests extends BaseGeoParsingTestCase {
|
|||
assertExpected(gcb.build(), gcb);
|
||||
}
|
||||
}
|
||||
|
||||
public void testUnexpectedShapeException() throws IOException {
|
||||
XContentBuilder builder = toWKTContent(new PointBuilder(-1, 2), false);
|
||||
XContentParser parser = createParser(builder);
|
||||
parser.nextToken();
|
||||
ElasticsearchParseException e = expectThrows(ElasticsearchParseException.class,
|
||||
() -> GeoWKTParser.parseExpectedType(parser, GeoShapeType.POLYGON));
|
||||
assertThat(e, hasToString(containsString("Expected geometry type [polygon] but found [point]")));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -406,6 +406,50 @@ public class GeoBoundingBoxQueryBuilderTests extends AbstractQueryTestCase<GeoBo
|
|||
assertEquals(json, GeoExecType.MEMORY, parsed.type());
|
||||
}
|
||||
|
||||
public void testFromWKT() throws IOException {
|
||||
String wkt =
|
||||
"{\n" +
|
||||
" \"geo_bounding_box\" : {\n" +
|
||||
" \"pin.location\" : {\n" +
|
||||
" \"wkt\" : \"BBOX (-74.1, -71.12, 40.73, 40.01)\"\n" +
|
||||
" },\n" +
|
||||
" \"validation_method\" : \"STRICT\",\n" +
|
||||
" \"type\" : \"MEMORY\",\n" +
|
||||
" \"ignore_unmapped\" : false,\n" +
|
||||
" \"boost\" : 1.0\n" +
|
||||
" }\n" +
|
||||
"}";
|
||||
|
||||
// toXContent generates the query in geojson only; for now we need to test against the expected
|
||||
// geojson generated content
|
||||
String expectedJson =
|
||||
"{\n" +
|
||||
" \"geo_bounding_box\" : {\n" +
|
||||
" \"pin.location\" : {\n" +
|
||||
" \"top_left\" : [ -74.1, 40.73 ],\n" +
|
||||
" \"bottom_right\" : [ -71.12, 40.01 ]\n" +
|
||||
" },\n" +
|
||||
" \"validation_method\" : \"STRICT\",\n" +
|
||||
" \"type\" : \"MEMORY\",\n" +
|
||||
" \"ignore_unmapped\" : false,\n" +
|
||||
" \"boost\" : 1.0\n" +
|
||||
" }\n" +
|
||||
"}";
|
||||
|
||||
// parse with wkt
|
||||
GeoBoundingBoxQueryBuilder parsed = (GeoBoundingBoxQueryBuilder) parseQuery(wkt);
|
||||
// check the builder's generated geojson content against the expected json output
|
||||
checkGeneratedJson(expectedJson, parsed);
|
||||
double delta = 0d;
|
||||
assertEquals(expectedJson, "pin.location", parsed.fieldName());
|
||||
assertEquals(expectedJson, -74.1, parsed.topLeft().getLon(), delta);
|
||||
assertEquals(expectedJson, 40.73, parsed.topLeft().getLat(), delta);
|
||||
assertEquals(expectedJson, -71.12, parsed.bottomRight().getLon(), delta);
|
||||
assertEquals(expectedJson, 40.01, parsed.bottomRight().getLat(), delta);
|
||||
assertEquals(expectedJson, 1.0, parsed.boost(), delta);
|
||||
assertEquals(expectedJson, GeoExecType.MEMORY, parsed.type());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void testMustRewrite() throws IOException {
|
||||
assumeTrue("test runs only when at least a type is registered", getCurrentTypes().length > 0);
|
||||
|
|
Loading…
Reference in New Issue