Skip to content

Commit 86b7b94

Browse files
committed
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 (cherry picked from commit f31b087) Includes line ending sensitive unit test from 0d0afdb
1 parent ed4205c commit 86b7b94

12 files changed

+1454
-82
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: 111 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,34 @@ public interface IGeoShape
1616
string Type { get; }
1717
}
1818

19+
internal enum GeoShapeFormat
20+
{
21+
GeoJson,
22+
WellKnownText
23+
}
24+
25+
internal static class GeoShapeType
26+
{
27+
public const string Point = "POINT";
28+
public const string MultiPoint = "MULTIPOINT";
29+
public const string LineString = "LINESTRING";
30+
public const string MultiLineString = "MULTILINESTRING";
31+
public const string Polygon = "POLYGON";
32+
public const string MultiPolygon = "MULTIPOLYGON";
33+
public const string Circle = "CIRCLE";
34+
public const string Envelope = "ENVELOPE";
35+
public const string GeometryCollection = "GEOMETRYCOLLECTION";
36+
37+
// WKT uses BBOX for envelope geo shape
38+
public const string BoundingBox = "BBOX";
39+
}
40+
1941
/// <summary>
2042
/// Base type for geo shapes
2143
/// </summary>
2244
public abstract class GeoShapeBase : IGeoShape
2345
{
46+
internal GeoShapeFormat Format { get; set; }
2447
protected GeoShapeBase(string type) => this.Type = type;
2548

2649
/// <inheritdoc />
@@ -29,42 +52,112 @@ public abstract class GeoShapeBase : IGeoShape
2952

3053
internal class GeoShapeConverter : JsonConverter
3154
{
32-
public override bool CanWrite => false;
55+
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
56+
{
57+
if (value == null)
58+
{
59+
writer.WriteNull();
60+
return;
61+
}
3362

34-
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) =>
35-
throw new NotSupportedException();
63+
if (value is IGeoShape shape)
64+
{
65+
if (value is GeoShapeBase shapeBase && shapeBase.Format == GeoShapeFormat.WellKnownText)
66+
{
67+
writer.WriteValue(GeoWKTWriter.Write(shapeBase));
68+
return;
69+
}
70+
71+
writer.WriteStartObject();
72+
writer.WritePropertyName("type");
73+
writer.WriteValue(shape.Type);
74+
75+
switch (shape)
76+
{
77+
case IPointGeoShape point:
78+
writer.WritePropertyName("coordinates");
79+
serializer.Serialize(writer, point.Coordinates);
80+
break;
81+
case IMultiPointGeoShape multiPoint:
82+
writer.WritePropertyName("coordinates");
83+
serializer.Serialize(writer, multiPoint.Coordinates);
84+
break;
85+
case ILineStringGeoShape lineString:
86+
writer.WritePropertyName("coordinates");
87+
serializer.Serialize(writer, lineString.Coordinates);
88+
break;
89+
case IMultiLineStringGeoShape multiLineString:
90+
writer.WritePropertyName("coordinates");
91+
serializer.Serialize(writer, multiLineString.Coordinates);
92+
break;
93+
case IPolygonGeoShape polygon:
94+
writer.WritePropertyName("coordinates");
95+
serializer.Serialize(writer, polygon.Coordinates);
96+
break;
97+
case IMultiPolygonGeoShape multiPolygon:
98+
writer.WritePropertyName("coordinates");
99+
serializer.Serialize(writer, multiPolygon.Coordinates);
100+
break;
101+
case IEnvelopeGeoShape envelope:
102+
writer.WritePropertyName("coordinates");
103+
serializer.Serialize(writer, envelope.Coordinates);
104+
break;
105+
case ICircleGeoShape circle:
106+
writer.WritePropertyName("coordinates");
107+
serializer.Serialize(writer, circle.Coordinates);
108+
writer.WritePropertyName("radius");
109+
writer.WriteValue(circle.Radius);
110+
break;
111+
case IGeometryCollection collection:
112+
writer.WritePropertyName("geometries");
113+
serializer.Serialize(writer, collection.Geometries);
114+
break;
115+
}
116+
117+
writer.WriteEndObject();
118+
}
119+
else
120+
{
121+
throw new NotSupportedException($"{value.GetType()} is not a supported {nameof(IGeoShape)}");
122+
}
123+
}
36124

37125
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
38126
{
39-
if (reader.TokenType == JsonToken.Null)
40-
return null;
41-
42-
var shape = JObject.Load(reader);
43-
return ReadJToken(shape, serializer);
127+
switch (reader.TokenType)
128+
{
129+
case JsonToken.Null:
130+
return null;
131+
case JsonToken.String:
132+
return GeoWKTReader.Read((string)reader.Value);
133+
default:
134+
var shape = JObject.Load(reader);
135+
return ReadJToken(shape, serializer);
136+
}
44137
}
45138

46139
internal static IGeoShape ReadJToken(JToken shape, JsonSerializer serializer)
47140
{
48-
var typeName = shape["type"]?.Value<string>();
141+
var typeName = shape["type"]?.Value<string>().ToUpperInvariant();
49142
switch (typeName)
50143
{
51-
case "circle":
144+
case GeoShapeType.Circle:
52145
return ParseCircleGeoShape(shape, serializer);
53-
case "envelope":
146+
case GeoShapeType.Envelope:
54147
return ParseEnvelopeGeoShape(shape, serializer);
55-
case "linestring":
148+
case GeoShapeType.LineString:
56149
return ParseLineStringGeoShape(shape, serializer);
57-
case "multilinestring":
150+
case GeoShapeType.MultiLineString:
58151
return ParseMultiLineStringGeoShape(shape, serializer);
59-
case "point":
152+
case GeoShapeType.Point:
60153
return ParsePointGeoShape(shape, serializer);
61-
case "multipoint":
154+
case GeoShapeType.MultiPoint:
62155
return ParseMultiPointGeoShape(shape, serializer);
63-
case "polygon":
156+
case GeoShapeType.Polygon:
64157
return ParsePolygonGeoShape(shape, serializer);
65-
case "multipolygon":
158+
case GeoShapeType.MultiPolygon:
66159
return ParseMultiPolygonGeoShape(shape, serializer);
67-
case "geometrycollection":
160+
case GeoShapeType.GeometryCollection:
68161
return ParseGeometryCollection(shape, serializer);
69162
default:
70163
return null;

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ public override object ReadJson(JsonReader reader, Type objectType, object exist
9595
}
9696

9797
private static IGeoShapeQuery ParseIndexedShapeQuery(JToken indexedShape) =>
98-
new GeoShapeQuery { IndexedShape = indexedShape.ToObject<FieldLookup>()};
98+
new GeoShapeQuery { IndexedShape = indexedShape.ToObject<FieldLookup>() };
9999

100100
private static IGeoShapeQuery ParseShapeQuery(JToken shape, JsonSerializer serializer)
101101
{
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
using System;
2+
3+
namespace Nest
4+
{
5+
/// <summary>
6+
/// An exception when handling <see cref="IGeoShape"/> in Well-Known Text format
7+
/// </summary>
8+
public class GeoWKTException : Exception
9+
{
10+
public GeoWKTException(string message)
11+
: base(message)
12+
{
13+
}
14+
15+
public GeoWKTException(string message, int lineNumber, int position)
16+
: base($"{message} at line {lineNumber}, position {position}")
17+
{
18+
}
19+
}
20+
}

0 commit comments

Comments
 (0)