Skip to content

Commit 0e772f2

Browse files
committed
Support for empty point with NaN in (E)WKB reader/writer
1 parent 7110c26 commit 0e772f2

File tree

10 files changed

+275
-17
lines changed

10 files changed

+275
-17
lines changed

src/Io/EwkbReader.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@
1717
*/
1818
final readonly class EwkbReader extends AbstractWkbReader
1919
{
20+
public function __construct(
21+
bool $supportEmptyPointWithNan = true,
22+
) {
23+
parent::__construct($supportEmptyPointWithNan);
24+
}
25+
2026
/**
2127
* @throws GeometryIoException
2228
*/

src/Io/EwkbWriter.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@
44

55
namespace Brick\Geo\Io;
66

7+
use Brick\Geo\Exception\GeometryIoException;
78
use Brick\Geo\Geometry;
89
use Brick\Geo\Io\Internal\AbstractWkbWriter;
10+
use Brick\Geo\Io\Internal\WkbByteOrder;
911
use Brick\Geo\Io\Internal\WkbTools;
1012
use Override;
1113

@@ -14,6 +16,18 @@
1416
*/
1517
final readonly class EwkbWriter extends AbstractWkbWriter
1618
{
19+
/**
20+
* @param bool $supportEmptyPointWithNan Whether to support PostGIS-style empty points with NaN coordinates.
21+
*
22+
* @throws GeometryIoException
23+
*/
24+
public function __construct(
25+
?WkbByteOrder $byteOrder = null,
26+
bool $supportEmptyPointWithNan = true,
27+
) {
28+
parent::__construct($byteOrder, $supportEmptyPointWithNan);
29+
}
30+
1731
#[Override]
1832
protected function packHeader(Geometry $geometry, bool $outer) : string
1933
{

src/Io/Internal/AbstractWkbReader.php

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,19 @@
2929
*/
3030
abstract readonly class AbstractWkbReader
3131
{
32+
/**
33+
* Whether to support PostGIS-style empty points with NaN coordinates.
34+
* This is not part of the WKB standard.
35+
* This will be disabled by default in WKB, but enabled by default in EWKB.
36+
*/
37+
private bool $supportEmptyPointWithNan;
38+
39+
public function __construct(
40+
bool $supportEmptyPointWithNan,
41+
) {
42+
$this->supportEmptyPointWithNan = $supportEmptyPointWithNan;
43+
}
44+
3245
/**
3346
* @throws GeometryIoException
3447
*/
@@ -71,9 +84,35 @@ private function readPoint(WkbBuffer $buffer, CoordinateSystem $cs) : Point
7184
{
7285
$coords = $buffer->readDoubles($cs->coordinateDimension());
7386

87+
if ($this->onlyNan($coords)) {
88+
if (! $this->supportEmptyPointWithNan) {
89+
throw new GeometryIoException(
90+
'Points with NaN (not-a-number) coordinates are not supported. ' .
91+
'If you want to read points with NaN coordinates as empty points (PostGIS-style), ' .
92+
'enable the $supportEmptyPointWithNan option.',
93+
);
94+
}
95+
96+
return new Point($cs);
97+
}
98+
7499
return new Point($cs, ...$coords);
75100
}
76101

102+
/**
103+
* @param non-empty-list<float> $coords
104+
*/
105+
private function onlyNan(array $coords) : bool
106+
{
107+
foreach ($coords as $coord) {
108+
if (!is_nan($coord)) {
109+
return false;
110+
}
111+
}
112+
113+
return true;
114+
}
115+
77116
private function readLineString(WkbBuffer $buffer, CoordinateSystem $cs) : LineString
78117
{
79118
$numPoints = $buffer->readUnsignedLong();

src/Io/Internal/AbstractWkbWriter.php

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,23 @@
2626
private WkbByteOrder $byteOrder;
2727
private WkbByteOrder $machineByteOrder;
2828

29+
/**
30+
* Whether to support PostGIS-style empty points with NaN coordinates.
31+
* This is not part of the WKB standard.
32+
* This will be disabled by default in WKB, but enabled by default in EWKB.
33+
*/
34+
private bool $supportEmptyPointWithNan;
35+
2936
/**
3037
* @throws GeometryIoException
3138
*/
32-
public function __construct(?WkbByteOrder $byteOrder = null)
33-
{
39+
public function __construct(
40+
?WkbByteOrder $byteOrder = null,
41+
bool $supportEmptyPointWithNan,
42+
) {
3443
$this->machineByteOrder = WkbTools::getMachineByteOrder();
3544
$this->byteOrder = $byteOrder ?? $this->machineByteOrder;
45+
$this->supportEmptyPointWithNan = $supportEmptyPointWithNan;
3646
}
3747

3848
/**
@@ -121,18 +131,22 @@ private function packDouble(float $double) : string
121131
*/
122132
private function packPoint(Point $point) : string
123133
{
124-
if ($point->isEmpty()) {
125-
throw new GeometryIoException('Empty points have no WKB representation.');
134+
if ($point->isEmpty() && ! $this->supportEmptyPointWithNan) {
135+
throw new GeometryIoException(
136+
'Empty points have no WKB representation. ' .
137+
'If you want to output empty points with NaN coordinates (PostGIS-style), ' .
138+
'enable the $supportEmptyPointWithNan option.',
139+
);
126140
}
127141

128-
/** @psalm-suppress PossiblyNullArgument */
129-
$binary = $this->packDouble($point->x()) . $this->packDouble($point->y());
142+
$binary = $this->packDouble($point->x() ?? NAN) . $this->packDouble($point->y() ?? NAN);
130143

131-
if (null !== $z = $point->z()) {
132-
$binary .= $this->packDouble($z);
144+
if ($point->coordinateSystem()->hasZ()) {
145+
$binary .= $this->packDouble($point->z() ?? NAN);
133146
}
134-
if (null !== $m = $point->m()) {
135-
$binary .= $this->packDouble($m);
147+
148+
if ($point->coordinateSystem()->hasM()) {
149+
$binary .= $this->packDouble($point->m() ?? NAN);
136150
}
137151

138152
return $binary;

src/Io/WkbReader.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@
1717
*/
1818
final readonly class WkbReader extends AbstractWkbReader
1919
{
20+
public function __construct(
21+
bool $supportEmptyPointWithNan = false,
22+
) {
23+
parent::__construct($supportEmptyPointWithNan);
24+
}
25+
2026
/**
2127
* @param string $wkb The WKB to read.
2228
* @param int $srid The optional SRID of the geometry.

src/Io/WkbWriter.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,29 @@
44

55
namespace Brick\Geo\Io;
66

7+
use Brick\Geo\Exception\GeometryIoException;
78
use Brick\Geo\Geometry;
89
use Brick\Geo\Io\Internal\AbstractWkbWriter;
10+
use Brick\Geo\Io\Internal\WkbByteOrder;
911
use Override;
1012

1113
/**
1214
* Writes geometries in the WKB format.
1315
*/
1416
final readonly class WkbWriter extends AbstractWkbWriter
1517
{
18+
/**
19+
* @param bool $supportEmptyPointWithNan Whether to support PostGIS-style empty points with NaN coordinates.
20+
*
21+
* @throws GeometryIoException
22+
*/
23+
public function __construct(
24+
?WkbByteOrder $byteOrder = null,
25+
bool $supportEmptyPointWithNan = false,
26+
) {
27+
parent::__construct($byteOrder, $supportEmptyPointWithNan);
28+
}
29+
1630
#[Override]
1731
protected function packHeader(Geometry $geometry, bool $outer) : string
1832
{

tests/IO/EwkbReaderTest.php

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
namespace Brick\Geo\Tests\IO;
66

7+
use Brick\Geo\Exception\GeometryIoException;
8+
use Brick\Geo\Exception\InvalidGeometryException;
79
use Brick\Geo\Io\EwkbReader;
810
use Brick\Geo\Io\EwktWriter;
911
use PHPUnit\Framework\Attributes\DataProvider;
@@ -55,4 +57,56 @@ public static function providerRead() : \Generator
5557
yield [$wkb, $wkt];
5658
}
5759
}
60+
61+
#[DataProvider('providerReadEmptyPointWithoutNanSupportThrowsException')]
62+
public function testReadEmptyPointWithoutNanSupportThrowsException(string $ewkbHex) : void
63+
{
64+
$reader = new EwkbReader(supportEmptyPointWithNan: false);
65+
66+
$this->expectException(GeometryIoException::class);
67+
$this->expectExceptionMessage(
68+
'Points with NaN (not-a-number) coordinates are not supported. If you want to read points with NaN ' .
69+
'coordinates as empty points (PostGIS-style), enable the $supportEmptyPointWithNan option.',
70+
);
71+
72+
$reader->read(hex2bin($ewkbHex));
73+
}
74+
75+
public static function providerReadEmptyPointWithoutNanSupportThrowsException() : array
76+
{
77+
return [
78+
'xy_BigEndian' => ['00000000017ff80000000000007ff8000000000000'],
79+
'xy_LittleEndian' => ['0101000000000000000000f87f000000000000f87f'],
80+
'xyz_BigEndian' => ['00800000017ff80000000000007ff80000000000007ff8000000000000'],
81+
'xyz_LittleEndian' => ['0101000080000000000000f87f000000000000f87f000000000000f87f'],
82+
'xym_BigEndian' => ['00400000017ff80000000000007ff80000000000007ff8000000000000'],
83+
'xym_LittleEndian' => ['0101000040000000000000f87f000000000000f87f000000000000f87f'],
84+
'xyzm_BigEndian' => ['00c00000017ff80000000000007ff80000000000007ff80000000000007ff8000000000000'],
85+
'xyzm_LittleEndian' => ['01010000c0000000000000f87f000000000000f87f000000000000f87f000000000000f87f'],
86+
];
87+
}
88+
89+
#[DataProvider('providerReadEmptyPointWithNanSupport')]
90+
public function testReadEmptyPointWithNanSupport(string $ewkbHex, string $expectedEwkt) : void
91+
{
92+
$ewkbReader = new EwkbReader();
93+
$ewktWriter = new EwktWriter();
94+
95+
$point = $ewkbReader->read(hex2bin($ewkbHex));
96+
self::assertSame($expectedEwkt, $ewktWriter->write($point));
97+
}
98+
99+
public static function providerReadEmptyPointWithNanSupport() : array
100+
{
101+
return [
102+
'xy_BigEndian' => ['00000000017ff80000000000007ff8000000000000', 'POINT EMPTY'],
103+
'xy_LittleEndian' => ['0101000000000000000000f87f000000000000f87f', 'POINT EMPTY'],
104+
'xyz_BigEndian' => ['00800000017ff80000000000007ff80000000000007ff8000000000000', 'POINT Z EMPTY'],
105+
'xyz_LittleEndian' => ['0101000080000000000000f87f000000000000f87f000000000000f87f', 'POINT Z EMPTY'],
106+
'xym_BigEndian' => ['00400000017ff80000000000007ff80000000000007ff8000000000000', 'POINT M EMPTY'],
107+
'xym_LittleEndian' => ['0101000040000000000000f87f000000000000f87f000000000000f87f', 'POINT M EMPTY'],
108+
'xyzm_BigEndian' => ['00c00000017ff80000000000007ff80000000000007ff80000000000007ff8000000000000', 'POINT ZM EMPTY'],
109+
'xyzm_LittleEndian' => ['01010000c0000000000000f87f000000000000f87f000000000000f87f000000000000f87f', 'POINT ZM EMPTY'],
110+
];
111+
}
58112
}

tests/IO/EwkbWriterTest.php

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,16 +52,21 @@ public static function providerWrite() : \Generator
5252
}
5353
}
5454

55-
#[DataProvider('providerWriteEmptyPointThrowsException')]
56-
public function testWriteEmptyPointThrowsException(Point $point) : void
55+
#[DataProvider('providerWriteEmptyPointWithoutNanSupportThrowsException')]
56+
public function testWriteEmptyPointWithoutNanSupportThrowsException(Point $point) : void
5757
{
58-
$writer = new EwkbWriter();
58+
$writer = new EwkbWriter(supportEmptyPointWithNan: false);
5959

6060
$this->expectException(GeometryIoException::class);
61+
$this->expectExceptionMessage(
62+
'Empty points have no WKB representation. If you want to output empty points with NaN coordinates ' .
63+
'(PostGIS-style), enable the $supportEmptyPointWithNan option.',
64+
);
65+
6166
$writer->write($point);
6267
}
6368

64-
public static function providerWriteEmptyPointThrowsException() : array
69+
public static function providerWriteEmptyPointWithoutNanSupportThrowsException() : array
6570
{
6671
return [
6772
[Point::xyEmpty()],
@@ -70,4 +75,27 @@ public static function providerWriteEmptyPointThrowsException() : array
7075
[Point::xyzmEmpty()]
7176
];
7277
}
78+
79+
#[DataProvider('providerWriteEmptyPointWithNanSupport')]
80+
public function testWriteEmptyPointWithNanSupport(Point $point, WkbByteOrder $byteOrder, string $expectedHex) : void
81+
{
82+
$writer = new EwkbWriter(byteOrder: $byteOrder);
83+
84+
$actualHex = bin2hex($writer->write($point));
85+
self::assertSame($expectedHex, $actualHex);
86+
}
87+
88+
public static function providerWriteEmptyPointWithNanSupport() : array
89+
{
90+
return [
91+
[Point::xyEmpty(), WkbByteOrder::BigEndian, '00000000017ff80000000000007ff8000000000000'],
92+
[Point::xyEmpty(), WkbByteOrder::LittleEndian, '0101000000000000000000f87f000000000000f87f'],
93+
[Point::xyzEmpty(), WkbByteOrder::BigEndian, '00800000017ff80000000000007ff80000000000007ff8000000000000'],
94+
[Point::xyzEmpty(), WkbByteOrder::LittleEndian, '0101000080000000000000f87f000000000000f87f000000000000f87f'],
95+
[Point::xymEmpty(), WkbByteOrder::BigEndian, '00400000017ff80000000000007ff80000000000007ff8000000000000'],
96+
[Point::xymEmpty(), WkbByteOrder::LittleEndian, '0101000040000000000000f87f000000000000f87f000000000000f87f'],
97+
[Point::xyzmEmpty(), WkbByteOrder::BigEndian, '00c00000017ff80000000000007ff80000000000007ff80000000000007ff8000000000000'],
98+
[Point::xyzmEmpty(), WkbByteOrder::LittleEndian, '01010000c0000000000000f87f000000000000f87f000000000000f87f000000000000f87f'],
99+
];
100+
}
73101
}

tests/IO/WkbReaderTest.php

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace Brick\Geo\Tests\IO;
66

7+
use Brick\Geo\Exception\GeometryIoException;
78
use Brick\Geo\Io\WkbReader;
89
use PHPUnit\Framework\Attributes\DataProvider;
910

@@ -36,4 +37,55 @@ public static function providerRead() : \Generator
3637
yield [$wkb, $wkt];
3738
}
3839
}
40+
41+
#[DataProvider('providerReadEmptyPointWithoutNanSupportThrowsException')]
42+
public function testReadEmptyPointWithoutNanSupportThrowsException(string $wkbHex) : void
43+
{
44+
$reader = new WkbReader();
45+
46+
$this->expectException(GeometryIoException::class);
47+
$this->expectExceptionMessage(
48+
'Points with NaN (not-a-number) coordinates are not supported. If you want to read points with NaN ' .
49+
'coordinates as empty points (PostGIS-style), enable the $supportEmptyPointWithNan option.',
50+
);
51+
52+
$reader->read(hex2bin($wkbHex));
53+
}
54+
55+
public static function providerReadEmptyPointWithoutNanSupportThrowsException() : array
56+
{
57+
return [
58+
'xy_BigEndian' => ['00000000017ff80000000000007ff8000000000000'],
59+
'xy_LittleEndian' => ['0101000000000000000000f87f000000000000f87f'],
60+
'xyz_BigEndian' => ['00000003e97ff80000000000007ff80000000000007ff8000000000000'],
61+
'xyz_LittleEndian' => ['01e9030000000000000000f87f000000000000f87f000000000000f87f'],
62+
'xym_BigEndian' => ['00000007d17ff80000000000007ff80000000000007ff8000000000000'],
63+
'xym_LittleEndian' => ['01d1070000000000000000f87f000000000000f87f000000000000f87f'],
64+
'xyzm_BigEndian' => ['0000000bb97ff80000000000007ff80000000000007ff80000000000007ff8000000000000'],
65+
'xyzm_LittleEndian' => ['01b90b0000000000000000f87f000000000000f87f000000000000f87f000000000000f87f'],
66+
];
67+
}
68+
69+
#[DataProvider('providerReadEmptyPointWithNanSupport')]
70+
public function testReadEmptyPointWithNanSupport(string $wkbHex, string $expectedWkt) : void
71+
{
72+
$reader = new WkbReader(supportEmptyPointWithNan: true);
73+
74+
$point = $reader->read(hex2bin($wkbHex));
75+
self::assertSame($expectedWkt, $point->asText());
76+
}
77+
78+
public static function providerReadEmptyPointWithNanSupport() : array
79+
{
80+
return [
81+
'xy_BigEndian' => ['00000000017ff80000000000007ff8000000000000', 'POINT EMPTY'],
82+
'xy_LittleEndian' => ['0101000000000000000000f87f000000000000f87f', 'POINT EMPTY'],
83+
'xyz_BigEndian' => ['00000003e97ff80000000000007ff80000000000007ff8000000000000', 'POINT Z EMPTY'],
84+
'xyz_LittleEndian' => ['01e9030000000000000000f87f000000000000f87f000000000000f87f', 'POINT Z EMPTY'],
85+
'xym_BigEndian' => ['00000007d17ff80000000000007ff80000000000007ff8000000000000', 'POINT M EMPTY'],
86+
'xym_LittleEndian' => ['01d1070000000000000000f87f000000000000f87f000000000000f87f', 'POINT M EMPTY'],
87+
'xyzm_BigEndian' => ['0000000bb97ff80000000000007ff80000000000007ff80000000000007ff8000000000000', 'POINT ZM EMPTY'],
88+
'xyzm_LittleEndian' => ['01b90b0000000000000000f87f000000000000f87f000000000000f87f000000000000f87f', 'POINT ZM EMPTY'],
89+
];
90+
}
3991
}

0 commit comments

Comments
 (0)