diff --git a/composer.json b/composer.json index 0212376..2b34305 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,7 @@ "license": "MIT", "require": { "php": "^8.4", - "longitude-one/spatial-types": "0.0.1-alpha.1" + "longitude-one/spatial-types": "0.0.1-alpha.2" }, "require-dev": { "phpunit/phpunit": "^13", diff --git a/docs/strategies.md b/docs/strategies.md index 66404a3..ec81ba0 100644 --- a/docs/strategies.md +++ b/docs/strategies.md @@ -312,10 +312,10 @@ under the same caller responsibilities described above. The approved MultiPoint contract rejects any EMPTY Point member with `UnsupportedGeometryStructureException`, including an aggregate made entirely -of EMPTY members. No member is omitted or replaced. The installed spatial-types -`0.0.1-alpha.1` model currently prevents constructing such aggregates: its shared -point-collection constructor throws `LongitudeOne\SpatialTypes\Exception\InvalidValueException` -before the writer is called. An empty aggregate with no members remains supported. +of EMPTY members. No member is omitted or replaced. The required spatial-types +`0.0.1-alpha.2` model supports constructing these aggregates, so the writer rejects +them through its public structural exception. An empty aggregate with no members +remains supported. The approved contracts for later geometry contributions preserve nested, singleton and homogeneous GeometryCollections. These deliberately do not apply diff --git a/tests/LongitudeOne/SpatialWriter/Tests/Unit/Strategy/GeoJson/GeographyMultiPointTest.php b/tests/LongitudeOne/SpatialWriter/Tests/Unit/Strategy/GeoJson/GeographyMultiPointTest.php index 1650163..dc2009c 100644 --- a/tests/LongitudeOne/SpatialWriter/Tests/Unit/Strategy/GeoJson/GeographyMultiPointTest.php +++ b/tests/LongitudeOne/SpatialWriter/Tests/Unit/Strategy/GeoJson/GeographyMultiPointTest.php @@ -18,7 +18,9 @@ use LongitudeOne\SpatialTypes\Reference\SpatialReference; use LongitudeOne\SpatialTypes\Types\Dimension2\Geography\MultiPoint; +use LongitudeOne\SpatialTypes\Types\Dimension2\Geography\Point; use LongitudeOne\SpatialWriter\Exception\UnsupportedDimensionException; +use LongitudeOne\SpatialWriter\Exception\UnsupportedGeometryStructureException; use LongitudeOne\SpatialWriter\Strategy\GeoJsonStrategy; use LongitudeOne\SpatialWriter\Writer; use PHPUnit\Framework\Attributes\CoversClass; @@ -109,6 +111,38 @@ public function testRejectEmptyXyzm(): void (new Writer(new GeoJsonStrategy()))->convert($multiPoint); } + /** Reject an EMPTY member after a non-empty position without returning partial output. */ + public function testRejectMixedEmptyMembersXy(): void + { + $multiPoint = new MultiPoint([new Point(1, 2), new Point()]); + $this->expectException(UnsupportedGeometryStructureException::class); + (new Writer(new GeoJsonStrategy()))->convert($multiPoint); + } + + /** Reject an EMPTY member after a non-empty position without returning partial output. */ + public function testRejectMixedEmptyMembersXyz(): void + { + $multiPoint = new \LongitudeOne\SpatialTypes\Types\Dimension3z\Geography\MultiPoint([new \LongitudeOne\SpatialTypes\Types\Dimension3z\Geography\Point(1, 2, 3), new \LongitudeOne\SpatialTypes\Types\Dimension3z\Geography\Point()]); + $this->expectException(UnsupportedGeometryStructureException::class); + (new Writer(new GeoJsonStrategy()))->convert($multiPoint); + } + + /** Distinguish an aggregate containing only EMPTY members from one with no members. */ + public function testRejectOnlyEmptyMembersXy(): void + { + $multiPoint = new MultiPoint([new Point(), new Point()]); + $this->expectException(UnsupportedGeometryStructureException::class); + (new Writer(new GeoJsonStrategy()))->convert($multiPoint); + } + + /** Distinguish an aggregate containing only EMPTY members from one with no members. */ + public function testRejectOnlyEmptyMembersXyz(): void + { + $multiPoint = new \LongitudeOne\SpatialTypes\Types\Dimension3z\Geography\MultiPoint([new \LongitudeOne\SpatialTypes\Types\Dimension3z\Geography\Point(), new \LongitudeOne\SpatialTypes\Types\Dimension3z\Geography\Point()]); + $this->expectException(UnsupportedGeometryStructureException::class); + (new Writer(new GeoJsonStrategy()))->convert($multiPoint); + } + /** Reject an incompatible concrete input without modifying it. */ public function testRejectXym(): void { diff --git a/tests/LongitudeOne/SpatialWriter/Tests/Unit/Strategy/GeoJson/GeometryMultiPointTest.php b/tests/LongitudeOne/SpatialWriter/Tests/Unit/Strategy/GeoJson/GeometryMultiPointTest.php index 675e91a..63cbbea 100644 --- a/tests/LongitudeOne/SpatialWriter/Tests/Unit/Strategy/GeoJson/GeometryMultiPointTest.php +++ b/tests/LongitudeOne/SpatialWriter/Tests/Unit/Strategy/GeoJson/GeometryMultiPointTest.php @@ -18,7 +18,9 @@ use LongitudeOne\SpatialTypes\Reference\SpatialReference; use LongitudeOne\SpatialTypes\Types\Dimension2\Geometry\MultiPoint; +use LongitudeOne\SpatialTypes\Types\Dimension2\Geometry\Point; use LongitudeOne\SpatialWriter\Exception\UnsupportedDimensionException; +use LongitudeOne\SpatialWriter\Exception\UnsupportedGeometryStructureException; use LongitudeOne\SpatialWriter\Strategy\GeoJsonStrategy; use LongitudeOne\SpatialWriter\Writer; use PHPUnit\Framework\Attributes\CoversClass; @@ -109,6 +111,38 @@ public function testRejectEmptyXyzm(): void (new Writer(new GeoJsonStrategy()))->convert($multiPoint); } + /** Reject an EMPTY member after a non-empty position without returning partial output. */ + public function testRejectMixedEmptyMembersXy(): void + { + $multiPoint = new MultiPoint([new Point(1, 2), new Point()]); + $this->expectException(UnsupportedGeometryStructureException::class); + (new Writer(new GeoJsonStrategy()))->convert($multiPoint); + } + + /** Reject an EMPTY member after a non-empty position without returning partial output. */ + public function testRejectMixedEmptyMembersXyz(): void + { + $multiPoint = new \LongitudeOne\SpatialTypes\Types\Dimension3z\Geometry\MultiPoint([new \LongitudeOne\SpatialTypes\Types\Dimension3z\Geometry\Point(1, 2, 3), new \LongitudeOne\SpatialTypes\Types\Dimension3z\Geometry\Point()]); + $this->expectException(UnsupportedGeometryStructureException::class); + (new Writer(new GeoJsonStrategy()))->convert($multiPoint); + } + + /** Distinguish an aggregate containing only EMPTY members from one with no members. */ + public function testRejectOnlyEmptyMembersXy(): void + { + $multiPoint = new MultiPoint([new Point(), new Point()]); + $this->expectException(UnsupportedGeometryStructureException::class); + (new Writer(new GeoJsonStrategy()))->convert($multiPoint); + } + + /** Distinguish an aggregate containing only EMPTY members from one with no members. */ + public function testRejectOnlyEmptyMembersXyz(): void + { + $multiPoint = new \LongitudeOne\SpatialTypes\Types\Dimension3z\Geometry\MultiPoint([new \LongitudeOne\SpatialTypes\Types\Dimension3z\Geometry\Point(), new \LongitudeOne\SpatialTypes\Types\Dimension3z\Geometry\Point()]); + $this->expectException(UnsupportedGeometryStructureException::class); + (new Writer(new GeoJsonStrategy()))->convert($multiPoint); + } + /** Reject an incompatible concrete input without modifying it. */ public function testRejectXym(): void { diff --git a/tests/README.md b/tests/README.md index 7bb43e2..f794eb3 100644 --- a/tests/README.md +++ b/tests/README.md @@ -184,3 +184,6 @@ failure and the unchanged internal exception for a missing LineString interface. MultiPoint examples cover both families in XY/XYZ, empty aggregates, singletons, duplicate positions, member and ordinate order, omitted reference metadata, and XYM/XYZM rejection including empty aggregates. +Concrete XY/XYZ examples also verify rejection of mixed non-empty/EMPTY members +and aggregates containing only EMPTY Points in both families, using spatial-types +`0.0.1-alpha.2`.