Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 51 additions & 5 deletions src/Npgsql.GeoJSON/Internal/GeoJSONConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,7 @@ static Position ReadPosition(PgReader reader, EwkbGeometryType type, bool little
var position = new Position(
longitude: ReadDouble(littleEndian),
latitude: ReadDouble(littleEndian),
altitude: HasZ(type) ? reader.ReadDouble() : null);
altitude: HasZ(type) ? ReadDouble(littleEndian) : null);
if (HasM(type)) ReadDouble(littleEndian);
return position;

Expand Down Expand Up @@ -423,16 +423,34 @@ public static ValueTask Write(bool async, PgWriter writer, IGeoJSONObject value,
_ => throw UnknownPostGisType()
};

static bool FirstCoordinateHasZ(IGeoJSONObject value)
=> value switch
{
Point p => HasZ(p.Coordinates),
LineString ls => ls.Coordinates is [var c, ..] && c.Altitude.HasValue,
Polygon poly => poly.Coordinates is [var ring, ..] && ring.Coordinates is [var c, ..] && HasZ(c),
MultiPoint mp => mp.Coordinates is [var p, ..] && FirstCoordinateHasZ(p),
MultiLineString mls => mls.Coordinates is [var l, ..] && FirstCoordinateHasZ(l),
MultiPolygon mpoly => mpoly.Coordinates is [var p, ..] && FirstCoordinateHasZ(p),
GeometryCollection c => c.Geometries is [var g, ..] && FirstCoordinateHasZ((IGeoJSONObject)g),
_ => throw UnknownPostGisType()
};

static async ValueTask Write(bool async, PgWriter writer, Point value, CancellationToken cancellationToken)
{
var type = EwkbGeometryType.Point;
var size = SizeOfHeader;
var srid = GetSrid(value.CRS);
var hasZ = FirstCoordinateHasZ(value);
if (srid != 0)
{
size += sizeof(int);
type |= EwkbGeometryType.HasSrid;
}
if (hasZ)
{
type |= EwkbGeometryType.HasZ;
}

if (writer.ShouldFlush(size))
await writer.Flush(async, cancellationToken).ConfigureAwait(false);
Expand All @@ -443,19 +461,24 @@ static async ValueTask Write(bool async, PgWriter writer, Point value, Cancellat
if (srid != 0)
writer.WriteInt32(srid);

await WritePosition(async, writer, value.Coordinates, cancellationToken).ConfigureAwait(false);
await WritePosition(async, writer, value.Coordinates, hasZ, cancellationToken).ConfigureAwait(false);
}

static async ValueTask Write(bool async, PgWriter writer, LineString value, CancellationToken cancellationToken)
{
var type = EwkbGeometryType.LineString;
var size = SizeOfHeaderWithLength;
var srid = GetSrid(value.CRS);
var hasZ = FirstCoordinateHasZ(value);
if (srid != 0)
{
size += sizeof(int);
type |= EwkbGeometryType.HasSrid;
}
if (hasZ)
{
type |= EwkbGeometryType.HasZ;
}

if (writer.ShouldFlush(size))
await writer.Flush(async, cancellationToken).ConfigureAwait(false);
Expand All @@ -470,19 +493,24 @@ static async ValueTask Write(bool async, PgWriter writer, LineString value, Canc
writer.WriteInt32(srid);

foreach (var t in coordinates)
await WritePosition(async, writer, t, cancellationToken).ConfigureAwait(false);
await WritePosition(async, writer, t, hasZ, cancellationToken).ConfigureAwait(false);
}

static async ValueTask Write(bool async, PgWriter writer, Polygon value, CancellationToken cancellationToken)
{
var type = EwkbGeometryType.Polygon;
var size = SizeOfHeaderWithLength;
var srid = GetSrid(value.CRS);
var hasZ = FirstCoordinateHasZ(value);
if (srid != 0)
{
size += sizeof(int);
type |= EwkbGeometryType.HasSrid;
}
if (hasZ)
{
type |= EwkbGeometryType.HasZ;
}

if (writer.ShouldFlush(size))
await writer.Flush(async, cancellationToken).ConfigureAwait(false);
Expand All @@ -503,7 +531,7 @@ static async ValueTask Write(bool async, PgWriter writer, Polygon value, Cancell
var coordinates = t.Coordinates;
writer.WriteInt32(coordinates.Count);
foreach (var t1 in coordinates)
await WritePosition(async, writer, t1, cancellationToken).ConfigureAwait(false);
await WritePosition(async, writer, t1, hasZ, cancellationToken).ConfigureAwait(false);
}
}

Expand All @@ -517,6 +545,10 @@ static async ValueTask Write(bool async, PgWriter writer, MultiPoint value, Canc
size += sizeof(int);
type |= EwkbGeometryType.HasSrid;
}
if (FirstCoordinateHasZ(value))
{
type |= EwkbGeometryType.HasZ;
}

if (writer.ShouldFlush(size))
await writer.Flush(async, cancellationToken).ConfigureAwait(false);
Expand Down Expand Up @@ -544,6 +576,10 @@ static async ValueTask Write(bool async, PgWriter writer, MultiLineString value,
size += sizeof(int);
type |= EwkbGeometryType.HasSrid;
}
if (FirstCoordinateHasZ(value))
{
type |= EwkbGeometryType.HasZ;
}

if (writer.ShouldFlush(size))
await writer.Flush(async, cancellationToken).ConfigureAwait(false);
Expand Down Expand Up @@ -571,6 +607,10 @@ static async ValueTask Write(bool async, PgWriter writer, MultiPolygon value, Ca
size += sizeof(int);
type |= EwkbGeometryType.HasSrid;
}
if (FirstCoordinateHasZ(value))
{
type |= EwkbGeometryType.HasZ;
}

if (writer.ShouldFlush(size))
await writer.Flush(async, cancellationToken).ConfigureAwait(false);
Expand All @@ -597,6 +637,10 @@ static async ValueTask Write(bool async, PgWriter writer, GeometryCollection val
size += sizeof(int);
type |= EwkbGeometryType.HasSrid;
}
if (FirstCoordinateHasZ(value))
{
type |= EwkbGeometryType.HasZ;
}

if (writer.ShouldFlush(size))
await writer.Flush(async, cancellationToken).ConfigureAwait(false);
Expand All @@ -614,7 +658,7 @@ static async ValueTask Write(bool async, PgWriter writer, GeometryCollection val
await Write(async, writer, (IGeoJSONObject)t, cancellationToken).ConfigureAwait(false);
}

static async ValueTask WritePosition(bool async, PgWriter writer, IPosition coordinate, CancellationToken cancellationToken)
static async ValueTask WritePosition(bool async, PgWriter writer, IPosition coordinate, bool hasZ, CancellationToken cancellationToken)
{
var altitude = coordinate.Altitude;
if (SizeOfPoint(altitude.HasValue) is var size && writer.ShouldFlush(size))
Expand All @@ -624,6 +668,8 @@ static async ValueTask WritePosition(bool async, PgWriter writer, IPosition coor
writer.WriteDouble(coordinate.Latitude);
if (altitude.HasValue)
writer.WriteDouble(altitude.Value);
else if (hasZ)
writer.WriteDouble(0d);
}

static ValueTask BufferData(this PgReader reader, bool async, int byteCount, CancellationToken cancellationToken)
Expand Down
49 changes: 48 additions & 1 deletion test/Npgsql.PluginTests/GeoJSONTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@ public struct TestData
public string CommandText;
}

public static readonly TestData[] Tests =
/// <summary>The Z coordinate applied to every position when deriving the 3D variants of <see cref="TwoDimensionalTests"/>.</summary>
const double Z = 5d;

static readonly TestData[] TwoDimensionalTests =
[
new()
{
Expand Down Expand Up @@ -107,6 +110,41 @@ public struct TestData
}
];

public static readonly TestData[] Tests =
[
..TwoDimensionalTests,
..TwoDimensionalTests.Select(static t => new TestData
{
Geometry = WithZ(t.Geometry),
CommandText = $"st_force3d({t.CommandText}, {Z})"
})
];

// Derives a 3D copy of a geometry by adding the Z altitude to every position, leaving the originals untouched.
static GeoJSONObject WithZ(GeoJSONObject geometry)
{
static Position AddZ(IPosition p) => new(latitude: p.Latitude, longitude: p.Longitude, altitude: Z);
static LineString Line(LineString line) => new(line.Coordinates.Select(AddZ));

GeoJSONObject result = geometry switch
{
Point point => new Point(AddZ(point.Coordinates)),
LineString line => Line(line),
Polygon polygon => new Polygon(polygon.Coordinates.Select(Line)),
MultiPoint multiPoint => new MultiPoint(multiPoint.Coordinates.Select(pt => new Point(AddZ(pt.Coordinates)))),
MultiLineString multiLine => new MultiLineString(multiLine.Coordinates.Select(Line)),
MultiPolygon multiPolygon => new MultiPolygon(multiPolygon.Coordinates.Select(p => new Polygon(p.Coordinates.Select(Line)))),
GeometryCollection collection => new GeometryCollection(collection.Geometries.Select(g => (IGeometryObject)WithZ((GeoJSONObject)g))),
_ => throw new NotSupportedException($"Unexpected geometry type {geometry.Type}")
};

// A 2D bounding box [minX, minY, maxX, maxY] becomes the 3D form [minX, minY, minZ, maxX, maxY, maxZ].
if (geometry.BoundingBoxes is { } bbox)
result.BoundingBoxes = [bbox[0], bbox[1], Z, bbox[2], bbox[3], Z];

return result;
}

[Test, TestCaseSource(nameof(Tests))]
public async Task Read(TestData data)
{
Expand Down Expand Up @@ -154,6 +192,15 @@ public async Task IgnoreM()
new Position(3d, 3d),
new Position(4d, 4d)
])
},
new()
{
Geometry = new LineString([
new Position(1d, 1d),
new Position(2d, 2d),
new Position(3d, 3d, 10d),
new Position(4d, 4d)
])
}
];

Expand Down
Loading