Skip to content

Add support for geo_shape represented as Well-Known Text (WKT) (#3377) #3401

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Sep 14, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions src/Nest/QueryDsl/Geo/BoundingBox/BoundingBox.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,26 +10,30 @@ public interface IBoundingBox

[JsonProperty("bottom_right")]
GeoLocation BottomRight { get; set; }

[JsonProperty("wkt")]
string WellKnownText { get; set; }
}

public class BoundingBox : IBoundingBox
{
public GeoLocation TopLeft { get; set; }
public GeoLocation BottomRight { get; set; }
public string WellKnownText { get; set; }
}

public class BoundingBoxDescriptor : DescriptorBase<BoundingBoxDescriptor, IBoundingBox>, IBoundingBox
{
GeoLocation IBoundingBox.TopLeft { get; set; }
GeoLocation IBoundingBox.BottomRight { get; set; }
string IBoundingBox.WellKnownText { get; set; }


public BoundingBoxDescriptor TopLeft(GeoLocation topLeft) => Assign(a => a.TopLeft = topLeft);
public BoundingBoxDescriptor TopLeft(double lat, double lon) => Assign(a => a.TopLeft = new GeoLocation(lat,lon));

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


public BoundingBoxDescriptor WellKnownText(string wkt)=> Assign(a => a.WellKnownText = wkt);
}
}
}
5 changes: 4 additions & 1 deletion src/Nest/QueryDsl/Geo/BoundingBox/GeoBoundingBoxQuery.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public class GeoBoundingBoxQuery : FieldNameQueryBase, IGeoBoundingBoxQuery
internal override void InternalWrapInContainer(IQueryContainer c) => c.GeoBoundingBox = this;

internal static bool IsConditionless(IGeoBoundingBoxQuery q) =>
q.Field.IsConditionless() || q.BoundingBox?.BottomRight == null || q.BoundingBox?.TopLeft == null;
q.Field.IsConditionless() || (q.BoundingBox?.BottomRight == null && q.BoundingBox?.TopLeft == null && q.BoundingBox?.WellKnownText == null);
}

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

public GeoBoundingBoxQueryDescriptor<T> BoundingBox(string wkt) =>
BoundingBox(f=>f.WellKnownText(wkt));

public GeoBoundingBoxQueryDescriptor<T> BoundingBox(Func<BoundingBoxDescriptor, IBoundingBox> boundingBoxSelector) =>
Assign(a => a.BoundingBox = boundingBoxSelector?.Invoke(new BoundingBoxDescriptor()));

Expand Down
15 changes: 12 additions & 3 deletions src/Nest/QueryDsl/Geo/GeoCoordinateJsonConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,24 @@ public override void WriteJson(JsonWriter writer, object value, JsonSerializer s
writer.WriteStartArray();
serializer.Serialize(writer, p.Longitude);
serializer.Serialize(writer, p.Latitude);
if (p.Z.HasValue)
serializer.Serialize(writer, p.Z.Value);
writer.WriteEndArray();
}

public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader.TokenType != JsonToken.StartArray) return null;
var doubles = serializer.Deserialize<double[]>(reader);
if (doubles.Length != 2) return null;
return new GeoCoordinate(doubles[1], doubles[0]);
switch (doubles.Length)
{
case 2:
return new GeoCoordinate(doubles[1], doubles[0]);
case 3:
return new GeoCoordinate(doubles[1], doubles[0], doubles[2]);
default:
return null;
}
}
}
}
}
37 changes: 28 additions & 9 deletions src/Nest/QueryDsl/Geo/GeoLocation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,8 @@ public static implicit operator GeoLocation(double[] lonLat)
}

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

/// <summary>
/// Creates a new instance of <see cref="GeoCoordinate"/> from a pair of coordinates
/// in the order Latitude then Longitude.
/// Creates a new instance of <see cref="GeoCoordinate"/>
/// </summary>
public GeoCoordinate(double latitude, double longitude, double z) : base(latitude, longitude) =>
Z = z;

/// <summary>
/// Gets or sets the Z value
/// </summary>
public double? Z { get; set; }

/// <summary>
/// Creates a new instance of <see cref="GeoCoordinate"/> from an array
/// of 2 or 3 doubles, in the order Latitude, Longitude, and optional Z value.
/// </summary>
public static implicit operator GeoCoordinate(double[] coordinates)
{
if (coordinates == null || coordinates.Length != 2)
throw new ArgumentOutOfRangeException(
nameof(coordinates),
$"Can not create a {nameof(GeoCoordinate)} from an array that does not have two doubles");

return new GeoCoordinate(coordinates[0], coordinates[1]);
if (coordinates == null) return null;

switch (coordinates.Length)
{
case 2:
return new GeoCoordinate(coordinates[0], coordinates[1]);
case 3:
return new GeoCoordinate(coordinates[0], coordinates[1], coordinates[2]);
}

throw new ArgumentOutOfRangeException(
nameof(coordinates),
$"Cannot create a {nameof(GeoCoordinate)} from an array that does not contain 2 or 3 values");
}
}
}
129 changes: 111 additions & 18 deletions src/Nest/QueryDsl/Geo/Shape/GeoShapeBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,34 @@ public interface IGeoShape
string Type { get; }
}

internal enum GeoShapeFormat
{
GeoJson,
WellKnownText
}

internal static class GeoShapeType
{
public const string Point = "POINT";
public const string MultiPoint = "MULTIPOINT";
public const string LineString = "LINESTRING";
public const string MultiLineString = "MULTILINESTRING";
public const string Polygon = "POLYGON";
public const string MultiPolygon = "MULTIPOLYGON";
public const string Circle = "CIRCLE";
public const string Envelope = "ENVELOPE";
public const string GeometryCollection = "GEOMETRYCOLLECTION";

// WKT uses BBOX for envelope geo shape
public const string BoundingBox = "BBOX";
}

/// <summary>
/// Base type for geo shapes
/// </summary>
public abstract class GeoShapeBase : IGeoShape
{
internal GeoShapeFormat Format { get; set; }
protected GeoShapeBase(string type) => this.Type = type;

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

internal class GeoShapeConverter : JsonConverter
{
public override bool CanWrite => false;
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
if (value == null)
{
writer.WriteNull();
return;
}

public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) =>
throw new NotSupportedException();
if (value is IGeoShape shape)
{
if (value is GeoShapeBase shapeBase && shapeBase.Format == GeoShapeFormat.WellKnownText)
{
writer.WriteValue(GeoWKTWriter.Write(shapeBase));
return;
}

writer.WriteStartObject();
writer.WritePropertyName("type");
writer.WriteValue(shape.Type);

switch (shape)
{
case IPointGeoShape point:
writer.WritePropertyName("coordinates");
serializer.Serialize(writer, point.Coordinates);
break;
case IMultiPointGeoShape multiPoint:
writer.WritePropertyName("coordinates");
serializer.Serialize(writer, multiPoint.Coordinates);
break;
case ILineStringGeoShape lineString:
writer.WritePropertyName("coordinates");
serializer.Serialize(writer, lineString.Coordinates);
break;
case IMultiLineStringGeoShape multiLineString:
writer.WritePropertyName("coordinates");
serializer.Serialize(writer, multiLineString.Coordinates);
break;
case IPolygonGeoShape polygon:
writer.WritePropertyName("coordinates");
serializer.Serialize(writer, polygon.Coordinates);
break;
case IMultiPolygonGeoShape multiPolygon:
writer.WritePropertyName("coordinates");
serializer.Serialize(writer, multiPolygon.Coordinates);
break;
case IEnvelopeGeoShape envelope:
writer.WritePropertyName("coordinates");
serializer.Serialize(writer, envelope.Coordinates);
break;
case ICircleGeoShape circle:
writer.WritePropertyName("coordinates");
serializer.Serialize(writer, circle.Coordinates);
writer.WritePropertyName("radius");
writer.WriteValue(circle.Radius);
break;
case IGeometryCollection collection:
writer.WritePropertyName("geometries");
serializer.Serialize(writer, collection.Geometries);
break;
}

writer.WriteEndObject();
}
else
{
throw new NotSupportedException($"{value.GetType()} is not a supported {nameof(IGeoShape)}");
}
}

public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.Null)
return null;

var shape = JObject.Load(reader);
return ReadJToken(shape, serializer);
switch (reader.TokenType)
{
case JsonToken.Null:
return null;
case JsonToken.String:
return GeoWKTReader.Read((string)reader.Value);
default:
var shape = JObject.Load(reader);
return ReadJToken(shape, serializer);
}
}

internal static IGeoShape ReadJToken(JToken shape, JsonSerializer serializer)
{
var typeName = shape["type"]?.Value<string>();
var typeName = shape["type"]?.Value<string>().ToUpperInvariant();
switch (typeName)
{
case "circle":
case GeoShapeType.Circle:
return ParseCircleGeoShape(shape, serializer);
case "envelope":
case GeoShapeType.Envelope:
return ParseEnvelopeGeoShape(shape, serializer);
case "linestring":
case GeoShapeType.LineString:
return ParseLineStringGeoShape(shape, serializer);
case "multilinestring":
case GeoShapeType.MultiLineString:
return ParseMultiLineStringGeoShape(shape, serializer);
case "point":
case GeoShapeType.Point:
return ParsePointGeoShape(shape, serializer);
case "multipoint":
case GeoShapeType.MultiPoint:
return ParseMultiPointGeoShape(shape, serializer);
case "polygon":
case GeoShapeType.Polygon:
return ParsePolygonGeoShape(shape, serializer);
case "multipolygon":
case GeoShapeType.MultiPolygon:
return ParseMultiPolygonGeoShape(shape, serializer);
case "geometrycollection":
case GeoShapeType.GeometryCollection:
return ParseGeometryCollection(shape, serializer);
default:
return null;
Expand Down
2 changes: 1 addition & 1 deletion src/Nest/QueryDsl/Geo/Shape/GeoShapeQueryJsonConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ public override object ReadJson(JsonReader reader, Type objectType, object exist
}

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

private static IGeoShapeQuery ParseShapeQuery(JToken shape, JsonSerializer serializer)
{
Expand Down
20 changes: 20 additions & 0 deletions src/Nest/QueryDsl/Geo/WKT/GeoWKTException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using System;

namespace Nest
{
/// <summary>
/// An exception when handling <see cref="IGeoShape"/> in Well-Known Text format
/// </summary>
public class GeoWKTException : Exception
{
public GeoWKTException(string message)
: base(message)
{
}

public GeoWKTException(string message, int lineNumber, int position)
: base($"{message} at line {lineNumber}, position {position}")
{
}
}
}
Loading