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));
+ }
}
}