diff --git a/src/Nest/QueryDsl/Geo/BoundingBox/BoundingBox.cs b/src/Nest/QueryDsl/Geo/BoundingBox/BoundingBox.cs index 4cb99b917ed..79a2e530f73 100644 --- a/src/Nest/QueryDsl/Geo/BoundingBox/BoundingBox.cs +++ b/src/Nest/QueryDsl/Geo/BoundingBox/BoundingBox.cs @@ -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, 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); } -} \ No newline at end of file +} diff --git a/src/Nest/QueryDsl/Geo/BoundingBox/GeoBoundingBoxQuery.cs b/src/Nest/QueryDsl/Geo/BoundingBox/GeoBoundingBoxQuery.cs index 34cd44e5163..3303dd1a366 100644 --- a/src/Nest/QueryDsl/Geo/BoundingBox/GeoBoundingBoxQuery.cs +++ b/src/Nest/QueryDsl/Geo/BoundingBox/GeoBoundingBoxQuery.cs @@ -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 @@ -47,6 +47,9 @@ public GeoBoundingBoxQueryDescriptor BoundingBox(double topLeftLat, double to public GeoBoundingBoxQueryDescriptor BoundingBox(GeoLocation topLeft, GeoLocation bottomRight) => BoundingBox(f=>f.TopLeft(topLeft).BottomRight(bottomRight)); + public GeoBoundingBoxQueryDescriptor BoundingBox(string wkt) => + BoundingBox(f=>f.WellKnownText(wkt)); + public GeoBoundingBoxQueryDescriptor BoundingBox(Func boundingBoxSelector) => Assign(a => a.BoundingBox = boundingBoxSelector?.Invoke(new BoundingBoxDescriptor())); diff --git a/src/Nest/QueryDsl/Geo/GeoCoordinateJsonConverter.cs b/src/Nest/QueryDsl/Geo/GeoCoordinateJsonConverter.cs index a85b2a5d7b3..5931559903e 100644 --- a/src/Nest/QueryDsl/Geo/GeoCoordinateJsonConverter.cs +++ b/src/Nest/QueryDsl/Geo/GeoCoordinateJsonConverter.cs @@ -17,6 +17,8 @@ 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(); } @@ -24,8 +26,15 @@ public override object ReadJson(JsonReader reader, Type objectType, object exist { if (reader.TokenType != JsonToken.StartArray) return null; var doubles = serializer.Deserialize(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; + } } } -} \ No newline at end of file +} diff --git a/src/Nest/QueryDsl/Geo/GeoLocation.cs b/src/Nest/QueryDsl/Geo/GeoLocation.cs index 4047d8b89bd..5c1b22741fd 100644 --- a/src/Nest/QueryDsl/Geo/GeoLocation.cs +++ b/src/Nest/QueryDsl/Geo/GeoLocation.cs @@ -123,7 +123,8 @@ public static implicit operator GeoLocation(double[] lonLat) } /// - /// 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] } /// [JsonConverter(typeof(GeoCoordinateJsonConverter))] public class GeoCoordinate : GeoLocation @@ -134,17 +135,35 @@ public class GeoCoordinate : GeoLocation public GeoCoordinate(double latitude, double longitude) : base(latitude, longitude) { } /// - /// Creates a new instance of from a pair of coordinates - /// in the order Latitude then Longitude. + /// Creates a new instance of + /// + public GeoCoordinate(double latitude, double longitude, double z) : base(latitude, longitude) => + Z = z; + + /// + /// Gets or sets the Z value + /// + public double? Z { get; set; } + + /// + /// Creates a new instance of from an array + /// of 2 or 3 doubles, in the order Latitude, Longitude, and optional Z value. /// 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"); } } } diff --git a/src/Nest/QueryDsl/Geo/Shape/GeoShapeBase.cs b/src/Nest/QueryDsl/Geo/Shape/GeoShapeBase.cs index a095fdb9cc5..3852534b80c 100644 --- a/src/Nest/QueryDsl/Geo/Shape/GeoShapeBase.cs +++ b/src/Nest/QueryDsl/Geo/Shape/GeoShapeBase.cs @@ -24,8 +24,32 @@ public interface IGeoShape bool? IgnoreUnmapped { get; set; } } + 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"; + } + public abstract class GeoShapeBase : IGeoShape { + internal GeoShapeFormat Format { get; set; } + protected GeoShapeBase(string type) => this.Type = type; /// @@ -38,52 +62,121 @@ 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(); + // IGeometryCollection needs to be handled separately because it does not + // implement IGeoShape, and can't because it would be a binary breaking change. + // Fixed in 7.x + if (value is IGeometryCollection collection) + { + if (collection is GeometryCollection geometryCollection && geometryCollection.Format == GeoShapeFormat.WellKnownText) + { + writer.WriteValue(GeoWKTWriter.Write(collection)); + return; + } + + writer.WriteStartObject(); + writer.WritePropertyName("type"); + writer.WriteValue(collection.Type); + writer.WritePropertyName("geometries"); + serializer.Serialize(writer, collection.Geometries); + writer.WriteEndObject(); + } + else 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); + writer.WritePropertyName("coordinates"); + switch (shape) + { + case IPointGeoShape point: + serializer.Serialize(writer, point.Coordinates); + break; + case IMultiPointGeoShape multiPoint: + serializer.Serialize(writer, multiPoint.Coordinates); + break; + case ILineStringGeoShape lineString: + serializer.Serialize(writer, lineString.Coordinates); + break; + case IMultiLineStringGeoShape multiLineString: + serializer.Serialize(writer, multiLineString.Coordinates); + break; + case IPolygonGeoShape polygon: + serializer.Serialize(writer, polygon.Coordinates); + break; + case IMultiPolygonGeoShape multiPolyon: + serializer.Serialize(writer, multiPolyon.Coordinates); + break; + case IEnvelopeGeoShape envelope: + serializer.Serialize(writer, envelope.Coordinates); + break; + case ICircleGeoShape circle: + serializer.Serialize(writer, circle.Coordinates); + writer.WritePropertyName("radius"); + writer.WriteValue(circle.Radius); + break; + } + writer.WriteEndObject(); + } + } 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 object ReadJToken(JToken shape, JsonSerializer serializer) { - var type = shape["type"]; - var typeName = type?.Value(); + var typeName = shape["type"]?.Value().ToUpperInvariant(); switch (typeName) { - case "circle": - var radius = shape["radius"]; - return ParseCircleGeoShape(shape, serializer, radius); - case "envelope": + case GeoShapeType.Circle: + return ParseCircleGeoShape(shape, serializer); + 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; } } - public override bool CanConvert(Type objectType) => typeof(IGeoShape).IsAssignableFrom(objectType) || - typeof(IGeometryCollection).IsAssignableFrom(objectType); + public override bool CanConvert(Type objectType) => + typeof(IGeoShape).IsAssignableFrom(objectType) || typeof(IGeometryCollection).IsAssignableFrom(objectType); private static GeometryCollection ParseGeometryCollection(JToken shape, JsonSerializer serializer) { @@ -128,11 +221,11 @@ private static LineStringGeoShape ParseLineStringGeoShape(JToken shape, JsonSeri private static EnvelopeGeoShape ParseEnvelopeGeoShape(JToken shape, JsonSerializer serializer) => new EnvelopeGeoShape {Coordinates = GetCoordinates>(shape, serializer)}; - private static CircleGeoShape ParseCircleGeoShape(JToken shape, JsonSerializer serializer, JToken radius) => + private static CircleGeoShape ParseCircleGeoShape(JToken shape, JsonSerializer serializer) => new CircleGeoShape { Coordinates = GetCoordinates(shape, serializer), - Radius = radius?.Value() + Radius = shape["radius"]?.Value() }; private static T GetCoordinates(JToken shape, JsonSerializer serializer) diff --git a/src/Nest/QueryDsl/Geo/Shape/GeoShapeQueryJsonConverter.cs b/src/Nest/QueryDsl/Geo/Shape/GeoShapeQueryJsonConverter.cs index fe011cc16fd..992e46b2e7c 100644 --- a/src/Nest/QueryDsl/Geo/Shape/GeoShapeQueryJsonConverter.cs +++ b/src/Nest/QueryDsl/Geo/Shape/GeoShapeQueryJsonConverter.cs @@ -43,6 +43,7 @@ internal class GeoShapeQueryJsonConverter : JsonConverter public override bool CanRead => true; public override bool CanWrite => false; + // TODO: remove in 7.x public virtual T GetCoordinates(JToken shape, JsonSerializer serializer) { var coordinates = shape["coordinates"]; @@ -104,27 +105,27 @@ private static IGeoShapeQuery ParseIndexedShapeQuery(JToken indexedShape) => private static IGeoShapeQuery ParseShapeQuery(JToken shape, JsonSerializer serializer) { var type = shape["type"]; - var typeName = type?.Value(); + var typeName = type?.Value().ToUpperInvariant(); var geometry = GeoShapeConverter.ReadJToken(shape, serializer); switch (typeName) { - case "circle": + case GeoShapeType.Circle: return new GeoShapeCircleQuery { Shape = geometry as ICircleGeoShape }; - case "envelope": + case GeoShapeType.Envelope: return new GeoShapeEnvelopeQuery { Shape = geometry as IEnvelopeGeoShape }; - case "linestring": + case GeoShapeType.LineString: return new GeoShapeLineStringQuery { Shape = geometry as ILineStringGeoShape }; - case "multilinestring": + case GeoShapeType.MultiLineString: return new GeoShapeMultiLineStringQuery { Shape = geometry as IMultiLineStringGeoShape }; - case "point": + case GeoShapeType.Point: return new GeoShapePointQuery { Shape = geometry as IPointGeoShape }; - case "multipoint": + case GeoShapeType.MultiPoint: return new GeoShapeMultiPointQuery { Shape = geometry as IMultiPointGeoShape }; - case "polygon": + case GeoShapeType.Polygon: return new GeoShapePolygonQuery { Shape = geometry as IPolygonGeoShape }; - case "multipolygon": + case GeoShapeType.MultiPolygon: return new GeoShapeMultiPolygonQuery { Shape = geometry as IMultiPolygonGeoShape }; - case "geometrycollection": + case GeoShapeType.GeometryCollection: return new GeoShapeGeometryCollectionQuery { Shape = geometry as IGeometryCollection }; default: return null; diff --git a/src/Nest/QueryDsl/Geo/Shape/GeometryCollection/GeometryCollection.cs b/src/Nest/QueryDsl/Geo/Shape/GeometryCollection/GeometryCollection.cs index c687c6056d6..c12bbb395c2 100644 --- a/src/Nest/QueryDsl/Geo/Shape/GeometryCollection/GeometryCollection.cs +++ b/src/Nest/QueryDsl/Geo/Shape/GeometryCollection/GeometryCollection.cs @@ -27,6 +27,8 @@ public interface IGeometryCollection /// public class GeometryCollection : IGeometryCollection, IGeoShape { + internal GeoShapeFormat Format { get; set; } + /// public string Type => "geometrycollection"; diff --git a/src/Nest/QueryDsl/Geo/WKT/GeoWKTException.cs b/src/Nest/QueryDsl/Geo/WKT/GeoWKTException.cs new file mode 100644 index 00000000000..8c5dbf737da --- /dev/null +++ b/src/Nest/QueryDsl/Geo/WKT/GeoWKTException.cs @@ -0,0 +1,20 @@ +using System; + +namespace Nest +{ + /// + /// An exception when handling in Well-Known Text format + /// + 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}") + { + } + } +} diff --git a/src/Nest/QueryDsl/Geo/WKT/GeoWKTReader.cs b/src/Nest/QueryDsl/Geo/WKT/GeoWKTReader.cs new file mode 100644 index 00000000000..19e1542109e --- /dev/null +++ b/src/Nest/QueryDsl/Geo/WKT/GeoWKTReader.cs @@ -0,0 +1,585 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; + +namespace Nest +{ + /// + /// Reads Well-Known Text (WKT) into types + /// + public class GeoWKTReader + { + /// + /// Reads Well-Known Text (WKT) into a new instance of + /// + public static IGeoShape Read(string wellKnownText) + { + using (var tokenizer = new WellKnownTextTokenizer(new StringReader(wellKnownText))) + return Read(tokenizer, null); + } + + private static IGeoShape Read(WellKnownTextTokenizer tokenizer, string shapeType) + { + var token = tokenizer.NextToken(); + + if (token != TokenType.Word) + throw new GeoWKTException( + $"Expected word but found {tokenizer.TokenString()}", tokenizer.LineNumber, tokenizer.Position); + + var type = tokenizer.TokenValue.ToUpperInvariant(); + + if (shapeType != null && shapeType != GeoShapeType.GeometryCollection && type != shapeType) + throw new GeoWKTException($"Expected geometry type {shapeType} but found {type}"); + + switch (type) + { + case GeoShapeType.Point: + var point = ParsePoint(tokenizer); + point.Format = GeoShapeFormat.WellKnownText; + return point; + case GeoShapeType.MultiPoint: + var multiPoint = ParseMultiPoint(tokenizer); + multiPoint.Format = GeoShapeFormat.WellKnownText; + return multiPoint; + case GeoShapeType.LineString: + var lineString = ParseLineString(tokenizer); + lineString.Format = GeoShapeFormat.WellKnownText; + return lineString; + case GeoShapeType.MultiLineString: + var multiLineString = ParseMultiLineString(tokenizer); + multiLineString.Format = GeoShapeFormat.WellKnownText; + return multiLineString; + case GeoShapeType.Polygon: + var polygon = ParsePolygon(tokenizer); + polygon.Format = GeoShapeFormat.WellKnownText; + return polygon; + case GeoShapeType.MultiPolygon: + var multiPolygon = ParseMultiPolygon(tokenizer); + multiPolygon.Format = GeoShapeFormat.WellKnownText; + return multiPolygon; + case GeoShapeType.BoundingBox: + var envelope = ParseBoundingBox(tokenizer); + envelope.Format = GeoShapeFormat.WellKnownText; + return envelope; + case GeoShapeType.GeometryCollection: + var geometryCollection = ParseGeometryCollection(tokenizer); + geometryCollection.Format = GeoShapeFormat.WellKnownText; + return geometryCollection; + default: + throw new GeoWKTException($"Unknown geometry type: {type}"); + } + } + + private static PointGeoShape ParsePoint(WellKnownTextTokenizer tokenizer) + { + if (NextEmptyOrOpen(tokenizer) == TokenType.Word) + return null; + + var point = new PointGeoShape(ParseCoordinate(tokenizer)); + NextCloser(tokenizer); + + return point; + } + + private static MultiPointGeoShape ParseMultiPoint(WellKnownTextTokenizer tokenizer) + { + if (NextEmptyOrOpen(tokenizer) == TokenType.Word) + return null; + + var coordinates = ParseCoordinates(tokenizer); + return new MultiPointGeoShape(coordinates); + } + + private static LineStringGeoShape ParseLineString(WellKnownTextTokenizer tokenizer) + { + if (NextEmptyOrOpen(tokenizer) == TokenType.Word) + return null; + + var coordinates = ParseCoordinates(tokenizer); + return new LineStringGeoShape(coordinates); + } + + private static MultiLineStringGeoShape ParseMultiLineString(WellKnownTextTokenizer tokenizer) + { + if (NextEmptyOrOpen(tokenizer) == TokenType.Word) + return null; + + var coordinates = ParseCoordinateLists(tokenizer); + return new MultiLineStringGeoShape(coordinates); + } + + private static PolygonGeoShape ParsePolygon(WellKnownTextTokenizer tokenizer) + { + if (NextEmptyOrOpen(tokenizer) == TokenType.Word) + return null; + + var coordinates = ParseCoordinateLists(tokenizer); + return new PolygonGeoShape(coordinates); + } + + private static MultiPolygonGeoShape ParseMultiPolygon(WellKnownTextTokenizer tokenizer) + { + if (NextEmptyOrOpen(tokenizer) == TokenType.Word) + return null; + + var coordinates = new List>> + { + ParseCoordinateLists(tokenizer) + }; + + while (NextCloserOrComma(tokenizer) == TokenType.Comma) + coordinates.Add(ParseCoordinateLists(tokenizer)); + + return new MultiPolygonGeoShape(coordinates); + } + + private static EnvelopeGeoShape ParseBoundingBox(WellKnownTextTokenizer tokenizer) + { + if (NextEmptyOrOpen(tokenizer) == TokenType.Word) + return null; + + var minLon = NextNumber(tokenizer); + NextComma(tokenizer); + var maxLon = NextNumber(tokenizer); + NextComma(tokenizer); + var maxLat = NextNumber(tokenizer); + NextComma(tokenizer); + var minLat = NextNumber(tokenizer); + NextCloser(tokenizer); + return new EnvelopeGeoShape(new [] { new GeoCoordinate(maxLat, minLon), new GeoCoordinate(minLat, maxLon) }); + } + + private static GeometryCollection ParseGeometryCollection(WellKnownTextTokenizer tokenizer) + { + if (NextEmptyOrOpen(tokenizer) == TokenType.Word) + return null; + + var geometries = new List + { + Read(tokenizer, GeoShapeType.GeometryCollection) + }; + + while (NextCloserOrComma(tokenizer) == TokenType.Comma) + geometries.Add(Read(tokenizer, null)); + + return new GeometryCollection { Geometries = geometries }; + } + + private static List> ParseCoordinateLists(WellKnownTextTokenizer tokenizer) + { + var coordinates = new List>(); + + NextEmptyOrOpen(tokenizer); + coordinates.Add(ParseCoordinates(tokenizer)); + + while (NextCloserOrComma(tokenizer) == TokenType.Comma) + { + NextEmptyOrOpen(tokenizer); + coordinates.Add(ParseCoordinates(tokenizer)); + } + + return coordinates; + } + + private static List ParseCoordinates(WellKnownTextTokenizer tokenizer) + { + var coordinates = new List(); + + if (IsNumberNext(tokenizer) || (tokenizer.NextToken() == TokenType.LParen)) + coordinates.Add(ParseCoordinate(tokenizer)); + + while (NextCloserOrComma(tokenizer) == TokenType.Comma) + { + var isOpenParen = false; + + if (IsNumberNext(tokenizer) || (isOpenParen = tokenizer.NextToken() == TokenType.LParen)) + coordinates.Add(ParseCoordinate(tokenizer)); + + if (isOpenParen) + NextCloser(tokenizer); + } + + return coordinates; + } + + private static GeoCoordinate ParseCoordinate(WellKnownTextTokenizer tokenizer) + { + var lon = NextNumber(tokenizer); + var lat = NextNumber(tokenizer); + double? z = null; + + if (IsNumberNext(tokenizer)) + z = NextNumber(tokenizer); + + return z == null + ? new GeoCoordinate(lat, lon) + : new GeoCoordinate(lat, lon, z.Value); + } + + private static void NextCloser(WellKnownTextTokenizer tokenizer) + { + if (tokenizer.NextToken() != TokenType.RParen) + throw new GeoWKTException( + $"Expected {(char)WellKnownTextTokenizer.RParen} " + + $"but found: {tokenizer.TokenString()}", tokenizer.LineNumber, tokenizer.Position); + } + + private static void NextComma(WellKnownTextTokenizer tokenizer) + { + if (tokenizer.NextToken() != TokenType.Comma) + throw new GeoWKTException( + $"Expected {(char)WellKnownTextTokenizer.Comma} but found: {tokenizer.TokenString()}", + tokenizer.LineNumber, + tokenizer.Position); + } + + private static TokenType NextEmptyOrOpen(WellKnownTextTokenizer tokenizer) + { + var token = tokenizer.NextToken(); + if (token == TokenType.LParen || + token == TokenType.Word && tokenizer.TokenValue.Equals(WellKnownTextTokenizer.Empty, StringComparison.OrdinalIgnoreCase)) + return token; + + throw new GeoWKTException( + $"Expected {WellKnownTextTokenizer.Empty} or {(char)WellKnownTextTokenizer.LParen} " + + $"but found: {tokenizer.TokenString()}", tokenizer.LineNumber, tokenizer.Position); + } + + private static TokenType NextCloserOrComma(WellKnownTextTokenizer tokenizer) + { + var token = tokenizer.NextToken(); + if (token == TokenType.Comma || token == TokenType.RParen) + return token; + + throw new GeoWKTException( + $"Expected {(char)WellKnownTextTokenizer.Comma} or {(char)WellKnownTextTokenizer.RParen} " + + $"but found: {tokenizer.TokenString()}", tokenizer.LineNumber, tokenizer.Position); + } + + private static double NextNumber(WellKnownTextTokenizer tokenizer) + { + if (tokenizer.NextToken() == TokenType.Number) + { + if (string.Equals(tokenizer.TokenValue, WellKnownTextTokenizer.NaN, StringComparison.OrdinalIgnoreCase)) + return double.NaN; + + if (double.TryParse( + tokenizer.TokenValue, + NumberStyles.AllowDecimalPoint | NumberStyles.AllowLeadingSign, + CultureInfo.InvariantCulture, out var d)) + return d; + } + + throw new GeoWKTException( + $"Expected number but found: {tokenizer.TokenString()}", tokenizer.LineNumber, tokenizer.Position); + } + + private static bool IsNumberNext(WellKnownTextTokenizer tokenizer) + { + var token = tokenizer.PeekToken(); + return token == TokenType.Number; + } + } + + /// + /// Character types when parsing Well-Known Text + /// + internal enum CharacterType : byte + { + Whitespace, + Digit, + Alpha, + Comment + } + + /// + /// Well-Known Text token types + /// + internal enum TokenType : byte + { + None, + Word, + Number, + LParen, + RParen, + Comma + } + + /// + /// Tokenizes a sequence of characters into Well-Known Text + /// (WKT) + /// + internal class WellKnownTextTokenizer : IDisposable + { + private const int NeedChar = int.MaxValue; + private const int CharacterTypesLength = 256; + + public const int Linefeed = '\n'; + public const int CarriageReturn = '\r'; + public const int LParen = '('; + public const int RParen = ')'; + public const int Comma = ','; + public const int Comment = '#'; + public const int Dot = '.'; + public const int Plus = '+'; + public const int Minus = '-'; + public const string NaN = "NAN"; + public const string Empty = "EMPTY"; + + private static readonly CharacterType[] CharacterTypes = new CharacterType[CharacterTypesLength]; + + private static void Chars(int low, int high, CharacterType type) + { + if (low < 0) + low = 0; + + if (high >= CharacterTypesLength) + high = CharacterTypesLength - 1; + + while (low <= high) + CharacterTypes[low++] = type; + } + + static WellKnownTextTokenizer() + { + // build a map of ASCII chars and their types + // Any unmapped ASCII will be considered whitespace + // and anything > 0 outside of ASCII will be considered alpha. + // Treat + - and . as digit characters to make parsing numbers easier. + Chars('a', 'z', CharacterType.Alpha); + Chars('A', 'Z', CharacterType.Alpha); + Chars(128 + 32, 255, CharacterType.Alpha); + Chars('0', '9', CharacterType.Digit); + Chars(LParen, RParen, CharacterType.Alpha); + Chars(Plus, Plus, CharacterType.Digit); + Chars(Comma, Comma, CharacterType.Alpha); + Chars(Minus, Dot, CharacterType.Digit); + Chars(Comment, Comment, CharacterType.Comment); + } + + private readonly TextReader _reader; + private readonly List _buffer = new List(); + private bool _pushed; + private int _peekChar = NeedChar; + + // TODO: use ReadOnlySpan in future + public WellKnownTextTokenizer(TextReader reader) => + _reader = reader ?? throw new ArgumentNullException(nameof(reader)); + + /// + /// Gets the current position + /// + public int Position { get; private set; } + + /// + /// Gets the current line number + /// + public int LineNumber { get; private set; } = 1; + + /// + /// Gets the current token value + /// + public string TokenValue { get; private set; } + + /// + /// Gets the current token type + /// + public TokenType TokenType { get; private set; } = TokenType.None; + + /// + /// A user friendly string for the current token + /// + public string TokenString() + { + switch (TokenType) + { + case TokenType.Word: + case TokenType.Number: + return TokenValue; + case TokenType.None: + return "END-OF-STREAM"; + case TokenType.LParen: + return "("; + case TokenType.RParen: + return ")"; + case TokenType.Comma: + return ","; + default: + return $"\'{(char)_peekChar}\'"; + } + } + + private int Read() + { + Position++; + return _reader.Read(); + } + + /// + /// Peeks at the next token without changing the state + /// of the reader + /// + public TokenType PeekToken() + { + var position = Position; + var token = NextToken(); + Position = position; + _pushed = true; + return token; + } + + /// + /// Gets the next token, advancing the position + /// + public TokenType NextToken() + { + if (_pushed) + { + _pushed = false; + + // Add the length of peeked token + Position += !string.IsNullOrEmpty(TokenValue) + ? 1 + TokenValue.Length + : 1; + + return TokenType; + } + + TokenValue = null; + + var c = _peekChar; + if (c < 0) + c = NeedChar; + + if (c == NeedChar) + { + c = Read(); + if (c < 0) + return TokenType = TokenType.None; + } + + // reset the peek character for next token + _peekChar = NeedChar; + + var characterType = c < CharacterTypesLength + ? CharacterTypes[c] + : CharacterType.Alpha; + + // consume all whitespace + while (characterType == CharacterType.Whitespace) + { + if (c == CarriageReturn) + { + LineNumber++; + Position = 0; + c = Read(); + if (c == Linefeed) + c = Read(); + } + else + { + if (c == Linefeed) + { + LineNumber++; + Position = 0; + } + + c = Read(); + } + + if (c < 0) + return TokenType = TokenType.None; + + characterType = c < CharacterTypesLength + ? CharacterTypes[c] + : CharacterType.Alpha; + } + + switch (c) + { + case LParen: + return TokenType = TokenType.LParen; + case RParen: + return TokenType = TokenType.RParen; + case Comma: + return TokenType = TokenType.Comma; + } + + if (characterType == CharacterType.Alpha) + { + var i = 0; + + do + { + _buffer.Insert(i++, (char)c); + c = Read(); + + if (c < 0) + characterType = CharacterType.Whitespace; + else if (c < CharacterTypesLength) + characterType = CharacterTypes[c]; + else + characterType = CharacterType.Alpha; + + } while (characterType == CharacterType.Alpha); + + _peekChar = c; + TokenValue = new string(_buffer.ToArray(), 0, i); + + // special case for NaN + if (string.Equals(TokenValue, NaN, StringComparison.OrdinalIgnoreCase)) + return TokenType = TokenType.Number; + + return TokenType = TokenType.Word; + } + + if (characterType == CharacterType.Digit) + { + var i = 0; + var dots = 0; + do + { + _buffer.Insert(i++, (char)c); + c = Read(); + + if (c < 0) + characterType = CharacterType.Whitespace; + else if (c < CharacterTypesLength) + { + characterType = CharacterTypes[c]; + if (c == Dot) + dots++; + } + else + characterType = CharacterType.Alpha; + } while (characterType == CharacterType.Digit); + + _peekChar = c; + TokenValue = new string(_buffer.ToArray(), 0, i); + + return dots > 1 + ? TokenType = TokenType.Word + : TokenType = TokenType.Number; + } + + if (characterType == CharacterType.Comment) + { + // consume all characters on comment line + while ((c = Read()) != Linefeed && c != CarriageReturn && c >= 0) + { + } + + _peekChar = c; + return NextToken(); + } + + return TokenType = TokenType.None; + } + + /// + /// Disposes of the reader from which characters are read + /// + public void Dispose() => _reader?.Dispose(); + } +} diff --git a/src/Nest/QueryDsl/Geo/WKT/GeoWKTWriter.cs b/src/Nest/QueryDsl/Geo/WKT/GeoWKTWriter.cs new file mode 100644 index 00000000000..d0a658df21d --- /dev/null +++ b/src/Nest/QueryDsl/Geo/WKT/GeoWKTWriter.cs @@ -0,0 +1,188 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Nest +{ + /// + /// Writes types to Well-Known Text (WKT) + /// + public class GeoWKTWriter + { + /// + /// Writes a to Well-Known Text (WKT) + /// + public static string Write(IGeoShape shape) => + shape == null ? null : Write(shape, new StringBuilder()); + + /// + /// Writes a to Well-Known Text (WKT) + /// + // TODO: Remove in 7.x, where IGeometryCollection implements IGeoShape + public static string Write(IGeometryCollection collection) + { + if (collection == null) + return null; + + var builder = new StringBuilder(); + WriteGeometryCollection(collection, builder); + return builder.ToString(); + } + + private static string Write(IGeoShape shape, StringBuilder builder) + { + switch (shape) + { + case IPointGeoShape point: + WritePoint(point, builder); + break; + case IMultiPointGeoShape multiPoint: + WriteMultiPoint(multiPoint, builder); + break; + case ILineStringGeoShape lineString: + WriteLineString(lineString, builder); + break; + case IMultiLineStringGeoShape multiLineString: + WriteMultiLineString(multiLineString, builder); + break; + case IPolygonGeoShape polygon: + WritePolygon(polygon, builder); + break; + case IMultiPolygonGeoShape multiPolygon: + WriteMultiPolygon(multiPolygon, builder); + break; + case IGeometryCollection geometryCollection: + WriteGeometryCollection(geometryCollection, builder); + break; + case IEnvelopeGeoShape envelope: + WriteEnvelope(envelope, builder); + break; + default: + throw new GeoWKTException($"Unknown geometry type: {shape.GetType().Name}"); + } + + return builder.ToString(); + } + + private static void WritePoint(IPointGeoShape point, StringBuilder builder) + { + builder.Append(GeoShapeType.Point).Append(" ("); + WriteCoordinate(point.Coordinates, builder); + builder.Append(")"); + } + + private static void WriteMultiPoint(IMultiPointGeoShape multiPoint, StringBuilder builder) + { + builder.Append(GeoShapeType.MultiPoint).Append(" ("); + WriteCoordinates(multiPoint.Coordinates, builder); + builder.Append(")"); + } + + private static void WriteLineString(ILineStringGeoShape lineString, StringBuilder builder) + { + builder.Append(GeoShapeType.LineString).Append(" ("); + WriteCoordinates(lineString.Coordinates, builder); + builder.Append(")"); + } + + private static void WriteMultiLineString(IMultiLineStringGeoShape multiLineString, StringBuilder builder) + { + builder.Append(GeoShapeType.MultiLineString).Append(" "); + WriteCoordinatesList(multiLineString.Coordinates, builder); + } + + private static void WritePolygon(IPolygonGeoShape polygon, StringBuilder builder) + { + builder.Append(GeoShapeType.Polygon).Append(" "); + WriteCoordinatesList(polygon.Coordinates, builder); + } + + private static void WriteMultiPolygon(IMultiPolygonGeoShape multiPolygon, StringBuilder builder) + { + builder.Append(GeoShapeType.MultiPolygon).Append(" ("); + var i = 0; + foreach (var polygon in multiPolygon.Coordinates) + { + if (i > 0) + builder.Append(", "); + + WriteCoordinatesList(polygon, builder); + i++; + } + builder.Append(")"); + } + + private static void WriteGeometryCollection(IGeometryCollection geometryCollection, StringBuilder builder) + { + builder.Append(GeoShapeType.GeometryCollection).Append(" ("); + var i = 0; + foreach (var shape in geometryCollection.Geometries) + { + if (i > 0) + builder.Append(", "); + + Write(shape, builder); + i++; + } + builder.Append(")"); + } + + private static void WriteEnvelope(IEnvelopeGeoShape envelope, StringBuilder builder) + { + builder.Append(GeoShapeType.BoundingBox).Append(" ("); + var topLeft = envelope.Coordinates.ElementAt(0); + var bottomRight = envelope.Coordinates.ElementAt(1); + + // WKT specification expects the following order: minLon, maxLon, maxLat, minLat. + // envelope is top_left (minLon, maxLat), bottom_right (maxLon, minLat) + builder.Append(topLeft.Longitude) + .Append(", ") + .Append(bottomRight.Longitude) + .Append(", ") + .Append(topLeft.Latitude) + .Append(", ") + .Append(bottomRight.Latitude) + .Append(")"); + } + + private static void WriteCoordinatesList(IEnumerable> coordinates, StringBuilder builder) + { + builder.Append("("); + var i = 0; + foreach (var coordinateGroup in coordinates) + { + if (i > 0) + builder.Append(", "); + + builder.Append("("); + WriteCoordinates(coordinateGroup, builder); + builder.Append(")"); + i++; + } + builder.Append(")"); + } + + private static void WriteCoordinates(IEnumerable coordinates, StringBuilder builder) + { + var i = 0; + foreach (var coordinate in coordinates) + { + if (i > 0) + builder.Append(", "); + WriteCoordinate(coordinate, builder); + i++; + } + } + + private static void WriteCoordinate(GeoCoordinate coordinate, StringBuilder builder) + { + builder.Append(coordinate.Longitude) + .Append(" ") + .Append(coordinate.Latitude); + + if (coordinate.Z.HasValue) + builder.Append(" ").Append(coordinate.Z.Value); + } + } +} diff --git a/src/Tests/QueryDsl/Geo/GeoShapeSerializationTests.cs b/src/Tests/QueryDsl/Geo/GeoShapeSerializationTests.cs deleted file mode 100644 index af92686741a..00000000000 --- a/src/Tests/QueryDsl/Geo/GeoShapeSerializationTests.cs +++ /dev/null @@ -1,132 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Elasticsearch.Net; -using FluentAssertions; -using Nest; -using Tests.Framework; -using Tests.Framework.Integration; -using Tests.Framework.ManagedElasticsearch.Clusters; - -namespace Tests.QueryDsl.Geo -{ - public class GeoShapeSerializationTests : - ApiIntegrationTestBase, - ISearchRequest, - SearchDescriptor, - SearchRequest> - { - public GeoShapeSerializationTests(IntrusiveOperationCluster cluster, EndpointUsage usage) - : base(cluster, usage) { } - - private const string Index = "shapes"; - - protected override void IntegrationSetup(IElasticClient client, CallUniqueValues values) - { - if (client.IndexExists(Index).Exists) - return; - - var createIndexResponse = client.CreateIndex(Index, c => c - .Settings(s => s - .NumberOfShards(1) - ) - .Mappings(m => m - .Map(mm => mm - .AutoMap() - .Properties(p => p - .GeoShape(g => g - .Name(n => n.GeometryCollection) - ) - .GeoShape(g => g - .Name(n => n.Envelope) - ) - .GeoShape(g => g - .Name(n => n.Circle) - ) - ) - ) - ) - ); - - if (!createIndexResponse.IsValid) - throw new Exception($"Error creating index for integration test: {createIndexResponse.DebugInformation}"); - - var bulkResponse = this.Client.Bulk(b => b - .IndexMany(Framework.MockData.Shape.Shapes) - .Refresh(Refresh.WaitFor) - ); - - if (!bulkResponse.IsValid) - throw new Exception($"Error indexing shapes for integration test: {bulkResponse.DebugInformation}"); - } - - protected override LazyResponses ClientUsage() => Calls( - fluent: (client, f) => client.Search(f), - fluentAsync: (client, f) => client.SearchAsync(f), - request: (client, r) => client.Search(r), - requestAsync: (client, r) => client.SearchAsync(r) - ); - - private readonly IEnumerable _coordinates = - Framework.MockData.Shape.Shapes.First().Envelope.Coordinates; - - protected override object ExpectJson => new - { - query = new - { - geo_shape = new - { - _name="named_query", - boost = 1.1, - ignore_unmapped = true, - envelope = new - { - relation = "intersects", - shape = new - { - type = "envelope", - coordinates = this._coordinates - } - } - } - } - }; - - protected override int ExpectStatusCode => 200; - protected override bool ExpectIsValid => true; - protected override string UrlPath => $"/shapes/doc/_search"; - protected override HttpMethod HttpMethod => HttpMethod.POST; - - protected override SearchRequest Initializer => new SearchRequest - { - Query = new GeoShapeEnvelopeQuery - { - Name = "named_query", - Boost = 1.1, - Field = Infer.Field(p => p.Envelope), - Shape = new EnvelopeGeoShape(this._coordinates), - Relation = GeoShapeRelation.Intersects, - IgnoreUnmapped = true - } - }; - - protected override Func, ISearchRequest> Fluent => s => s - .Query(q => q - .GeoShapeEnvelope(c => c - .Name("named_query") - .Boost(1.1) - .Field(p => p.Envelope) - .Coordinates(this._coordinates) - .Relation(GeoShapeRelation.Intersects) - .IgnoreUnmapped() - ) - ); - - protected override void ExpectResponse(ISearchResponse response) - { - response.ShouldBeValid(); - response.Documents.Count.Should().Be(10); - } - } -} diff --git a/src/Tests/Tests/QueryDsl/Geo/BoundingBox/GeoBoundingBoxQueryUsageTests.cs b/src/Tests/Tests/QueryDsl/Geo/BoundingBox/GeoBoundingBoxQueryUsageTests.cs index aca7cfa4a47..43f17991f90 100644 --- a/src/Tests/Tests/QueryDsl/Geo/BoundingBox/GeoBoundingBoxQueryUsageTests.cs +++ b/src/Tests/Tests/QueryDsl/Geo/BoundingBox/GeoBoundingBoxQueryUsageTests.cs @@ -68,4 +68,56 @@ protected override QueryContainer QueryFluent(QueryContainerDescriptor q => q.Field = null }; } + + public class GeoBoundingBoxWKTQueryUsageTests : QueryDslUsageTestsBase + { + public GeoBoundingBoxWKTQueryUsageTests(ReadOnlyCluster i, EndpointUsage usage) : base(i, usage) { } + + protected override object QueryJson => new + { + geo_bounding_box = new + { + type = "indexed", + validation_method = "strict", + _name = "named_query", + boost = 1.1, + location = new + { + wkt = "BBOX (34, -34, -34, 34)" + } + } + }; + + protected override QueryContainer QueryInitializer => new GeoBoundingBoxQuery + { + Boost = 1.1, + Name = "named_query", + Field = Infer.Field(p => p.Location), + BoundingBox = new Nest.BoundingBox + { + WellKnownText = "BBOX (34, -34, -34, 34)" + }, + Type = GeoExecution.Indexed, + ValidationMethod = GeoValidationMethod.Strict + }; + + protected override QueryContainer QueryFluent(QueryContainerDescriptor q) => q + .GeoBoundingBox(g=>g + .Boost(1.1) + .Name("named_query") + .Field(p=>p.Location) + .BoundingBox(b=>b + .WellKnownText("BBOX (34, -34, -34, 34)") + ) + .ValidationMethod(GeoValidationMethod.Strict) + .Type(GeoExecution.Indexed) + ); + + protected override ConditionlessWhen ConditionlessWhen => new ConditionlessWhen(a => a.GeoBoundingBox) + { + q => q.BoundingBox = null, + q => q.BoundingBox = new Nest.BoundingBox { } , + q => q.Field = null + }; + } } diff --git a/src/Tests/Tests/QueryDsl/Geo/GeoShapeSerializationTests.cs b/src/Tests/Tests/QueryDsl/Geo/GeoShapeSerializationTests.cs new file mode 100644 index 00000000000..700516cbdb3 --- /dev/null +++ b/src/Tests/Tests/QueryDsl/Geo/GeoShapeSerializationTests.cs @@ -0,0 +1,254 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.IO; +using System.Linq; +using System.Text; +using Elastic.Xunit.XunitPlumbing; +using Elasticsearch.Net; +using FluentAssertions; +using Nest; +using Newtonsoft.Json.Linq; +using Tests.Core.Extensions; +using Tests.Core.ManagedElasticsearch.Clusters; +using Tests.Framework; +using Tests.Framework.Integration; +using Tests.Framework.ManagedElasticsearch.Clusters; +using Tests.Domain; + +namespace Tests.QueryDsl.Geo +{ + public abstract class GeoShapeSerializationTestsBase : + ApiIntegrationTestBase, + ISearchRequest, + SearchDescriptor, + SearchRequest> + { + protected GeoShapeSerializationTestsBase(IntrusiveOperationCluster cluster, EndpointUsage usage) + : base(cluster, usage) { } + + protected abstract string Index { get; } + + protected override LazyResponses ClientUsage() => Calls( + fluent: (client, f) => client.Search(f), + fluentAsync: (client, f) => client.SearchAsync(f), + request: (client, r) => client.Search(r), + requestAsync: (client, r) => client.SearchAsync(r) + ); + + private readonly IEnumerable _coordinates = + Domain.Shape.Shapes.First().Envelope.Coordinates; + + protected override object ExpectJson => new + { + query = new + { + geo_shape = new + { + _name="named_query", + boost = 1.1, + ignore_unmapped = true, + envelope = new + { + relation = "intersects", + shape = new + { + type = "envelope", + coordinates = this._coordinates + } + } + } + } + }; + + protected override int ExpectStatusCode => 200; + protected override bool ExpectIsValid => true; + protected override string UrlPath => $"/{Index}/doc/_search"; + protected override HttpMethod HttpMethod => HttpMethod.POST; + + protected override SearchRequest Initializer => new SearchRequest(Index) + { + Query = new GeoShapeEnvelopeQuery + { + Name = "named_query", + Boost = 1.1, + Field = Infer.Field(p => p.Envelope), + Shape = new EnvelopeGeoShape(this._coordinates), + Relation = GeoShapeRelation.Intersects, + IgnoreUnmapped = true + } + }; + + protected override Func, ISearchRequest> Fluent => s => s + .Index(Index) + .Query(q => q + .GeoShapeEnvelope(c => c + .Name("named_query") + .Boost(1.1) + .Field(p => p.Envelope) + .Coordinates(this._coordinates) + .Relation(GeoShapeRelation.Intersects) + .IgnoreUnmapped() + ) + ); + + protected override void ExpectResponse(ISearchResponse response) + { + response.IsValid.Should().BeTrue(); + response.Documents.Count.Should().Be(10); + } + } + + public class GeoShapeSerializationTests : GeoShapeSerializationTestsBase + { + public GeoShapeSerializationTests(IntrusiveOperationCluster cluster, EndpointUsage usage) + : base(cluster, usage) { } + + protected override void IntegrationSetup(IElasticClient client, CallUniqueValues values) + { + if (client.IndexExists(Index).Exists) + return; + + var createIndexResponse = client.CreateIndex(Index, c => c + .Settings(s => s + .NumberOfShards(1) + .NumberOfReplicas(0) + ) + .Mappings(m => m + .Map(mm => mm + .AutoMap() + .Properties(p => p + .GeoShape(g => g + .Name(n => n.GeometryCollection) + ) + .GeoShape(g => g + .Name(n => n.Envelope) + ) + .GeoShape(g => g + .Name(n => n.Circle) + ) + ) + ) + ) + ); + + if (!createIndexResponse.IsValid) + throw new Exception($"Error creating index for integration test: {createIndexResponse.DebugInformation}"); + + var bulkResponse = this.Client.Bulk(b => b + .Index(Index) + .IndexMany(Domain.Shape.Shapes) + .Refresh(Refresh.WaitFor) + ); + + if (!bulkResponse.IsValid) + throw new Exception($"Error indexing shapes for integration test: {bulkResponse.DebugInformation}"); + } + + protected override string Index => "geoshapes"; + } + + [SkipVersion("<6.2.0", "Support for WKT in Elasticsearch 6.2.0+")] + public class GeoShapeWKTSerializationTests : GeoShapeSerializationTestsBase + { + public GeoShapeWKTSerializationTests(IntrusiveOperationCluster cluster, EndpointUsage usage) + : base(cluster, usage) { } + + protected override void IntegrationSetup(IElasticClient client, CallUniqueValues values) + { + if (client.IndexExists(Index).Exists) + return; + + var createIndexResponse = client.CreateIndex(Index, c => c + .Settings(s => s + .NumberOfShards(1) + .NumberOfReplicas(0) + ) + .Mappings(m => m + .Map(mm => mm + .AutoMap() + .Properties(p => p + .GeoShape(g => g + .Name(n => n.GeometryCollection) + ) + .GeoShape(g => g + .Name(n => n.Envelope) + ) + .GeoShape(g => g + .Name(n => n.Circle) + ) + ) + ) + ) + ); + + if (!createIndexResponse.IsValid) + throw new Exception($"Error creating index for integration test: {createIndexResponse.DebugInformation}"); + + var bulk = new List(); + + // use the low level client to force WKT + var typeName = this.Client.Infer.TypeName(); + foreach (var shape in Domain.Shape.Shapes) + { + bulk.Add(new { index = new { _index = Index, _type = typeName, _id = shape.Id } }); + bulk.Add(new + { + id = shape.Id, + geometryCollection = GeoWKTWriter.Write(shape.GeometryCollection), + envelope = GeoWKTWriter.Write(shape.Envelope), + circle = shape.Circle + }); + } + + var bulkResponse = this.Client.LowLevel.Bulk( + PostData.MultiJson(bulk), + new BulkRequestParameters{ Refresh = Refresh.WaitFor } + ); + + if (!bulkResponse.IsValid) + throw new Exception($"Error indexing shapes for integration test: {bulkResponse.DebugInformation}"); + } + + protected override string Index => "wkt-geoshapes"; + + protected override void ExpectResponse(ISearchResponse response) + { + base.ExpectResponse(response); + + // index shapes again + var bulkResponse = this.Client.Bulk(b => b + .Index(Index) + .IndexMany(response.Documents) + .Refresh(Refresh.WaitFor) + .RequestConfiguration(r => r + .DisableDirectStreaming() + ) + ); + + bulkResponse.IsValid.Should().BeTrue(); + + // ensure they were indexed as WKT + var request = Encoding.UTF8.GetString(bulkResponse.ApiCall.RequestBodyInBytes); + using (var reader = new StringReader(request)) + { + string line; + var i = 0; + while ((line = reader.ReadLine()) != null) + { + i++; + if (i % 2 != 0) + continue; + + var jObject = JObject.Parse(line); + var jValue = (JValue)jObject["geometryCollection"]; + jValue.Value.Should().BeOfType(); + jValue = (JValue)jObject["envelope"]; + jValue.Value.Should().BeOfType(); + jObject["circle"].Should().BeOfType(); + } + } + } + } +} diff --git a/src/Tests/Tests/QueryDsl/Geo/GeoWKTTests.cs b/src/Tests/Tests/QueryDsl/Geo/GeoWKTTests.cs new file mode 100644 index 00000000000..1da89c36711 --- /dev/null +++ b/src/Tests/Tests/QueryDsl/Geo/GeoWKTTests.cs @@ -0,0 +1,298 @@ +using System; +using System.Linq; +using Elastic.Xunit.XunitPlumbing; +using FluentAssertions; +using Nest; +using Tests.Framework; + +namespace Tests.QueryDsl.Geo +{ + public class GeoWKTTests + { + [U] + public void ReadAndWritePoint() + { + var wkt = "POINT (-77.03653 38.897676)"; + var shape = GeoWKTReader.Read(wkt); + + shape.Should().BeOfType(); + var point = (PointGeoShape)shape; + + point.Coordinates.Latitude.Should().Be(38.897676); + point.Coordinates.Longitude.Should().Be(-77.03653); + + GeoWKTWriter.Write(point).Should().Be(wkt); + } + + [U] + public void ReadAndWriteMultiPoint() + { + var wkt = "MULTIPOINT (102.0 2.0, 103.0 2.0)"; + var shape = GeoWKTReader.Read(wkt); + + var multiPoint = shape as MultiPointGeoShape; + + multiPoint.Should().NotBeNull(); + + var firstPoint = multiPoint.Coordinates.First(); + firstPoint.Latitude.Should().Be(2); + firstPoint.Longitude.Should().Be(102); + + var lastPoint = multiPoint.Coordinates.Last(); + lastPoint.Latitude.Should().Be(2); + lastPoint.Longitude.Should().Be(103); + + GeoWKTWriter.Write(multiPoint).Should().Be("MULTIPOINT (102 2, 103 2)"); + } + + [U] + public void ReadAndWriteLineString() + { + var wkt = "LINESTRING (-77.03653 38.897676, -77.009051 38.889939)"; + + var shape = GeoWKTReader.Read(wkt); + + var lineString = shape as LineStringGeoShape; + + lineString.Should().NotBeNull(); + lineString.Coordinates.First().Latitude.Should().Be(38.897676); + lineString.Coordinates.First().Longitude.Should().Be(-77.03653); + + lineString.Coordinates.Last().Latitude.Should().Be(38.889939); + lineString.Coordinates.Last().Longitude.Should().Be(-77.009051); + + GeoWKTWriter.Write(lineString).Should().Be(wkt); + } + + [U] + public void ReadMultiLineString() + { + var wkt = @"MULTILINESTRING ((102.0 2.0, 103.0 2.0, 103.0 3.0, 102.0 3.0), + (100.0 0.0, 101.0 0.0, 101.0 1.0, 100.0 1.0), + (100.2 0.2, 100.8 0.2, 100.8 0.8, 100.2 0.8))"; + + var shape = GeoWKTReader.Read(wkt); + + var multiLineString = shape as MultiLineStringGeoShape; + + multiLineString.Should().NotBeNull(); + + foreach (var lineString in multiLineString.Coordinates) + foreach (var coordinate in lineString) + { + coordinate.Latitude.Should().BeGreaterOrEqualTo(0).And.BeLessOrEqualTo(3); + coordinate.Longitude.Should().BeGreaterOrEqualTo(100).And.BeLessOrEqualTo(103); + } + } + + [U] + public void WriteMultiLineString() + { + var multLineString = new MultiLineStringGeoShape(new [] + { + new [] + { + new GeoCoordinate(2, 102), + new GeoCoordinate(2, 103), + new GeoCoordinate(3, 103), + new GeoCoordinate(3, 102), + }, + new [] + { + new GeoCoordinate(0, 100), + new GeoCoordinate(0, 101), + new GeoCoordinate(1, 101), + new GeoCoordinate(1, 100), + } + }); + + var wkt = GeoWKTWriter.Write(multLineString); + wkt.Should().Be("MULTILINESTRING ((102 2, 103 2, 103 3, 102 3), (100 0, 101 0, 101 1, 100 1))"); + } + + [U] + public void ReadPolygon() + { + var wkt = @"POLYGON ((100.0 0.0, 101.0 0.0, 101.0 1.0, 100.0 1.0, 100.0 0.0), + (100.2 0.2, 100.8 0.2, 100.8 0.8, 100.2 0.8, 100.2 0.2))"; + + var shape = GeoWKTReader.Read(wkt); + + var polygon = shape as PolygonGeoShape; + + polygon.Should().NotBeNull(); + + foreach (var ring in polygon.Coordinates) + foreach (var coordinate in ring) + { + coordinate.Latitude.Should().BeLessOrEqualTo(1.0); + coordinate.Longitude.Should().BeGreaterOrEqualTo(100.0); + } + } + + [U] + public void WritePolygon() + { + var polygon = new PolygonGeoShape(new [] + { + new [] + { + new GeoCoordinate(2, 102), + new GeoCoordinate(2, 103), + new GeoCoordinate(3, 103), + new GeoCoordinate(3, 102), + }, + new [] + { + new GeoCoordinate(0, 100), + new GeoCoordinate(0, 101), + new GeoCoordinate(1, 101), + new GeoCoordinate(1, 100), + } + }); + + var wkt = GeoWKTWriter.Write(polygon); + wkt.Should().Be("POLYGON ((102 2, 103 2, 103 3, 102 3), (100 0, 101 0, 101 1, 100 1))"); + } + + [U] + public void ReadMultiPolygon() + { + var wkt = @"MULTIPOLYGON ( + ((102.0 2.0, 103.0 2.0, 103.0 3.0, 102.0 3.0, 102.0 2.0)), + ((100.0 0.0, 101.0 0.0, 101.0 1.0, 100.0 1.0, 100.0 0.0), + (100.2 0.2, 100.8 0.2, 100.8 0.8, 100.2 0.8, 100.2 0.2)))"; + + var shape = GeoWKTReader.Read(wkt); + + var multiPolygon = shape as MultiPolygonGeoShape; + + multiPolygon.Should().NotBeNull(); + multiPolygon.Coordinates.Should().HaveCount(2); + + foreach (var polygon in multiPolygon.Coordinates) + foreach (var ring in polygon) + { + ring.Should().HaveCount(5); + foreach (var coordinate in ring) + { + coordinate.Latitude.Should().BeLessOrEqualTo(3.0).And.BeGreaterOrEqualTo(0); + coordinate.Longitude.Should().BeGreaterOrEqualTo(100.0).And.BeLessOrEqualTo(103.0); + } + } + } + + [U] + public void WriteMultiPolygon() + { + var multiPolygon = new MultiPolygonGeoShape(new [] + { + new [] + { + new [] + { + new GeoCoordinate(2, 102), + new GeoCoordinate(2, 103), + new GeoCoordinate(3, 103), + new GeoCoordinate(3, 102), + new GeoCoordinate(2, 102), + } + }, + new [] + { + new [] + { + new GeoCoordinate(0, 100), + new GeoCoordinate(0, 101), + new GeoCoordinate(1, 101), + new GeoCoordinate(1, 100), + new GeoCoordinate(0, 100), + }, + new [] + { + new GeoCoordinate(0.2, 100.2), + new GeoCoordinate(0.2, 100.8), + new GeoCoordinate(0.8, 100.8), + new GeoCoordinate(0.8, 100.2), + new GeoCoordinate(0.2, 100.2), + } + } + }); + + GeoWKTWriter.Write(multiPolygon).Should().Be("MULTIPOLYGON (((102 2, 103 2, 103 3, 102 3, 102 2)), ((100 0, 101 0, 101 1, 100 1, 100 0), (100.2 0.2, 100.8 0.2, 100.8 0.8, 100.2 0.8, 100.2 0.2)))"); + } + + [U] + public void ReadAndWriteEnvelope() + { + var wkt = "BBOX (-74.1, -71.12, 40.73, 40.01)"; + var shape = GeoWKTReader.Read(wkt); + var envelope = shape as EnvelopeGeoShape; + + envelope.Should().NotBeNull(); + envelope.Coordinates.First().Latitude.Should().Be(40.73); + envelope.Coordinates.First().Longitude.Should().Be(-74.1); + envelope.Coordinates.Last().Latitude.Should().Be(40.01); + envelope.Coordinates.Last().Longitude.Should().Be(-71.12); + + GeoWKTWriter.Write(shape).Should().Be(wkt); + } + + [U] + public void WriteEnvelopeIgnoresZValues() + { + var envelope = new EnvelopeGeoShape(new [] + { + new GeoCoordinate(40.73, -74.1, 3), + new GeoCoordinate(40.01, -71.12, 2) + }); + + GeoWKTWriter.Write(envelope).Should().Be("BBOX (-74.1, -71.12, 40.73, 40.01)"); + } + + [U] + public void ReadAndWriteGeometryCollection() + { + var wkt = "GEOMETRYCOLLECTION (POINT (100 0), LINESTRING (101 0, 102 1))"; + var shape = GeoWKTReader.Read(wkt); + var geometryCollection = shape as IGeometryCollection; + + geometryCollection.Should().NotBeNull(); + geometryCollection.Geometries.Should().HaveCount(2); + geometryCollection.Geometries.First().Should().BeOfType(); + geometryCollection.Geometries.Last().Should().BeOfType(); + + GeoWKTWriter.Write(geometryCollection).Should().Be(wkt); + } + + [U] + public void UnknownGeometryThrowsGeoWKTException() + { + var wkt = "UNKNOWN (100 0)"; + Action action = () => GeoWKTReader.Read(wkt); + action.ShouldThrow().Which.Message.Should().Be("Unknown geometry type: UNKNOWN"); + } + + [U] + public void MalformedPolygonThrowsGeoWKTException() + { + var wkt = "POLYGON ((100, 5) (100, 10) (90, 10), (90, 5), (100, 5)"; + Action action = () => GeoWKTReader.Read(wkt); + action.ShouldThrow().Which.Message.Should().Be("Expected number but found: , at line 1, position 14"); + } + + [U] + public void GeoWKTExceptionReturnsCorrectLineNumberAndPosition() + { + var wkt = @"POLYGON ( + (100, 5) + (100, 10) + (90, 10), + (90, 5), + (100, 5)"; + Action action = () => GeoWKTReader.Read(wkt); + action.ShouldThrow().Which.Message.Should().Be("Expected number but found: , at line 2, position 14"); + } + + } +} diff --git a/src/Tests/QueryDsl/Geo/Shape/Circle/GeoShapeCircleQueryUsageTests.cs b/src/Tests/Tests/QueryDsl/Geo/Shape/Circle/GeoShapeCircleQueryUsageTests.cs similarity index 93% rename from src/Tests/QueryDsl/Geo/Shape/Circle/GeoShapeCircleQueryUsageTests.cs rename to src/Tests/Tests/QueryDsl/Geo/Shape/Circle/GeoShapeCircleQueryUsageTests.cs index f4d20e13ccd..778276f4a92 100644 --- a/src/Tests/QueryDsl/Geo/Shape/Circle/GeoShapeCircleQueryUsageTests.cs +++ b/src/Tests/Tests/QueryDsl/Geo/Shape/Circle/GeoShapeCircleQueryUsageTests.cs @@ -1,7 +1,7 @@ using Nest; +using Tests.Core.ManagedElasticsearch.Clusters; using Tests.Framework.Integration; -using Tests.Framework.ManagedElasticsearch.Clusters; -using Tests.Framework.MockData; +using Tests.Domain; using static Nest.Infer; namespace Tests.QueryDsl.Geo.Shape.Circle diff --git a/src/Tests/QueryDsl/Geo/Shape/Envelope/GeoShapeEnvelopeQueryUsageTests.cs b/src/Tests/Tests/QueryDsl/Geo/Shape/Envelope/GeoShapeEnvelopeQueryUsageTests.cs similarity index 94% rename from src/Tests/QueryDsl/Geo/Shape/Envelope/GeoShapeEnvelopeQueryUsageTests.cs rename to src/Tests/Tests/QueryDsl/Geo/Shape/Envelope/GeoShapeEnvelopeQueryUsageTests.cs index b2d5956d42c..2d2c4ff8a6f 100644 --- a/src/Tests/QueryDsl/Geo/Shape/Envelope/GeoShapeEnvelopeQueryUsageTests.cs +++ b/src/Tests/Tests/QueryDsl/Geo/Shape/Envelope/GeoShapeEnvelopeQueryUsageTests.cs @@ -1,8 +1,8 @@ using System.Collections.Generic; using Nest; +using Tests.Core.ManagedElasticsearch.Clusters; +using Tests.Domain; using Tests.Framework.Integration; -using Tests.Framework.ManagedElasticsearch.Clusters; -using Tests.Framework.MockData; using static Nest.Infer; namespace Tests.QueryDsl.Geo.Shape.Envelope diff --git a/src/Tests/QueryDsl/Geo/Shape/GeoShapeQueryUsageTestsBase.cs b/src/Tests/Tests/QueryDsl/Geo/Shape/GeoShapeQueryUsageTestsBase.cs similarity index 77% rename from src/Tests/QueryDsl/Geo/Shape/GeoShapeQueryUsageTestsBase.cs rename to src/Tests/Tests/QueryDsl/Geo/Shape/GeoShapeQueryUsageTestsBase.cs index fc7dde8b5a0..91386c02a0b 100644 --- a/src/Tests/QueryDsl/Geo/Shape/GeoShapeQueryUsageTestsBase.cs +++ b/src/Tests/Tests/QueryDsl/Geo/Shape/GeoShapeQueryUsageTestsBase.cs @@ -1,8 +1,7 @@ -using Tests.Framework.Integration; -using Tests.Framework.ManagedElasticsearch.Clusters; -using Xunit; +using Tests.Core.ManagedElasticsearch.Clusters; +using Tests.Framework.Integration; -namespace Tests.QueryDsl +namespace Tests.QueryDsl.Geo.Shape { public abstract class GeoShapeQueryUsageTestsBase : QueryDslUsageTestsBase { diff --git a/src/Tests/QueryDsl/Geo/Shape/GeometryCollection/GeoShapeGeometryCollectionQueryUsageTests.cs b/src/Tests/Tests/QueryDsl/Geo/Shape/GeometryCollection/GeoShapeGeometryCollectionQueryUsageTests.cs similarity index 98% rename from src/Tests/QueryDsl/Geo/Shape/GeometryCollection/GeoShapeGeometryCollectionQueryUsageTests.cs rename to src/Tests/Tests/QueryDsl/Geo/Shape/GeometryCollection/GeoShapeGeometryCollectionQueryUsageTests.cs index 67032be5a36..2975cd7cdd5 100644 --- a/src/Tests/QueryDsl/Geo/Shape/GeometryCollection/GeoShapeGeometryCollectionQueryUsageTests.cs +++ b/src/Tests/Tests/QueryDsl/Geo/Shape/GeometryCollection/GeoShapeGeometryCollectionQueryUsageTests.cs @@ -1,8 +1,8 @@ using System.Collections.Generic; using Nest; +using Tests.Core.ManagedElasticsearch.Clusters; +using Tests.Domain; using Tests.Framework.Integration; -using Tests.Framework.ManagedElasticsearch.Clusters; -using Tests.Framework.MockData; namespace Tests.QueryDsl.Geo.Shape.GeometryCollection { diff --git a/src/Tests/QueryDsl/Geo/Shape/IndexedShape/GeoShapeIndexedShapeQueryUsageTests.cs b/src/Tests/Tests/QueryDsl/Geo/Shape/IndexedShape/GeoShapeIndexedShapeQueryUsageTests.cs similarity index 96% rename from src/Tests/QueryDsl/Geo/Shape/IndexedShape/GeoShapeIndexedShapeQueryUsageTests.cs rename to src/Tests/Tests/QueryDsl/Geo/Shape/IndexedShape/GeoShapeIndexedShapeQueryUsageTests.cs index 7a0e169cd01..7d83b749e07 100644 --- a/src/Tests/QueryDsl/Geo/Shape/IndexedShape/GeoShapeIndexedShapeQueryUsageTests.cs +++ b/src/Tests/Tests/QueryDsl/Geo/Shape/IndexedShape/GeoShapeIndexedShapeQueryUsageTests.cs @@ -1,7 +1,7 @@ using Nest; +using Tests.Core.ManagedElasticsearch.Clusters; +using Tests.Domain; using Tests.Framework.Integration; -using Tests.Framework.ManagedElasticsearch.Clusters; -using Tests.Framework.MockData; using static Nest.Infer; namespace Tests.QueryDsl.Geo.Shape.IndexedShape diff --git a/src/Tests/QueryDsl/Geo/Shape/LineString/GeoShapeLineStringQueryUsageTests.cs b/src/Tests/Tests/QueryDsl/Geo/Shape/LineString/GeoShapeLineStringQueryUsageTests.cs similarity index 94% rename from src/Tests/QueryDsl/Geo/Shape/LineString/GeoShapeLineStringQueryUsageTests.cs rename to src/Tests/Tests/QueryDsl/Geo/Shape/LineString/GeoShapeLineStringQueryUsageTests.cs index d8b5b234f48..ba02a0bce6e 100644 --- a/src/Tests/QueryDsl/Geo/Shape/LineString/GeoShapeLineStringQueryUsageTests.cs +++ b/src/Tests/Tests/QueryDsl/Geo/Shape/LineString/GeoShapeLineStringQueryUsageTests.cs @@ -1,8 +1,8 @@ using System.Collections.Generic; using Nest; +using Tests.Core.ManagedElasticsearch.Clusters; +using Tests.Domain; using Tests.Framework.Integration; -using Tests.Framework.ManagedElasticsearch.Clusters; -using Tests.Framework.MockData; using static Nest.Infer; namespace Tests.QueryDsl.Geo.Shape.LineString diff --git a/src/Tests/QueryDsl/Geo/Shape/MultiLineString/GeoShapeMultiLineStringQueryUsageTests.cs b/src/Tests/Tests/QueryDsl/Geo/Shape/MultiLineString/GeoShapeMultiLineStringQueryUsageTests.cs similarity index 95% rename from src/Tests/QueryDsl/Geo/Shape/MultiLineString/GeoShapeMultiLineStringQueryUsageTests.cs rename to src/Tests/Tests/QueryDsl/Geo/Shape/MultiLineString/GeoShapeMultiLineStringQueryUsageTests.cs index 8125eefb461..2d8e7c3f80c 100644 --- a/src/Tests/QueryDsl/Geo/Shape/MultiLineString/GeoShapeMultiLineStringQueryUsageTests.cs +++ b/src/Tests/Tests/QueryDsl/Geo/Shape/MultiLineString/GeoShapeMultiLineStringQueryUsageTests.cs @@ -1,8 +1,8 @@ using System.Collections.Generic; using Nest; +using Tests.Core.ManagedElasticsearch.Clusters; +using Tests.Domain; using Tests.Framework.Integration; -using Tests.Framework.ManagedElasticsearch.Clusters; -using Tests.Framework.MockData; using static Nest.Infer; namespace Tests.QueryDsl.Geo.Shape.MultiLineString diff --git a/src/Tests/QueryDsl/Geo/Shape/MultiPoint/GeoShapeMultiPointQueryUsageTests.cs b/src/Tests/Tests/QueryDsl/Geo/Shape/MultiPoint/GeoShapeMultiPointQueryUsageTests.cs similarity index 94% rename from src/Tests/QueryDsl/Geo/Shape/MultiPoint/GeoShapeMultiPointQueryUsageTests.cs rename to src/Tests/Tests/QueryDsl/Geo/Shape/MultiPoint/GeoShapeMultiPointQueryUsageTests.cs index 34445a63091..cab167212b8 100644 --- a/src/Tests/QueryDsl/Geo/Shape/MultiPoint/GeoShapeMultiPointQueryUsageTests.cs +++ b/src/Tests/Tests/QueryDsl/Geo/Shape/MultiPoint/GeoShapeMultiPointQueryUsageTests.cs @@ -1,8 +1,8 @@ using System.Collections.Generic; using Nest; +using Tests.Core.ManagedElasticsearch.Clusters; +using Tests.Domain; using Tests.Framework.Integration; -using Tests.Framework.ManagedElasticsearch.Clusters; -using Tests.Framework.MockData; using static Nest.Infer; namespace Tests.QueryDsl.Geo.Shape.MultiPoint diff --git a/src/Tests/QueryDsl/Geo/Shape/MultiPolygon/GeoShapeMultiPolygonQueryUsageTests.cs b/src/Tests/Tests/QueryDsl/Geo/Shape/MultiPolygon/GeoShapeMultiPolygonQueryUsageTests.cs similarity index 95% rename from src/Tests/QueryDsl/Geo/Shape/MultiPolygon/GeoShapeMultiPolygonQueryUsageTests.cs rename to src/Tests/Tests/QueryDsl/Geo/Shape/MultiPolygon/GeoShapeMultiPolygonQueryUsageTests.cs index 8726e7e67cc..a39478bb20d 100644 --- a/src/Tests/QueryDsl/Geo/Shape/MultiPolygon/GeoShapeMultiPolygonQueryUsageTests.cs +++ b/src/Tests/Tests/QueryDsl/Geo/Shape/MultiPolygon/GeoShapeMultiPolygonQueryUsageTests.cs @@ -1,8 +1,8 @@ using System.Collections.Generic; using Nest; +using Tests.Core.ManagedElasticsearch.Clusters; +using Tests.Domain; using Tests.Framework.Integration; -using Tests.Framework.ManagedElasticsearch.Clusters; -using Tests.Framework.MockData; using static Nest.Infer; namespace Tests.QueryDsl.Geo.Shape.MultiPolygon diff --git a/src/Tests/QueryDsl/Geo/Shape/Point/GeoShapePointQueryUsageTests.cs b/src/Tests/Tests/QueryDsl/Geo/Shape/Point/GeoShapePointQueryUsageTests.cs similarity index 93% rename from src/Tests/QueryDsl/Geo/Shape/Point/GeoShapePointQueryUsageTests.cs rename to src/Tests/Tests/QueryDsl/Geo/Shape/Point/GeoShapePointQueryUsageTests.cs index 29914011347..0ea7bfbb8c7 100644 --- a/src/Tests/QueryDsl/Geo/Shape/Point/GeoShapePointQueryUsageTests.cs +++ b/src/Tests/Tests/QueryDsl/Geo/Shape/Point/GeoShapePointQueryUsageTests.cs @@ -1,7 +1,7 @@ using Nest; +using Tests.Core.ManagedElasticsearch.Clusters; +using Tests.Domain; using Tests.Framework.Integration; -using Tests.Framework.ManagedElasticsearch.Clusters; -using Tests.Framework.MockData; using static Nest.Infer; namespace Tests.QueryDsl.Geo.Shape.Point diff --git a/src/Tests/QueryDsl/Geo/Shape/Polygon/GeoShapePolygonQueryUsageTests.cs b/src/Tests/Tests/QueryDsl/Geo/Shape/Polygon/GeoShapePolygonQueryUsageTests.cs similarity index 95% rename from src/Tests/QueryDsl/Geo/Shape/Polygon/GeoShapePolygonQueryUsageTests.cs rename to src/Tests/Tests/QueryDsl/Geo/Shape/Polygon/GeoShapePolygonQueryUsageTests.cs index 3c7ae2021c8..9226688444e 100644 --- a/src/Tests/QueryDsl/Geo/Shape/Polygon/GeoShapePolygonQueryUsageTests.cs +++ b/src/Tests/Tests/QueryDsl/Geo/Shape/Polygon/GeoShapePolygonQueryUsageTests.cs @@ -1,8 +1,8 @@ using System.Collections.Generic; using Nest; +using Tests.Core.ManagedElasticsearch.Clusters; +using Tests.Domain; using Tests.Framework.Integration; -using Tests.Framework.ManagedElasticsearch.Clusters; -using Tests.Framework.MockData; using static Nest.Infer; namespace Tests.QueryDsl.Geo.Shape.Polygon diff --git a/src/Tests/Tests/Tests.csproj b/src/Tests/Tests/Tests.csproj index 3c7b3eaab00..9c8ea43f139 100644 --- a/src/Tests/Tests/Tests.csproj +++ b/src/Tests/Tests/Tests.csproj @@ -7,7 +7,7 @@ $(NoWarn);xUnit1013 - +