Skip to content

Commit

Permalink
Add support for WKT with GeoLocation type (#4222)
Browse files Browse the repository at this point in the history
Relates: elastic/elasticsearch#44107

This commit adds support for Well Known Text (WKT) representations for GeoLocation, the type used
to represent geo_point in the client. Similar to WKT support in IGeoShape, an internal format field
is used to store whether an instance was deserialized from JSON or WKT, so that re-serialization
honours the original form deserialized from.

(cherry picked from commit 3f3f98c)
  • Loading branch information
russcam committed Nov 14, 2019
1 parent 3d2648a commit e342916
Show file tree
Hide file tree
Showing 5 changed files with 123 additions and 45 deletions.
3 changes: 3 additions & 0 deletions src/Nest/QueryDsl/Geo/GeoLocation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ public GeoLocation(double latitude, double longitude)
[DataMember(Name = "lon")]
public double Longitude { get; }

[IgnoreDataMember]
internal GeoFormat Format { get; set; }

public bool Equals(GeoLocation other)
{
if (ReferenceEquals(null, other))
Expand Down
106 changes: 75 additions & 31 deletions src/Nest/QueryDsl/Geo/GeoLocationFormatter.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
using Elasticsearch.Net.Utf8Json;
using System.Globalization;
using System.IO;
using System.Text;
using Elasticsearch.Net.Utf8Json;
using Elasticsearch.Net.Utf8Json.Internal;


namespace Nest
{
internal class GeoLocationFormatter : IJsonFormatter<GeoLocation>
Expand All @@ -14,35 +16,59 @@ internal class GeoLocationFormatter : IJsonFormatter<GeoLocation>

public GeoLocation Deserialize(ref JsonReader reader, IJsonFormatterResolver formatterResolver)
{
if (reader.GetCurrentJsonToken() == JsonToken.Null)
switch (reader.GetCurrentJsonToken())
{
reader.ReadNext();
return null;
}
case JsonToken.Null:
reader.ReadNext();
return null;
case JsonToken.String:
var wkt = reader.ReadString();
using (var tokenizer = new WellKnownTextTokenizer(new StringReader(wkt)))
{
var token = tokenizer.NextToken();
if (token != TokenType.Word)
throw new GeoWKTException(
$"Expected word but found {tokenizer.TokenString()}", tokenizer.LineNumber, tokenizer.Position);

var count = 0;
double lat = 0;
double lon = 0;
while (reader.ReadIsInObject(ref count))
{
var propertyName = reader.ReadPropertyNameSegmentRaw();
if (Fields.TryGetValue(propertyName, out var value))
var type = tokenizer.TokenValue.ToUpperInvariant();
if (type != GeoShapeType.Point)
throw new GeoWKTException(
$"Expected {GeoShapeType.Point} but found {type}", tokenizer.LineNumber, tokenizer.Position);

if (GeoWKTReader.NextEmptyOrOpen(tokenizer) == TokenType.Word)
return null;

var lon = GeoWKTReader.NextNumber(tokenizer);
var lat = GeoWKTReader.NextNumber(tokenizer);
return new GeoLocation(lat, lon) { Format = GeoFormat.WellKnownText };
}
default:
{
switch (value)
var count = 0;
double lat = 0;
double lon = 0;
while (reader.ReadIsInObject(ref count))
{
case 0:
lat = reader.ReadDouble();
break;
case 1:
lon = reader.ReadDouble();
break;
var propertyName = reader.ReadPropertyNameSegmentRaw();
if (Fields.TryGetValue(propertyName, out var value))
{
switch (value)
{
case 0:
lat = reader.ReadDouble();
break;
case 1:
lon = reader.ReadDouble();
break;
}
}
else
reader.ReadNextBlock();
}

return new GeoLocation(lat, lon) { Format = GeoFormat.GeoJson };
}
else
reader.ReadNextBlock();
}

return new GeoLocation(lat, lon);
}

public void Serialize(ref JsonWriter writer, GeoLocation value, IJsonFormatterResolver formatterResolver)
Expand All @@ -53,13 +79,31 @@ public void Serialize(ref JsonWriter writer, GeoLocation value, IJsonFormatterRe
return;
}

writer.WriteBeginObject();
writer.WritePropertyName("lat");
writer.WriteDouble(value.Latitude);
writer.WriteValueSeparator();
writer.WritePropertyName("lon");
writer.WriteDouble(value.Longitude);
writer.WriteEndObject();
switch (value.Format)
{
case GeoFormat.GeoJson:
writer.WriteBeginObject();
writer.WritePropertyName("lat");
writer.WriteDouble(value.Latitude);
writer.WriteValueSeparator();
writer.WritePropertyName("lon");
writer.WriteDouble(value.Longitude);
writer.WriteEndObject();
break;
case GeoFormat.WellKnownText:
var lon = value.Longitude.ToString(CultureInfo.InvariantCulture);
var lat = value.Latitude.ToString(CultureInfo.InvariantCulture);
var length = GeoShapeType.Point.Length + lon.Length + lat.Length + 4;
var builder = new StringBuilder(length)
.Append(GeoShapeType.Point)
.Append(" (")
.Append(lon)
.Append(" ")
.Append(lat)
.Append(")");
writer.WriteString(builder.ToString());
break;
}
}
}
}
6 changes: 3 additions & 3 deletions src/Nest/QueryDsl/Geo/Shape/GeoShapeBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public interface IGeoShape
string Type { get; }
}

internal enum GeoShapeFormat
internal enum GeoFormat
{
GeoJson,
WellKnownText
Expand Down Expand Up @@ -48,7 +48,7 @@ public abstract class GeoShapeBase : IGeoShape
/// <inheritdoc />
public string Type { get; protected set; }

internal GeoShapeFormat Format { get; set; }
internal GeoFormat Format { get; set; }
}

internal class GeoShapeFormatter<TShape> : IJsonFormatter<TShape>
Expand Down Expand Up @@ -93,7 +93,7 @@ public void Serialize(ref JsonWriter writer, IGeoShape value, IJsonFormatterReso
return;
}

if (value is GeoShapeBase shapeBase && shapeBase.Format == GeoShapeFormat.WellKnownText)
if (value is GeoShapeBase shapeBase && shapeBase.Format == GeoFormat.WellKnownText)
{
writer.WriteString(GeoWKTWriter.Write(shapeBase));
return;
Expand Down
22 changes: 11 additions & 11 deletions src/Nest/QueryDsl/Geo/WKT/GeoWKTReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,35 +36,35 @@ private static IGeoShape Read(WellKnownTextTokenizer tokenizer, string shapeType
{
case GeoShapeType.Point:
var point = ParsePoint(tokenizer);
point.Format = GeoShapeFormat.WellKnownText;
point.Format = GeoFormat.WellKnownText;
return point;
case GeoShapeType.MultiPoint:
var multiPoint = ParseMultiPoint(tokenizer);
multiPoint.Format = GeoShapeFormat.WellKnownText;
multiPoint.Format = GeoFormat.WellKnownText;
return multiPoint;
case GeoShapeType.LineString:
var lineString = ParseLineString(tokenizer);
lineString.Format = GeoShapeFormat.WellKnownText;
lineString.Format = GeoFormat.WellKnownText;
return lineString;
case GeoShapeType.MultiLineString:
var multiLineString = ParseMultiLineString(tokenizer);
multiLineString.Format = GeoShapeFormat.WellKnownText;
multiLineString.Format = GeoFormat.WellKnownText;
return multiLineString;
case GeoShapeType.Polygon:
var polygon = ParsePolygon(tokenizer);
polygon.Format = GeoShapeFormat.WellKnownText;
polygon.Format = GeoFormat.WellKnownText;
return polygon;
case GeoShapeType.MultiPolygon:
var multiPolygon = ParseMultiPolygon(tokenizer);
multiPolygon.Format = GeoShapeFormat.WellKnownText;
multiPolygon.Format = GeoFormat.WellKnownText;
return multiPolygon;
case GeoShapeType.BoundingBox:
var envelope = ParseBoundingBox(tokenizer);
envelope.Format = GeoShapeFormat.WellKnownText;
envelope.Format = GeoFormat.WellKnownText;
return envelope;
case GeoShapeType.GeometryCollection:
var geometryCollection = ParseGeometryCollection(tokenizer);
geometryCollection.Format = GeoShapeFormat.WellKnownText;
geometryCollection.Format = GeoFormat.WellKnownText;
return geometryCollection;
default:
throw new GeoWKTException($"Unknown geometry type: {type}");
Expand Down Expand Up @@ -217,7 +217,7 @@ private static GeoCoordinate ParseCoordinate(WellKnownTextTokenizer tokenizer)
: new GeoCoordinate(lat, lon, z.Value);
}

private static void NextCloser(WellKnownTextTokenizer tokenizer)
internal static void NextCloser(WellKnownTextTokenizer tokenizer)
{
if (tokenizer.NextToken() != TokenType.RParen)
throw new GeoWKTException(
Expand All @@ -234,7 +234,7 @@ private static void NextComma(WellKnownTextTokenizer tokenizer)
tokenizer.Position);
}

private static TokenType NextEmptyOrOpen(WellKnownTextTokenizer tokenizer)
internal static TokenType NextEmptyOrOpen(WellKnownTextTokenizer tokenizer)
{
var token = tokenizer.NextToken();
if (token == TokenType.LParen ||
Expand All @@ -257,7 +257,7 @@ private static TokenType NextCloserOrComma(WellKnownTextTokenizer tokenizer)
$"but found: {tokenizer.TokenString()}", tokenizer.LineNumber, tokenizer.Position);
}

private static double NextNumber(WellKnownTextTokenizer tokenizer)
internal static double NextNumber(WellKnownTextTokenizer tokenizer)
{
if (tokenizer.NextToken() == TokenType.Word)
{
Expand Down
31 changes: 31 additions & 0 deletions src/Tests/Tests/CodeStandards/Serialization/GeoLocationTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using System.Text;
using Elastic.Xunit.XunitPlumbing;
using Elasticsearch.Net;
using FluentAssertions;
using Nest;
using Tests.Core.Client;

namespace Tests.CodeStandards.Serialization
{
public class GeoLocationTests
{
[U]
public void CanDeserializeAndSerializeToWellKnownText()
{
var wkt = "{\"location\":\"POINT (-90 90)\"}";
var client = TestClient.DisabledStreaming;

Doc deserialized;
using (var stream = MemoryStreamFactory.Default.Create(Encoding.UTF8.GetBytes(wkt)))
deserialized = client.RequestResponseSerializer.Deserialize<Doc>(stream);

deserialized.Location.Should().Be(new GeoLocation(90, -90));
client.RequestResponseSerializer.SerializeToString(deserialized).Should().Be(wkt);
}

private class Doc
{
public GeoLocation Location { get; set; }
}
}
}

0 comments on commit e342916

Please sign in to comment.