Skip to content

Commit 017d8b6

Browse files
russcamMpdreamz
authored andcommitted
Add support for geo_shape represented as Well-Known Text (WKT) (#3377)
* Add support for well known text (wkt) to geo bounding box queries * Move geo_shape queries into Tests project This commit moves the geo_shape queries into the Test project. Missed in the Tests refactoring * Add support for Z values to GeoCoordinate * Add support for WKT geo shapes This commit adds support for Well-Known Text (WKT) representations of geo_shape. The extent of the implementation is only as far as is required by the WKT support in Elasticsearch. Deserialize from WKT to IGeoShape types. The format from which the shape is deserialized is assigned to an internal format property on the concrete implementations of GeoShape as the intention is not to expose this as a property on the IGeoShape interface. The format is assigned to the instance so that the IGeoShape instance is serialized to the same format if indexed again. GeoWKTReader is a simple tokenizer implementation for the purposes of parsing only WKT concepts that are supported by Elasticsearch. GeoShapeConverter now implements WriteJson because the original format of IGeoShape now needs to be taken into account when serializing. Add tests for roundtrip serialization of WKT. Closes #3256
1 parent 5bf170d commit 017d8b6

25 files changed

+1602
-207
lines changed

src/Nest/QueryDsl/Geo/BoundingBox/BoundingBox.cs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,26 +10,30 @@ public interface IBoundingBox
1010

1111
[JsonProperty("bottom_right")]
1212
GeoLocation BottomRight { get; set; }
13+
14+
[JsonProperty("wkt")]
15+
string WellKnownText { get; set; }
1316
}
1417

1518
public class BoundingBox : IBoundingBox
1619
{
1720
public GeoLocation TopLeft { get; set; }
1821
public GeoLocation BottomRight { get; set; }
22+
public string WellKnownText { get; set; }
1923
}
2024

2125
public class BoundingBoxDescriptor : DescriptorBase<BoundingBoxDescriptor, IBoundingBox>, IBoundingBox
2226
{
2327
GeoLocation IBoundingBox.TopLeft { get; set; }
2428
GeoLocation IBoundingBox.BottomRight { get; set; }
29+
string IBoundingBox.WellKnownText { get; set; }
2530

26-
2731
public BoundingBoxDescriptor TopLeft(GeoLocation topLeft) => Assign(a => a.TopLeft = topLeft);
2832
public BoundingBoxDescriptor TopLeft(double lat, double lon) => Assign(a => a.TopLeft = new GeoLocation(lat,lon));
2933

3034
public BoundingBoxDescriptor BottomRight(GeoLocation bottomRight) => Assign(a => a.BottomRight = bottomRight);
3135
public BoundingBoxDescriptor BottomRight(double lat, double lon) => Assign(a => a.BottomRight = new GeoLocation(lat, lon));
3236

33-
37+
public BoundingBoxDescriptor WellKnownText(string wkt)=> Assign(a => a.WellKnownText = wkt);
3438
}
35-
}
39+
}

src/Nest/QueryDsl/Geo/BoundingBox/GeoBoundingBoxQuery.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ public class GeoBoundingBoxQuery : FieldNameQueryBase, IGeoBoundingBoxQuery
2929
internal override void InternalWrapInContainer(IQueryContainer c) => c.GeoBoundingBox = this;
3030

3131
internal static bool IsConditionless(IGeoBoundingBoxQuery q) =>
32-
q.Field.IsConditionless() || q.BoundingBox?.BottomRight == null || q.BoundingBox?.TopLeft == null;
32+
q.Field.IsConditionless() || (q.BoundingBox?.BottomRight == null && q.BoundingBox?.TopLeft == null && q.BoundingBox?.WellKnownText == null);
3333
}
3434

3535
public class GeoBoundingBoxQueryDescriptor<T>
@@ -47,6 +47,9 @@ public GeoBoundingBoxQueryDescriptor<T> BoundingBox(double topLeftLat, double to
4747
public GeoBoundingBoxQueryDescriptor<T> BoundingBox(GeoLocation topLeft, GeoLocation bottomRight) =>
4848
BoundingBox(f=>f.TopLeft(topLeft).BottomRight(bottomRight));
4949

50+
public GeoBoundingBoxQueryDescriptor<T> BoundingBox(string wkt) =>
51+
BoundingBox(f=>f.WellKnownText(wkt));
52+
5053
public GeoBoundingBoxQueryDescriptor<T> BoundingBox(Func<BoundingBoxDescriptor, IBoundingBox> boundingBoxSelector) =>
5154
Assign(a => a.BoundingBox = boundingBoxSelector?.Invoke(new BoundingBoxDescriptor()));
5255

src/Nest/QueryDsl/Geo/GeoCoordinateJsonConverter.cs

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,24 @@ public override void WriteJson(JsonWriter writer, object value, JsonSerializer s
1717
writer.WriteStartArray();
1818
serializer.Serialize(writer, p.Longitude);
1919
serializer.Serialize(writer, p.Latitude);
20+
if (p.Z.HasValue)
21+
serializer.Serialize(writer, p.Z.Value);
2022
writer.WriteEndArray();
2123
}
2224

2325
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
2426
{
2527
if (reader.TokenType != JsonToken.StartArray) return null;
2628
var doubles = serializer.Deserialize<double[]>(reader);
27-
if (doubles.Length != 2) return null;
28-
return new GeoCoordinate(doubles[1], doubles[0]);
29+
switch (doubles.Length)
30+
{
31+
case 2:
32+
return new GeoCoordinate(doubles[1], doubles[0]);
33+
case 3:
34+
return new GeoCoordinate(doubles[1], doubles[0], doubles[2]);
35+
default:
36+
return null;
37+
}
2938
}
3039
}
31-
}
40+
}

src/Nest/QueryDsl/Geo/GeoLocation.cs

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,8 @@ public static implicit operator GeoLocation(double[] lonLat)
123123
}
124124

125125
/// <summary>
126-
/// Represents a Latitude/Longitude as a 2 dimensional point that gets serialized as new [] { lon, lat }
126+
/// Represents a Latitude/Longitude and optional Z value as a 2 or 3 dimensional point
127+
/// that gets serialized as new [] { lon, lat, [z] }
127128
/// </summary>
128129
[JsonConverter(typeof(GeoCoordinateJsonConverter))]
129130
public class GeoCoordinate : GeoLocation
@@ -134,17 +135,35 @@ public class GeoCoordinate : GeoLocation
134135
public GeoCoordinate(double latitude, double longitude) : base(latitude, longitude) { }
135136

136137
/// <summary>
137-
/// Creates a new instance of <see cref="GeoCoordinate"/> from a pair of coordinates
138-
/// in the order Latitude then Longitude.
138+
/// Creates a new instance of <see cref="GeoCoordinate"/>
139+
/// </summary>
140+
public GeoCoordinate(double latitude, double longitude, double z) : base(latitude, longitude) =>
141+
Z = z;
142+
143+
/// <summary>
144+
/// Gets or sets the Z value
145+
/// </summary>
146+
public double? Z { get; set; }
147+
148+
/// <summary>
149+
/// Creates a new instance of <see cref="GeoCoordinate"/> from an array
150+
/// of 2 or 3 doubles, in the order Latitude, Longitude, and optional Z value.
139151
/// </summary>
140152
public static implicit operator GeoCoordinate(double[] coordinates)
141153
{
142-
if (coordinates == null || coordinates.Length != 2)
143-
throw new ArgumentOutOfRangeException(
144-
nameof(coordinates),
145-
$"Can not create a {nameof(GeoCoordinate)} from an array that does not have two doubles");
146-
147-
return new GeoCoordinate(coordinates[0], coordinates[1]);
154+
if (coordinates == null) return null;
155+
156+
switch (coordinates.Length)
157+
{
158+
case 2:
159+
return new GeoCoordinate(coordinates[0], coordinates[1]);
160+
case 3:
161+
return new GeoCoordinate(coordinates[0], coordinates[1], coordinates[2]);
162+
}
163+
164+
throw new ArgumentOutOfRangeException(
165+
nameof(coordinates),
166+
$"Cannot create a {nameof(GeoCoordinate)} from an array that does not contain 2 or 3 values");
148167
}
149168
}
150169
}

src/Nest/QueryDsl/Geo/Shape/GeoShapeBase.cs

Lines changed: 118 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,32 @@ public interface IGeoShape
2424
bool? IgnoreUnmapped { get; set; }
2525
}
2626

27+
internal enum GeoShapeFormat
28+
{
29+
GeoJson,
30+
WellKnownText
31+
}
32+
33+
internal static class GeoShapeType
34+
{
35+
public const string Point = "POINT";
36+
public const string MultiPoint = "MULTIPOINT";
37+
public const string LineString = "LINESTRING";
38+
public const string MultiLineString = "MULTILINESTRING";
39+
public const string Polygon = "POLYGON";
40+
public const string MultiPolygon = "MULTIPOLYGON";
41+
public const string Circle = "CIRCLE";
42+
public const string Envelope = "ENVELOPE";
43+
public const string GeometryCollection = "GEOMETRYCOLLECTION";
44+
45+
// WKT uses BBOX for envelope geo shape
46+
public const string BoundingBox = "BBOX";
47+
}
48+
2749
public abstract class GeoShapeBase : IGeoShape
2850
{
51+
internal GeoShapeFormat Format { get; set; }
52+
2953
protected GeoShapeBase(string type) => this.Type = type;
3054

3155
/// <inheritdoc />
@@ -38,52 +62,121 @@ public abstract class GeoShapeBase : IGeoShape
3862

3963
internal class GeoShapeConverter : JsonConverter
4064
{
41-
public override bool CanWrite => false;
65+
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
66+
{
67+
if (value == null)
68+
{
69+
writer.WriteNull();
70+
return;
71+
}
4272

43-
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) =>
44-
throw new NotSupportedException();
73+
// IGeometryCollection needs to be handled separately because it does not
74+
// implement IGeoShape, and can't because it would be a binary breaking change.
75+
// Fixed in 7.x
76+
if (value is IGeometryCollection collection)
77+
{
78+
if (collection is GeometryCollection geometryCollection && geometryCollection.Format == GeoShapeFormat.WellKnownText)
79+
{
80+
writer.WriteValue(GeoWKTWriter.Write(collection));
81+
return;
82+
}
83+
84+
writer.WriteStartObject();
85+
writer.WritePropertyName("type");
86+
writer.WriteValue(collection.Type);
87+
writer.WritePropertyName("geometries");
88+
serializer.Serialize(writer, collection.Geometries);
89+
writer.WriteEndObject();
90+
}
91+
else if (value is IGeoShape shape)
92+
{
93+
if (value is GeoShapeBase shapeBase && shapeBase.Format == GeoShapeFormat.WellKnownText)
94+
{
95+
writer.WriteValue(GeoWKTWriter.Write(shapeBase));
96+
return;
97+
}
98+
99+
writer.WriteStartObject();
100+
writer.WritePropertyName("type");
101+
writer.WriteValue(shape.Type);
102+
writer.WritePropertyName("coordinates");
103+
switch (shape)
104+
{
105+
case IPointGeoShape point:
106+
serializer.Serialize(writer, point.Coordinates);
107+
break;
108+
case IMultiPointGeoShape multiPoint:
109+
serializer.Serialize(writer, multiPoint.Coordinates);
110+
break;
111+
case ILineStringGeoShape lineString:
112+
serializer.Serialize(writer, lineString.Coordinates);
113+
break;
114+
case IMultiLineStringGeoShape multiLineString:
115+
serializer.Serialize(writer, multiLineString.Coordinates);
116+
break;
117+
case IPolygonGeoShape polygon:
118+
serializer.Serialize(writer, polygon.Coordinates);
119+
break;
120+
case IMultiPolygonGeoShape multiPolyon:
121+
serializer.Serialize(writer, multiPolyon.Coordinates);
122+
break;
123+
case IEnvelopeGeoShape envelope:
124+
serializer.Serialize(writer, envelope.Coordinates);
125+
break;
126+
case ICircleGeoShape circle:
127+
serializer.Serialize(writer, circle.Coordinates);
128+
writer.WritePropertyName("radius");
129+
writer.WriteValue(circle.Radius);
130+
break;
131+
}
132+
writer.WriteEndObject();
133+
}
134+
}
45135

46136
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
47137
{
48-
if (reader.TokenType == JsonToken.Null)
49-
return null;
50-
51-
var shape = JObject.Load(reader);
52-
return ReadJToken(shape, serializer);
138+
switch (reader.TokenType)
139+
{
140+
case JsonToken.Null:
141+
return null;
142+
case JsonToken.String:
143+
return GeoWKTReader.Read((string)reader.Value);
144+
default:
145+
var shape = JObject.Load(reader);
146+
return ReadJToken(shape, serializer);
147+
}
53148
}
54149

55150
internal static object ReadJToken(JToken shape, JsonSerializer serializer)
56151
{
57-
var type = shape["type"];
58-
var typeName = type?.Value<string>();
152+
var typeName = shape["type"]?.Value<string>().ToUpperInvariant();
59153
switch (typeName)
60154
{
61-
case "circle":
62-
var radius = shape["radius"];
63-
return ParseCircleGeoShape(shape, serializer, radius);
64-
case "envelope":
155+
case GeoShapeType.Circle:
156+
return ParseCircleGeoShape(shape, serializer);
157+
case GeoShapeType.Envelope:
65158
return ParseEnvelopeGeoShape(shape, serializer);
66-
case "linestring":
159+
case GeoShapeType.LineString:
67160
return ParseLineStringGeoShape(shape, serializer);
68-
case "multilinestring":
161+
case GeoShapeType.MultiLineString:
69162
return ParseMultiLineStringGeoShape(shape, serializer);
70-
case "point":
163+
case GeoShapeType.Point:
71164
return ParsePointGeoShape(shape, serializer);
72-
case "multipoint":
165+
case GeoShapeType.MultiPoint:
73166
return ParseMultiPointGeoShape(shape, serializer);
74-
case "polygon":
167+
case GeoShapeType.Polygon:
75168
return ParsePolygonGeoShape(shape, serializer);
76-
case "multipolygon":
169+
case GeoShapeType.MultiPolygon:
77170
return ParseMultiPolygonGeoShape(shape, serializer);
78-
case "geometrycollection":
171+
case GeoShapeType.GeometryCollection:
79172
return ParseGeometryCollection(shape, serializer);
80173
default:
81174
return null;
82175
}
83176
}
84177

85-
public override bool CanConvert(Type objectType) => typeof(IGeoShape).IsAssignableFrom(objectType) ||
86-
typeof(IGeometryCollection).IsAssignableFrom(objectType);
178+
public override bool CanConvert(Type objectType) =>
179+
typeof(IGeoShape).IsAssignableFrom(objectType) || typeof(IGeometryCollection).IsAssignableFrom(objectType);
87180

88181
private static GeometryCollection ParseGeometryCollection(JToken shape, JsonSerializer serializer)
89182
{
@@ -128,11 +221,11 @@ private static LineStringGeoShape ParseLineStringGeoShape(JToken shape, JsonSeri
128221
private static EnvelopeGeoShape ParseEnvelopeGeoShape(JToken shape, JsonSerializer serializer) =>
129222
new EnvelopeGeoShape {Coordinates = GetCoordinates<IEnumerable<GeoCoordinate>>(shape, serializer)};
130223

131-
private static CircleGeoShape ParseCircleGeoShape(JToken shape, JsonSerializer serializer, JToken radius) =>
224+
private static CircleGeoShape ParseCircleGeoShape(JToken shape, JsonSerializer serializer) =>
132225
new CircleGeoShape
133226
{
134227
Coordinates = GetCoordinates<GeoCoordinate>(shape, serializer),
135-
Radius = radius?.Value<string>()
228+
Radius = shape["radius"]?.Value<string>()
136229
};
137230

138231
private static T GetCoordinates<T>(JToken shape, JsonSerializer serializer)

src/Nest/QueryDsl/Geo/Shape/GeoShapeQueryJsonConverter.cs

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ internal class GeoShapeQueryJsonConverter : JsonConverter
4343
public override bool CanRead => true;
4444
public override bool CanWrite => false;
4545

46+
// TODO: remove in 7.x
4647
public virtual T GetCoordinates<T>(JToken shape, JsonSerializer serializer)
4748
{
4849
var coordinates = shape["coordinates"];
@@ -104,27 +105,27 @@ private static IGeoShapeQuery ParseIndexedShapeQuery(JToken indexedShape) =>
104105
private static IGeoShapeQuery ParseShapeQuery(JToken shape, JsonSerializer serializer)
105106
{
106107
var type = shape["type"];
107-
var typeName = type?.Value<string>();
108+
var typeName = type?.Value<string>().ToUpperInvariant();
108109
var geometry = GeoShapeConverter.ReadJToken(shape, serializer);
109110
switch (typeName)
110111
{
111-
case "circle":
112+
case GeoShapeType.Circle:
112113
return new GeoShapeCircleQuery { Shape = geometry as ICircleGeoShape };
113-
case "envelope":
114+
case GeoShapeType.Envelope:
114115
return new GeoShapeEnvelopeQuery { Shape = geometry as IEnvelopeGeoShape };
115-
case "linestring":
116+
case GeoShapeType.LineString:
116117
return new GeoShapeLineStringQuery { Shape = geometry as ILineStringGeoShape };
117-
case "multilinestring":
118+
case GeoShapeType.MultiLineString:
118119
return new GeoShapeMultiLineStringQuery { Shape = geometry as IMultiLineStringGeoShape };
119-
case "point":
120+
case GeoShapeType.Point:
120121
return new GeoShapePointQuery { Shape = geometry as IPointGeoShape };
121-
case "multipoint":
122+
case GeoShapeType.MultiPoint:
122123
return new GeoShapeMultiPointQuery { Shape = geometry as IMultiPointGeoShape };
123-
case "polygon":
124+
case GeoShapeType.Polygon:
124125
return new GeoShapePolygonQuery { Shape = geometry as IPolygonGeoShape };
125-
case "multipolygon":
126+
case GeoShapeType.MultiPolygon:
126127
return new GeoShapeMultiPolygonQuery { Shape = geometry as IMultiPolygonGeoShape };
127-
case "geometrycollection":
128+
case GeoShapeType.GeometryCollection:
128129
return new GeoShapeGeometryCollectionQuery { Shape = geometry as IGeometryCollection };
129130
default:
130131
return null;

src/Nest/QueryDsl/Geo/Shape/GeometryCollection/GeometryCollection.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ public interface IGeometryCollection
2727
/// <inheritdoc cref="IGeometryCollection"/>
2828
public class GeometryCollection : IGeometryCollection, IGeoShape
2929
{
30+
internal GeoShapeFormat Format { get; set; }
31+
3032
/// <inheritdoc />
3133
public string Type => "geometrycollection";
3234

0 commit comments

Comments
 (0)