From 64acd247bc3055b84b0bdd00cf71738da5c4f93f Mon Sep 17 00:00:00 2001 From: Alexandre Tranchant Date: Mon, 21 Sep 2026 19:29:02 +0200 Subject: [PATCH] feat(geojson): encode MultiLineString geometries --- docs/strategies.md | 32 ++- .../Strategy/GeoJsonStrategy.php | 25 ++ .../GeoJson/ExceptionContractTest.php | 26 ++ .../GeoJson/GeographyMultiLineStringTest.php | 226 ++++++++++++++++++ .../GeoJson/GeometryMultiLineStringTest.php | 226 ++++++++++++++++++ 5 files changed, 532 insertions(+), 3 deletions(-) create mode 100644 tests/LongitudeOne/SpatialWriter/Tests/Unit/Strategy/GeoJson/GeographyMultiLineStringTest.php create mode 100644 tests/LongitudeOne/SpatialWriter/Tests/Unit/Strategy/GeoJson/GeometryMultiLineStringTest.php diff --git a/docs/strategies.md b/docs/strategies.md index ec81ba0..2d81f9d 100644 --- a/docs/strategies.md +++ b/docs/strategies.md @@ -13,7 +13,7 @@ This table describes the capabilities of the repository's current implementation | Class. | Output | SRID in output | Dimensions written | Usage | | --------------------- | ------------------------------- | ----------------- | ------------------ | ---------------------------------------------------------------- | -| `GeoJsonStrategy` | GeoJSON geometry text | No | XY, XYZ | Point, LineString and MultiPoint encoding according to RFC 7946 | +| `GeoJsonStrategy` | GeoJSON geometry text | No | XY, XYZ | Point, LineString, MultiPoint and MultiLineString encoding according to RFC 7946 | | `WktTextStrategy` | WKT Well Known Text | No | XY, XYZ, XYM, XYZM | Display, text exchanges, functions accepting WKT | | `EwktTextStrategy` | EWKT Extended Well Known Text. | Yes, when nonzero | XY, XYZ, XYM, XYZM | Text exchanges that include the spatial reference | | `WkbBinaryStrategy` | ISO WKB Well Known Binary. | No | XY, XYZ, XYM, XYZM | Exchanges with a standard WKB consumer | @@ -235,7 +235,7 @@ and the [testing guide](../tests/README.md). To add a format, implement ## GeoJSON — `GeoJsonStrategy` -This increment supports Geometry and Geography Points, LineStrings and MultiPoints in XY and +This increment supports Geometry and Geography Points, LineStrings, MultiPoints and MultiLineStrings in XY and XYZ, including EMPTY. Other geometry types are not yet supported. Feature and FeatureCollection composition belongs to the consuming application. @@ -294,7 +294,7 @@ antimeridian, such as `[[170,45],[-170,45]]`, are encoded unchanged, without cutting or detection for rejection. This deliberately does not apply the SHOULD in RFC 7946 section 3.1.9: consumers may interpret or display an uncut line as crossing the long way around the globe. Callers must prepare any cutting required -by their consumers before encoding; this increment does not support MultiLineString. +by their consumers before encoding. MultiPoints preserve the number and order of their positions, including duplicates and Z. A singleton remains a MultiPoint rather than becoming a Point: @@ -317,6 +317,31 @@ of EMPTY members. No member is omitted or replaced. The required spatial-types them through its public structural exception. An empty aggregate with no members remains supported. +MultiLineStrings preserve each supplied line, its position order and Z, and the +order of the members. A single member remains wrapped as a MultiLineString: + +```php +$multiLine = new \LongitudeOne\SpatialTypes\Types\Dimension2\Geometry\MultiLineString([ + [[1, 2], [3, 4]], + [[5, 6], [7, 8]], +]); +echo $writer->convert($multiLine); +// {"type":"MultiLineString","coordinates":[[[1,2],[3,4]],[[5,6],[7,8]]]} +``` + +An XY or XYZ MultiLineString with no members produces +`{"type":"MultiLineString","coordinates":[]}`. Any EMPTY LineString member, +including an aggregate containing only EMPTY members, throws +`UnsupportedGeometryStructureException`. A member with only one position also +throws that exception, as a GeoJSON line requires at least two positions. +The model permits these line members; the writer rejects them without omitting, +flattening or repairing them, and never returns partial output or nested empty +coordinate arrays. XYM and XYZM are rejected even with no members. + +Antimeridian-crossing members are preserved unchanged under the deliberate +non-application of RFC 7946 section 3.1.9 described above. The same caller +responsibilities, reference omission and JSON failure contract apply. + The approved contracts for later geometry contributions preserve nested, singleton and homogeneous GeometryCollections. These deliberately do not apply the SHOULD guidance of RFC 7946 section 3.1.8. Consumers may have limited support @@ -326,5 +351,6 @@ this increment. References: [RFC 7946 geometry objects and positions](https://www.rfc-editor.org/rfc/rfc7946.html#section-3.1), [MultiPoint positions](https://www.rfc-editor.org/rfc/rfc7946.html#section-3.1.3), +[MultiLineString members](https://www.rfc-editor.org/rfc/rfc7946.html#section-3.1.5), [coordinate reference system](https://www.rfc-editor.org/rfc/rfc7946.html#section-4), [non-extensible types](https://www.rfc-editor.org/rfc/rfc7946.html#section-7). diff --git a/lib/LongitudeOne/SpatialWriter/Strategy/GeoJsonStrategy.php b/lib/LongitudeOne/SpatialWriter/Strategy/GeoJsonStrategy.php index a2630b7..0e6ddf9 100644 --- a/lib/LongitudeOne/SpatialWriter/Strategy/GeoJsonStrategy.php +++ b/lib/LongitudeOne/SpatialWriter/Strategy/GeoJsonStrategy.php @@ -18,6 +18,7 @@ use LongitudeOne\Core\Enum\GeometryTypeEnum; use LongitudeOne\SpatialTypes\Interfaces\LineStringInterface; +use LongitudeOne\SpatialTypes\Interfaces\MultiLineStringInterface; use LongitudeOne\SpatialTypes\Interfaces\MultiPointInterface; use LongitudeOne\SpatialTypes\Interfaces\PointInterface; use LongitudeOne\SpatialTypes\Interfaces\SpatialInterface; @@ -53,6 +54,7 @@ public function executeStrategy(SpatialInterface $spatial): string GeometryTypeEnum::POINT => $this->encodePoint($spatial), GeometryTypeEnum::LINESTRING => $this->encodeLineString($spatial), GeometryTypeEnum::MULTIPOINT => $this->encodeMultiPoint($spatial), + GeometryTypeEnum::MULTILINESTRING => $this->encodeMultiLineString($spatial), default => throw new UnsupportedSpatialTypeException('This GeoJSON strategy does not support '.$spatial->getType()->name.'.'), }; @@ -84,6 +86,29 @@ private function encodeLineString(SpatialInterface $spatial): array return ['type' => 'LineString', 'coordinates' => $coordinates]; } + /** + * Preserve line membership and order, rejecting EMPTY and singleton members. + * + * @param SpatialInterface $spatial the multi-line string to encode + * + * @return array{type: string, coordinates: (float|int)[][][]} + */ + private function encodeMultiLineString(SpatialInterface $spatial): array + { + if (!$spatial instanceof MultiLineStringInterface) { + throw new UnsupportedSpatialInterfaceException($spatial::class); + } + + $coordinates = $spatial->toArray(); + foreach ($coordinates as $line) { + if (count($line) < 2) { + throw new UnsupportedGeometryStructureException('Every GeoJSON MultiLineString member requires at least two positions.'); + } + } + + return ['type' => 'MultiLineString', 'coordinates' => $coordinates]; + } + /** * Preserve all positions, rejecting EMPTY members without omitting them. * diff --git a/tests/LongitudeOne/SpatialWriter/Tests/Unit/Strategy/GeoJson/ExceptionContractTest.php b/tests/LongitudeOne/SpatialWriter/Tests/Unit/Strategy/GeoJson/ExceptionContractTest.php index a5fa0c7..2a3533f 100644 --- a/tests/LongitudeOne/SpatialWriter/Tests/Unit/Strategy/GeoJson/ExceptionContractTest.php +++ b/tests/LongitudeOne/SpatialWriter/Tests/Unit/Strategy/GeoJson/ExceptionContractTest.php @@ -19,6 +19,7 @@ use LongitudeOne\Core\Enum\GeometryTypeEnum; use LongitudeOne\SpatialTypes\Interfaces\SpatialInterface; use LongitudeOne\SpatialTypes\Types\Dimension2\Geometry\LineString; +use LongitudeOne\SpatialTypes\Types\Dimension2\Geometry\MultiLineString; use LongitudeOne\SpatialTypes\Types\Dimension2\Geometry\MultiPoint; use LongitudeOne\SpatialTypes\Types\Dimension2\Geometry\Point; use LongitudeOne\SpatialWriter\Exception\ExceptionInterface; @@ -90,6 +91,20 @@ public function testLineStringEncodingFailure(): void } } + /** Non-finite line members use the shared JSON failure contract. */ + public function testMultiLineStringEncodingFailure(): void + { + $multiLine = new MultiLineString([[[1, 2], [3, 4]], [[5, 6], [INF, 8]]]); + + try { + (new Writer(new GeoJsonStrategy()))->convert($multiLine); + static::fail('JSON encoding must fail.'); + } catch (JsonEncodingException $exception) { + static::assertInstanceOf(\JsonException::class, $exception->getPrevious()); + static::assertSame(JSON_ERROR_INF_OR_NAN, $exception->getPrevious()->getCode()); + } + } + /** Non-finite MultiPoint members use the shared JSON failure contract. */ public function testMultiPointEncodingFailure(): void { @@ -175,6 +190,17 @@ public function testUnsupportedLineStringInterfaceThroughWriter(): void (new Writer(new GeoJsonStrategy()))->convert($spatial); } + /** Reject a declared MultiLineString without its corresponding interface. */ + public function testUnsupportedMultiLineStringInterfaceThroughWriter(): void + { + $spatial = static::createStub(SpatialInterface::class); + $spatial->method('getType')->willReturn(GeometryTypeEnum::MULTILINESTRING); + $spatial->method('hasM')->willReturn(false); + + $this->expectException(UnsupportedSpatialInterfaceException::class); + (new Writer(new GeoJsonStrategy()))->convert($spatial); + } + /** Reject a declared MultiPoint without its corresponding interface. */ public function testUnsupportedMultiPointInterfaceThroughWriter(): void { diff --git a/tests/LongitudeOne/SpatialWriter/Tests/Unit/Strategy/GeoJson/GeographyMultiLineStringTest.php b/tests/LongitudeOne/SpatialWriter/Tests/Unit/Strategy/GeoJson/GeographyMultiLineStringTest.php new file mode 100644 index 0000000..c19a82e --- /dev/null +++ b/tests/LongitudeOne/SpatialWriter/Tests/Unit/Strategy/GeoJson/GeographyMultiLineStringTest.php @@ -0,0 +1,226 @@ + 2024-2026 + * Copyright Longitude One 2024-2026 + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + * + */ + +declare(strict_types=1); + +namespace LongitudeOne\SpatialWriter\Tests\Unit\Strategy\GeoJson; + +use LongitudeOne\SpatialTypes\Reference\SpatialReference; +use LongitudeOne\SpatialTypes\Types\Dimension2\Geography\LineString; +use LongitudeOne\SpatialTypes\Types\Dimension2\Geography\MultiLineString; +use LongitudeOne\SpatialWriter\Exception\UnsupportedDimensionException; +use LongitudeOne\SpatialWriter\Exception\UnsupportedGeometryStructureException; +use LongitudeOne\SpatialWriter\Strategy\GeoJsonStrategy; +use LongitudeOne\SpatialWriter\Writer; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\TestCase; + +/** + * Explicit Geography MultiLineString GeoJSON examples. + * + * @internal + */ +#[CoversClass(GeoJsonStrategy::class)] +#[CoversClass(Writer::class)] +class GeographyMultiLineStringTest extends TestCase +{ + /** Preserve line membership, coordinates and order without reference metadata. */ + public function testAntimeridian(): void + { + $multiLine = new MultiLineString([[[170, 45], [-170, 45]], [[1, 2], [3, 4]]], 4326); + + static::assertJsonStringEqualsJsonString('{"type":"MultiLineString","coordinates":[[[170,45],[-170,45]],[[1,2],[3,4]]]}', (new Writer(new GeoJsonStrategy()))->convert($multiLine)); + static::assertJsonStringEqualsJsonString('{"type":"MultiLineString","coordinates":[[[170,45],[-170,45]],[[1,2],[3,4]]]}', (new GeoJsonStrategy())->executeStrategy($multiLine)); + } + + /** Preserve line membership, coordinates and order without reference metadata. */ + public function testDeclaredReference(): void + { + $multiLine = new \LongitudeOne\SpatialTypes\Types\Dimension3z\Geography\MultiLineString([[[1, 2, 3], [4, 5, 6]]], new SpatialReference(4326, 'EPSG')); + + static::assertJsonStringEqualsJsonString('{"type":"MultiLineString","coordinates":[[[1,2,3],[4,5,6]]]}', (new Writer(new GeoJsonStrategy()))->convert($multiLine)); + static::assertJsonStringEqualsJsonString('{"type":"MultiLineString","coordinates":[[[1,2,3],[4,5,6]]]}', (new GeoJsonStrategy())->executeStrategy($multiLine)); + } + + /** Preserve line membership, coordinates and order without reference metadata. */ + public function testEmptyXy(): void + { + $multiLine = new MultiLineString([]); + + static::assertJsonStringEqualsJsonString('{"type":"MultiLineString","coordinates":[]}', (new Writer(new GeoJsonStrategy()))->convert($multiLine)); + static::assertJsonStringEqualsJsonString('{"type":"MultiLineString","coordinates":[]}', (new GeoJsonStrategy())->executeStrategy($multiLine)); + } + + /** Preserve line membership, coordinates and order without reference metadata. */ + public function testEmptyXyz(): void + { + $multiLine = new \LongitudeOne\SpatialTypes\Types\Dimension3z\Geography\MultiLineString([]); + + static::assertJsonStringEqualsJsonString('{"type":"MultiLineString","coordinates":[]}', (new Writer(new GeoJsonStrategy()))->convert($multiLine)); + static::assertJsonStringEqualsJsonString('{"type":"MultiLineString","coordinates":[]}', (new GeoJsonStrategy())->executeStrategy($multiLine)); + } + + /** Preserve line membership, coordinates and order without reference metadata. */ + public function testMemberAndPositionOrder(): void + { + $multiLine = new MultiLineString([[[7.5, 8], [5, 6], [-2, 1]], [[3, 4], [1, 2]]]); + + static::assertJsonStringEqualsJsonString('{"type":"MultiLineString","coordinates":[[[7.5,8],[5,6],[-2,1]],[[3,4],[1,2]]]}', (new Writer(new GeoJsonStrategy()))->convert($multiLine)); + static::assertJsonStringEqualsJsonString('{"type":"MultiLineString","coordinates":[[[7.5,8],[5,6],[-2,1]],[[3,4],[1,2]]]}', (new GeoJsonStrategy())->executeStrategy($multiLine)); + } + + /** Preserve line membership, coordinates and order without reference metadata. */ + public function testOtherSrid(): void + { + $multiLine = new MultiLineString([new LineString([[1, 2], [3, 4]], 3857)], 3857); + + static::assertJsonStringEqualsJsonString('{"type":"MultiLineString","coordinates":[[[1,2],[3,4]]]}', (new Writer(new GeoJsonStrategy()))->convert($multiLine)); + static::assertJsonStringEqualsJsonString('{"type":"MultiLineString","coordinates":[[[1,2],[3,4]]]}', (new GeoJsonStrategy())->executeStrategy($multiLine)); + } + + /** Reject an incompatible line member instead of returning partial output. */ + public function testRejectAllEmpty(): void + { + $multiLine = new MultiLineString([new LineString([]), new LineString([])]); + + $this->expectException(UnsupportedGeometryStructureException::class); + (new Writer(new GeoJsonStrategy()))->convert($multiLine); + } + + /** Reject an incompatible line member instead of returning partial output. */ + public function testRejectAllEmptyXyz(): void + { + $multiLine = new \LongitudeOne\SpatialTypes\Types\Dimension3z\Geography\MultiLineString([[], []]); + + $this->expectException(UnsupportedGeometryStructureException::class); + (new Writer(new GeoJsonStrategy()))->convert($multiLine); + } + + /** Reject an incompatible line member instead of returning partial output. */ + public function testRejectEmptyFirst(): void + { + $multiLine = new MultiLineString([new LineString([]), new LineString([[1, 2], [3, 4]])]); + + $this->expectException(UnsupportedGeometryStructureException::class); + (new Writer(new GeoJsonStrategy()))->convert($multiLine); + } + + /** Reject an incompatible line member instead of returning partial output. */ + public function testRejectEmptyLast(): void + { + $multiLine = new MultiLineString([new LineString([[1, 2], [3, 4]]), new LineString([])]); + + $this->expectException(UnsupportedGeometryStructureException::class); + (new Writer(new GeoJsonStrategy()))->convert($multiLine); + } + + /** Reject measured dimensions even when no line members are present. */ + public function testRejectEmptyXym(): void + { + $multiLine = new \LongitudeOne\SpatialTypes\Types\Dimension3m\Geography\MultiLineString([]); + + $this->expectException(UnsupportedDimensionException::class); + (new Writer(new GeoJsonStrategy()))->convert($multiLine); + } + + /** Reject measured dimensions even when no line members are present. */ + public function testRejectEmptyXyzm(): void + { + $multiLine = new \LongitudeOne\SpatialTypes\Types\Dimension4zm\Geography\MultiLineString([]); + + $this->expectException(UnsupportedDimensionException::class); + (new Writer(new GeoJsonStrategy()))->convert($multiLine); + } + + /** Reject an incompatible line member instead of returning partial output. */ + public function testRejectEmptyXyzMember(): void + { + $multiLine = new \LongitudeOne\SpatialTypes\Types\Dimension3z\Geography\MultiLineString([[[1, 2, 3], [4, 5, 6]], []]); + + $this->expectException(UnsupportedGeometryStructureException::class); + (new Writer(new GeoJsonStrategy()))->convert($multiLine); + } + + /** Reject an incompatible line member instead of returning partial output. */ + public function testRejectSingletonMember(): void + { + $multiLine = new MultiLineString([[[1, 2], [3, 4]], [[5, 6]]]); + + $this->expectException(UnsupportedGeometryStructureException::class); + (new Writer(new GeoJsonStrategy()))->convert($multiLine); + } + + /** Reject an incompatible line member instead of returning partial output. */ + public function testRejectSingletonXyzMember(): void + { + $multiLine = new \LongitudeOne\SpatialTypes\Types\Dimension3z\Geography\MultiLineString([[[1, 2, 3]]]); + + $this->expectException(UnsupportedGeometryStructureException::class); + (new Writer(new GeoJsonStrategy()))->convert($multiLine); + } + + /** Reject measured dimensions even when no line members are present. */ + public function testRejectXym(): void + { + $multiLine = new \LongitudeOne\SpatialTypes\Types\Dimension3m\Geography\MultiLineString([[[1, 2, 3], [4, 5, 6]]]); + + $this->expectException(UnsupportedDimensionException::class); + (new Writer(new GeoJsonStrategy()))->convert($multiLine); + } + + /** Reject measured dimensions even when no line members are present. */ + public function testRejectXyzm(): void + { + $multiLine = new \LongitudeOne\SpatialTypes\Types\Dimension4zm\Geography\MultiLineString([[[1, 2, 3, 4], [5, 6, 7, 8]]]); + + $this->expectException(UnsupportedDimensionException::class); + (new Writer(new GeoJsonStrategy()))->convert($multiLine); + } + + /** Preserve line membership, coordinates and order without reference metadata. */ + public function testSingleMember(): void + { + $multiLine = new MultiLineString([[[3, 4], [1, 2]]]); + + static::assertJsonStringEqualsJsonString('{"type":"MultiLineString","coordinates":[[[3,4],[1,2]]]}', (new Writer(new GeoJsonStrategy()))->convert($multiLine)); + static::assertJsonStringEqualsJsonString('{"type":"MultiLineString","coordinates":[[[3,4],[1,2]]]}', (new GeoJsonStrategy())->executeStrategy($multiLine)); + } + + /** Preserve line membership, coordinates and order without reference metadata. */ + public function testUnknownReference(): void + { + $multiLine = new MultiLineString([[[1, 2], [3, 4]]], new SpatialReference(999999, 'CUSTOM')); + + static::assertJsonStringEqualsJsonString('{"type":"MultiLineString","coordinates":[[[1,2],[3,4]]]}', (new Writer(new GeoJsonStrategy()))->convert($multiLine)); + static::assertJsonStringEqualsJsonString('{"type":"MultiLineString","coordinates":[[[1,2],[3,4]]]}', (new GeoJsonStrategy())->executeStrategy($multiLine)); + } + + /** Preserve line membership, coordinates and order without reference metadata. */ + public function testXy(): void + { + $multiLine = new MultiLineString([[[1, 2], [3, 4]], [[5, 6], [7, 8]]]); + + static::assertJsonStringEqualsJsonString('{"type":"MultiLineString","coordinates":[[[1,2],[3,4]],[[5,6],[7,8]]]}', (new Writer(new GeoJsonStrategy()))->convert($multiLine)); + static::assertJsonStringEqualsJsonString('{"type":"MultiLineString","coordinates":[[[1,2],[3,4]],[[5,6],[7,8]]]}', (new GeoJsonStrategy())->executeStrategy($multiLine)); + } + + /** Preserve line membership, coordinates and order without reference metadata. */ + public function testXyz(): void + { + $multiLine = new \LongitudeOne\SpatialTypes\Types\Dimension3z\Geography\MultiLineString([[[1, 2, 3], [4, 5, 6]], [[7, 8, 9], [10, 11, 12]]]); + + static::assertJsonStringEqualsJsonString('{"type":"MultiLineString","coordinates":[[[1,2,3],[4,5,6]],[[7,8,9],[10,11,12]]]}', (new Writer(new GeoJsonStrategy()))->convert($multiLine)); + static::assertJsonStringEqualsJsonString('{"type":"MultiLineString","coordinates":[[[1,2,3],[4,5,6]],[[7,8,9],[10,11,12]]]}', (new GeoJsonStrategy())->executeStrategy($multiLine)); + } +} diff --git a/tests/LongitudeOne/SpatialWriter/Tests/Unit/Strategy/GeoJson/GeometryMultiLineStringTest.php b/tests/LongitudeOne/SpatialWriter/Tests/Unit/Strategy/GeoJson/GeometryMultiLineStringTest.php new file mode 100644 index 0000000..1310156 --- /dev/null +++ b/tests/LongitudeOne/SpatialWriter/Tests/Unit/Strategy/GeoJson/GeometryMultiLineStringTest.php @@ -0,0 +1,226 @@ + 2024-2026 + * Copyright Longitude One 2024-2026 + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + * + */ + +declare(strict_types=1); + +namespace LongitudeOne\SpatialWriter\Tests\Unit\Strategy\GeoJson; + +use LongitudeOne\SpatialTypes\Reference\SpatialReference; +use LongitudeOne\SpatialTypes\Types\Dimension2\Geometry\LineString; +use LongitudeOne\SpatialTypes\Types\Dimension2\Geometry\MultiLineString; +use LongitudeOne\SpatialWriter\Exception\UnsupportedDimensionException; +use LongitudeOne\SpatialWriter\Exception\UnsupportedGeometryStructureException; +use LongitudeOne\SpatialWriter\Strategy\GeoJsonStrategy; +use LongitudeOne\SpatialWriter\Writer; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\TestCase; + +/** + * Explicit Geometry MultiLineString GeoJSON examples. + * + * @internal + */ +#[CoversClass(GeoJsonStrategy::class)] +#[CoversClass(Writer::class)] +class GeometryMultiLineStringTest extends TestCase +{ + /** Preserve line membership, coordinates and order without reference metadata. */ + public function testAntimeridian(): void + { + $multiLine = new MultiLineString([[[170, 45], [-170, 45]], [[1, 2], [3, 4]]], 4326); + + static::assertJsonStringEqualsJsonString('{"type":"MultiLineString","coordinates":[[[170,45],[-170,45]],[[1,2],[3,4]]]}', (new Writer(new GeoJsonStrategy()))->convert($multiLine)); + static::assertJsonStringEqualsJsonString('{"type":"MultiLineString","coordinates":[[[170,45],[-170,45]],[[1,2],[3,4]]]}', (new GeoJsonStrategy())->executeStrategy($multiLine)); + } + + /** Preserve line membership, coordinates and order without reference metadata. */ + public function testDeclaredReference(): void + { + $multiLine = new \LongitudeOne\SpatialTypes\Types\Dimension3z\Geometry\MultiLineString([[[1, 2, 3], [4, 5, 6]]], new SpatialReference(4326, 'EPSG')); + + static::assertJsonStringEqualsJsonString('{"type":"MultiLineString","coordinates":[[[1,2,3],[4,5,6]]]}', (new Writer(new GeoJsonStrategy()))->convert($multiLine)); + static::assertJsonStringEqualsJsonString('{"type":"MultiLineString","coordinates":[[[1,2,3],[4,5,6]]]}', (new GeoJsonStrategy())->executeStrategy($multiLine)); + } + + /** Preserve line membership, coordinates and order without reference metadata. */ + public function testEmptyXy(): void + { + $multiLine = new MultiLineString([]); + + static::assertJsonStringEqualsJsonString('{"type":"MultiLineString","coordinates":[]}', (new Writer(new GeoJsonStrategy()))->convert($multiLine)); + static::assertJsonStringEqualsJsonString('{"type":"MultiLineString","coordinates":[]}', (new GeoJsonStrategy())->executeStrategy($multiLine)); + } + + /** Preserve line membership, coordinates and order without reference metadata. */ + public function testEmptyXyz(): void + { + $multiLine = new \LongitudeOne\SpatialTypes\Types\Dimension3z\Geometry\MultiLineString([]); + + static::assertJsonStringEqualsJsonString('{"type":"MultiLineString","coordinates":[]}', (new Writer(new GeoJsonStrategy()))->convert($multiLine)); + static::assertJsonStringEqualsJsonString('{"type":"MultiLineString","coordinates":[]}', (new GeoJsonStrategy())->executeStrategy($multiLine)); + } + + /** Preserve line membership, coordinates and order without reference metadata. */ + public function testMemberAndPositionOrder(): void + { + $multiLine = new MultiLineString([[[7.5, 8], [5, 6], [-2, 1]], [[3, 4], [1, 2]]]); + + static::assertJsonStringEqualsJsonString('{"type":"MultiLineString","coordinates":[[[7.5,8],[5,6],[-2,1]],[[3,4],[1,2]]]}', (new Writer(new GeoJsonStrategy()))->convert($multiLine)); + static::assertJsonStringEqualsJsonString('{"type":"MultiLineString","coordinates":[[[7.5,8],[5,6],[-2,1]],[[3,4],[1,2]]]}', (new GeoJsonStrategy())->executeStrategy($multiLine)); + } + + /** Preserve line membership, coordinates and order without reference metadata. */ + public function testOtherSrid(): void + { + $multiLine = new MultiLineString([new LineString([[1, 2], [3, 4]], 3857)], 3857); + + static::assertJsonStringEqualsJsonString('{"type":"MultiLineString","coordinates":[[[1,2],[3,4]]]}', (new Writer(new GeoJsonStrategy()))->convert($multiLine)); + static::assertJsonStringEqualsJsonString('{"type":"MultiLineString","coordinates":[[[1,2],[3,4]]]}', (new GeoJsonStrategy())->executeStrategy($multiLine)); + } + + /** Reject an incompatible line member instead of returning partial output. */ + public function testRejectAllEmpty(): void + { + $multiLine = new MultiLineString([new LineString([]), new LineString([])]); + + $this->expectException(UnsupportedGeometryStructureException::class); + (new Writer(new GeoJsonStrategy()))->convert($multiLine); + } + + /** Reject an incompatible line member instead of returning partial output. */ + public function testRejectAllEmptyXyz(): void + { + $multiLine = new \LongitudeOne\SpatialTypes\Types\Dimension3z\Geometry\MultiLineString([[], []]); + + $this->expectException(UnsupportedGeometryStructureException::class); + (new Writer(new GeoJsonStrategy()))->convert($multiLine); + } + + /** Reject an incompatible line member instead of returning partial output. */ + public function testRejectEmptyFirst(): void + { + $multiLine = new MultiLineString([new LineString([]), new LineString([[1, 2], [3, 4]])]); + + $this->expectException(UnsupportedGeometryStructureException::class); + (new Writer(new GeoJsonStrategy()))->convert($multiLine); + } + + /** Reject an incompatible line member instead of returning partial output. */ + public function testRejectEmptyLast(): void + { + $multiLine = new MultiLineString([new LineString([[1, 2], [3, 4]]), new LineString([])]); + + $this->expectException(UnsupportedGeometryStructureException::class); + (new Writer(new GeoJsonStrategy()))->convert($multiLine); + } + + /** Reject measured dimensions even when no line members are present. */ + public function testRejectEmptyXym(): void + { + $multiLine = new \LongitudeOne\SpatialTypes\Types\Dimension3m\Geometry\MultiLineString([]); + + $this->expectException(UnsupportedDimensionException::class); + (new Writer(new GeoJsonStrategy()))->convert($multiLine); + } + + /** Reject measured dimensions even when no line members are present. */ + public function testRejectEmptyXyzm(): void + { + $multiLine = new \LongitudeOne\SpatialTypes\Types\Dimension4zm\Geometry\MultiLineString([]); + + $this->expectException(UnsupportedDimensionException::class); + (new Writer(new GeoJsonStrategy()))->convert($multiLine); + } + + /** Reject an incompatible line member instead of returning partial output. */ + public function testRejectEmptyXyzMember(): void + { + $multiLine = new \LongitudeOne\SpatialTypes\Types\Dimension3z\Geometry\MultiLineString([[[1, 2, 3], [4, 5, 6]], []]); + + $this->expectException(UnsupportedGeometryStructureException::class); + (new Writer(new GeoJsonStrategy()))->convert($multiLine); + } + + /** Reject an incompatible line member instead of returning partial output. */ + public function testRejectSingletonMember(): void + { + $multiLine = new MultiLineString([[[1, 2], [3, 4]], [[5, 6]]]); + + $this->expectException(UnsupportedGeometryStructureException::class); + (new Writer(new GeoJsonStrategy()))->convert($multiLine); + } + + /** Reject an incompatible line member instead of returning partial output. */ + public function testRejectSingletonXyzMember(): void + { + $multiLine = new \LongitudeOne\SpatialTypes\Types\Dimension3z\Geometry\MultiLineString([[[1, 2, 3]]]); + + $this->expectException(UnsupportedGeometryStructureException::class); + (new Writer(new GeoJsonStrategy()))->convert($multiLine); + } + + /** Reject measured dimensions even when no line members are present. */ + public function testRejectXym(): void + { + $multiLine = new \LongitudeOne\SpatialTypes\Types\Dimension3m\Geometry\MultiLineString([[[1, 2, 3], [4, 5, 6]]]); + + $this->expectException(UnsupportedDimensionException::class); + (new Writer(new GeoJsonStrategy()))->convert($multiLine); + } + + /** Reject measured dimensions even when no line members are present. */ + public function testRejectXyzm(): void + { + $multiLine = new \LongitudeOne\SpatialTypes\Types\Dimension4zm\Geometry\MultiLineString([[[1, 2, 3, 4], [5, 6, 7, 8]]]); + + $this->expectException(UnsupportedDimensionException::class); + (new Writer(new GeoJsonStrategy()))->convert($multiLine); + } + + /** Preserve line membership, coordinates and order without reference metadata. */ + public function testSingleMember(): void + { + $multiLine = new MultiLineString([[[3, 4], [1, 2]]]); + + static::assertJsonStringEqualsJsonString('{"type":"MultiLineString","coordinates":[[[3,4],[1,2]]]}', (new Writer(new GeoJsonStrategy()))->convert($multiLine)); + static::assertJsonStringEqualsJsonString('{"type":"MultiLineString","coordinates":[[[3,4],[1,2]]]}', (new GeoJsonStrategy())->executeStrategy($multiLine)); + } + + /** Preserve line membership, coordinates and order without reference metadata. */ + public function testUnknownReference(): void + { + $multiLine = new MultiLineString([[[1, 2], [3, 4]]], new SpatialReference(999999, 'CUSTOM')); + + static::assertJsonStringEqualsJsonString('{"type":"MultiLineString","coordinates":[[[1,2],[3,4]]]}', (new Writer(new GeoJsonStrategy()))->convert($multiLine)); + static::assertJsonStringEqualsJsonString('{"type":"MultiLineString","coordinates":[[[1,2],[3,4]]]}', (new GeoJsonStrategy())->executeStrategy($multiLine)); + } + + /** Preserve line membership, coordinates and order without reference metadata. */ + public function testXy(): void + { + $multiLine = new MultiLineString([[[1, 2], [3, 4]], [[5, 6], [7, 8]]]); + + static::assertJsonStringEqualsJsonString('{"type":"MultiLineString","coordinates":[[[1,2],[3,4]],[[5,6],[7,8]]]}', (new Writer(new GeoJsonStrategy()))->convert($multiLine)); + static::assertJsonStringEqualsJsonString('{"type":"MultiLineString","coordinates":[[[1,2],[3,4]],[[5,6],[7,8]]]}', (new GeoJsonStrategy())->executeStrategy($multiLine)); + } + + /** Preserve line membership, coordinates and order without reference metadata. */ + public function testXyz(): void + { + $multiLine = new \LongitudeOne\SpatialTypes\Types\Dimension3z\Geometry\MultiLineString([[[1, 2, 3], [4, 5, 6]], [[7, 8, 9], [10, 11, 12]]]); + + static::assertJsonStringEqualsJsonString('{"type":"MultiLineString","coordinates":[[[1,2,3],[4,5,6]],[[7,8,9],[10,11,12]]]}', (new Writer(new GeoJsonStrategy()))->convert($multiLine)); + static::assertJsonStringEqualsJsonString('{"type":"MultiLineString","coordinates":[[[1,2,3],[4,5,6]],[[7,8,9],[10,11,12]]]}', (new GeoJsonStrategy())->executeStrategy($multiLine)); + } +}