diff --git a/src/Microsoft.Spatial/DataServicesSpatialImplementation.cs b/src/Microsoft.Spatial/DataServicesSpatialImplementation.cs index 14647178ee..e23ad5066f 100644 --- a/src/Microsoft.Spatial/DataServicesSpatialImplementation.cs +++ b/src/Microsoft.Spatial/DataServicesSpatialImplementation.cs @@ -82,5 +82,13 @@ public override SpatialPipeline CreateValidator() { return new ForwardingSegment(new SpatialValidatorImplementation()); } + + /// Creates a WellKnownBinaryFormatter for this implementation. + /// The WellKnownBinaryFormatter created. + /// Controls the writing settings. + public override WellKnownBinaryFormatter CreateWellKnownBinaryFormatter(WellKnownBinaryWriterSettings settings) + { + return new WellKnownBinaryFormatterImplementation(this, settings); + } } } diff --git a/src/Microsoft.Spatial/PublicAPI/net8.0/PublicAPI.Unshipped.txt b/src/Microsoft.Spatial/PublicAPI/net8.0/PublicAPI.Unshipped.txt index 5f282702bb..5833399c72 100644 --- a/src/Microsoft.Spatial/PublicAPI/net8.0/PublicAPI.Unshipped.txt +++ b/src/Microsoft.Spatial/PublicAPI/net8.0/PublicAPI.Unshipped.txt @@ -1 +1,19 @@ - \ No newline at end of file +abstract Microsoft.Spatial.SpatialImplementation.CreateWellKnownBinaryFormatter(Microsoft.Spatial.WellKnownBinaryWriterSettings settings) -> Microsoft.Spatial.WellKnownBinaryFormatter +Microsoft.Spatial.ByteOrder +Microsoft.Spatial.ByteOrder.BigEndian = 0 -> Microsoft.Spatial.ByteOrder +Microsoft.Spatial.ByteOrder.LittleEndian = 1 -> Microsoft.Spatial.ByteOrder +Microsoft.Spatial.WellKnownBinaryFormatter +Microsoft.Spatial.WellKnownBinaryFormatter.WellKnownBinaryFormatter(Microsoft.Spatial.SpatialImplementation creator) -> void +Microsoft.Spatial.WellKnownBinaryWriterSettings +Microsoft.Spatial.WellKnownBinaryWriterSettings.HandleM.get -> bool +Microsoft.Spatial.WellKnownBinaryWriterSettings.HandleM.set -> void +Microsoft.Spatial.WellKnownBinaryWriterSettings.HandleSRID.get -> bool +Microsoft.Spatial.WellKnownBinaryWriterSettings.HandleSRID.set -> void +Microsoft.Spatial.WellKnownBinaryWriterSettings.HandleZ.get -> bool +Microsoft.Spatial.WellKnownBinaryWriterSettings.HandleZ.set -> void +Microsoft.Spatial.WellKnownBinaryWriterSettings.IsoWKB.get -> bool +Microsoft.Spatial.WellKnownBinaryWriterSettings.IsoWKB.set -> void +Microsoft.Spatial.WellKnownBinaryWriterSettings.Order.get -> Microsoft.Spatial.ByteOrder +Microsoft.Spatial.WellKnownBinaryWriterSettings.Order.set -> void +Microsoft.Spatial.WellKnownBinaryWriterSettings.WellKnownBinaryWriterSettings() -> void +static Microsoft.Spatial.WellKnownBinaryFormatter.Create(Microsoft.Spatial.WellKnownBinaryWriterSettings settings) -> Microsoft.Spatial.WellKnownBinaryFormatter \ No newline at end of file diff --git a/src/Microsoft.Spatial/SRResources.Designer.cs b/src/Microsoft.Spatial/SRResources.Designer.cs index 9172bd5ec7..6c884896f5 100644 --- a/src/Microsoft.Spatial/SRResources.Designer.cs +++ b/src/Microsoft.Spatial/SRResources.Designer.cs @@ -402,6 +402,96 @@ internal static string Validator_UnexpectedGeometry { } } + /// + /// Looks up a localized string similar to The WKB reader is reading a "{0}" value with "{1}" bytes expected but only get "{2}" bytes.. + /// + internal static string WellKnownBinary_ByteLengthNotEnough { + get { + return ResourceManager.GetString("WellKnownBinary_ByteLengthNotEnough", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The WKB writer: Invalid to AddLineTo for "{0}".{1}. + /// + internal static string WellKnownBinary_InvalidAddLineTo { + get { + return ResourceManager.GetString("WellKnownBinary_InvalidAddLineTo", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The WKB writer: Invalid to begin a figure for spatial type "{0}".. + /// + internal static string WellKnownBinary_InvalidBeginFigureOnSpatial { + get { + return ResourceManager.GetString("WellKnownBinary_InvalidBeginFigureOnSpatial", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The WKB writer: Invalid to begin a new figure "{0}" without ending the previou figure.. + /// + internal static string WellKnownBinary_InvalidBeginFigureWithoutEndingPrevious { + get { + return ResourceManager.GetString("WellKnownBinary_InvalidBeginFigureWithoutEndingPrevious", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The WKB writer: Invalid to "{0}" without specify the spatial type. Please call BeginGeometry(), or beginGeography() first.. + /// + internal static string WellKnownBinary_InvalidBeginOrEndFigureOrAddLine { + get { + return ResourceManager.GetString("WellKnownBinary_InvalidBeginOrEndFigureOrAddLine", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The WKB writer: Invalid to end figure on "{0}".{1}. + /// + internal static string WellKnownBinary_InvalidEndFigure { + get { + return ResourceManager.GetString("WellKnownBinary_InvalidEndFigure", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The WKB writer: Invalid to end spatial type "{0}".{1}. + /// + internal static string WellKnownBinary_InvalidEndGeo { + get { + return ResourceManager.GetString("WellKnownBinary_InvalidEndGeo", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The WKB writer: Invalid to begin a "{0}" under "{1}", Details: "{2}".. + /// + internal static string WellKnownBinary_InvalidSubSpatial { + get { + return ResourceManager.GetString("WellKnownBinary_InvalidSubSpatial", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The WKB writer: Spatial type "{0}" is not supported.. + /// + internal static string WellKnownBinary_NotSupportedSpatial { + get { + return ResourceManager.GetString("WellKnownBinary_NotSupportedSpatial", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The WKB reader: the byte order '{0}' is unknown. It should be 0x00 (BigEndian) and 0x01 (LittleEndian).. + /// + internal static string WellKnownBinary_UnknownByteOrder { + get { + return ResourceManager.GetString("WellKnownBinary_UnknownByteOrder", resourceCulture); + } + } + /// /// Looks up a localized string similar to The WellKnownTextReader is configured to allow only two dimensions, and a third dimension was encountered.. /// diff --git a/src/Microsoft.Spatial/SRResources.resx b/src/Microsoft.Spatial/SRResources.resx index e3834447f2..b9f47a8bfa 100644 --- a/src/Microsoft.Spatial/SRResources.resx +++ b/src/Microsoft.Spatial/SRResources.resx @@ -149,6 +149,36 @@ The WellKnownTextReader is configured to allow only two dimensions, and a third dimension was encountered. + + The WKB writer: Spatial type "{0}" is not supported. + + + The WKB writer: Invalid to begin a "{0}" under "{1}", Details: "{2}". + + + The WKB writer: Invalid to "{0}" without specify the spatial type. Please call BeginGeometry(), or beginGeography() first. + + + The WKB writer: Invalid to begin a figure for spatial type "{0}". + + + The WKB writer: Invalid to begin a new figure "{0}" without ending the previou figure. + + + The WKB writer: Invalid to AddLineTo for "{0}".{1} + + + The WKB writer: Invalid to end figure on "{0}".{1} + + + The WKB writer: Invalid to end spatial type "{0}".{1} + + + The WKB reader is reading a "{0}" value with "{1}" bytes expected but only get "{2}" bytes. + + + The WKB reader: the byte order '{0}' is unknown. It should be 0x00 (BigEndian) and 0x01 (LittleEndian). + Invalid spatial data: An instance of spatial type can have only one unique CoordinateSystem for all of its coordinates. diff --git a/src/Microsoft.Spatial/Spatial/SpatialImplementation.cs b/src/Microsoft.Spatial/Spatial/SpatialImplementation.cs index de2cda246e..9732f3b181 100644 --- a/src/Microsoft.Spatial/Spatial/SpatialImplementation.cs +++ b/src/Microsoft.Spatial/Spatial/SpatialImplementation.cs @@ -61,6 +61,11 @@ public abstract SpatialOperations Operations /// Controls the writing and reading of the Z and M dimension. public abstract WellKnownTextSqlFormatter CreateWellKnownTextSqlFormatter(bool allowOnlyTwoDimensions); + /// Creates a WellKnownBinaryFormatter for this implementation. + /// The WellKnownBinaryFormatter created. + /// Controls the writing settings. + public abstract WellKnownBinaryFormatter CreateWellKnownBinaryFormatter(WellKnownBinaryWriterSettings settings); + /// Creates a spatial Validator. /// The SpatialValidator created. public abstract SpatialPipeline CreateValidator(); diff --git a/src/Microsoft.Spatial/WellKnown/BinaryFormatterExtensions.cs b/src/Microsoft.Spatial/WellKnown/BinaryFormatterExtensions.cs new file mode 100644 index 0000000000..0aa65e3ef0 --- /dev/null +++ b/src/Microsoft.Spatial/WellKnown/BinaryFormatterExtensions.cs @@ -0,0 +1,147 @@ +//--------------------------------------------------------------------- +// +// Copyright (C) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information. +// +//--------------------------------------------------------------------- + +namespace Microsoft.Spatial +{ + using System; + using System.Buffers.Binary; + using System.IO; + + internal static class BinaryFormatterExtensions + { + /// + /// Writes the double value based on the byte order setting. + /// + /// The binary writer. + /// The double value. + /// The byte order. + public static void Write(this BinaryWriter writer, double value, ByteOrder order) + { + Span buffer = stackalloc byte[8]; + if (order == ByteOrder.LittleEndian) + { + BinaryPrimitives.WriteDoubleLittleEndian(buffer, value); + } + else + { + BinaryPrimitives.WriteDoubleBigEndian(buffer, value); + } + + writer.Write(buffer); + } + + /// + /// Writes the uint value based on the byte order setting. + /// + /// The binary writer. + /// The uint value. + /// The byte order. + public static void Write(this BinaryWriter writer, uint value, ByteOrder order) + { + Span buffer = stackalloc byte[4]; + if (order == ByteOrder.LittleEndian) + { + BinaryPrimitives.WriteUInt32LittleEndian(buffer, value); + } + else + { + BinaryPrimitives.WriteUInt32BigEndian(buffer, value); + } + + writer.Write(buffer); + } + + /// + /// Writes the int value based on the byte order setting. + /// + /// The binary writer. + /// The int value. + /// The byte order. + public static void Write(this BinaryWriter writer, int value, ByteOrder order) + { + Span buffer = stackalloc byte[4]; + if (order == ByteOrder.LittleEndian) + { + BinaryPrimitives.WriteInt32LittleEndian(buffer, value); + } + else + { + BinaryPrimitives.WriteInt32BigEndian(buffer, value); + } + + writer.Write(buffer); + } + + /// + /// Reads the uint value from reader based on the byte order setting. + /// + /// The binary reader. + /// The byte order. + /// The uint value read from the reader. + public static uint ReadUInt32(this BinaryReader reader, ByteOrder order) + { + Span buffer = stackalloc byte[4]; + int num = reader.Read(buffer); + if (num != 4) + { + throw new FormatException(Error.Format(SRResources.WellKnownBinary_ByteLengthNotEnough, "UInt32", 4, num)); + } + + if (order == ByteOrder.LittleEndian) + { + return BinaryPrimitives.ReadUInt32LittleEndian(buffer); + } + + return BinaryPrimitives.ReadUInt32BigEndian(buffer); + } + + /// + /// Reads the int value from reader based on the byte order setting. + /// + /// The binary reader. + /// The byte order. + /// The int value read from the reader. + public static int ReadInt32(this BinaryReader reader, ByteOrder order) + { + Span buffer = stackalloc byte[4]; + int num = reader.Read(buffer); + if (num != 4) + { + throw new FormatException(Error.Format(SRResources.WellKnownBinary_ByteLengthNotEnough, "Int32", 4, num)); + } + + if (order == ByteOrder.LittleEndian) + { + return BinaryPrimitives.ReadInt32LittleEndian(buffer); + } + + return BinaryPrimitives.ReadInt32BigEndian(buffer); + } + + /// + /// Reads the double value from reader based on the byte order setting. + /// + /// The binary reader. + /// The byte order. + /// The double value read from the reader. + public static double ReadDouble(this BinaryReader reader, ByteOrder order) + { + Span buffer = stackalloc byte[8]; + int num = reader.Read(buffer); + if (num != 8) + { + throw new FormatException(Error.Format(SRResources.WellKnownBinary_ByteLengthNotEnough, "Double", 8, num)); + } + + if (order == ByteOrder.LittleEndian) + { + return BinaryPrimitives.ReadDoubleLittleEndian(buffer); + } + + return BinaryPrimitives.ReadDoubleBigEndian(buffer); + } + } +} diff --git a/src/Microsoft.Spatial/WellKnown/ByteOrder.cs b/src/Microsoft.Spatial/WellKnown/ByteOrder.cs new file mode 100644 index 0000000000..d174ed9092 --- /dev/null +++ b/src/Microsoft.Spatial/WellKnown/ByteOrder.cs @@ -0,0 +1,24 @@ +//--------------------------------------------------------------------- +// +// Copyright (C) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information. +// +//--------------------------------------------------------------------- + +namespace Microsoft.Spatial +{ + /// + /// Byte order + /// + public enum ByteOrder + { + /// + /// Big Endian + /// + BigEndian = 0x00, + + /// + /// Little Endian + /// + LittleEndian = 0x01, + } +} diff --git a/src/Microsoft.Spatial/WellKnown/WellKnownBinaryFormatter.cs b/src/Microsoft.Spatial/WellKnown/WellKnownBinaryFormatter.cs new file mode 100644 index 0000000000..2cb6a8c081 --- /dev/null +++ b/src/Microsoft.Spatial/WellKnown/WellKnownBinaryFormatter.cs @@ -0,0 +1,32 @@ +//--------------------------------------------------------------------- +// +// Copyright (C) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information. +// +//--------------------------------------------------------------------- + +namespace Microsoft.Spatial +{ + using System.IO; + + /// + /// The object to move spatial types to and from the WellKnownBinary format + /// + public abstract class WellKnownBinaryFormatter : SpatialFormatter + { + /// + /// Initializes a new instance of the class. + /// + /// The implementation that created this instance. + protected WellKnownBinaryFormatter(SpatialImplementation creator) + : base(creator) + { + } + + /// + /// Creates the implementation of the formatter. + /// + /// Returns the created implementation. + public static WellKnownBinaryFormatter Create(WellKnownBinaryWriterSettings settings) + => SpatialImplementation.CurrentImplementation.CreateWellKnownBinaryFormatter(settings); + } +} diff --git a/src/Microsoft.Spatial/WellKnown/WellKnownBinaryFormatterImplementation.cs b/src/Microsoft.Spatial/WellKnown/WellKnownBinaryFormatterImplementation.cs new file mode 100644 index 0000000000..885d6f3c4a --- /dev/null +++ b/src/Microsoft.Spatial/WellKnown/WellKnownBinaryFormatterImplementation.cs @@ -0,0 +1,63 @@ +//--------------------------------------------------------------------- +// +// Copyright (C) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information. +// +//--------------------------------------------------------------------- + +namespace Microsoft.Spatial +{ + using System; + using System.IO; + + /// + /// The object to move spatial types to and from the WellKnownBinary (WKB) formatter. + /// + internal class WellKnownBinaryFormatterImplementation : WellKnownBinaryFormatter + { + /// + /// Initializes a new instance of the class. + /// + /// The implementation that created this instance. + /// The setting for WKB writering. + internal WellKnownBinaryFormatterImplementation(SpatialImplementation creator, WellKnownBinaryWriterSettings settings) + : base(creator) + { + Settings = settings ?? throw new ArgumentNullException(nameof(settings)); + } + + /// + /// Gets the WKB writer settings. + /// + protected WellKnownBinaryWriterSettings Settings { get; } + + /// + /// Create the writer + /// + /// The wrtier stream. + /// A writer that implements ISpatialPipeline. + public override SpatialPipeline CreateWriter(Stream writerStream) + { + return new ForwardingSegment(new WellKnownBinaryWriter(writerStream, Settings)); + } + + /// + /// Reads the geography. + /// + /// The reader stream. + /// The pipeline. + protected override void ReadGeography(Stream readerStream, SpatialPipeline pipeline) + { + new WellKnownBinaryReader(pipeline).ReadGeography(readerStream); + } + + /// + /// Reads the geometry. + /// + /// The reader stream. + /// The pipeline. + protected override void ReadGeometry(Stream readerStream, SpatialPipeline pipeline) + { + new WellKnownBinaryReader(pipeline).ReadGeometry(readerStream); + } + } +} diff --git a/src/Microsoft.Spatial/WellKnown/WellKnownBinaryReader.cs b/src/Microsoft.Spatial/WellKnown/WellKnownBinaryReader.cs new file mode 100644 index 0000000000..5cbacffc42 --- /dev/null +++ b/src/Microsoft.Spatial/WellKnown/WellKnownBinaryReader.cs @@ -0,0 +1,364 @@ +//--------------------------------------------------------------------- +// +// Copyright (C) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information. +// +//--------------------------------------------------------------------- + +namespace Microsoft.Spatial +{ + using System; + using System.Diagnostics; + using System.IO; + + /// + /// Reads and Converts a Well-Known Binary (WKB) byte data to a spatial data. + /// It supports Extended WKB format meanwhile, for compatiblilty, suppport ISO WKB formatter. + /// + internal class WellKnownBinaryReader : SpatialReader + { + /// + /// Initializes a new instance of the class. + /// + /// The spatial pipelien destination. + public WellKnownBinaryReader(SpatialPipeline destination) + : base(destination) + { + } + + /// + /// Parses some serialized format that represents one or more Geography spatial values, passing the first one down the pipeline. + /// + /// The input stream. + protected override void ReadGeographyImplementation(Stream input) + { + // Geography in WKB has lat/long reversed, should be y (long), x (lat), z, m + TypeWashedPipeline pipeline = new TypeWashedToGeographyLongLatPipeline(Destination); + Read(pipeline, input, CoordinateSystem.DefaultGeography); + } + + /// + /// Parses some serialized format that represents one or more Geometry spatial values, passing the first one down the pipeline. + /// + /// The input stream. + protected override void ReadGeometryImplementation(Stream input) + { + TypeWashedPipeline pipeline = new TypeWashedToGeometryPipeline(Destination); + Read(pipeline, input, CoordinateSystem.DefaultGeometry); + } + + /// + /// Reads a or in binary WKB format from an . + /// + /// The spatial pipeline to read from. + /// The stream to read from. + /// The default coordinate. + private static void Read(TypeWashedPipeline pipeline, Stream stream, CoordinateSystem defaultCoordinate) + { + Debug.Assert(pipeline != null); + + using (var reader = new BinaryReader(stream)) + { + try + { + ReadSpatial(pipeline, reader, defaultCoordinate); + } + catch (FormatException ex) + { + throw ex; + } + catch (Exception ex) + { + throw new FormatException($"Reading the spatial type failed, see details in inner exception.", ex); + } + } + } + + private static void ReadSpatial(TypeWashedPipeline pipeline, BinaryReader reader, CoordinateSystem defaultCoordinate) + { + ByteOrder byteOrder = ReadByteOrder(reader); + Header header = ReadHeader(reader, byteOrder); + if (header.Srid >= 0) + { + pipeline.SetCoordinateSystem(header.Srid); + } + else if (defaultCoordinate != null) + { + pipeline.SetCoordinateSystem(defaultCoordinate?.EpsgId); + } + + header.ByteOrder = byteOrder; + switch (header.Type) + { + case SpatialType.Point: + ReadPoint(pipeline, reader, header); + break; + + case SpatialType.LineString: + ReadLineString(pipeline, reader, header, true); + break; + + case SpatialType.Polygon: + ReadPolygon(pipeline, reader, header); + break; + + case SpatialType.MultiPoint: + ReadMultiPoint(pipeline, reader, header); + break; + + case SpatialType.MultiLineString: + ReadMultiLineString(pipeline, reader, header); + break; + + case SpatialType.MultiPolygon: + ReadMultiPolygon(pipeline, reader, header); + break; + + case SpatialType.Collection: + ReadCollection(pipeline, reader, header); + break; + + default: + throw new FormatException($"Spatial type {header.Type} not recognized."); + } + } + + private static void ReadPoint(TypeWashedPipeline pipeline, BinaryReader reader, Header header) + { + (double x, double y, double? z, double? m) = ReadPoint(reader, header); + + pipeline.BeginGeo(SpatialType.Point); + + if (!IsEmptyPoint(x, y)) + { + pipeline.BeginFigure(x, y, z, m); + pipeline.EndFigure(); + } + + pipeline.EndGeo(); + } + + private static void ReadLineString(TypeWashedPipeline pipeline, BinaryReader reader, Header header, bool hasGeo = true) + { + // Read the num of points in the LineString + int num = reader.ReadInt32(header.ByteOrder); + + if (hasGeo) + { + pipeline.BeginGeo(SpatialType.LineString); + } + + if (num > 0) + { + for (int i = 0; i < num; i++) + { + (double x, double y, double? z, double? m) = ReadPoint(reader, header); + if (i == 0) + { + pipeline.BeginFigure(x, y, z, m); + } + else + { + pipeline.LineTo(x, y, z, m); + } + } + + pipeline.EndFigure(); + } + + if (hasGeo) + { + pipeline.EndGeo(); + } + } + + private static void ReadPolygon(TypeWashedPipeline pipeline, BinaryReader reader, Header header) + { + // Read the num of Rings in Polygon + int num = reader.ReadInt32(header.ByteOrder); + + pipeline.BeginGeo(SpatialType.Polygon); + + for (int i = 0; i < num; i++) + { + ReadLineString(pipeline, reader, header, false); + } + + pipeline.EndGeo(); + } + + private static void ReadMultiPoint(TypeWashedPipeline pipeline, BinaryReader reader, Header header) + { + // read the number of the points + int num = reader.ReadInt32(header.ByteOrder); + + pipeline.BeginGeo(SpatialType.MultiPoint); + + for (int i = 0; i < num; i++) + { + ReadSpatial(pipeline, reader, null); + } + + pipeline.EndGeo(); + } + + private static void ReadMultiLineString(TypeWashedPipeline pipeline, BinaryReader reader, Header header) + { + // read the number of the LineStrings + int num = reader.ReadInt32(header.ByteOrder); + + pipeline.BeginGeo(SpatialType.MultiLineString); + + for (int i = 0; i < num; i++) + { + ReadSpatial(pipeline, reader, null); + } + + pipeline.EndGeo(); + } + + private static void ReadMultiPolygon(TypeWashedPipeline pipeline, BinaryReader reader, Header header) + { + // read the number of the Polygon + int num = reader.ReadInt32(header.ByteOrder); + + pipeline.BeginGeo(SpatialType.MultiPolygon); + + for (int i = 0; i < num; i++) + { + ReadSpatial(pipeline, reader, null); + } + + pipeline.EndGeo(); + } + + private static void ReadCollection(TypeWashedPipeline pipeline, BinaryReader reader, Header header) + { + // read the number of the items in the collection + int num = reader.ReadInt32(header.ByteOrder); + + pipeline.BeginGeo(SpatialType.Collection); + + for (int i = 0; i < num; i++) + { + ReadSpatial(pipeline, reader, null); + } + + pipeline.EndGeo(); + } + + /// + /// Function to read a coordinate sequence. + /// + /// The reader. + /// The header information. + /// The read coordinate. + private static (double, double, double?, double?) ReadPoint(BinaryReader reader, Header header) + { + double x = reader.ReadDouble(header.ByteOrder); + double y = reader.ReadDouble(header.ByteOrder); + + double? z = null; + if (header.HasZ) + { + double zV = reader.ReadDouble(header.ByteOrder); + if (!double.IsNaN(zV)) + { + z = zV; + } + } + + double? m = null; + if (header.HasM) + { + double mV = reader.ReadDouble(header.ByteOrder); + if (!double.IsNaN(mV)) + { + m = mV; + } + } + + return (x, y, z, m); + } + + private static bool IsEmptyPoint(double x, double y) + { + return double.IsNaN(x) || double.IsNaN(y); + } + + private static ByteOrder ReadByteOrder(BinaryReader reader) + { + byte byteOrder = reader.ReadByte(); + if (byteOrder == 0) + { + return ByteOrder.BigEndian; + } + else if (byteOrder == 1) + { + return ByteOrder.LittleEndian; + } + + throw new FormatException(Error.Format(SRResources.WellKnownBinary_UnknownByteOrder, byteOrder)); + } + + private static Header ReadHeader(BinaryReader reader, ByteOrder byteOrder) + { + uint type = reader.ReadUInt32(byteOrder); + + Header header = new Header(); + header.HasZ = false; + header.HasM = false; + + // Determine Z, M, SRID existed for extended WKB + if ((type & (0x80000000 | 0x40000000)) == (0x80000000 | 0x40000000)) + { + header.HasZ = true; + header.HasM = true; + } + else if ((type & 0x80000000) == 0x80000000) + { + header.HasZ = true; + } + else if ((type & 0x40000000) == 0x40000000) + { + header.HasM = true; + } + + // Has SRID + header.Srid = (type & 0x20000000) != 0 ? reader.ReadInt32(byteOrder) : -1; + + // Support TopologySuit + uint ordinate = (type & 0xffff) / 1000; + switch (ordinate) + { + case 1: + header.HasZ = true; + header.HasM = false; + break; + case 2: + header.HasZ = false; + header.HasM = true; + break; + case 3: + header.HasZ = true; + header.HasM = true; + break; + } + + header.Type = (SpatialType)((type & 0xffff) % 1000); + return header; + } + + private class Header + { + public ByteOrder ByteOrder { get; set; } + + public SpatialType Type { get; set; } + + public bool HasZ { get; set; } + + public bool HasM { get; set; } + + public int Srid { get; set; } + } + } +} diff --git a/src/Microsoft.Spatial/WellKnown/WellKnownBinaryWriter.cs b/src/Microsoft.Spatial/WellKnown/WellKnownBinaryWriter.cs new file mode 100644 index 0000000000..56cfe4fdfd --- /dev/null +++ b/src/Microsoft.Spatial/WellKnown/WellKnownBinaryWriter.cs @@ -0,0 +1,855 @@ +//--------------------------------------------------------------------- +// +// Copyright (C) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information. +// +//--------------------------------------------------------------------- + +namespace Microsoft.Spatial +{ + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.IO; + using System.Linq; + + internal sealed class WellKnownBinaryWriter : DrawBoth, IDisposable + { + /// + /// The writer settings. + /// + private WellKnownBinaryWriterSettings _settings; + + /// + /// The underlying binary writer + /// + private BinaryWriter _writer; + + /// + /// Stack of spatial types currently been built. + /// + private Stack _stack; + + /// + /// The coordinate system. + /// + private CoordinateSystem _coordinateSystem; + + /// + /// Initializes a new instance of the class using default writer settings. + /// + /// The output stream. + public WellKnownBinaryWriter(Stream stream) + : this(stream, new WellKnownBinaryWriterSettings()) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The output streamr. + /// The writer settings. + public WellKnownBinaryWriter(Stream stream, WellKnownBinaryWriterSettings settings) + { + _settings = settings ?? throw new ArgumentNullException(nameof(settings)); + _writer = new BinaryWriter(stream); + _stack = new Stack(); + Reset(); + } + + #region DrawBoth + /// + /// Draw a point in the specified coordinate + /// + /// Next position + /// + /// The position to be passed down the pipeline + /// + protected override GeographyPosition OnLineTo(GeographyPosition position) + { + this.AddLineTo(position.Longitude, position.Latitude, position.Z, position.M); + return position; + } + + /// + /// Draw a point in the specified coordinate + /// + /// Next position + /// + /// The position to be passed down the pipeline + /// + protected override GeometryPosition OnLineTo(GeometryPosition position) + { + this.AddLineTo(position.X, position.Y, position.Z, position.M); + return position; + } + + /// + /// Begin drawing a spatial object + /// + /// The spatial type of the object + /// + /// The type to be passed down the pipeline + /// + protected override SpatialType OnBeginGeography(SpatialType type) + { + BeginGeo(type); + return type; + } + + /// + /// Begin drawing a spatial object + /// + /// The spatial type of the object + /// + /// The type to be passed down the pipeline + /// + protected override SpatialType OnBeginGeometry(SpatialType type) + { + BeginGeo(type); + return type; + } + + /// + /// Begin drawing a figure + /// + /// Next position + /// The position to be passed down the pipeline + protected override GeographyPosition OnBeginFigure(GeographyPosition position) + { + BeginFigure(position.Longitude, position.Latitude, position.Z, position.M); + return position; + } + + /// + /// Begin drawing a figure + /// + /// Next position + /// The position to be passed down the pipeline + protected override GeometryPosition OnBeginFigure(GeometryPosition position) + { + BeginFigure(position.X, position.Y, position.Z, position.M); + return position; + } + + /// + /// Ends the current figure + /// + protected override void OnEndFigure() + { + EndFigure(); + } + + /// + /// Ends the current spatial object + /// + protected override void OnEndGeography() + { + EndGeo(); + } + + /// + /// Ends the current spatial object + /// + protected override void OnEndGeometry() + { + EndGeo(); + } + + /// + /// Set the coordinate system + /// + /// The CoordinateSystem + /// + /// the coordinate system to be passed down the pipeline + /// + protected override CoordinateSystem OnSetCoordinateSystem(CoordinateSystem coordinateSystem) + { + _coordinateSystem = coordinateSystem; + return coordinateSystem; + } + + /// + /// Setup the pipeline for reuse + /// + protected override void OnReset() + { + Reset(); + } + #endregion + + /// + /// Setup the pipeline for reuse + /// + private void Reset() + { + _writer.Seek(0, SeekOrigin.Begin); + _stack.Clear(); + _coordinateSystem = null; + } + + /// + /// Start to Begin a new Geography/Geometry + /// + /// The SpatialType + private void BeginGeo(SpatialType type) + { + Scope parent = _stack.Count == 0 ? null : _stack.Peek(); + + if (parent != null) + { + switch (parent.Type) + { + case SpatialType.Point: + case SpatialType.LineString: + case SpatialType.Polygon: + throw new InvalidOperationException(Error.Format(SRResources.WellKnownBinary_InvalidSubSpatial, type, parent.Type, $"{parent.Type} cannot contain other spatial types")); + + case SpatialType.MultiPoint: + if (type != SpatialType.Point) + { + throw new InvalidOperationException(Error.Format(SRResources.WellKnownBinary_InvalidSubSpatial, type, parent.Type, "MultiPoint can only contain Points.")); + } + break; + + case SpatialType.MultiLineString: + if (type != SpatialType.LineString) + { + throw new InvalidOperationException(Error.Format(SRResources.WellKnownBinary_InvalidSubSpatial, type, parent.Type, "MultiLineString can only contain LineStrings.")); + } + + break; + + case SpatialType.MultiPolygon: + if (type != SpatialType.Polygon) + { + throw new InvalidOperationException(Error.Format(SRResources.WellKnownBinary_InvalidSubSpatial, type, parent.Type, "MultiPolygon can only contain Polygons.")); + } + break; + + case SpatialType.Collection: + // Collection accepts all kind of spatial types. + break; + } + } + + int? srid = _coordinateSystem?.EpsgId; + IWKBObject current = null; + switch (type) + { + case SpatialType.Point: + current = new WKBPoint { X = double.NaN, Y = double.NaN, Z = _settings.HandleZ ? double.NaN : null, M = _settings.HandleM ? double.NaN : null }; + break; + case SpatialType.LineString: + current = new WKBLineString(); + break; + case SpatialType.Polygon: + current = new WKBPolygon(); + break; + case SpatialType.MultiPoint: + current = new WKBMultiPoint(); + break; + case SpatialType.MultiLineString: + current = new WKBMultiLineString(); + break; + case SpatialType.MultiPolygon: + current = new WKBMultiPolygon(); + break; + case SpatialType.Collection: + current = new WKBCollection(); + break; + + default: + throw new NotSupportedException(Error.Format(SRResources.WellKnownBinary_NotSupportedSpatial, type)); + } + + _stack.Push(new Scope { Type = type, Value = current }); + return; + } + + /// + /// Start to begin a figure, begin a figure should be only on 'Point, LineString, Polygon' + /// + /// The coordinate1 (X). + /// The coordinate2.(Y) + /// The coordinate3.(Z) + /// The coordinate4.(M) + private void BeginFigure(double coordinate1, double coordinate2, double? coordinate3, double? coordinate4) + { + if (_stack.Count == 0) + { + throw new InvalidOperationException(Error.Format(SRResources.WellKnownBinary_InvalidBeginOrEndFigureOrAddLine, "BeginFigure")); + } + + Debug.Assert(_stack.Count > 0, "Should have called BeginGeo"); + Scope current = _stack.Peek(); + + if (current.Type == SpatialType.MultiPoint || + current.Type == SpatialType.MultiLineString || + current.Type == SpatialType.MultiPolygon || + current.Type == SpatialType.Collection) + { + throw new InvalidOperationException(Error.Format(SRResources.WellKnownBinary_InvalidBeginFigureOnSpatial, current.Type)); + } + + if (current.IsFigureBegin) + { + throw new InvalidOperationException(Error.Format(SRResources.WellKnownBinary_InvalidBeginFigureWithoutEndingPrevious, current.Type)); + } + + int? srid = _coordinateSystem?.EpsgId; + WKBPoint point = new WKBPoint { X = coordinate1, Y = coordinate2, Z = coordinate3, M = coordinate4 }; + + if (current.Type == SpatialType.Point) + { + current.Value = point; // this one will replace the default (dummy) point created when calling BeginGeo on Point. + } + else if (current.Type == SpatialType.LineString) + { + WKBLineString wkbLineString = (WKBLineString)current.Value; + wkbLineString.Points.Add(point); + } + else if (current.Type == SpatialType.Polygon) + { + WKBPolygon wkbPg = (WKBPolygon)current.Value; + WKBLineString wkbLs = new WKBLineString(); + wkbLs.Points.Add(point); + wkbPg.Rings.Add(wkbLs); + } + else + { + // should never be here, since BeginGeo makes the correct type + Debug.Assert(false, $"BeginFigure on unknown type: {current.Type}"); + throw new InvalidOperationException($"BeginFigure on unknown type: {current.Type}"); + } + + current.IsFigureBegin = true; + } + + /// + /// Adds the control point. + /// + /// The x. + /// The y. + /// The z. + /// The m. + private void AddLineTo(double x, double y, double? z, double? m) + { + if (_stack.Count == 0) + { + throw new InvalidOperationException(Error.Format(SRResources.WellKnownBinary_InvalidBeginOrEndFigureOrAddLine, "LineTo")); + } + + Debug.Assert(_stack.Count > 0, "Should have called BeginGeo"); + Scope current = _stack.Peek(); + + int? srid = _coordinateSystem?.EpsgId; + switch (current.Type) + { + case SpatialType.LineString: + WKBLineString wkbLs = current.Value as WKBLineString; + if (wkbLs == null || wkbLs.Points.Count == 0) + { + throw new InvalidOperationException(Error.Format(SRResources.WellKnownBinary_InvalidAddLineTo, current.Type, "Should call BeginFigure first on LineString.")); + } + wkbLs.Points.Add(new WKBPoint { X = x, Y = y, Z = z, M = m }); + break; + + case SpatialType.Polygon: + WKBPolygon wkbPg = current.Value as WKBPolygon; + if (wkbPg == null || wkbPg.Rings.Count == 0) + { + throw new InvalidOperationException(Error.Format(SRResources.WellKnownBinary_InvalidAddLineTo, current.Type, "Should call BeginFigure first on Polygon.")); + } + wkbPg.Rings.Last().Points.Add(new WKBPoint { X = x, Y = y, Z = z, M = m }); + break; + + case SpatialType.Point: + case SpatialType.MultiPoint: + case SpatialType.MultiLineString: + case SpatialType.MultiPolygon: + case SpatialType.Collection: + default: + throw new InvalidOperationException(Error.Format(SRResources.WellKnownBinary_InvalidAddLineTo, current.Type, string.Empty)); + } + } + + /// + /// Ends the figure. + /// + private void EndFigure() + { + // End to write the figure. + if (_stack.Count == 0) + { + throw new InvalidOperationException(Error.Format(SRResources.WellKnownBinary_InvalidBeginOrEndFigureOrAddLine, "EndFigure")); + } + + Debug.Assert(_stack.Count > 0, "Should have called BeginGeo"); + Scope current = _stack.Peek(); + + if (current.Type == SpatialType.Point || + current.Type == SpatialType.LineString || + current.Type == SpatialType.Polygon) + { + if (!current.IsFigureBegin) + { + throw new InvalidOperationException(Error.Format(SRResources.WellKnownBinary_InvalidEndFigure, current.Type, "You haven't begun the figure.")); + } + + current.IsFigureBegin = false; // EndFigure + } + else + { + // for other spatial types (Multi*, Collection) + throw new InvalidOperationException(Error.Format(SRResources.WellKnownBinary_InvalidEndFigure, current.Type, string.Empty)); + } + } + + /// + /// End the current Geography/Geometry + /// + private void EndGeo() + { + if (_stack.Count <= 0) + { + throw new InvalidOperationException(Error.Format(SRResources.WellKnownBinary_InvalidBeginOrEndFigureOrAddLine, "EndGeo")); + } + + Scope current = _stack.Pop(); + if (_stack.Count == 0) + { + // Now, we are in the top level EndGeo, let's write it. + WriteWKB(current.Value, true); + return; + } + + Scope parent = _stack.Peek(); + switch (parent.Type) + { + case SpatialType.Point: + case SpatialType.LineString: + case SpatialType.Polygon: + throw new InvalidOperationException(Error.Format(SRResources.WellKnownBinary_InvalidEndGeo, current.Type, parent.Type)); + + case SpatialType.MultiPoint: + if (current.Type != SpatialType.Point) + { + throw new InvalidOperationException(Error.Format(SRResources.WellKnownBinary_InvalidEndGeo, current.Type, parent.Type)); + } + + ((WKBMultiPoint)parent.Value).Points.Add((WKBPoint)current.Value); + + break; + + case SpatialType.MultiLineString: + if (current.Type != SpatialType.LineString) + { + throw new InvalidOperationException(Error.Format(SRResources.WellKnownBinary_InvalidEndGeo, current.Type, parent.Type)); + } + + ((WKBMultiLineString)parent.Value).LineStrings.Add((WKBLineString)current.Value); + break; + + case SpatialType.MultiPolygon: + if (current.Type != SpatialType.Polygon) + { + throw new InvalidOperationException(Error.Format(SRResources.WellKnownBinary_InvalidEndGeo, current.Type, parent.Type)); + } + + ((WKBMultiPolygon)parent.Value).Polygons.Add((WKBPolygon)current.Value); + break; + + case SpatialType.Collection: + ((WKBCollection)parent.Value).Items.Add(current.Value); + break; + + default: + // should never be here, since BeginGeo makes the correct type + Debug.Assert(false, $"EndGeo on unknown type: {current.Type}"); + throw new InvalidOperationException($"EndGeo on unknown type: {current.Type}"); + } + } + + private void WriteWKB(IWKBObject wkbObj, bool includeSRID) + { + switch (wkbObj.SpatialType) + { + case SpatialType.Point: + Write((WKBPoint)wkbObj, includeSRID); + break; + case SpatialType.LineString: + Write((WKBLineString)wkbObj, includeSRID); + break; + case SpatialType.Polygon: + Write((WKBPolygon)wkbObj, includeSRID); + break; + case SpatialType.MultiPoint: + Write((WKBMultiPoint)wkbObj, includeSRID); + break; + case SpatialType.MultiLineString: + Write((WKBMultiLineString)wkbObj, includeSRID); + break; + case SpatialType.MultiPolygon: + Write((WKBMultiPolygon)wkbObj, includeSRID); + break; + case SpatialType.Collection: + Write((WKBCollection)wkbObj, includeSRID); + break; + default: + Debug.Assert(false, "Write unknown spatial type {wkbObj.SpatialType}"); + throw new InvalidOperationException($"Write unknown spatial type {wkbObj.SpatialType}. Should never be here"); + } + } + + /// + /// Write the WKB Header for the geometry & geography + /// + /// The spatial type. + /// A boolean value indicating to write the SRID or not. + /// + /// The Header typically contains: + /// 1 byte : Required, (0 -> Big Endian, 1 -> Little Endian) + /// 4 bytes : 32 bit unsigned integer, Required for spatial type and flags for SRID, Z and M + /// 4 bytes : 32 bit unsigned integer, Optional for SRID + /// + private void WriteHeader(SpatialType type, bool includeSRID) + { + uint intSpatialType = (uint)type & 0xff; + + if (_settings.HandleZ) + { + // compatible for ISO WKB + if (_settings.IsoWKB) + { + intSpatialType += 1000; + } + + // support Extended WKB + intSpatialType |= 0x80000000; + } + + if (_settings.HandleM) + { + // compatible for ISO WKB + if (_settings.IsoWKB) + { + intSpatialType += 2000; + } + + // support Extended WKB + intSpatialType |= 0x40000000; + } + + bool hasSRID = HasSRID(out int srid); + includeSRID &= _settings.HandleSRID; + + if (includeSRID && hasSRID) + { + intSpatialType |= 0x20000000; + } + + // Required, 1 byte for bit order + _writer.Write((byte)_settings.Order); + + // Required, 4 bytes for spatial type and flags for SRID, Z and M + _writer.Write(intSpatialType, _settings.Order); + + // Optional, 4 bytes for SRID value. + if (includeSRID && hasSRID) + { + _writer.Write(srid, _settings.Order); + } + } + + private bool HasSRID(out int srid) + { + srid = 0; + if (_coordinateSystem == null || !_coordinateSystem.EpsgId.HasValue) + { + return false; + } + + srid = _coordinateSystem.EpsgId.Value; + return true; + } + + /// + /// Point { + /// double x; + /// double y; + /// doulbe z; (optional) + /// doulbe m; (optional) + /// } + /// + /// WKBPoint { + /// byte byteOrder; + /// uint32 wkbType; // 1 + /// Point point; + /// } + /// + /// The WKBPoint. + /// A boolean value indicating to write the SRID or not. + private void Write(WKBPoint point, bool includeSRID) + { + WriteHeader(SpatialType.Point, includeSRID); + Write(point.X, point.Y, point.Z, point.M); + } + + /// + /// WKBLineString { + /// byte byteOrder; + /// uint32 wkbType; // 2 + /// uint32 numPoints; + /// Point points[numPoints]; + /// } + /// + /// The WKBLineString. + /// A boolean value indicating to write the SRID or not. + private void Write(WKBLineString lineString, bool includeSRID) + { + WriteHeader(SpatialType.LineString, includeSRID); + + // Write the number of Points in LineString + _writer.Write(lineString.Points.Count, _settings.Order); + + Write(lineString.Points, false); + } + + /// + /// LinearRing { + /// uint32 numPoints; + /// Point points[numPoints]; + /// }; + /// + /// WKBPolygon { + /// byte byteOrder; + /// uint32 wkbType; // 3 + /// uint32 numRings; + /// LinearRing rings[numRings] + /// } + /// + /// The WKBPolygon. + /// A boolean value indicating to write the SRID or not. + private void Write(WKBPolygon polygon, bool includeSRID) + { + WriteHeader(SpatialType.Polygon, includeSRID); + + // Write the number of Rings in Polygon + _writer.Write(polygon.Rings.Count, _settings.Order); + + // Write all points for each ring + foreach (var ring in polygon.Rings) + { + Write(ring.Points, true); + } + } + + /// + /// WKBMultiPoint { + /// byte byteOrder; + /// uint32 wkbType; // 4 + /// uint32 numWkbPoints; + /// WKBPoint WKBPoints[numWkbPoints]; + /// } + /// + /// The Multipoint. + /// A boolean value indicating to write the SRID or not. + private void Write(WKBMultiPoint multiPoint, bool includeSRID) + { + WriteHeader(SpatialType.MultiPoint, includeSRID); + + _writer.Write(multiPoint.Points.Count, _settings.Order); + + foreach (var point in multiPoint.Points) + { + // So far, the sub points should have the same SRID, so should skip the SRID for all sub points. + Write(point, false); + } + } + + /// + /// WKBMultiLineString { + /// byte byteOrder; + /// uint32 wkbType; // 5 + /// uint32 numWkbLineStrings; + /// WKBLineString WKBLineStrings[numWkbLineStrings]; + /// } + /// + /// The MultiLineString. + /// A boolean value indicating to write the SRID or not. + private void Write(WKBMultiLineString multiLineString, bool includeSRID) + { + WriteHeader(SpatialType.MultiLineString, includeSRID); + + _writer.Write(multiLineString.LineStrings.Count, _settings.Order); + + // Write all points for each LineString + foreach (var lineString in multiLineString.LineStrings) + { + // So far, the sub LineString should have the same SRID, so should skip the SRID for all sub LineStrings. + Write(lineString, false); + } + } + + /// + /// WKBMultiPolygon { + /// byte byteOrder; + /// uint32 wkbType; // 6 + /// uint32 numWkbPolygons; + /// WKBPolygon wkbPolygons[numWkbPolygons]; + /// } + /// + /// The MultiPolygon. + /// A boolean value indicating to write the SRID or not. + private void Write(WKBMultiPolygon multiPolygon, bool includeSRID) + { + WriteHeader(SpatialType.MultiPolygon, includeSRID); + + _writer.Write(multiPolygon.Polygons.Count, _settings.Order); + + foreach (var polygon in multiPolygon.Polygons) + { + // So far, the sub Polygon should have the same SRID, so should skip the SRID for all sub Polygons. + Write(polygon, false); + } + } + + /// + /// Write a GeometryCollection in its WKB format. + /// WKBGeometry { + /// union { + /// WKBPoint point; + /// WKBLineString linestring; + /// WKBPolygon polygon; + /// WKBGeometryCollection collection; + /// WKBMultiPoint mpoint; + /// WKBMultiLineString mlinestring; + /// WKBMultiPolygon mpolygon; + /// } + /// } + /// + /// WKBGeometryCollection { + /// byte byteOrder; + /// uint32 wkbType; // 7 + /// uint32 numWkbGeometries; + /// WKBGeometry wkbGeometries[numWkbGeometries]; + /// } + /// + /// The WKBCollection + /// A boolean value indicating to write the SRID or not. + private void Write(WKBCollection collection, bool includeSRID) + { + WriteHeader(SpatialType.Collection, includeSRID); + + _writer.Write(collection.Items.Count, _settings.Order); + + foreach (var item in collection.Items) + { + // So far, the sub item should have the same SRID, so should skip the SRID for all sub items. + WriteWKB(item, false); + } + } + + private void Write(IList points, bool writeSize) + { + if (writeSize) + { + _writer.Write(points.Count, _settings.Order); + } + + foreach (var point in points) + { + if (point == null) + { + Write(double.NaN, double.NaN, double.NaN, double.NaN); + } + else + { + Write(point.X, point.Y, point.Z, point.M); + } + } + } + + private void Write(double x, double y, double? z, double? m) + { + _writer.Write(x, _settings.Order); + _writer.Write(y, _settings.Order); + + double zValue = z.HasValue ? z.Value : double.NaN; + if (_settings.HandleZ) + { + _writer.Write(zValue, _settings.Order); + } + + double mValue = m.HasValue ? m.Value : double.NaN; + if (_settings.HandleM) + { + _writer.Write(mValue, _settings.Order); + } + } + + public void Dispose() + { + _writer?.Dispose(); + } + } + + internal class Scope + { + public SpatialType Type { get; set; } + + public IWKBObject Value { get; set; } + + public bool IsFigureBegin { get; set; } = false; + } + + internal interface IWKBObject + { + SpatialType SpatialType { get; } + } + + internal class WKBPoint : IWKBObject + { + public SpatialType SpatialType => SpatialType.Point; + public double X { get; set; } + public double Y { get; set; } + public double? Z { get; set; } + public double? M { get; set; } + }; + + internal class WKBLineString : IWKBObject + { + public SpatialType SpatialType => SpatialType.LineString; + public IList Points { get; set; } = new List(); + } + + internal class WKBPolygon : IWKBObject + { + public SpatialType SpatialType => SpatialType.Polygon; + public IList Rings { get; set; } = new List(); + } + + internal class WKBMultiPoint : IWKBObject + { + public SpatialType SpatialType => SpatialType.MultiPoint; + public IList Points { get; set; } = new List(); + } + + internal class WKBMultiLineString : IWKBObject + { + public SpatialType SpatialType => SpatialType.MultiLineString; + public IList LineStrings { get; set; } = new List(); + } + + internal class WKBMultiPolygon : IWKBObject + { + public SpatialType SpatialType => SpatialType.MultiPolygon; + public IList Polygons { get; set; } = new List(); + } + + internal class WKBCollection : IWKBObject + { + public SpatialType SpatialType => SpatialType.Collection; + public IList Items { get; set; } = new List(); + } +} diff --git a/src/Microsoft.Spatial/WellKnown/WellKnownBinaryWriterSettings.cs b/src/Microsoft.Spatial/WellKnown/WellKnownBinaryWriterSettings.cs new file mode 100644 index 0000000000..98456fa6cb --- /dev/null +++ b/src/Microsoft.Spatial/WellKnown/WellKnownBinaryWriterSettings.cs @@ -0,0 +1,47 @@ +//--------------------------------------------------------------------- +// +// Copyright (C) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information. +// +//--------------------------------------------------------------------- + +namespace Microsoft.Spatial +{ + /// + /// The writer setting for Well Known Binary (WKB) + /// + public class WellKnownBinaryWriterSettings + { + /// + /// Gets/sets a the byte order. + /// + public ByteOrder Order { get; set; } = ByteOrder.LittleEndian; + + /// + /// Gets/sets a boolean value indicating whether to support the ISO WKB. + /// In ISO WKB, it simply adds a round number to the type number to indicate extra dimensions. + /// +1000 a flag for Z + /// +2000 a flag for M + /// +3000 a flag for ZM + /// + public bool IsoWKB { get; set; } = true; + + /// + /// Gets/sets a boolean value indicating whether the SRID values, present or not, should be emitted. + /// To back compatibility for the extended WKB, let's use a flag as: + /// 0x20000000 + /// + public bool HandleSRID { get; set; } = true; + + /// + /// Gets/sets a boolean value indicating whether the Z values, present or not, should be emitted. + /// 0x80000000 a flag for Z + /// + public bool HandleZ { get; set; } = true; + + /// + /// Gets/sets a boolean value indicating whether the M values, present or not, should be emitted. + /// 0x40000000 a flag for M + /// + public bool HandleM { get; set; } = true; + } +} diff --git a/test/UnitTests/Microsoft.Spatial.Tests/WellKnownBinaryReaderTests.cs b/test/UnitTests/Microsoft.Spatial.Tests/WellKnownBinaryReaderTests.cs new file mode 100644 index 0000000000..2732d5c5b6 --- /dev/null +++ b/test/UnitTests/Microsoft.Spatial.Tests/WellKnownBinaryReaderTests.cs @@ -0,0 +1,478 @@ +//--------------------------------------------------------------------- +// +// Copyright (C) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information. +// +//--------------------------------------------------------------------- + +using System; +using System.IO; +using Xunit; + +namespace Microsoft.Spatial.Tests +{ + public class WellKnownBinaryReaderTests + { + private readonly WellKnownBinaryFormatter _formatter = new WellKnownBinaryFormatterImplementation(new DataServicesSpatialImplementation(), new WellKnownBinaryWriterSettings()); + + [Fact] + public void ReadWKBPoint_Works() + { + // Empty Point + ReadAndVerify("0101000020E6100000000000000000F8FF000000000000F8FF", + p => + { + Assert.NotNull(p.CoordinateSystem); + Assert.Equal(4326, p.CoordinateSystem.EpsgId.Value); + Assert.True(p.IsEmpty); + }, + p => + { + Assert.NotNull(p.CoordinateSystem); + Assert.Equal(4326, p.CoordinateSystem.EpsgId.Value); + Assert.True(p.IsEmpty); + }); + + // X, Y + ReadAndVerify("0101000020E610000000000000000034400000000000002440", + p => + { + Assert.NotNull(p.CoordinateSystem); + Assert.Equal(4326, p.CoordinateSystem.EpsgId.Value); + Assert.False(p.IsEmpty); + Assert.Equal(10, p.Latitude); + Assert.Equal(20, p.Longitude); + Assert.Null(p.Z); + Assert.Null(p.M); + }, + p => + { + Assert.NotNull(p.CoordinateSystem); + Assert.Equal(4326, p.CoordinateSystem.EpsgId.Value); + Assert.False(p.IsEmpty); + Assert.Equal(20, p.X); + Assert.Equal(10, p.Y); + Assert.Null(p.Z); + Assert.Null(p.M); + }); + + // X, Y, Z, M + ReadAndVerify("01B90B00E0E6100000000000000000344000000000000024400000000000003E400000000000004440", + p => + { + Assert.NotNull(p.CoordinateSystem); + Assert.Equal(4326, p.CoordinateSystem.EpsgId.Value); + Assert.False(p.IsEmpty); + Assert.Equal(10, p.Latitude); + Assert.Equal(20, p.Longitude); + Assert.Equal(30, p.Z.Value); + Assert.Equal(40, p.M.Value); + }, + p => + { + Assert.NotNull(p.CoordinateSystem); + Assert.Equal(4326, p.CoordinateSystem.EpsgId.Value); + Assert.False(p.IsEmpty); + Assert.Equal(20, p.X); + Assert.Equal(10, p.Y); + Assert.Equal(30, p.Z.Value); + Assert.Equal(40, p.M.Value); + }); + } + + [Theory] + [InlineData("01BA0B00E01A00000000000000")] // Little Endian (HasZ, HasM) + [InlineData("00E0000BBA0000001A00000000")] // Big Endian (HasZ, HasM) + [InlineData("01020000201A00000000000000")] // Little Endian + [InlineData("00200000020000001A00000000")] // Big Endian + public void ReadWKBLineString_ForEmpty_Works(string value) + { + ReadAndVerify(value, + p => + { + Assert.NotNull(p.CoordinateSystem); + Assert.Equal(26, p.CoordinateSystem.EpsgId.Value); + Assert.True(p.IsEmpty); + }, + p => + { + Assert.NotNull(p.CoordinateSystem); + Assert.Equal(26, p.CoordinateSystem.EpsgId.Value); + Assert.True(p.IsEmpty); + }); + } + + [Theory] + [InlineData("01020000207E000000020000000000000000002440000000000000344033333333333324409A99999999193440")] // Little Endian + [InlineData("00200000020000007E00000002402400000000000040340000000000004024333333333333403419999999999A")] // Big Endian + [InlineData("01020000A07E0000000200000000000000000024400000000000003440000000000000F8FF33333333333324409A99999999193440000000000000F8FF")] // Little Endian (HasZ) + [InlineData("01020000E07E0000000200000000000000000024400000000000003440000000000000F8FF000000000000F8FF33333333333324409A99999999193440000000000000F8FF000000000000F8FF")] // Little Endian (HasZ, HasM) + public void ReadWKBLineString_WithTwoPoints_NoIsoWKB_Works(string value) + { + ReadAndVerify(value, + p => + { + Assert.NotNull(p.CoordinateSystem); + Assert.Equal(126, p.CoordinateSystem.EpsgId.Value); + p.VerifyAsLineString(new PositionData(20, 10, null, null), new PositionData(20.1, 10.1, null, null)); + }, + p => + { + Assert.NotNull(p.CoordinateSystem); + Assert.Equal(126, p.CoordinateSystem.EpsgId.Value); + p.VerifyAsLineString(new PositionData(10, 20, null, null), new PositionData(10.1, 20.1, null, null)); + }); + } + + + [Theory] + [InlineData("01BA0B00E02C00000002000000000000000000244000000000000034400000000000003E40000000000000444000000000000044400000000000003E4000000000000034400000000000002440")] // Little Endian + [InlineData("00E0000BBA0000002C0000000240240000000000004034000000000000403E00000000000040440000000000004044000000000000403E00000000000040340000000000004024000000000000")] // Big Endian + public void ReadWKBLineString_WithTwoPointsWithZAndM_Works(string value) + { + ReadAndVerify(value, + p => + { + Assert.NotNull(p.CoordinateSystem); + Assert.Equal(44, p.CoordinateSystem.EpsgId.Value); + p.VerifyAsLineString(new PositionData(20, 10, 30, 40), new PositionData(30, 40, 20, 10)); + }, + p => + { + Assert.NotNull(p.CoordinateSystem); + Assert.Equal(44, p.CoordinateSystem.EpsgId.Value); + p.VerifyAsLineString(new PositionData(10, 20, 30, 40), new PositionData(40, 30, 20, 10)); + }); + } + + [Theory] + [InlineData("010300000000000000")] // Little Endian + [InlineData("000000000300000000")] // Big Endian + public void ReadWKBPolygon_ForEmpty_StandardWKB_Works(string value) + { + ReadAndVerify(value, + p => + { + Assert.NotNull(p.CoordinateSystem); + Assert.Equal(CoordinateSystem.DefaultGeography.EpsgId, p.CoordinateSystem.EpsgId); + Assert.True(p.IsEmpty); + }, + p => + { + Assert.NotNull(p.CoordinateSystem); + Assert.Equal(CoordinateSystem.DefaultGeometry.EpsgId, p.CoordinateSystem.EpsgId); + Assert.True(p.IsEmpty); + }); + } + + [Theory] + [InlineData("01030000000400000004000000000000000000244000000000000034400000000000002E40000000000000394000000000000034400000000000003E4000000000000024400000000000003440040000000000000000002E40000000000000394000000000000034400000000000003E40000000000000394000000000008041400000000000002E400000000000003940000000000400000000000000000014400000000000001440000000000000184000000000000018400000000000001C400000000000001C4000000000000014400000000000001440")] + [InlineData("0000000003000000040000000440240000000000004034000000000000402E00000000000040390000000000004034000000000000403E0000000000004024000000000000403400000000000000000004402E00000000000040390000000000004034000000000000403E00000000000040390000000000004041800000000000402E000000000000403900000000000000000000000000044014000000000000401400000000000040180000000000004018000000000000401C000000000000401C00000000000040140000000000004014000000000000")] + public void ReadWKBPolygon_WithRings_StardardWKB_Works(string value) + { + // Be noted: the Polygon WKB contains 4 rings, but the third rings is empty (0 points), when reading this empty point ring, it's ignore and therefore there's only 3 rings in the polygon. + // It's same as WKT logic. + ReadAndVerify(value, + p => + { + Assert.NotNull(p.CoordinateSystem); + Assert.Equal(CoordinateSystem.DefaultGeography.EpsgId, p.CoordinateSystem.EpsgId); + p.VerifyAsPolygon( + [new PositionData(20, 10), new PositionData(25, 15), new PositionData(30, 20), new PositionData(20, 10)], + [new PositionData(25, 15), new PositionData(30, 20), new PositionData(35, 25), new PositionData(25, 15)], + [new PositionData(5, 5), new PositionData(6, 6), new PositionData(7, 7), new PositionData(5, 5)]); + }, + p => + { + Assert.NotNull(p.CoordinateSystem); + Assert.Equal(CoordinateSystem.DefaultGeometry.EpsgId, p.CoordinateSystem.EpsgId); + p.VerifyAsPolygon( + [new PositionData(10, 20), new PositionData(15, 25), new PositionData(20, 30), new PositionData(10, 20)], + [new PositionData(15, 25), new PositionData(20, 30), new PositionData(25, 35), new PositionData(15, 25)], + [new PositionData(5, 5), new PositionData(6, 6), new PositionData(7, 7), new PositionData(5, 5)]); + }); + } + + [Theory] + [InlineData("0104000020E610000000000000")] // Little Endian (X, Y) + [InlineData("0020000004000010E600000000")] // Big Endian (X, Y) + [InlineData("01BC0B00E0E610000000000000")] // Little Endian (HasZ, HasM) + [InlineData("00E0000BBC000010E600000000")] // Big Endian (HasZ, HasM) + public void ReadWKBMultiPoints_ForEmpty_Works(string value) + { + ReadAndVerify(value, + p => + { + Assert.NotNull(p.CoordinateSystem); + Assert.Equal(4326, p.CoordinateSystem.EpsgId.Value); + Assert.True(p.IsEmpty); + }, + p => + { + Assert.NotNull(p.CoordinateSystem); + Assert.Equal(4326, p.CoordinateSystem.EpsgId.Value); + Assert.True(p.IsEmpty); + }); + } + + [Theory] + [InlineData("0104000020E6100000020000000101000000000000000000F8FF000000000000F8FF0101000000000000000000F8FF000000000000F8FF")] // Little Endian + [InlineData("0020000004000010E6000000020000000001FFF8000000000000FFF80000000000000000000001FFF8000000000000FFF8000000000000")] // Big Endian + [InlineData("01BC0B00E0E61000000200000001B90B00C0000000000000F8FF000000000000F8FF000000000000F8FF000000000000F8FF01B90B00C0000000000000F8FF000000000000F8FF000000000000F8FF000000000000F8FF")] + public void ReadMultiPoints_WithTwoEmptyPoints_Works(string value) + { + ReadAndVerify(value, + p => + { + Assert.NotNull(p.CoordinateSystem); + Assert.Equal(4326, p.CoordinateSystem.EpsgId.Value); + p.VerifyAsMultiPoint(null, null); + }, + p => + { + Assert.NotNull(p.CoordinateSystem); + Assert.Equal(4326, p.CoordinateSystem.EpsgId.Value); + p.VerifyAsMultiPoint(null, null); + }); + } + + [Theory] + [InlineData("0104000020E6100000030000000101000000000000000000244000000000000034400101000000000000000000F8FF000000000000F8FF01010000000000000000003E400000000000004440")] // Little Endian + [InlineData("0020000004000010E6000000030000000001402400000000000040340000000000000000000001FFF8000000000000FFF80000000000000000000001403E0000000000004044000000000000")] // Big Endian + [InlineData("01BC0B00E0E61000000300000001B90B00C000000000000024400000000000003440000000000000F8FF000000000000F8FF01B90B00C0000000000000F8FF000000000000F8FF000000000000F8FF000000000000F8FF01B90B00C00000000000003E400000000000004440000000000000F8FF000000000000F8FF")] + public void ReadMultiPoints_WithPoints_Works(string value) + { + ReadAndVerify(value, + p => + { + Assert.NotNull(p.CoordinateSystem); + Assert.Equal(4326, p.CoordinateSystem.EpsgId.Value); + p.VerifyAsMultiPoint(new PositionData(20, 10), null, new PositionData(40, 30)); + }, + p => + { + Assert.NotNull(p.CoordinateSystem); + Assert.Equal(4326, p.CoordinateSystem.EpsgId.Value); + p.VerifyAsMultiPoint(new PositionData(10, 20), null, new PositionData(30, 40)); + }); + } + + [Theory] + [InlineData("01BD0B00E00F00000000000000")] // Little Endian (X, Y, HasZ, HasM) + [InlineData("00E0000BBD0000000F00000000")] // Big Endian (X, Y, HasZ, HasM) + [InlineData("01050000200F00000000000000")] // Little Endian + [InlineData("00200000050000000F00000000")] // Big Endian + public void ReadWKBMultiLineString_ForEmpty_Works(string value) + { + ReadAndVerify(value, + p => + { + Assert.NotNull(p.CoordinateSystem); + Assert.Equal(15, p.CoordinateSystem.EpsgId.Value); + Assert.True(p.IsEmpty); + }, + p => + { + Assert.NotNull(p.CoordinateSystem); + Assert.Equal(15, p.CoordinateSystem.EpsgId.Value); + Assert.True(p.IsEmpty); + }); + } + + [Theory] + [InlineData("0105000020010000000300000001020000000200000000000000000024400000000000002440000000000000344000000000000034400102000000000000000102000000030000000000000000003E400000000000003E400000000000004440000000000000444000000000000049400000000000004940")] // Little Endian (X, Y) + [InlineData("002000000500000001000000030000000002000000024024000000000000402400000000000040340000000000004034000000000000000000000200000000000000000200000003403E000000000000403E0000000000004044000000000000404400000000000040490000000000004049000000000000")] // Big Endian (X, Y) + public void ReadWKBMultiLineString_WithLineStrings_Works(string value) + { + ReadAndVerify(value, + p => + { + Assert.NotNull(p.CoordinateSystem); + Assert.Equal(1, p.CoordinateSystem.EpsgId.Value); + p.VerifyAsMultiLineString( + [ new PositionData(10, 10), new PositionData(20, 20)], + null, + [ new PositionData(30, 30), new PositionData(40, 40), new PositionData(50, 50)]); + }, + p => + { + Assert.NotNull(p.CoordinateSystem); + Assert.Equal(1, p.CoordinateSystem.EpsgId.Value); + p.VerifyAsMultiLineString( + [new PositionData(10, 10), new PositionData(20, 20)], + null, + [new PositionData(30, 30), new PositionData(40, 40), new PositionData(50, 50)]); + }); + } + + [Theory] + [InlineData("01BE0B00E01700000000000000")] // Little Endian + [InlineData("00E0000BBE0000001700000000")] // Big Endia + public void ReadWKBMultiPolygon_ForEmpty_Works(string value) + { + ReadAndVerify(value, + p => + { + Assert.NotNull(p.CoordinateSystem); + Assert.Equal(23, p.CoordinateSystem.EpsgId.Value); + Assert.True(p.IsEmpty); + }, + p => + { + Assert.NotNull(p.CoordinateSystem); + Assert.Equal(23, p.CoordinateSystem.EpsgId.Value); + Assert.True(p.IsEmpty); + }); + } + + [Theory] + [InlineData("01060000201700000003000000010300000002000000040000000000000000002440000000000000344000000000000034400000000000003E400000000000003E40000000000000444000000000000024400000000000003440040000000000000000000840000000000000104000000000000010400000000000001440000000000000144000000000000018400000000000000840000000000000104001030000000000000001030000000100000004000000000000000000F03F00000000000000400000000000000040000000000000084000000000000008400000000000001040000000000000F03F0000000000000040")] // Little Endian (X, Y) + [InlineData("0020000006000000170000000300000000030000000200000004402400000000000040340000000000004034000000000000403E000000000000403E0000000000004044000000000000402400000000000040340000000000000000000440080000000000004010000000000000401000000000000040140000000000004014000000000000401800000000000040080000000000004010000000000000000000000300000000000000000300000001000000043FF0000000000000400000000000000040000000000000004008000000000000400800000000000040100000000000003FF00000000000004000000000000000")] // Big Endian (X, Y) + public void ReadWKBMultiPolygon_WithPolygons_Works(string value) + { + ReadAndVerify(value, + p => + { + Assert.NotNull(p.CoordinateSystem); + Assert.Equal(23, p.CoordinateSystem.EpsgId.Value); + p.VerifyAsMultiPolygon( + [[new PositionData(20, 10), new PositionData(30, 20), new PositionData(40, 30), new PositionData(20, 10)], [new PositionData(4, 3), new PositionData(5, 4), new PositionData(6, 5), new PositionData(4, 3)]], + null, + [[new PositionData(2, 1), new PositionData(3, 2), new PositionData(4, 3), new PositionData(2, 1)]]); + }, + p => + { + Assert.NotNull(p.CoordinateSystem); + Assert.Equal(23, p.CoordinateSystem.EpsgId.Value); + p.VerifyAsMultiPolygon( + [[new PositionData(10, 20), new PositionData(20, 30), new PositionData(30, 40), new PositionData(10, 20)], [new PositionData(3, 4), new PositionData(4, 5), new PositionData(5, 6), new PositionData(3, 4)]], + null, + [[new PositionData(1, 2), new PositionData(2, 3), new PositionData(3, 4), new PositionData(1, 2)]]); + }); + } + + [Theory] + [InlineData("01BF0B00E02E00000000000000")] // Little Endian + [InlineData("00E0000BBF0000002E00000000")] // Big Endia + public void ReadWKBMultiCollection_ForEmpty_Works(string value) + { + ReadAndVerify(value, + p => + { + Assert.NotNull(p.CoordinateSystem); + Assert.Equal(46, p.CoordinateSystem.EpsgId.Value); + Assert.True(p.IsEmpty); + }, + p => + { + Assert.NotNull(p.CoordinateSystem); + Assert.Equal(46, p.CoordinateSystem.EpsgId.Value); + Assert.True(p.IsEmpty); + }); + } + + [Theory] + [InlineData("01070000000400000001010000000000000000002440000000000000344001020000000200000000000000000034400000000000003E4000000000000044400000000000004940010300000000000000010700000001000000010100000000000000000010400000000000001440")] // Little Endian + [InlineData("0000000007000000040000000001402400000000000040340000000000000000000002000000024034000000000000403E00000000000040440000000000004049000000000000000000000300000000000000000700000001000000000140100000000000004014000000000000")] // Big Endian + public void ReadWKBCollection_WithSpatials_StandardWKB_Works(string value) + { + ReadAndVerify(value, + p => + { + Assert.NotNull(p.CoordinateSystem); + Assert.Equal(CoordinateSystem.DefaultGeography.EpsgId, p.CoordinateSystem.EpsgId.Value); + p.VerifyAsCollection( + (g) => g.VerifyAsPoint(new PositionData(20, 10)), + (g) => g.VerifyAsLineString(new PositionData(30, 20), new PositionData(50, 40)), + (g) => g.VerifyAsPolygon(null), + (g) => g.VerifyAsCollection( + (g1) => g1.VerifyAsPoint(new PositionData(5, 4)))); + }, + p => + { + Assert.NotNull(p.CoordinateSystem); + Assert.Equal(CoordinateSystem.DefaultGeometry.EpsgId, p.CoordinateSystem.EpsgId.Value); + p.VerifyAsCollection( + (g) => g.VerifyAsPoint(new PositionData(10, 20)), + (g) => g.VerifyAsLineString(new PositionData(20, 30), new PositionData(40, 50)), + (g) => g.VerifyAsPolygon(null), + (g) => g.VerifyAsCollection( + (g1) => g1.VerifyAsPoint(new PositionData(4, 5)))); + }); + } + + private void ReadAndVerify(string bytes, Action verify1, Action verify2) + where TSpatial1 : class, ISpatial + where TSpatial2 : class, ISpatial + { + byte[] bs = HexToBytes(bytes); + if (verify1 != null) + { + using (var stream1 = new MemoryStream(bs)) + { + var spatial1 = _formatter.Read(stream1); + verify1(spatial1); + } + } + + if (verify2 != null) + { + using (var stream2 = new MemoryStream(bs)) + { + var spatial2 = _formatter.Read(stream2); + verify2(spatial2); + } + } + } + + private static byte[] HexToBytes(string hex) + { + int byteLen = hex.Length / 2; + byte[] bytes = new byte[byteLen]; + + for (int i = 0; i < hex.Length / 2; i++) + { + int i2 = 2 * i; + if (i2 + 1 > hex.Length) + throw new ArgumentException("Hex string has odd length"); + + int nib1 = HexToInt(hex[i2]); + int nib0 = HexToInt(hex[i2 + 1]); + bytes[i] = (byte)((nib1 << 4) + (byte)nib0); + } + return bytes; + } + + private static int HexToInt(char hex) + { + switch (hex) + { + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + return hex - '0'; + case 'A': + case 'B': + case 'C': + case 'D': + case 'E': + case 'F': + return hex - 'A' + 10; + case 'a': + case 'b': + case 'c': + case 'd': + case 'e': + case 'f': + return hex - 'a' + 10; + } + throw new ArgumentException("Invalid hex digit: " + hex); + } + } +} diff --git a/test/UnitTests/Microsoft.Spatial.Tests/WellKnownBinaryWriterTests.cs b/test/UnitTests/Microsoft.Spatial.Tests/WellKnownBinaryWriterTests.cs new file mode 100644 index 0000000000..be79f19050 --- /dev/null +++ b/test/UnitTests/Microsoft.Spatial.Tests/WellKnownBinaryWriterTests.cs @@ -0,0 +1,721 @@ +//--------------------------------------------------------------------- +// +// Copyright (C) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information. +// +//--------------------------------------------------------------------- + +using System; +using System.IO; +using System.Text; +using Xunit; + +namespace Microsoft.Spatial.Tests +{ + public class WellKnownBinaryWriterTests + { + private readonly WellKnownBinaryFormatter d2Formatter = new WellKnownBinaryFormatterImplementation(new DataServicesSpatialImplementation(), new WellKnownBinaryWriterSettings { HandleZ = false, HandleM = false }); + private readonly WellKnownBinaryFormatter d4Formatter = new WellKnownBinaryFormatterImplementation(new DataServicesSpatialImplementation(), new WellKnownBinaryWriterSettings()); + + [Theory] + [InlineData(SpatialType.Point, SpatialType.Point)] + [InlineData(SpatialType.Point, SpatialType.LineString)] + [InlineData(SpatialType.LineString, SpatialType.Point)] + [InlineData(SpatialType.Polygon, SpatialType.LineString)] + [InlineData(SpatialType.Polygon, SpatialType.Point)] + public void CallBeginGeo_Throws_OnInvalidSubSpatialType(SpatialType parent, SpatialType child) + { + Action testCall = w => + { + w.BeginGeometry(parent); + w.BeginGeometry(child); + }; + + var stream = new MemoryStream(); + var w = d4Formatter.CreateWriter(stream); + InvalidOperationException exception = Assert.Throws(() => testCall(w)); + Assert.NotNull(exception); + Assert.Equal(Error.Format(SRResources.WellKnownBinary_InvalidSubSpatial, child, parent, $"{parent} cannot contain other spatial types"), exception.Message); + } + + [Theory] + [InlineData(SpatialType.LineString)] + [InlineData(SpatialType.Polygon)] + [InlineData(SpatialType.MultiPoint)] + public void CallBeginGeoOnMultiPoint_Throws_OnInvalidSubSpatialType(SpatialType child) + { + Action testCall = w => + { + w.BeginGeometry(SpatialType.MultiPoint); + w.BeginGeometry(child); + }; + + var stream = new MemoryStream(); + var w = d4Formatter.CreateWriter(stream); + InvalidOperationException exception = Assert.Throws(() => testCall(w)); + Assert.NotNull(exception); + Assert.Equal(Error.Format(SRResources.WellKnownBinary_InvalidSubSpatial, child, SpatialType.MultiPoint, "MultiPoint can only contain Points."), exception.Message); + } + + [Theory] + [InlineData(SpatialType.Point)] + [InlineData(SpatialType.Polygon)] + [InlineData(SpatialType.MultiPoint)] + public void CallBeginGeoOnMultiLineString_Throws_OnInvalidSubSpatialType(SpatialType child) + { + Action testCall = w => + { + w.BeginGeometry(SpatialType.MultiLineString); + w.BeginGeometry(child); + }; + + var stream = new MemoryStream(); + var w = d4Formatter.CreateWriter(stream); + InvalidOperationException exception = Assert.Throws(() => testCall(w)); + Assert.NotNull(exception); + Assert.Equal(Error.Format(SRResources.WellKnownBinary_InvalidSubSpatial, child, SpatialType.MultiLineString, "MultiLineString can only contain LineStrings."), exception.Message); + } + + [Theory] + [InlineData(SpatialType.Point)] + [InlineData(SpatialType.LineString)] + [InlineData(SpatialType.MultiPoint)] + public void CallBeginGeoOnMultiPolygon_Throws_OnInvalidSubSpatialType(SpatialType child) + { + Action testCall = w => + { + w.BeginGeometry(SpatialType.MultiPolygon); + w.BeginGeometry(child); + }; + + var stream = new MemoryStream(); + var w = d4Formatter.CreateWriter(stream); + InvalidOperationException exception = Assert.Throws(() => testCall(w)); + Assert.NotNull(exception); + Assert.Equal(Error.Format(SRResources.WellKnownBinary_InvalidSubSpatial, child, SpatialType.MultiPolygon, "MultiPolygon can only contain Polygons."), exception.Message); + } + + [Fact] + public void CallBeginFigure_Throws_OnNoSpatialTypeBegun() + { + Action testCall = w => + { + w.BeginFigure(new GeometryPosition(10, 20, 30, 40)); + }; + + var stream = new MemoryStream(); + var w = d4Formatter.CreateWriter(stream); + InvalidOperationException exception = Assert.Throws(() => testCall(w)); + Assert.NotNull(exception); + Assert.Equal(Error.Format(SRResources.WellKnownBinary_InvalidBeginOrEndFigureOrAddLine, "BeginFigure"), exception.Message); + } + + [Theory] + [InlineData(SpatialType.MultiPoint)] + [InlineData(SpatialType.MultiLineString)] + [InlineData(SpatialType.MultiPolygon)] + [InlineData(SpatialType.Collection)] + public void CallBeginFigure_Throws_OnInvalidSpatialTypeBegun(SpatialType parent) + { + Action testCall = w => + { + w.BeginGeometry(parent); + w.BeginFigure(new GeometryPosition(10, 20, 30, 40)); + }; + + var stream = new MemoryStream(); + var w = d4Formatter.CreateWriter(stream); + InvalidOperationException exception = Assert.Throws(() => testCall(w)); + Assert.NotNull(exception); + Assert.Equal(Error.Format(SRResources.WellKnownBinary_InvalidBeginFigureOnSpatial, parent), exception.Message); + } + + [Fact] + public void CallBeginFigure_Throws_OnMultipleBeginFigureBegun() + { + Action testCall = w => + { + w.BeginGeometry(SpatialType.LineString); + w.BeginFigure(new GeometryPosition(10, 20)); + w.BeginFigure(new GeometryPosition(30, 20)); + }; + + var stream = new MemoryStream(); + var w = d4Formatter.CreateWriter(stream); + InvalidOperationException exception = Assert.Throws(() => testCall(w)); + Assert.NotNull(exception); + Assert.Equal(Error.Format(SRResources.WellKnownBinary_InvalidBeginFigureWithoutEndingPrevious, SpatialType.LineString), exception.Message); + } + + [Fact] + public void CallLineTo_Throws_OnNoSpatialTypeBegun() + { + Action testCall = w => + { + w.LineTo(new GeometryPosition(10, 20, 30, 40)); + }; + + var stream = new MemoryStream(); + var w = d4Formatter.CreateWriter(stream); + InvalidOperationException exception = Assert.Throws(() => testCall(w)); + Assert.NotNull(exception); + Assert.Equal(Error.Format(SRResources.WellKnownBinary_InvalidBeginOrEndFigureOrAddLine, "LineTo"), exception.Message); + } + + [Theory] + [InlineData(SpatialType.LineString, "Should call BeginFigure first on LineString.")] + [InlineData(SpatialType.Polygon, "Should call BeginFigure first on Polygon.")] + [InlineData(SpatialType.Point, null)] + [InlineData(SpatialType.MultiPoint, null)] + [InlineData(SpatialType.MultiLineString, null)] + [InlineData(SpatialType.MultiPolygon, null)] + [InlineData(SpatialType.Collection, null)] + public void CallLineTo_Throws_OnFigureBegun(SpatialType type, string details) + { + Action testCall = w => + { + w.BeginGeometry(type); + w.LineTo(new GeometryPosition(10, 20, 30, 40)); + }; + + var stream = new MemoryStream(); + var w = d4Formatter.CreateWriter(stream); + InvalidOperationException exception = Assert.Throws(() => testCall(w)); + Assert.NotNull(exception); + Assert.Equal(Error.Format(SRResources.WellKnownBinary_InvalidAddLineTo, type, details ?? string.Empty), exception.Message); + } + + [Fact] + public void CallEndFigure_Throws_OnNoSpatialTypeBegun() + { + Action testCall = w => + { + w.EndFigure(); + }; + + var stream = new MemoryStream(); + var w = d4Formatter.CreateWriter(stream); + InvalidOperationException exception = Assert.Throws(() => testCall(w)); + Assert.NotNull(exception); + Assert.Equal(Error.Format(SRResources.WellKnownBinary_InvalidBeginOrEndFigureOrAddLine, "EndFigure"), exception.Message); + } + + [Theory] + [InlineData(SpatialType.Point, "You haven't begun the figure.")] + [InlineData(SpatialType.LineString, "You haven't begun the figure.")] + [InlineData(SpatialType.Polygon, "You haven't begun the figure.")] + [InlineData(SpatialType.MultiLineString, null)] + [InlineData(SpatialType.MultiPolygon, null)] + [InlineData(SpatialType.MultiPoint, null)] + [InlineData(SpatialType.Collection, null)] + public void CallEndFigure_Throws_OnSpatialTypeBegun(SpatialType type, string details) + { + Action testCall = w => + { + w.BeginGeometry(type); + w.EndFigure(); + }; + + var stream = new MemoryStream(); + var w = d4Formatter.CreateWriter(stream); + InvalidOperationException exception = Assert.Throws(() => testCall(w)); + Assert.NotNull(exception); + Assert.Equal(Error.Format(SRResources.WellKnownBinary_InvalidEndFigure, type, details ?? string.Empty), exception.Message); + } + + [Fact] + public void CallEndGeo_Throws_OnNoSpatialTypeBegun() + { + Action testCall = w => + { + w.EndGeometry(); + }; + + var stream = new MemoryStream(); + var w = d4Formatter.CreateWriter(stream); + InvalidOperationException exception = Assert.Throws(() => testCall(w)); + Assert.NotNull(exception); + Assert.Equal(Error.Format(SRResources.WellKnownBinary_InvalidBeginOrEndFigureOrAddLine, "EndGeo"), exception.Message); + } + + [Fact] + public void WritePoint_AsWKB() + { + Action emptyCalls = (w) => + { + w.BeginGeography(SpatialType.Point); + w.EndGeography(); + }; + GeographyToWkbTest(this.d2Formatter, emptyCalls, "0101000020E6100000000000000000F8FF000000000000F8FF"); + GeographyToWkbTest(this.d4Formatter, emptyCalls, "01B90B00E0E6100000000000000000F8FF000000000000F8FF000000000000F8FF000000000000F8FF"); + + Action d4PointCalls = (w) => + { + w.BeginGeography(SpatialType.Point); + w.BeginFigure(new GeographyPosition(10, 20, 30, 40)); + w.EndFigure(); + w.EndGeography(); + }; + GeographyToWkbTest(this.d2Formatter, d4PointCalls, "0101000020E610000000000000000034400000000000002440"); + GeographyToWkbTest(this.d4Formatter, d4PointCalls, "01B90B00E0E6100000000000000000344000000000000024400000000000003E400000000000004440"); + + Action d3PointCalls = (w) => + { + w.BeginGeography(SpatialType.Point); + w.BeginFigure(new GeographyPosition(10, 20, 30, null)); + w.EndFigure(); + w.EndGeography(); + }; + GeographyToWkbTest(this.d2Formatter, d3PointCalls, "0101000020E610000000000000000034400000000000002440"); + GeographyToWkbTest(this.d4Formatter, d3PointCalls, "01B90B00E0E6100000000000000000344000000000000024400000000000003E40000000000000F8FF"); + + Action d2PointCalls = (w) => + { + w.BeginGeography(SpatialType.Point); + w.BeginFigure(new GeographyPosition(10, 20, null, null)); // y,x + w.EndFigure(); + w.EndGeography(); + }; + GeographyToWkbTest(this.d2Formatter, d2PointCalls, "0101000020E610000000000000000034400000000000002440"); + GeographyToWkbTest(this.d4Formatter, d2PointCalls, "01B90B00E0E610000000000000000034400000000000002440000000000000F8FF000000000000F8FF"); + + Action skipPointCalls = + (w) => + { + w.BeginGeography(SpatialType.Point); + w.BeginFigure(new GeographyPosition(10, 20, null, 40)); // latitude, longitude + w.EndFigure(); + w.EndGeography(); + }; + GeographyToWkbTest(this.d2Formatter, skipPointCalls, "0101000020E610000000000000000034400000000000002440"); + GeographyToWkbTest(this.d4Formatter, skipPointCalls, "01B90B00E0E610000000000000000034400000000000002440000000000000F8FF0000000000004440"); + } + + [Fact] + public void WriteLineString_AsWKB() + { + Action emptyCalls = (w) => + { + w.BeginGeography(SpatialType.LineString); + w.EndGeography(); + }; + + GeographyToWkbTest(this.d2Formatter, emptyCalls, "0102000020E610000000000000"); + GeographyToWkbTest(this.d4Formatter, emptyCalls, "01BA0B00E0E610000000000000"); + + Action twoD2Point = (w) => + { + w.BeginGeography(SpatialType.LineString); + w.BeginFigure(new GeographyPosition(10, 20, null, null)); + w.LineTo(new GeographyPosition(20, 30, null, null)); + w.EndFigure(); + w.EndGeography(); + }; + + GeographyToWkbTest(this.d2Formatter, twoD2Point, "0102000020E610000002000000000000000000344000000000000024400000000000003E400000000000003440"); + GeographyToWkbTest(this.d4Formatter, twoD2Point, "01BA0B00E0E61000000200000000000000000034400000000000002440000000000000F8FF000000000000F8FF0000000000003E400000000000003440000000000000F8FF000000000000F8FF"); + + Action threeD2Point = (w) => + { + w.BeginGeography(SpatialType.LineString); + w.BeginFigure(new GeographyPosition(10, 20, null, null)); + w.LineTo(new GeographyPosition(-20.5, -30, null, null)); + w.LineTo(new GeographyPosition(30, 40, null, null)); + w.EndFigure(); + w.EndGeography(); + }; + GeographyToWkbTest(this.d2Formatter, threeD2Point, "0102000020E610000003000000000000000000344000000000000024400000000000003EC000000000008034C000000000000044400000000000003E40"); + GeographyToWkbTest(this.d4Formatter, threeD2Point, "01BA0B00E0E61000000300000000000000000034400000000000002440000000000000F8FF000000000000F8FF0000000000003EC000000000008034C0000000000000F8FF000000000000F8FF00000000000044400000000000003E40000000000000F8FF000000000000F8FF"); + + + Action twoD4Point = (w) => + { + w.BeginGeography(SpatialType.LineString); + w.BeginFigure(new GeographyPosition(10, 20, 30, 40)); + w.LineTo(new GeographyPosition(20, 30, 40, 50)); + w.EndFigure(); + w.EndGeography(); + }; + + GeographyToWkbTest(this.d2Formatter, twoD4Point, "0102000020E610000002000000000000000000344000000000000024400000000000003E400000000000003440"); + GeographyToWkbTest(this.d4Formatter, twoD4Point, "01BA0B00E0E610000002000000000000000000344000000000000024400000000000003E4000000000000044400000000000003E40000000000000344000000000000044400000000000004940"); + } + + [Fact] + public void WritePolygon_AsWKB() + { + Action emptyCalls = (w) => + { + w.BeginGeography(SpatialType.Polygon); + w.EndGeography(); + }; + GeographyToWkbTest(this.d2Formatter, emptyCalls, "0103000020E610000000000000"); + GeographyToWkbTest(this.d4Formatter, emptyCalls, "01BB0B00E0E610000000000000"); + + + Action fourD2Point = (w) => + { + w.BeginGeography(SpatialType.Polygon); + w.BeginFigure(new GeographyPosition(10, 20, null, null)); + w.LineTo(new GeographyPosition(20, 30, null, null)); + w.LineTo(new GeographyPosition(30, 40, null, null)); + w.LineTo(new GeographyPosition(10, 20, null, null)); + w.EndFigure(); + w.EndGeography(); + }; + GeographyToWkbTest(this.d2Formatter, fourD2Point, "0103000020E61000000100000004000000000000000000344000000000000024400000000000003E40000000000000344000000000000044400000000000003E4000000000000034400000000000002440"); + GeographyToWkbTest(this.d4Formatter, fourD2Point, "01BB0B00E0E6100000010000000400000000000000000034400000000000002440000000000000F8FF000000000000F8FF0000000000003E400000000000003440000000000000F8FF000000000000F8FF00000000000044400000000000003E40000000000000F8FF000000000000F8FF00000000000034400000000000002440000000000000F8FF000000000000F8FF"); + + Action fourD2PointWith2D2Holes = (w) => + { + w.BeginGeography(SpatialType.Polygon); + w.BeginFigure(new GeographyPosition(10, 20, null, null)); + w.LineTo(new GeographyPosition(20, 30, null, null)); + w.LineTo(new GeographyPosition(30, 40, null, null)); + w.LineTo(new GeographyPosition(10, 20, null, null)); + w.EndFigure(); + + w.BeginFigure(new GeographyPosition(-10, -20, null, null)); + w.LineTo(new GeographyPosition(-20, -30, null, null)); + w.LineTo(new GeographyPosition(-30, -40, null, null)); + w.LineTo(new GeographyPosition(-10, -20, null, null)); + w.EndFigure(); + + w.BeginFigure(new GeographyPosition(-10.5, -20.5, null, null)); + w.LineTo(new GeographyPosition(-20.5, -30.5, null, null)); + w.LineTo(new GeographyPosition(-30.5, -40.5, null, null)); + w.LineTo(new GeographyPosition(-10.5, -20.5, null, null)); + w.EndFigure(); + w.EndGeography(); + }; + + GeographyToWkbTest(this.d2Formatter, fourD2PointWith2D2Holes, "0103000020E61000000300000004000000000000000000344000000000000024400000000000003E40000000000000344000000000000044400000000000003E40000000000000344000000000000024400400000000000000000034C000000000000024C00000000000003EC000000000000034C000000000000044C00000000000003EC000000000000034C000000000000024C00400000000000000008034C000000000000025C00000000000803EC000000000008034C000000000004044C00000000000803EC000000000008034C000000000000025C0"); + GeographyToWkbTest(this.d4Formatter, fourD2PointWith2D2Holes, "01BB0B00E0E6100000030000000400000000000000000034400000000000002440000000000000F8FF000000000000F8FF0000000000003E400000000000003440000000000000F8FF000000000000F8FF00000000000044400000000000003E40000000000000F8FF000000000000F8FF00000000000034400000000000002440000000000000F8FF000000000000F8FF0400000000000000000034C000000000000024C0000000000000F8FF000000000000F8FF0000000000003EC000000000000034C0000000000000F8FF000000000000F8FF00000000000044C00000000000003EC0000000000000F8FF000000000000F8FF00000000000034C000000000000024C0000000000000F8FF000000000000F8FF0400000000000000008034C000000000000025C0000000000000F8FF000000000000F8FF0000000000803EC000000000008034C0000000000000F8FF000000000000F8FF00000000004044C00000000000803EC0000000000000F8FF000000000000F8FF00000000008034C000000000000025C0000000000000F8FF000000000000F8FF"); + } + + [Fact] + public void WriteMultiPoint_AsWKB() + { + Action noPointsCalls = (w) => + { + w.BeginGeography(SpatialType.MultiPoint); + w.EndGeography(); + }; + GeographyToWkbTest(this.d2Formatter, noPointsCalls, "0104000020E610000000000000"); + GeographyToWkbTest(this.d4Formatter, noPointsCalls, "01BC0B00E0E610000000000000"); + + Action twoEmptyPointsCalls = (w) => + { + w.BeginGeography(SpatialType.MultiPoint); + w.BeginGeography(SpatialType.Point); + w.EndGeography(); + w.BeginGeography(SpatialType.Point); + w.EndGeography(); + w.EndGeography(); + }; + GeographyToWkbTest(this.d2Formatter, twoEmptyPointsCalls, "0104000020E6100000020000000101000000000000000000F8FF000000000000F8FF0101000000000000000000F8FF000000000000F8FF"); + GeographyToWkbTest(this.d4Formatter, twoEmptyPointsCalls, "01BC0B00E0E61000000200000001B90B00C0000000000000F8FF000000000000F8FF000000000000F8FF000000000000F8FF01B90B00C0000000000000F8FF000000000000F8FF000000000000F8FF000000000000F8FF"); + + Action twoD2PointsCalls = (w) => + { + w.BeginGeography(SpatialType.MultiPoint); + w.BeginGeography(SpatialType.Point); + w.BeginFigure(new GeographyPosition(10, 20, null, null)); + w.EndFigure(); + w.EndGeography(); + + w.BeginGeography(SpatialType.Point); + w.EndGeography(); + + w.BeginGeography(SpatialType.Point); + w.BeginFigure(new GeographyPosition(30, 40, null, null)); + w.EndFigure(); + w.EndGeography(); + w.EndGeography(); + }; + GeographyToWkbTest(this.d2Formatter, twoD2PointsCalls, "0104000020E6100000030000000101000000000000000000344000000000000024400101000000000000000000F8FF000000000000F8FF010100000000000000000044400000000000003E40"); + GeographyToWkbTest(this.d4Formatter, twoD2PointsCalls, "01BC0B00E0E61000000300000001B90B00C000000000000034400000000000002440000000000000F8FF000000000000F8FF01B90B00C0000000000000F8FF000000000000F8FF000000000000F8FF000000000000F8FF01B90B00C000000000000044400000000000003E40000000000000F8FF000000000000F8FF"); + + Action singleD3PointCalls = (w) => + { + w.BeginGeography(SpatialType.MultiPoint); + w.BeginGeography(SpatialType.Point); + w.BeginFigure(new GeographyPosition(10, 20, 30, 40)); + w.EndFigure(); + w.EndGeography(); + w.EndGeography(); + }; + GeographyToWkbTest(this.d2Formatter, singleD3PointCalls, "0104000020E610000001000000010100000000000000000034400000000000002440"); + GeographyToWkbTest(this.d4Formatter, singleD3PointCalls, "01BC0B00E0E61000000100000001B90B00C0000000000000344000000000000024400000000000003E400000000000004440"); + } + + [Fact] + public void WriteMultiLineString_AsWKB() + { + Action emptyCalls2 = (w) => + { + w.BeginGeography(SpatialType.MultiLineString); + w.EndGeography(); + }; + GeographyToWkbTest(this.d2Formatter, emptyCalls2, "0105000020E610000000000000"); + GeographyToWkbTest(this.d4Formatter, emptyCalls2, "01BD0B00E0E610000000000000"); + + Action emptyCalls = (w) => + { + w.BeginGeography(SpatialType.MultiLineString); + w.BeginGeography(SpatialType.LineString); + w.EndGeography(); + w.EndGeography(); + }; + GeographyToWkbTest(this.d2Formatter, emptyCalls, "0105000020E610000001000000010200000000000000"); + GeographyToWkbTest(this.d4Formatter, emptyCalls, "01BD0B00E0E61000000100000001BA0B00C000000000"); + + Action twoD2LineStringCalls = (w) => + { + w.BeginGeography(SpatialType.MultiLineString); + w.BeginGeography(SpatialType.LineString); + w.BeginFigure(new GeographyPosition(10, 20, null, null)); + w.LineTo(new GeographyPosition(20, 30, null, null)); + w.EndFigure(); + w.EndGeography(); + + w.BeginGeography(SpatialType.LineString); + w.EndGeography(); + + w.BeginGeography(SpatialType.LineString); + w.BeginFigure(new GeographyPosition(30, 40, null, null)); + w.LineTo(new GeographyPosition(40, 50, null, null)); + w.EndFigure(); + w.EndGeography(); + w.EndGeography(); + }; + GeographyToWkbTest(this.d2Formatter, twoD2LineStringCalls, "0105000020E610000003000000010200000002000000000000000000344000000000000024400000000000003E40000000000000344001020000000000000001020000000200000000000000000044400000000000003E4000000000000049400000000000004440"); + GeographyToWkbTest(this.d4Formatter, twoD2LineStringCalls, "01BD0B00E0E61000000300000001BA0B00C00200000000000000000034400000000000002440000000000000F8FF000000000000F8FF0000000000003E400000000000003440000000000000F8FF000000000000F8FF01BA0B00C00000000001BA0B00C00200000000000000000044400000000000003E40000000000000F8FF000000000000F8FF00000000000049400000000000004440000000000000F8FF000000000000F8FF"); + + Action singleD3LineStringCalls = (w) => + { + w.BeginGeography(SpatialType.MultiLineString); + w.BeginGeography(SpatialType.LineString); + w.BeginFigure(new GeographyPosition(10, 20, 40, null)); + w.LineTo(new GeographyPosition(20, 30, 50, null)); + w.EndFigure(); + w.EndGeography(); + w.EndGeography(); + }; + GeographyToWkbTest(this.d2Formatter, singleD3LineStringCalls, "0105000020E610000001000000010200000002000000000000000000344000000000000024400000000000003E400000000000003440"); + GeographyToWkbTest(this.d4Formatter, singleD3LineStringCalls, "01BD0B00E0E61000000100000001BA0B00C002000000000000000000344000000000000024400000000000004440000000000000F8FF0000000000003E4000000000000034400000000000004940000000000000F8FF"); + } + + [Fact] + public void WriteMultiPolygon_AsWKB() + { + Action emptyCalls2 = (w) => + { + w.BeginGeography(SpatialType.MultiPolygon); + w.EndGeography(); + }; + GeographyToWkbTest(this.d2Formatter, emptyCalls2, "0106000020E610000000000000"); + GeographyToWkbTest(this.d4Formatter, emptyCalls2, "01BE0B00E0E610000000000000"); + + Action emptyCalls = (w) => + { + w.BeginGeography(SpatialType.MultiPolygon); + w.BeginGeography(SpatialType.Polygon); + w.EndGeography(); + w.EndGeography(); + }; + GeographyToWkbTest(this.d2Formatter, emptyCalls, "0106000020E610000001000000010300000000000000"); + GeographyToWkbTest(this.d4Formatter, emptyCalls, "01BE0B00E0E61000000100000001BB0B00C000000000"); + + Action threeLineD2Calls = (w) => + { + w.BeginGeography(SpatialType.MultiPolygon); + w.BeginGeography(SpatialType.Polygon); + w.BeginFigure(new GeographyPosition(10, 20, null, null)); + w.LineTo(new GeographyPosition(20, 30, null, null)); + w.LineTo(new GeographyPosition(30, 40, null, null)); + w.LineTo(new GeographyPosition(10, 20, null, null)); + w.EndFigure(); + + w.BeginFigure(new GeographyPosition(-10.5, -20.5, null, null)); + w.LineTo(new GeographyPosition(-20.5, -30.5, null, null)); + w.LineTo(new GeographyPosition(-30.5, -40.5, null, null)); + w.LineTo(new GeographyPosition(-10.5, -20.5, null, null)); + w.EndFigure(); + w.EndGeography(); + + w.BeginGeography(SpatialType.Polygon); + w.EndGeography(); + + w.BeginGeography(SpatialType.Polygon); + + w.BeginFigure(new GeographyPosition(10, 20, null, null)); + w.LineTo(new GeographyPosition(20, 30, null, null)); + w.LineTo(new GeographyPosition(30, 40, null, null)); + w.LineTo(new GeographyPosition(10, 20, null, null)); + w.EndFigure(); + + w.EndGeography(); + w.EndGeography(); + }; + GeographyToWkbTest(this.d2Formatter, threeLineD2Calls, "0106000020E61000000300000001030000000200000004000000000000000000344000000000000024400000000000003E40000000000000344000000000000044400000000000003E40000000000000344000000000000024400400000000000000008034C000000000000025C00000000000803EC000000000008034C000000000004044C00000000000803EC000000000008034C000000000000025C001030000000000000001030000000100000004000000000000000000344000000000000024400000000000003E40000000000000344000000000000044400000000000003E4000000000000034400000000000002440"); + GeographyToWkbTest(this.d4Formatter, threeLineD2Calls, "01BE0B00E0E61000000300000001BB0B00C0020000000400000000000000000034400000000000002440000000000000F8FF000000000000F8FF0000000000003E400000000000003440000000000000F8FF000000000000F8FF00000000000044400000000000003E40000000000000F8FF000000000000F8FF00000000000034400000000000002440000000000000F8FF000000000000F8FF0400000000000000008034C000000000000025C0000000000000F8FF000000000000F8FF0000000000803EC000000000008034C0000000000000F8FF000000000000F8FF00000000004044C00000000000803EC0000000000000F8FF000000000000F8FF00000000008034C000000000000025C0000000000000F8FF000000000000F8FF01BB0B00C00000000001BB0B00C0010000000400000000000000000034400000000000002440000000000000F8FF000000000000F8FF0000000000003E400000000000003440000000000000F8FF000000000000F8FF00000000000044400000000000003E40000000000000F8FF000000000000F8FF00000000000034400000000000002440000000000000F8FF000000000000F8FF"); + } + + [Fact] + public void WriteCollection_AsWKB() + { + Action emptyCalls = (w) => + { + w.BeginGeography(SpatialType.Collection); + w.EndGeography(); + }; + GeographyToWkbTest(this.d2Formatter, emptyCalls, "0107000020E610000000000000"); + GeographyToWkbTest(this.d4Formatter, emptyCalls, "01BF0B00E0E610000000000000"); + + Action emptyCalls2 = (w) => + { + w.BeginGeography(SpatialType.Collection); + w.BeginGeography(SpatialType.Point); + w.EndGeography(); + w.BeginGeography(SpatialType.LineString); + w.EndGeography(); + w.EndGeography(); + }; + GeographyToWkbTest(this.d2Formatter, emptyCalls2, "0107000020E6100000020000000101000000000000000000F8FF000000000000F8FF010200000000000000"); + GeographyToWkbTest(this.d4Formatter, emptyCalls2, "01BF0B00E0E61000000200000001B90B00C0000000000000F8FF000000000000F8FF000000000000F8FF000000000000F8FF01BA0B00C000000000"); + + Action nestedEmptyCalls = (w) => + { + w.BeginGeography(SpatialType.Collection); + w.BeginGeography(SpatialType.Collection); + w.EndGeography(); + w.EndGeography(); + }; + GeographyToWkbTest(this.d2Formatter, nestedEmptyCalls, "0107000020E610000001000000010700000000000000"); + GeographyToWkbTest(this.d4Formatter, nestedEmptyCalls, "01BF0B00E0E61000000100000001BF0B00C000000000"); + + Action singlePointCalls = (w) => + { + w.BeginGeography(SpatialType.Collection); + w.BeginGeography(SpatialType.Point); + w.BeginFigure(new GeographyPosition(10, 20, 30, 40)); + w.EndFigure(); + w.EndGeography(); + w.EndGeography(); + }; + GeographyToWkbTest(this.d2Formatter, singlePointCalls, "0107000020E610000001000000010100000000000000000034400000000000002440"); + GeographyToWkbTest(this.d4Formatter, singlePointCalls, "01BF0B00E0E61000000100000001B90B00C0000000000000344000000000000024400000000000003E400000000000004440"); + + Action pointMultiPointCalls = (w) => + { + w.BeginGeography(SpatialType.Collection); + w.BeginGeography(SpatialType.Point); + w.BeginFigure(new GeographyPosition(10, 20, null, null)); + w.EndFigure(); + w.EndGeography(); + w.BeginGeography(SpatialType.MultiPoint); + w.BeginGeography(SpatialType.Point); + w.BeginFigure(new GeographyPosition(20, 30, null, null)); + w.EndFigure(); + w.EndGeography(); + w.BeginGeography(SpatialType.Point); + w.BeginFigure(new GeographyPosition(30, 40, null, null)); + w.EndFigure(); + w.EndGeography(); + w.EndGeography(); + + w.EndGeography(); + }; + GeographyToWkbTest(this.d2Formatter, pointMultiPointCalls, "0107000020E61000000200000001010000000000000000003440000000000000244001040000000200000001010000000000000000003E400000000000003440010100000000000000000044400000000000003E40"); + GeographyToWkbTest(this.d4Formatter, pointMultiPointCalls, "01BF0B00E0E61000000200000001B90B00C000000000000034400000000000002440000000000000F8FF000000000000F8FF01BC0B00C00200000001B90B00C00000000000003E400000000000003440000000000000F8FF000000000000F8FF01B90B00C000000000000044400000000000003E40000000000000F8FF000000000000F8FF"); + } + + [Fact] + public void WriteCollection_AsWKB_ForGeometry() + { + Action singlePointCalls = (w) => + { + w.BeginGeometry(SpatialType.Collection); + w.BeginGeometry(SpatialType.Point); + w.BeginFigure(new GeometryPosition(10, 20, 30, 40)); + w.EndFigure(); + w.EndGeometry(); + w.EndGeometry(); + }; + GeometryToWkbTest(this.d2Formatter, singlePointCalls, "01070000200000000001000000010100000000000000000024400000000000003440"); + GeometryToWkbTest(this.d4Formatter, singlePointCalls, "01BF0B00E0000000000100000001B90B00C0000000000000244000000000000034400000000000003E400000000000004440"); + + Action pointMultiPointCalls = (w) => + { + w.BeginGeometry(SpatialType.Collection); + w.BeginGeometry(SpatialType.Point); + w.BeginFigure(new GeometryPosition(10, 20, null, null)); + w.EndFigure(); + w.EndGeometry(); + w.BeginGeometry(SpatialType.MultiPoint); + w.BeginGeometry(SpatialType.Point); + w.BeginFigure(new GeometryPosition(20, 30, null, null)); + w.EndFigure(); + w.EndGeometry(); + w.BeginGeometry(SpatialType.Point); + w.BeginFigure(new GeometryPosition(30, 40, null, null)); + w.EndFigure(); + w.EndGeometry(); + w.EndGeometry(); + + w.EndGeometry(); + }; + GeometryToWkbTest(this.d2Formatter, pointMultiPointCalls, "01070000200000000002000000010100000000000000000024400000000000003440010400000002000000010100000000000000000034400000000000003E4001010000000000000000003E400000000000004440"); + GeometryToWkbTest(this.d4Formatter, pointMultiPointCalls, "01BF0B00E0000000000200000001B90B00C000000000000024400000000000003440000000000000F8FF000000000000F8FF01BC0B00C00200000001B90B00C000000000000034400000000000003E40000000000000F8FF000000000000F8FF01B90B00C00000000000003E400000000000004440000000000000F8FF000000000000F8FF"); + } + + private static void GeographyToWkbTest(WellKnownBinaryFormatter formatter, Action pipelineAction, string expectedWkb) + { + var stream = new MemoryStream(); + var w = formatter.CreateWriter(stream); + + w.GeographyPipeline.SetCoordinateSystem(CoordinateSystem.DefaultGeography); + pipelineAction(w); + + byte[] result = stream.ToArray(); + string actual = ToHex(result); + + Assert.Equal(expectedWkb, actual); + } + + private static void GeometryToWkbTest(WellKnownBinaryFormatter formatter, Action pipelineAction, string expectedWkb) + { + var stream = new MemoryStream(); + var w = formatter.CreateWriter(stream); + + w.GeographyPipeline.SetCoordinateSystem(CoordinateSystem.DefaultGeometry); + pipelineAction(w); + + byte[] result = stream.ToArray(); + string actual = ToHex(result); + + Assert.Equal(expectedWkb, actual); + } + + public static string ToHex(byte[] bytes) + { + var buf = new StringBuilder(bytes.Length * 2); + for (int i = 0; i < bytes.Length; i++) + { + byte b = bytes[i]; + buf.Append(ToHexDigit((b >> 4) & 0x0F)); + buf.Append(ToHexDigit(b & 0x0F)); + } + return buf.ToString(); + } + + private static char ToHexDigit(int n) + { + if (n < 0 || n > 15) + throw new ArgumentException("Value out of range: " + n); + if (n <= 9) + return (char)('0' + n); + return (char)('A' + (n - 10)); + } + } +} diff --git a/test/UnitTests/Microsoft.Spatial.Tests/WellKnownTextSqlFormatterTests.cs b/test/UnitTests/Microsoft.Spatial.Tests/WellKnownTextSqlFormatterTests.cs index 4d1c71b645..d37299b7dd 100644 --- a/test/UnitTests/Microsoft.Spatial.Tests/WellKnownTextSqlFormatterTests.cs +++ b/test/UnitTests/Microsoft.Spatial.Tests/WellKnownTextSqlFormatterTests.cs @@ -6,6 +6,7 @@ using System; using System.IO; +using System.Text; using Xunit; namespace Microsoft.Spatial.Tests @@ -773,6 +774,9 @@ public void WriteCollection() w.EndFigure(); w.EndGeography(); + // w.Reset(); + // w.SetCoordinateSystem(new CoordinateSystem(8, "my", CoordinateSystem.Topology.Geography)); + w.BeginGeography(SpatialType.MultiPoint); w.BeginGeography(SpatialType.Point); w.BeginFigure(new GeographyPosition(20, 30, null, null)); @@ -790,6 +794,228 @@ public void WriteCollection() GeographyToWktTest(this.d4Formatter, pointMultiPointCalls, "SRID=4326;GEOMETRYCOLLECTION (POINT (20 10), MULTIPOINT ((30 20), (40 30)))"); } + [Fact] + public void WriteCollection2() + { + WellKnownBinaryFormatter d2Formatter = new WellKnownBinaryFormatterImplementation(new DataServicesSpatialImplementation(), new WellKnownBinaryWriterSettings { HandleZ = false, HandleM = false}); + WellKnownBinaryFormatter d4Formatter = new WellKnownBinaryFormatterImplementation(new DataServicesSpatialImplementation(), new WellKnownBinaryWriterSettings()); + + //Action emptyCalls = (w) => + //{ + // w.BeginGeography(SpatialType.Collection); + // w.EndGeography(); + //}; + //GeographyToWkbTest(d2Formatter, emptyCalls, "SRID=4326;GEOMETRYCOLLECTION EMPTY"); + //GeographyToWkbTest(d4Formatter, emptyCalls, "SRID=4326;GEOMETRYCOLLECTION EMPTY"); + + Action emptyCalls2 = (w) => + { + w.BeginGeography(SpatialType.Collection); + w.BeginGeography(SpatialType.Point); + w.EndGeography(); + w.BeginGeography(SpatialType.LineString); + w.EndGeography(); + w.EndGeography(); + }; + GeographyToWkbTest(d2Formatter, emptyCalls2, "SRID=4326;GEOMETRYCOLLECTION (POINT EMPTY, LINESTRING EMPTY)"); + GeographyToWkbTest(d4Formatter, emptyCalls2, "SRID=4326;GEOMETRYCOLLECTION (POINT EMPTY, LINESTRING EMPTY)"); + + Action nestedEmptyCalls = (w) => + { + w.BeginGeography(SpatialType.Collection); + w.BeginGeography(SpatialType.Collection); + w.EndGeography(); + w.EndGeography(); + }; + GeographyToWkbTest(d2Formatter, nestedEmptyCalls, "SRID=4326;GEOMETRYCOLLECTION (GEOMETRYCOLLECTION EMPTY)"); + GeographyToWkbTest(d4Formatter, nestedEmptyCalls, "SRID=4326;GEOMETRYCOLLECTION (GEOMETRYCOLLECTION EMPTY)"); + + Action singlePointCalls = (w) => + { + w.BeginGeography(SpatialType.Collection); + w.BeginGeography(SpatialType.Point); + w.BeginFigure(new GeographyPosition(10, 20, 30, 40)); + w.EndFigure(); + + + w.EndGeography(); + w.EndGeography(); + }; + GeographyToWkbTest(d2Formatter, singlePointCalls, "SRID=4326;GEOMETRYCOLLECTION (POINT (20 10))"); + GeographyToWkbTest(d4Formatter, singlePointCalls, "SRID=4326;GEOMETRYCOLLECTION (POINT (20 10 30 40))"); + + Action pointMultiPointCalls = (w) => + { + w.BeginGeography(SpatialType.Collection); + w.BeginGeography(SpatialType.Point); + w.BeginFigure(new GeographyPosition(10, 20, null, null)); + w.EndFigure(); + w.EndGeography(); + + w.BeginGeography(SpatialType.MultiPoint); + w.BeginGeography(SpatialType.Point); + w.BeginFigure(new GeographyPosition(20, 30, null, null)); + w.EndFigure(); + w.EndGeography(); + w.BeginGeography(SpatialType.Point); + w.BeginFigure(new GeographyPosition(30, 40, null, null)); + w.EndFigure(); + w.EndGeography(); + w.EndGeography(); + + w.EndGeography(); + }; + GeographyToWkbTest(d2Formatter, pointMultiPointCalls, "SRID=4326;GEOMETRYCOLLECTION (POINT (20 10), MULTIPOINT ((30 20), (40 30)))"); + GeographyToWkbTest(d4Formatter, pointMultiPointCalls, "SRID=4326;GEOMETRYCOLLECTION (POINT (20 10), MULTIPOINT ((30 20), (40 30)))"); + } + + [Fact] + public void WriteCollection3() + { + WellKnownBinaryFormatter d2Formatter = new WellKnownBinaryFormatterImplementation(new DataServicesSpatialImplementation(), new WellKnownBinaryWriterSettings { HandleZ = false, HandleM = false }); + WellKnownBinaryFormatter d4Formatter = new WellKnownBinaryFormatterImplementation(new DataServicesSpatialImplementation(), new WellKnownBinaryWriterSettings()); + + Action pointMultiPointCalls = (w) => + { + w.BeginGeography(SpatialType.Collection); + w.BeginGeography(SpatialType.Point); + w.BeginFigure(new GeographyPosition(10, 20, null, null)); + w.EndFigure(); + w.EndGeography(); + + w.BeginGeography(SpatialType.MultiPoint); + w.BeginGeography(SpatialType.Point); + w.BeginFigure(new GeographyPosition(20, 30, null, null)); + w.EndFigure(); + w.EndGeography(); + w.BeginGeography(SpatialType.Point); + w.BeginFigure(new GeographyPosition(30, 40, null, null)); + w.EndFigure(); + w.EndGeography(); + w.EndGeography(); + + w.EndGeography(); + }; + GeographyToWkbTest(d2Formatter, pointMultiPointCalls, "SRID=4326;GEOMETRYCOLLECTION (POINT (20 10), MULTIPOINT ((30 20), (40 30)))"); + GeographyToWkbTest(d4Formatter, pointMultiPointCalls, "SRID=4326;GEOMETRYCOLLECTION (POINT (20 10), MULTIPOINT ((30 20), (40 30)))"); + } + + [Fact] + public void WriteMultiPoint2() + { + WellKnownBinaryFormatter d2Formatter = new WellKnownBinaryFormatterImplementation(new DataServicesSpatialImplementation(), new WellKnownBinaryWriterSettings { HandleZ = false, HandleM = false }); + WellKnownBinaryFormatter d4Formatter = new WellKnownBinaryFormatterImplementation(new DataServicesSpatialImplementation(), new WellKnownBinaryWriterSettings()); + /* + Action twoEmptyPointsCalls = (w) => + { + w.BeginGeography(SpatialType.MultiPoint); + w.BeginGeography(SpatialType.Point); + w.EndGeography(); + w.BeginGeography(SpatialType.Point); + w.EndGeography(); + w.EndGeography(); + }; + GeographyToWktTest(this.d2Formatter, twoEmptyPointsCalls, "SRID=4326;MULTIPOINT (EMPTY, EMPTY)"); + GeographyToWktTest(this.d4Formatter, twoEmptyPointsCalls, "SRID=4326;MULTIPOINT (EMPTY, EMPTY)"); + + Action noPointsCalls = (w) => + { + w.BeginGeography(SpatialType.MultiPoint); + w.EndGeography(); + }; + GeographyToWktTest(this.d2Formatter, noPointsCalls, "SRID=4326;MULTIPOINT EMPTY"); + GeographyToWktTest(this.d4Formatter, noPointsCalls, "SRID=4326;MULTIPOINT EMPTY"); +*/ + Action twoD2PointsCalls = (w) => + { + w.BeginGeography(SpatialType.MultiPoint); + w.BeginGeography(SpatialType.Point); + w.BeginFigure(new GeographyPosition(10, 20, null, null)); + w.EndFigure(); + w.EndGeography(); + + w.BeginGeography(SpatialType.Point); + w.EndGeography(); + + w.BeginGeography(SpatialType.Point); + w.BeginFigure(new GeographyPosition(30, 40, null, null)); + w.EndFigure(); + w.EndGeography(); + w.EndGeography(); + }; + GeographyToWkbTest(d2Formatter, twoD2PointsCalls, "SRID=4326;MULTIPOINT ((20 10), EMPTY, (40 30))"); + GeographyToWkbTest(d4Formatter, twoD2PointsCalls, "SRID=4326;MULTIPOINT ((20 10), EMPTY, (40 30))"); + + /* + Action singleD3PointCalls = (w) => + { + w.BeginGeography(SpatialType.MultiPoint); + w.BeginGeography(SpatialType.Point); + w.BeginFigure(new GeographyPosition(10, 20, 30, 40)); + w.EndFigure(); + w.EndGeography(); + w.EndGeography(); + }; + GeographyToWkbTest(d2Formatter, singleD3PointCalls, "SRID=4326;MULTIPOINT ((20 10))"); + GeographyToWkbTest(d4Formatter, singleD3PointCalls, "SRID=4326;MULTIPOINT ((20 10 30 40))");*/ + } + + [Fact] + public void WritePoint1() + { + WellKnownBinaryFormatter d2Formatter = new WellKnownBinaryFormatterImplementation(new DataServicesSpatialImplementation(), new WellKnownBinaryWriterSettings { HandleZ = false, HandleM = false }); + WellKnownBinaryFormatter d4Formatter = new WellKnownBinaryFormatterImplementation(new DataServicesSpatialImplementation(), new WellKnownBinaryWriterSettings()); + + //Action emptyCalls = (w) => + //{ + // w.BeginGeography(SpatialType.Point); + // w.EndGeography(); + //}; + //GeographyToWktTest(this.d2Formatter, emptyCalls, "SRID=4326;POINT EMPTY"); + //GeographyToWktTest(this.d4Formatter, emptyCalls, "SRID=4326;POINT EMPTY"); + + Action d4PointCalls = (w) => + { + w.BeginGeography(SpatialType.Point); + w.BeginFigure(new GeographyPosition(8.0, 7.0, null, null)); // lat, long + w.EndFigure(); + w.EndGeography(); + }; + GeographyToWkbTest(d2Formatter, d4PointCalls, "0101000020E61000000000000000001C400000000000002040"); + GeographyToWkbTest(d4Formatter, d4PointCalls, "01B90B0020E61000000000000000001C400000000000002040000000000000F8FF000000000000F8FF"); + + //Action d3PointCalls = (w) => + //{ + // w.BeginGeography(SpatialType.Point); + // w.BeginFigure(new GeographyPosition(10, 20, 30, null)); + // w.EndFigure(); + // w.EndGeography(); + //}; + //GeographyToWktTest(this.d2Formatter, d3PointCalls, "SRID=4326;POINT (20 10)"); + //GeographyToWktTest(this.d4Formatter, d3PointCalls, "SRID=4326;POINT (20 10 30)"); + + //Action d2PointCalls = (w) => + //{ + // w.BeginGeography(SpatialType.Point); + // w.BeginFigure(new GeographyPosition(10, 20, null, null)); + // w.EndFigure(); + // w.EndGeography(); + //}; + //GeographyToWktTest(this.d2Formatter, d2PointCalls, "SRID=4326;POINT (20 10)"); + //GeographyToWktTest(this.d4Formatter, d2PointCalls, "SRID=4326;POINT (20 10)"); + + //Action skipPointCalls = + //(w) => + //{ + // w.BeginGeography(SpatialType.Point); + // w.BeginFigure(new GeographyPosition(10, 20, null, 40)); + // w.EndFigure(); + // w.EndGeography(); + //}; + //GeographyToWktTest(this.d2Formatter, skipPointCalls, "SRID=4326;POINT (20 10)"); + //GeographyToWktTest(this.d4Formatter, skipPointCalls, "SRID=4326;POINT (20 10 NULL 40)"); + } + private static void GeographyToWktTest(WellKnownTextSqlFormatter formatter, Action pipelineAction, string expectedWkt) { var stringWriter = new StringWriter(); @@ -800,5 +1026,49 @@ private static void GeographyToWktTest(WellKnownTextSqlFormatter formatter, Acti Assert.Equal(expectedWkt, stringWriter.GetStringBuilder().ToString()); } + + private static void GeographyToWkbTest(WellKnownBinaryFormatter formatter, Action pipelineAction, string expectedWkt) + { + var stream = new MemoryStream(); + var w = formatter.CreateWriter(stream); + + w.GeographyPipeline.SetCoordinateSystem(CoordinateSystem.DefaultGeography); + pipelineAction(w); + + stream.Flush(); + stream.Seek(0, SeekOrigin.Begin); + byte[] bytes = new byte[1024]; + int count = stream.Read(bytes, 0, bytes.Length); + + byte[] result = new byte[count]; + for (int i = 0; i < count; i++) + { + result[i] = bytes[i]; + } + string actual = ToHex(result); + + Assert.Equal(expectedWkt, actual); + } + + public static string ToHex(byte[] bytes) + { + var buf = new StringBuilder(bytes.Length * 2); + for (int i = 0; i < bytes.Length; i++) + { + byte b = bytes[i]; + buf.Append(ToHexDigit((b >> 4) & 0x0F)); + buf.Append(ToHexDigit(b & 0x0F)); + } + return buf.ToString(); + } + + private static char ToHexDigit(int n) + { + if (n < 0 || n > 15) + throw new ArgumentException("Nibble value out of range: " + n); + if (n <= 9) + return (char)('0' + n); + return (char)('A' + (n - 10)); + } } }