diff --git a/docs/immutability.md b/docs/immutability.md index 05edac9..7adb197 100644 --- a/docs/immutability.md +++ b/docs/immutability.md @@ -106,7 +106,9 @@ for point ownership, propagation and arc indexing. Mutable `CompoundCurve` class reference. See [mutable compound curves](instantiable-spatial-types.md#mutable-compound-curves) for component editing and recursive context propagation. Mutable `Polygon` and `Triangle` classes retain rings and points by reference; see [mutable surfaces](instantiable-spatial-types.md#mutable-polygons-and-triangles) -for ring editing and failure atomicity. Other mutable aggregates +for ring editing and failure atomicity. Mutable `PolyhedralSurface` classes in XYZ and XYZM retain polygon patches, rings +and points by reference; see [mutable polyhedral surfaces](instantiable-spatial-types.md#mutable-polyhedral-surfaces) +for patch editing, shared vertices and propagation. Other mutable aggregates arrive in later Stories of Epic #38. Existing factories create the explicitly named immutable classes and expose immutable return contracts. diff --git a/docs/instantiable-spatial-types.md b/docs/instantiable-spatial-types.md index e255f51..14ba00b 100644 --- a/docs/instantiable-spatial-types.md +++ b/docs/instantiable-spatial-types.md @@ -856,3 +856,88 @@ geometry. ISO/IEC CD 13249-3:201x(E), clauses 8.3.1 and 8.4.1, define the polygo and triangle models; representing a value does not establish conformity with those geometric definitions. Request functional checks through [spatial-validator](validators.md) explicitly. + + +## Mutable polyhedral surfaces + +`Types\Dimension3z\PolyhedralSurface` (XYZ) and +`Types\Dimension4zm\PolyhedralSurface` (XYZM) implement +`MutablePolyhedralSurfaceInterface`. XY and XYM surfaces are not introduced. + +```php +new PolyhedralSurface(array $patches = [], int|SpatialReference $srid = 0, + SpatialModelEnum $family = SpatialModelEnum::GEOMETRY); +``` + +Each patch is a `MutablePolygonInterface` or an array of rings, using the +[mutable polygon input contract](#mutable-polygons-and-triangles). +Arrays create mutable polygons, rings and points in the surface's layout, +family and reference. Supplied components retain their own context and identity; +immutable patches, rings and points are rejected without conversion, including +immutable descendants exposed by custom mutable components. + +| Operation | Effect | +| --- | --- | +| `addPatch($patch)` | Append a mutable polygon | +| `insertPatch($index, $patch)` | Insert before a patch; the patch count appends | +| `removePatch($index)` | Remove an existing patch and reindex | +| `setPatch($index, $patch)` | Replace one existing patch | +| `addRing($patchIndex, $ring)` | Append a ring in an existing patch | +| `insertRing($patchIndex, $ringIndex, $ring)` | Insert a ring; the addressed patch's ring count appends | +| `removeRing($patchIndex, $ringIndex)` | Remove an existing ring from a patch | +| `setRing($patchIndex, $ringIndex, $ring)` | Replace one existing ring | +| `setPoint($patchIndex, $ringIndex, $pointIndex, $point)` | Replace one point in an existing ring | +| `setArrayOfCoordinates($coordinates)` | Replace the complete patch sequence from coordinates; `[]` empties it | +| `getPatch($index)`, `getPatches()`, `getElements()` | Expose the retained mutable patches | + +Every successful mutation returns the receiver as `static`. Indices start at +zero at each nesting level; access, replacement and removal require existing +elements. Negative or out-of-range indices raise `OutOfBoundsException`. +Failed addressing and technical input checks leave the surface and shared +descendants unchanged, including a late invalid coordinate in full replacement. + +```php +use LongitudeOne\Core\Enum\SpatialModelEnum; +use LongitudeOne\SpatialTypes\Types\Dimension3z\LineString; +use LongitudeOne\SpatialTypes\Types\Dimension3z\Point; +use LongitudeOne\SpatialTypes\Types\Dimension3z\Polygon; +use LongitudeOne\SpatialTypes\Types\Dimension3z\PolyhedralSurface; + +$vertex = new Point(0, null, 5, 4326); +$equalVertex = new Point(0, null, 5, 4326); +$first = new Polygon([new LineString([$vertex, $equalVertex], 4326)], 4326); +$second = new Polygon([new LineString([$vertex], 4326)], 4326); +$surface = new PolyhedralSurface([$first, $second], 4326); +$otherOwner = new PolyhedralSurface([$second], 4326); + +$vertex->setX(7); +assert([[[[7, null, 5], [0, null, 5]]], [[[7, null, 5]]]] === $surface->toArray()); +assert([[[[7, null, 5]]]] === $otherOwner->toArray()); +assert($first === $surface->getPatch(0)); +assert($vertex === $surface->getPatch(1)->getRing(0)->getPoint(0)); + +$surface->setFamily(SpatialModelEnum::GEOGRAPHY, false); // Only the surface. +$surface->setSrid(2154); // Surface, patches, rings and points. +assert(2154 === $otherOwner->getPatch(0)->getSrid()); +assert(4326 === $otherOwner->getSrid()); +``` + +Construction, insertion and replacement never clone supplied components. +Editing one shared point changes every occurrence of that instance; distinct +points with equal coordinates remain independent. Replacing a point at one +address only changes that ring entry. No vertex detection, merging, +synchronization or neighbouring-patch repair occurs. Complete coordinate +replacement creates new descendants and leaves previous components and their +other owners unchanged. + +`setSrid()`, `setSpatialReference()` and `setFamily()` each accept +`bool $propagate = true`. By default they update the receiver and all patches, +rings and points in place; `false` updates only the receiver. Ordinates never +change. Spatial type and layout stay fixed, and mutable surfaces expose neither +`with…` methods nor a dimension setter. Immutable counterparts retain their +separate copy contracts and reject mutable components. + +EMPTY or incomplete patches, open rings and null ordinates remain representable. +Mutation does not enforce face validity, adjacency, closure or manifold rules. +ISO/IEC CD 13249-3:201x(E), clause 8.5.1, defines polyhedral-surface validity; +this representation API leaves its evaluation to explicit spatial-validator use. diff --git a/docs/type-class-hierarchy.md b/docs/type-class-hierarchy.md index 3ebee7b..5c204fc 100644 --- a/docs/type-class-hierarchy.md +++ b/docs/type-class-hierarchy.md @@ -99,3 +99,18 @@ and provides ring editing and ring-then-point replacement. `MutableTriangleInter adds the distinct triangle type contract. Both concrete variants retain mutable rings and points by reference and expose no immutable copy operations. Their layout and spatial type are fixed; geometric validity remains explicit. + + +Mutable polyhedral surfaces use a separate patch-owning base: + +```text +AbstractSpatialType +└── AbstractPolyhedralSurface + └── Dimension{3z,4zm}\PolyhedralSurface +``` + +`MutablePolyhedralSurfaceInterface` refines patch accessors to +`MutablePolygonInterface` and exposes patch editing plus patch-then-ring-then-point +addressing. The base retains patches and their mutable descendants by reference. +Coordinate arrays create mutable polygons in the fixed XYZ or XYZM layout; +immutable surface implementations and their copy contracts remain separate. diff --git a/lib/Interfaces/MutablePolygonInterface.php b/lib/Interfaces/MutablePolygonInterface.php index a0ab79f..3a5343b 100644 --- a/lib/Interfaces/MutablePolygonInterface.php +++ b/lib/Interfaces/MutablePolygonInterface.php @@ -76,4 +76,7 @@ public function setSpatialReference(SpatialReference $reference, bool $propagate * @param bool $propagate Whether to update rings and their points */ public function setSrid(int $srid, bool $propagate = true): static; + + /** @return (null|float|int)[][][] */ + public function toArray(): array; } diff --git a/lib/Interfaces/MutablePolyhedralSurfaceInterface.php b/lib/Interfaces/MutablePolyhedralSurfaceInterface.php index a1a203f..298d704 100644 --- a/lib/Interfaces/MutablePolyhedralSurfaceInterface.php +++ b/lib/Interfaces/MutablePolyhedralSurfaceInterface.php @@ -16,7 +16,99 @@ namespace LongitudeOne\SpatialTypes\Interfaces; -/** Mutable polyhedralsurface contract; type-specific editing is introduced separately. */ +use LongitudeOne\Core\Enum\SpatialModelEnum; +use LongitudeOne\SpatialTypes\Reference\SpatialReference; + +/** In-place patch, ring and point editing without geometric validation. + * + * @phpstan-type CoordinateTuple array{0: null|float|int|string, 1: null|float|int|string, 2 ?: null|float|int, 3 ?: null|float|int} + * @phpstan-type PatchCoordinates array> + * @phpstan-type PatchInput MutablePolygonInterface|array> + */ interface MutablePolyhedralSurfaceInterface extends MutableSpatialInterface, PolyhedralSurfaceInterface { + /** @param MutablePolygonInterface $patch Patch retained without cloning */ + public function addPatch(MutablePolygonInterface $patch): static; + + /** + * @param int $patchIndex Existing zero-based patch index + * @param MutableLineStringInterface $ring Ring retained without cloning + */ + public function addRing(int $patchIndex, MutableLineStringInterface $ring): static; + + /** @return list */ + public function getElements(): array; + + /** @param int $index Existing zero-based patch index */ + public function getPatch(int $index): MutablePolygonInterface; + + /** @return list */ + public function getPatches(): array; + + /** + * @param int $index Zero-based insertion index, up to the patch count + * @param MutablePolygonInterface $patch Patch retained without cloning + */ + public function insertPatch(int $index, MutablePolygonInterface $patch): static; + + /** + * @param int $patchIndex Existing zero-based patch index + * @param int $ringIndex Zero-based insertion index, up to the ring count + * @param MutableLineStringInterface $ring Ring retained without cloning + */ + public function insertRing(int $patchIndex, int $ringIndex, MutableLineStringInterface $ring): static; + + /** @param int $index Existing zero-based patch index */ + public function removePatch(int $index): static; + + /** + * @param int $patchIndex Existing zero-based patch index + * @param int $ringIndex Existing zero-based ring index + */ + public function removeRing(int $patchIndex, int $ringIndex): static; + + /** @param array $coordinates Complete replacement patch coordinates */ + public function setArrayOfCoordinates(array $coordinates): static; + + /** + * @param SpatialModelEnum $family Declared family + * @param bool $propagate Whether to update patches, rings and points + */ + public function setFamily(SpatialModelEnum $family, bool $propagate = true): static; + + /** + * @param int $index Existing zero-based patch index + * @param MutablePolygonInterface $patch Patch retained without cloning + */ + public function setPatch(int $index, MutablePolygonInterface $patch): static; + + /** + * @param int $patchIndex Existing zero-based patch index + * @param int $ringIndex Existing zero-based ring index within the patch + * @param int $pointIndex Existing zero-based point index within the ring + * @param MutablePointInterface $point Point retained without cloning + */ + public function setPoint(int $patchIndex, int $ringIndex, int $pointIndex, MutablePointInterface $point): static; + + /** + * @param int $patchIndex Existing zero-based patch index + * @param int $ringIndex Existing zero-based ring index within the patch + * @param MutableLineStringInterface $ring Ring retained without cloning + */ + public function setRing(int $patchIndex, int $ringIndex, MutableLineStringInterface $ring): static; + + /** + * @param SpatialReference $reference Declared reference + * @param bool $propagate Whether to update patches, rings and points + */ + public function setSpatialReference(SpatialReference $reference, bool $propagate = true): static; + + /** + * @param int $srid Declared reference identifier + * @param bool $propagate Whether to update patches, rings and points + */ + public function setSrid(int $srid, bool $propagate = true): static; + + /** @return (null|float|int)[][][][] */ + public function toArray(): array; } diff --git a/lib/Interfaces/PolyhedralSurfaceInterface.php b/lib/Interfaces/PolyhedralSurfaceInterface.php index 1726ca8..815af3a 100644 --- a/lib/Interfaces/PolyhedralSurfaceInterface.php +++ b/lib/Interfaces/PolyhedralSurfaceInterface.php @@ -27,7 +27,7 @@ interface PolyhedralSurfaceInterface extends SpatialInterface public function getElements(): array; /** - * Return a patch using wrapped indexes, with negative indexes from the end. + * Return a patch by index; indexing conventions are defined by the variant. * * @param int $index Patch index */ diff --git a/lib/Types/AbstractPolyhedralSurface.php b/lib/Types/AbstractPolyhedralSurface.php new file mode 100644 index 0000000..af9e4d4 --- /dev/null +++ b/lib/Types/AbstractPolyhedralSurface.php @@ -0,0 +1,301 @@ + 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\SpatialTypes\Types; + +use LongitudeOne\Core\Enum\CoordinateDimensionEnum; +use LongitudeOne\Core\Enum\GeometryTypeEnum; +use LongitudeOne\Core\Enum\SpatialModelEnum; +use LongitudeOne\SpatialTypes\Exception\InvalidValueException; +use LongitudeOne\SpatialTypes\Exception\OutOfBoundsException; +use LongitudeOne\SpatialTypes\Interfaces\MutableLineStringInterface; +use LongitudeOne\SpatialTypes\Interfaces\MutablePointInterface; +use LongitudeOne\SpatialTypes\Interfaces\MutablePolygonInterface; +use LongitudeOne\SpatialTypes\Interfaces\MutablePolyhedralSurfaceInterface; +use LongitudeOne\SpatialTypes\Reference\SpatialReference; + +/** Mutable ordered patches without geometric validation. + * @internal + * + * @phpstan-import-type PatchInput from MutablePolyhedralSurfaceInterface + * @phpstan-import-type PatchCoordinates from MutablePolyhedralSurfaceInterface + */ +abstract class AbstractPolyhedralSurface extends AbstractSpatialType implements MutablePolyhedralSurfaceInterface +{ + protected SpatialModelEnum $family; + + /** @var list */ + private array $patches = []; + + /** + * @param array $patches Ordered patches or ring sequences + * @param int|SpatialReference $srid Declared reference + * @param SpatialModelEnum $family Declared family + */ + final public function __construct(array $patches = [], int|SpatialReference $srid = 0, SpatialModelEnum $family = SpatialModelEnum::GEOMETRY) + { + $this->family = $family; + $this->initializeSpatialReference($srid); + $this->patches = $this->hydratePatches($patches); + } + + /** @param MutablePolygonInterface $patch Patch retained without cloning */ + public function addPatch(MutablePolygonInterface $patch): static + { + $this->checkPatchComponents($patch); + $this->patches[] = $patch; + + return $this; + } + + /** + * @param int $patchIndex Existing zero-based patch index + * @param MutableLineStringInterface $ring Ring retained without cloning + */ + public function addRing(int $patchIndex, MutableLineStringInterface $ring): static + { + $this->getPatch($patchIndex)->addRing($ring); + + return $this; + } + + /** @return list */ + public function getElements(): array + { + return $this->patches; + } + + /** Return the declared family. */ + public function getFamily(): SpatialModelEnum + { + return $this->family; + } + + /** @param int $index Existing zero-based patch index */ + public function getPatch(int $index): MutablePolygonInterface + { + $this->requirePatchIndex($index); + + return $this->patches[$index]; + } + + /** @return list */ + public function getPatches(): array + { + return $this->patches; + } + + /** Return the fixed spatial type. */ + public function getType(): GeometryTypeEnum + { + return GeometryTypeEnum::POLYHEDRALSURFACE; + } + + /** + * @param int $index Zero-based insertion index, up to the patch count + * @param MutablePolygonInterface $patch Patch retained without cloning + */ + public function insertPatch(int $index, MutablePolygonInterface $patch): static + { + if ($index < 0 || $index > count($this->patches)) { + throw new OutOfBoundsException(sprintf('Patch index %d is outside the insertion range.', $index)); + } + $this->checkPatchComponents($patch); + array_splice($this->patches, $index, 0, [$patch]); + + return $this; + } + + /** + * @param int $patchIndex Existing zero-based patch index + * @param int $ringIndex Zero-based insertion index, up to the ring count + * @param MutableLineStringInterface $ring Ring retained without cloning + */ + public function insertRing(int $patchIndex, int $ringIndex, MutableLineStringInterface $ring): static + { + $this->getPatch($patchIndex)->insertRing($ringIndex, $ring); + + return $this; + } + + /** Whether there are no patches. */ + public function isEmpty(): bool + { + return [] === $this->patches; + } + + /** @param int $index Existing zero-based patch index */ + public function removePatch(int $index): static + { + $this->requirePatchIndex($index); + array_splice($this->patches, $index, 1); + + return $this; + } + + /** + * @param int $patchIndex Existing zero-based patch index + * @param int $ringIndex Existing zero-based ring index + */ + public function removeRing(int $patchIndex, int $ringIndex): static + { + $this->getPatch($patchIndex)->removeRing($ringIndex); + + return $this; + } + + /** @param array $coordinates Complete replacement patch coordinates */ + public function setArrayOfCoordinates(array $coordinates): static + { + $this->patches = $this->hydratePatches($coordinates); + + return $this; + } + + /** + * @param SpatialModelEnum $family Declared family + * @param bool $propagate Whether to update patches, rings and points + */ + public function setFamily(SpatialModelEnum $family, bool $propagate = true): static + { + if ($propagate) { + foreach ($this->patches as $patch) { + $patch->setFamily($family); + } + } + $this->family = $family; + + return $this; + } + + /** + * @param int $index Existing zero-based patch index + * @param MutablePolygonInterface $patch Patch retained without cloning + */ + public function setPatch(int $index, MutablePolygonInterface $patch): static + { + $this->requirePatchIndex($index); + $this->checkPatchComponents($patch); + array_splice($this->patches, $index, 1, [$patch]); + + return $this; + } + + /** + * @param int $patchIndex Existing zero-based patch index + * @param int $ringIndex Existing zero-based ring index within the patch + * @param int $pointIndex Existing zero-based point index within the ring + * @param MutablePointInterface $point Point retained without cloning + */ + public function setPoint(int $patchIndex, int $ringIndex, int $pointIndex, MutablePointInterface $point): static + { + $this->getPatch($patchIndex)->setPoint($ringIndex, $pointIndex, $point); + + return $this; + } + + /** + * @param int $patchIndex Existing zero-based patch index + * @param int $ringIndex Existing zero-based ring index within the patch + * @param MutableLineStringInterface $ring Ring retained without cloning + */ + public function setRing(int $patchIndex, int $ringIndex, MutableLineStringInterface $ring): static + { + $this->getPatch($patchIndex)->setRing($ringIndex, $ring); + + return $this; + } + + /** + * @param SpatialReference $reference Declared reference + * @param bool $propagate Whether to update patches, rings and points + */ + public function setSpatialReference(SpatialReference $reference, bool $propagate = true): static + { + if ($propagate) { + foreach ($this->patches as $patch) { + $patch->setSpatialReference($reference); + } + } + $this->spatialReference = $reference; + + return $this; + } + + /** + * @param int $srid Declared reference identifier + * @param bool $propagate Whether to update patches, rings and points + */ + public function setSrid(int $srid, bool $propagate = true): static + { + return $this->setSpatialReference(SpatialReference::fromSrid($srid), $propagate); + } + + /** @return (null|float|int)[][][][] */ + public function toArray(): array + { + return array_map(static fn (MutablePolygonInterface $patch): array => $patch->toArray(), $this->patches); + } + + /** @param MutablePolygonInterface $patch Patch whose descendants must be mutable */ + private function checkPatchComponents(MutablePolygonInterface $patch): void + { + foreach ($patch->getRings() as $ring) { + if (!$ring instanceof MutableLineStringInterface) { + throw new InvalidValueException('Mutable surface patches require mutable line-string rings.'); + } + foreach ($ring->getPoints() as $point) { + if (!$point instanceof MutablePointInterface) { + throw new InvalidValueException('Mutable surface patches require mutable point components.'); + } + } + } + } + + /** + * @param array $patches Ordered patches or ring sequences + * + * @return list + */ + private function hydratePatches(array $patches): array + { + $hydrated = []; + foreach ($patches as $patch) { + if (is_array($patch)) { + $patch = match ($this->getDimension()) { + CoordinateDimensionEnum::XYZ => new Dimension3z\Polygon($patch, $this->spatialReference, $this->family), + CoordinateDimensionEnum::XYZM => new Dimension4zm\Polygon($patch, $this->spatialReference, $this->family), + default => throw new InvalidValueException('Mutable polyhedral surfaces require XYZ or XYZM coordinates.'), + }; + } + if (!$patch instanceof MutablePolygonInterface) { + throw new InvalidValueException('Mutable surfaces require mutable polygon patches or ring sequences.'); + } + $this->checkPatchComponents($patch); + $hydrated[] = $patch; + } + + return $hydrated; + } + + /** @param int $index Existing zero-based patch index */ + private function requirePatchIndex(int $index): void + { + if ($index < 0 || $index >= count($this->patches)) { + throw new OutOfBoundsException(sprintf('Patch index %d does not address an existing patch.', $index)); + } + } +} diff --git a/lib/Types/Dimension3z/PolyhedralSurface.php b/lib/Types/Dimension3z/PolyhedralSurface.php new file mode 100644 index 0000000..ff94446 --- /dev/null +++ b/lib/Types/Dimension3z/PolyhedralSurface.php @@ -0,0 +1,30 @@ + 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\SpatialTypes\Types\Dimension3z; + +use LongitudeOne\Core\Enum\CoordinateDimensionEnum; +use LongitudeOne\SpatialTypes\Types\AbstractPolyhedralSurface; + +/** Mutable XYZ PolyhedralSurface with instance-level declared family. */ +final class PolyhedralSurface extends AbstractPolyhedralSurface +{ + /** Return the fixed coordinate layout. */ + public function getDimension(): CoordinateDimensionEnum + { + return CoordinateDimensionEnum::XYZ; + } +} diff --git a/lib/Types/Dimension4zm/PolyhedralSurface.php b/lib/Types/Dimension4zm/PolyhedralSurface.php new file mode 100644 index 0000000..1c04a8f --- /dev/null +++ b/lib/Types/Dimension4zm/PolyhedralSurface.php @@ -0,0 +1,30 @@ + 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\SpatialTypes\Types\Dimension4zm; + +use LongitudeOne\Core\Enum\CoordinateDimensionEnum; +use LongitudeOne\SpatialTypes\Types\AbstractPolyhedralSurface; + +/** Mutable XYZM PolyhedralSurface with instance-level declared family. */ +final class PolyhedralSurface extends AbstractPolyhedralSurface +{ + /** Return the fixed coordinate layout. */ + public function getDimension(): CoordinateDimensionEnum + { + return CoordinateDimensionEnum::XYZM; + } +} diff --git a/tests/Unit/Types/MutablePolyhedralSurfaceContextTest.php b/tests/Unit/Types/MutablePolyhedralSurfaceContextTest.php new file mode 100644 index 0000000..8c45baf --- /dev/null +++ b/tests/Unit/Types/MutablePolyhedralSurfaceContextTest.php @@ -0,0 +1,191 @@ + 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\SpatialTypes\Tests\Unit\Types; + +use LongitudeOne\Core\Enum\SpatialModelEnum; +use LongitudeOne\SpatialTypes\Reference\SpatialReference; +use LongitudeOne\SpatialTypes\Types\Dimension3z\LineString; +use LongitudeOne\SpatialTypes\Types\Dimension3z\Point; +use LongitudeOne\SpatialTypes\Types\Dimension3z\Polygon; +use LongitudeOne\SpatialTypes\Types\Dimension3z\PolyhedralSurface; +use PHPUnit\Framework\TestCase; + +/** Recursive surface context preserves shared component identity and coordinates. + * @internal + * + * @coversNothing + */ +class MutablePolyhedralSurfaceContextTest extends TestCase +{ + /** Declared context edits retain coordinates through every patch, ring and point. */ + public function testFamilyLocalOnly(): void + { + $initial = SpatialReference::epsg(4326); + $point = new Point(0, null, 8, $initial); + $firstRing = new LineString([$point], $initial); + $secondRing = new LineString([$point], $initial); + $first = new Polygon([$firstRing], $initial); + $second = new Polygon([$secondRing], $initial); + $surface = new PolyhedralSurface([$first, $second], $initial); + $owner = new PolyhedralSurface([$first], $initial); + static::assertSame($surface, $surface->setFamily(SpatialModelEnum::GEOGRAPHY, false)); + static::assertSame(SpatialModelEnum::GEOGRAPHY, $surface->getFamily()); + static::assertSame(SpatialModelEnum::GEOMETRY, $first->getFamily()); + static::assertSame(SpatialModelEnum::GEOMETRY, $second->getFamily()); + static::assertSame(SpatialModelEnum::GEOMETRY, $firstRing->getFamily()); + static::assertSame(SpatialModelEnum::GEOMETRY, $secondRing->getFamily()); + static::assertSame(SpatialModelEnum::GEOMETRY, $point->getFamily()); + static::assertSame(SpatialModelEnum::GEOMETRY, $owner->getFamily()); + static::assertSame($first, $surface->getPatch(0)); + static::assertSame($first, $owner->getPatch(0)); + static::assertSame($point, $second->getRing(0)->getPoint(0)); + static::assertSame([[[[0, null, 8]]], [[[0, null, 8]]]], $surface->toArray()); + static::assertSame([[[[0, null, 8]]]], $owner->toArray()); + } + + /** Declared context edits retain coordinates through every patch, ring and point. */ + public function testFamilyPropagation(): void + { + $initial = SpatialReference::epsg(4326); + $point = new Point(0, null, 8, $initial); + $firstRing = new LineString([$point], $initial); + $secondRing = new LineString([$point], $initial); + $first = new Polygon([$firstRing], $initial); + $second = new Polygon([$secondRing], $initial); + $surface = new PolyhedralSurface([$first, $second], $initial); + $owner = new PolyhedralSurface([$first], $initial); + static::assertSame($surface, $surface->setFamily(SpatialModelEnum::GEOGRAPHY)); + static::assertSame(SpatialModelEnum::GEOGRAPHY, $surface->getFamily()); + static::assertSame(SpatialModelEnum::GEOGRAPHY, $first->getFamily()); + static::assertSame(SpatialModelEnum::GEOGRAPHY, $second->getFamily()); + static::assertSame(SpatialModelEnum::GEOGRAPHY, $firstRing->getFamily()); + static::assertSame(SpatialModelEnum::GEOGRAPHY, $secondRing->getFamily()); + static::assertSame(SpatialModelEnum::GEOGRAPHY, $point->getFamily()); + static::assertSame(SpatialModelEnum::GEOMETRY, $owner->getFamily()); + static::assertSame($first, $surface->getPatch(0)); + static::assertSame($first, $owner->getPatch(0)); + static::assertSame($point, $second->getRing(0)->getPoint(0)); + static::assertSame([[[[0, null, 8]]], [[[0, null, 8]]]], $surface->toArray()); + static::assertSame([[[[0, null, 8]]]], $owner->toArray()); + } + + /** Declared context edits retain coordinates through every patch, ring and point. */ + public function testReferenceLocalOnly(): void + { + $initial = SpatialReference::epsg(4326); + $reference = SpatialReference::epsg(2154); + $point = new Point(0, null, 8, $initial); + $firstRing = new LineString([$point], $initial); + $secondRing = new LineString([$point], $initial); + $first = new Polygon([$firstRing], $initial); + $second = new Polygon([$secondRing], $initial); + $surface = new PolyhedralSurface([$first, $second], $initial); + $owner = new PolyhedralSurface([$first], $initial); + static::assertSame($surface, $surface->setSpatialReference($reference, false)); + static::assertSame($reference, $surface->getSpatialReference()); + static::assertSame($initial, $first->getSpatialReference()); + static::assertSame($initial, $second->getSpatialReference()); + static::assertSame($initial, $firstRing->getSpatialReference()); + static::assertSame($initial, $secondRing->getSpatialReference()); + static::assertSame($initial, $point->getSpatialReference()); + static::assertSame($initial, $owner->getSpatialReference()); + static::assertSame($first, $surface->getPatch(0)); + static::assertSame($first, $owner->getPatch(0)); + static::assertSame($point, $second->getRing(0)->getPoint(0)); + static::assertSame([[[[0, null, 8]]], [[[0, null, 8]]]], $surface->toArray()); + static::assertSame([[[[0, null, 8]]]], $owner->toArray()); + } + + /** Declared context edits retain coordinates through every patch, ring and point. */ + public function testReferencePropagation(): void + { + $initial = SpatialReference::epsg(4326); + $reference = SpatialReference::epsg(2154); + $point = new Point(0, null, 8, $initial); + $firstRing = new LineString([$point], $initial); + $secondRing = new LineString([$point], $initial); + $first = new Polygon([$firstRing], $initial); + $second = new Polygon([$secondRing], $initial); + $surface = new PolyhedralSurface([$first, $second], $initial); + $owner = new PolyhedralSurface([$first], $initial); + static::assertSame($surface, $surface->setSpatialReference($reference)); + static::assertSame($reference, $surface->getSpatialReference()); + static::assertSame($reference, $first->getSpatialReference()); + static::assertSame($reference, $second->getSpatialReference()); + static::assertSame($reference, $firstRing->getSpatialReference()); + static::assertSame($reference, $secondRing->getSpatialReference()); + static::assertSame($reference, $point->getSpatialReference()); + static::assertSame($initial, $owner->getSpatialReference()); + static::assertSame($first, $surface->getPatch(0)); + static::assertSame($first, $owner->getPatch(0)); + static::assertSame($point, $second->getRing(0)->getPoint(0)); + static::assertSame([[[[0, null, 8]]], [[[0, null, 8]]]], $surface->toArray()); + static::assertSame([[[[0, null, 8]]]], $owner->toArray()); + } + + /** Declared context edits retain coordinates through every patch, ring and point. */ + public function testSridLocalOnly(): void + { + $initial = SpatialReference::epsg(4326); + $point = new Point(0, null, 8, $initial); + $firstRing = new LineString([$point], $initial); + $secondRing = new LineString([$point], $initial); + $first = new Polygon([$firstRing], $initial); + $second = new Polygon([$secondRing], $initial); + $surface = new PolyhedralSurface([$first, $second], $initial); + $owner = new PolyhedralSurface([$first], $initial); + static::assertSame($surface, $surface->setSrid(2154, false)); + static::assertSame(2154, $surface->getSrid()); + static::assertSame(4326, $first->getSrid()); + static::assertSame(4326, $second->getSrid()); + static::assertSame(4326, $firstRing->getSrid()); + static::assertSame(4326, $secondRing->getSrid()); + static::assertSame(4326, $point->getSrid()); + static::assertSame(4326, $owner->getSrid()); + static::assertSame($first, $surface->getPatch(0)); + static::assertSame($first, $owner->getPatch(0)); + static::assertSame($point, $second->getRing(0)->getPoint(0)); + static::assertSame([[[[0, null, 8]]], [[[0, null, 8]]]], $surface->toArray()); + static::assertSame([[[[0, null, 8]]]], $owner->toArray()); + } + + /** Declared context edits retain coordinates through every patch, ring and point. */ + public function testSridPropagation(): void + { + $initial = SpatialReference::epsg(4326); + $point = new Point(0, null, 8, $initial); + $firstRing = new LineString([$point], $initial); + $secondRing = new LineString([$point], $initial); + $first = new Polygon([$firstRing], $initial); + $second = new Polygon([$secondRing], $initial); + $surface = new PolyhedralSurface([$first, $second], $initial); + $owner = new PolyhedralSurface([$first], $initial); + static::assertSame($surface, $surface->setSrid(2154)); + static::assertSame(2154, $surface->getSrid()); + static::assertSame(2154, $first->getSrid()); + static::assertSame(2154, $second->getSrid()); + static::assertSame(2154, $firstRing->getSrid()); + static::assertSame(2154, $secondRing->getSrid()); + static::assertSame(2154, $point->getSrid()); + static::assertSame(4326, $owner->getSrid()); + static::assertSame($first, $surface->getPatch(0)); + static::assertSame($first, $owner->getPatch(0)); + static::assertSame($point, $second->getRing(0)->getPoint(0)); + static::assertSame([[[[0, null, 8]]], [[[0, null, 8]]]], $surface->toArray()); + static::assertSame([[[[0, null, 8]]]], $owner->toArray()); + } +} diff --git a/tests/Unit/Types/MutablePolyhedralSurfaceFailureTest.php b/tests/Unit/Types/MutablePolyhedralSurfaceFailureTest.php new file mode 100644 index 0000000..84792c9 --- /dev/null +++ b/tests/Unit/Types/MutablePolyhedralSurfaceFailureTest.php @@ -0,0 +1,582 @@ + 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\SpatialTypes\Tests\Unit\Types; + +use LongitudeOne\SpatialTypes\Exception\OutOfBoundsException; +use LongitudeOne\SpatialTypes\Types\Dimension3z\LineString; +use LongitudeOne\SpatialTypes\Types\Dimension3z\Point; +use LongitudeOne\SpatialTypes\Types\Dimension3z\Polygon; +use LongitudeOne\SpatialTypes\Types\Dimension3z\PolyhedralSurface; +use PHPUnit\Framework\TestCase; + +/** Technical input and indexed failures never partially modify shared descendants. + * @internal + * + * @coversNothing + */ +class MutablePolyhedralSurfaceFailureTest extends TestCase +{ + /** Invalid zero-based addressing leaves every shared component unchanged. */ + public function testCountPatchAccessIsAtomic(): void + { + $point = new Point(0, null, 8, 4326); + $ring = new LineString([$point], 4326); + $patch = new Polygon([$ring], 4326); + $surface = new PolyhedralSurface([$patch], 2154); + $owner = new PolyhedralSurface([$patch]); + $this->expectException(OutOfBoundsException::class); + + try { + $surface->getPatch(1); + } finally { + static::assertSame([$patch], $surface->getPatches()); + static::assertSame($ring, $patch->getRing(0)); + static::assertSame($point, $ring->getPoint(0)); + static::assertSame([[[[0, null, 8]]]], $surface->toArray()); + static::assertSame([[[[0, null, 8]]]], $owner->toArray()); + static::assertSame(4326, $point->getSrid()); + static::assertSame(4326, $ring->getSrid()); + static::assertSame(4326, $patch->getSrid()); + static::assertSame(2154, $surface->getSrid()); + } + } + + /** Invalid zero-based addressing leaves every shared component unchanged. */ + public function testCountPatchRemovalIsAtomic(): void + { + $point = new Point(0, null, 8, 4326); + $ring = new LineString([$point], 4326); + $patch = new Polygon([$ring], 4326); + $surface = new PolyhedralSurface([$patch], 2154); + $owner = new PolyhedralSurface([$patch]); + $this->expectException(OutOfBoundsException::class); + + try { + $surface->removePatch(1); + } finally { + static::assertSame([$patch], $surface->getPatches()); + static::assertSame($ring, $patch->getRing(0)); + static::assertSame($point, $ring->getPoint(0)); + static::assertSame([[[[0, null, 8]]]], $surface->toArray()); + static::assertSame([[[[0, null, 8]]]], $owner->toArray()); + static::assertSame(4326, $point->getSrid()); + static::assertSame(4326, $ring->getSrid()); + static::assertSame(4326, $patch->getSrid()); + static::assertSame(2154, $surface->getSrid()); + } + } + + /** Invalid zero-based addressing leaves every shared component unchanged. */ + public function testCountPatchReplacementIsAtomic(): void + { + $point = new Point(0, null, 8, 4326); + $ring = new LineString([$point], 4326); + $patch = new Polygon([$ring], 4326); + $surface = new PolyhedralSurface([$patch], 2154); + $owner = new PolyhedralSurface([$patch]); + $this->expectException(OutOfBoundsException::class); + + try { + $surface->setPatch(1, $patch); + } finally { + static::assertSame([$patch], $surface->getPatches()); + static::assertSame($ring, $patch->getRing(0)); + static::assertSame($point, $ring->getPoint(0)); + static::assertSame([[[[0, null, 8]]]], $surface->toArray()); + static::assertSame([[[[0, null, 8]]]], $owner->toArray()); + static::assertSame(4326, $point->getSrid()); + static::assertSame(4326, $ring->getSrid()); + static::assertSame(4326, $patch->getSrid()); + static::assertSame(2154, $surface->getSrid()); + } + } + + /** Invalid zero-based addressing leaves every shared component unchanged. */ + public function testCountPointReplacementIsAtomic(): void + { + $point = new Point(0, null, 8, 4326); + $ring = new LineString([$point], 4326); + $patch = new Polygon([$ring], 4326); + $surface = new PolyhedralSurface([$patch], 2154); + $owner = new PolyhedralSurface([$patch]); + $this->expectException(OutOfBoundsException::class); + + try { + $surface->setPoint(0, 0, 1, $point); + } finally { + static::assertSame([$patch], $surface->getPatches()); + static::assertSame($ring, $patch->getRing(0)); + static::assertSame($point, $ring->getPoint(0)); + static::assertSame([[[[0, null, 8]]]], $surface->toArray()); + static::assertSame([[[[0, null, 8]]]], $owner->toArray()); + static::assertSame(4326, $point->getSrid()); + static::assertSame(4326, $ring->getSrid()); + static::assertSame(4326, $patch->getSrid()); + static::assertSame(2154, $surface->getSrid()); + } + } + + /** Invalid zero-based addressing leaves every shared component unchanged. */ + public function testCountRingRemovalIsAtomic(): void + { + $point = new Point(0, null, 8, 4326); + $ring = new LineString([$point], 4326); + $patch = new Polygon([$ring], 4326); + $surface = new PolyhedralSurface([$patch], 2154); + $owner = new PolyhedralSurface([$patch]); + $this->expectException(OutOfBoundsException::class); + + try { + $surface->removeRing(0, 1); + } finally { + static::assertSame([$patch], $surface->getPatches()); + static::assertSame($ring, $patch->getRing(0)); + static::assertSame($point, $ring->getPoint(0)); + static::assertSame([[[[0, null, 8]]]], $surface->toArray()); + static::assertSame([[[[0, null, 8]]]], $owner->toArray()); + static::assertSame(4326, $point->getSrid()); + static::assertSame(4326, $ring->getSrid()); + static::assertSame(4326, $patch->getSrid()); + static::assertSame(2154, $surface->getSrid()); + } + } + + /** Invalid zero-based addressing leaves every shared component unchanged. */ + public function testCountRingReplacementIsAtomic(): void + { + $point = new Point(0, null, 8, 4326); + $ring = new LineString([$point], 4326); + $patch = new Polygon([$ring], 4326); + $surface = new PolyhedralSurface([$patch], 2154); + $owner = new PolyhedralSurface([$patch]); + $this->expectException(OutOfBoundsException::class); + + try { + $surface->setRing(0, 1, $ring); + } finally { + static::assertSame([$patch], $surface->getPatches()); + static::assertSame($ring, $patch->getRing(0)); + static::assertSame($point, $ring->getPoint(0)); + static::assertSame([[[[0, null, 8]]]], $surface->toArray()); + static::assertSame([[[[0, null, 8]]]], $owner->toArray()); + static::assertSame(4326, $point->getSrid()); + static::assertSame(4326, $ring->getSrid()); + static::assertSame(4326, $patch->getSrid()); + static::assertSame(2154, $surface->getSrid()); + } + } + + /** Invalid zero-based addressing leaves every shared component unchanged. */ + public function testMissingPatchPointReplacementIsAtomic(): void + { + $point = new Point(0, null, 8, 4326); + $ring = new LineString([$point], 4326); + $patch = new Polygon([$ring], 4326); + $surface = new PolyhedralSurface([$patch], 2154); + $owner = new PolyhedralSurface([$patch]); + $this->expectException(OutOfBoundsException::class); + + try { + $surface->setPoint(1, 0, 0, $point); + } finally { + static::assertSame([$patch], $surface->getPatches()); + static::assertSame($ring, $patch->getRing(0)); + static::assertSame($point, $ring->getPoint(0)); + static::assertSame([[[[0, null, 8]]]], $surface->toArray()); + static::assertSame([[[[0, null, 8]]]], $owner->toArray()); + static::assertSame(4326, $point->getSrid()); + static::assertSame(4326, $ring->getSrid()); + static::assertSame(4326, $patch->getSrid()); + static::assertSame(2154, $surface->getSrid()); + } + } + + /** Invalid zero-based addressing leaves every shared component unchanged. */ + public function testMissingPatchRingAdditionIsAtomic(): void + { + $point = new Point(0, null, 8, 4326); + $ring = new LineString([$point], 4326); + $patch = new Polygon([$ring], 4326); + $surface = new PolyhedralSurface([$patch], 2154); + $owner = new PolyhedralSurface([$patch]); + $this->expectException(OutOfBoundsException::class); + + try { + $surface->addRing(1, $ring); + } finally { + static::assertSame([$patch], $surface->getPatches()); + static::assertSame($ring, $patch->getRing(0)); + static::assertSame($point, $ring->getPoint(0)); + static::assertSame([[[[0, null, 8]]]], $surface->toArray()); + static::assertSame([[[[0, null, 8]]]], $owner->toArray()); + static::assertSame(4326, $point->getSrid()); + static::assertSame(4326, $ring->getSrid()); + static::assertSame(4326, $patch->getSrid()); + static::assertSame(2154, $surface->getSrid()); + } + } + + /** Invalid zero-based addressing leaves every shared component unchanged. */ + public function testMissingPatchRingInsertionIsAtomic(): void + { + $point = new Point(0, null, 8, 4326); + $ring = new LineString([$point], 4326); + $patch = new Polygon([$ring], 4326); + $surface = new PolyhedralSurface([$patch], 2154); + $owner = new PolyhedralSurface([$patch]); + $this->expectException(OutOfBoundsException::class); + + try { + $surface->insertRing(1, 0, $ring); + } finally { + static::assertSame([$patch], $surface->getPatches()); + static::assertSame($ring, $patch->getRing(0)); + static::assertSame($point, $ring->getPoint(0)); + static::assertSame([[[[0, null, 8]]]], $surface->toArray()); + static::assertSame([[[[0, null, 8]]]], $owner->toArray()); + static::assertSame(4326, $point->getSrid()); + static::assertSame(4326, $ring->getSrid()); + static::assertSame(4326, $patch->getSrid()); + static::assertSame(2154, $surface->getSrid()); + } + } + + /** Invalid zero-based addressing leaves every shared component unchanged. */ + public function testMissingPatchRingRemovalIsAtomic(): void + { + $point = new Point(0, null, 8, 4326); + $ring = new LineString([$point], 4326); + $patch = new Polygon([$ring], 4326); + $surface = new PolyhedralSurface([$patch], 2154); + $owner = new PolyhedralSurface([$patch]); + $this->expectException(OutOfBoundsException::class); + + try { + $surface->removeRing(1, 0); + } finally { + static::assertSame([$patch], $surface->getPatches()); + static::assertSame($ring, $patch->getRing(0)); + static::assertSame($point, $ring->getPoint(0)); + static::assertSame([[[[0, null, 8]]]], $surface->toArray()); + static::assertSame([[[[0, null, 8]]]], $owner->toArray()); + static::assertSame(4326, $point->getSrid()); + static::assertSame(4326, $ring->getSrid()); + static::assertSame(4326, $patch->getSrid()); + static::assertSame(2154, $surface->getSrid()); + } + } + + /** Invalid zero-based addressing leaves every shared component unchanged. */ + public function testMissingPatchRingReplacementIsAtomic(): void + { + $point = new Point(0, null, 8, 4326); + $ring = new LineString([$point], 4326); + $patch = new Polygon([$ring], 4326); + $surface = new PolyhedralSurface([$patch], 2154); + $owner = new PolyhedralSurface([$patch]); + $this->expectException(OutOfBoundsException::class); + + try { + $surface->setRing(1, 0, $ring); + } finally { + static::assertSame([$patch], $surface->getPatches()); + static::assertSame($ring, $patch->getRing(0)); + static::assertSame($point, $ring->getPoint(0)); + static::assertSame([[[[0, null, 8]]]], $surface->toArray()); + static::assertSame([[[[0, null, 8]]]], $owner->toArray()); + static::assertSame(4326, $point->getSrid()); + static::assertSame(4326, $ring->getSrid()); + static::assertSame(4326, $patch->getSrid()); + static::assertSame(2154, $surface->getSrid()); + } + } + + /** Invalid zero-based addressing leaves every shared component unchanged. */ + public function testMissingRingPointReplacementIsAtomic(): void + { + $point = new Point(0, null, 8, 4326); + $ring = new LineString([$point], 4326); + $patch = new Polygon([$ring], 4326); + $surface = new PolyhedralSurface([$patch], 2154); + $owner = new PolyhedralSurface([$patch]); + $this->expectException(OutOfBoundsException::class); + + try { + $surface->setPoint(0, 1, 0, $point); + } finally { + static::assertSame([$patch], $surface->getPatches()); + static::assertSame($ring, $patch->getRing(0)); + static::assertSame($point, $ring->getPoint(0)); + static::assertSame([[[[0, null, 8]]]], $surface->toArray()); + static::assertSame([[[[0, null, 8]]]], $owner->toArray()); + static::assertSame(4326, $point->getSrid()); + static::assertSame(4326, $ring->getSrid()); + static::assertSame(4326, $patch->getSrid()); + static::assertSame(2154, $surface->getSrid()); + } + } + + /** Invalid zero-based addressing leaves every shared component unchanged. */ + public function testNegativePatchAccessIsAtomic(): void + { + $point = new Point(0, null, 8, 4326); + $ring = new LineString([$point], 4326); + $patch = new Polygon([$ring], 4326); + $surface = new PolyhedralSurface([$patch], 2154); + $owner = new PolyhedralSurface([$patch]); + $this->expectException(OutOfBoundsException::class); + + try { + $surface->getPatch(-1); + } finally { + static::assertSame([$patch], $surface->getPatches()); + static::assertSame($ring, $patch->getRing(0)); + static::assertSame($point, $ring->getPoint(0)); + static::assertSame([[[[0, null, 8]]]], $surface->toArray()); + static::assertSame([[[[0, null, 8]]]], $owner->toArray()); + static::assertSame(4326, $point->getSrid()); + static::assertSame(4326, $ring->getSrid()); + static::assertSame(4326, $patch->getSrid()); + static::assertSame(2154, $surface->getSrid()); + } + } + + /** Invalid zero-based addressing leaves every shared component unchanged. */ + public function testNegativePatchInsertionIsAtomic(): void + { + $point = new Point(0, null, 8, 4326); + $ring = new LineString([$point], 4326); + $patch = new Polygon([$ring], 4326); + $surface = new PolyhedralSurface([$patch], 2154); + $owner = new PolyhedralSurface([$patch]); + $this->expectException(OutOfBoundsException::class); + + try { + $surface->insertPatch(-1, $patch); + } finally { + static::assertSame([$patch], $surface->getPatches()); + static::assertSame($ring, $patch->getRing(0)); + static::assertSame($point, $ring->getPoint(0)); + static::assertSame([[[[0, null, 8]]]], $surface->toArray()); + static::assertSame([[[[0, null, 8]]]], $owner->toArray()); + static::assertSame(4326, $point->getSrid()); + static::assertSame(4326, $ring->getSrid()); + static::assertSame(4326, $patch->getSrid()); + static::assertSame(2154, $surface->getSrid()); + } + } + + /** Invalid zero-based addressing leaves every shared component unchanged. */ + public function testNegativePatchRemovalIsAtomic(): void + { + $point = new Point(0, null, 8, 4326); + $ring = new LineString([$point], 4326); + $patch = new Polygon([$ring], 4326); + $surface = new PolyhedralSurface([$patch], 2154); + $owner = new PolyhedralSurface([$patch]); + $this->expectException(OutOfBoundsException::class); + + try { + $surface->removePatch(-1); + } finally { + static::assertSame([$patch], $surface->getPatches()); + static::assertSame($ring, $patch->getRing(0)); + static::assertSame($point, $ring->getPoint(0)); + static::assertSame([[[[0, null, 8]]]], $surface->toArray()); + static::assertSame([[[[0, null, 8]]]], $owner->toArray()); + static::assertSame(4326, $point->getSrid()); + static::assertSame(4326, $ring->getSrid()); + static::assertSame(4326, $patch->getSrid()); + static::assertSame(2154, $surface->getSrid()); + } + } + + /** Invalid zero-based addressing leaves every shared component unchanged. */ + public function testNegativePatchReplacementIsAtomic(): void + { + $point = new Point(0, null, 8, 4326); + $ring = new LineString([$point], 4326); + $patch = new Polygon([$ring], 4326); + $surface = new PolyhedralSurface([$patch], 2154); + $owner = new PolyhedralSurface([$patch]); + $this->expectException(OutOfBoundsException::class); + + try { + $surface->setPatch(-1, $patch); + } finally { + static::assertSame([$patch], $surface->getPatches()); + static::assertSame($ring, $patch->getRing(0)); + static::assertSame($point, $ring->getPoint(0)); + static::assertSame([[[[0, null, 8]]]], $surface->toArray()); + static::assertSame([[[[0, null, 8]]]], $owner->toArray()); + static::assertSame(4326, $point->getSrid()); + static::assertSame(4326, $ring->getSrid()); + static::assertSame(4326, $patch->getSrid()); + static::assertSame(2154, $surface->getSrid()); + } + } + + /** Invalid zero-based addressing leaves every shared component unchanged. */ + public function testNegativePointReplacementIsAtomic(): void + { + $point = new Point(0, null, 8, 4326); + $ring = new LineString([$point], 4326); + $patch = new Polygon([$ring], 4326); + $surface = new PolyhedralSurface([$patch], 2154); + $owner = new PolyhedralSurface([$patch]); + $this->expectException(OutOfBoundsException::class); + + try { + $surface->setPoint(0, 0, -1, $point); + } finally { + static::assertSame([$patch], $surface->getPatches()); + static::assertSame($ring, $patch->getRing(0)); + static::assertSame($point, $ring->getPoint(0)); + static::assertSame([[[[0, null, 8]]]], $surface->toArray()); + static::assertSame([[[[0, null, 8]]]], $owner->toArray()); + static::assertSame(4326, $point->getSrid()); + static::assertSame(4326, $ring->getSrid()); + static::assertSame(4326, $patch->getSrid()); + static::assertSame(2154, $surface->getSrid()); + } + } + + /** Invalid zero-based addressing leaves every shared component unchanged. */ + public function testNegativeRingInsertionIsAtomic(): void + { + $point = new Point(0, null, 8, 4326); + $ring = new LineString([$point], 4326); + $patch = new Polygon([$ring], 4326); + $surface = new PolyhedralSurface([$patch], 2154); + $owner = new PolyhedralSurface([$patch]); + $this->expectException(OutOfBoundsException::class); + + try { + $surface->insertRing(0, -1, $ring); + } finally { + static::assertSame([$patch], $surface->getPatches()); + static::assertSame($ring, $patch->getRing(0)); + static::assertSame($point, $ring->getPoint(0)); + static::assertSame([[[[0, null, 8]]]], $surface->toArray()); + static::assertSame([[[[0, null, 8]]]], $owner->toArray()); + static::assertSame(4326, $point->getSrid()); + static::assertSame(4326, $ring->getSrid()); + static::assertSame(4326, $patch->getSrid()); + static::assertSame(2154, $surface->getSrid()); + } + } + + /** Invalid zero-based addressing leaves every shared component unchanged. */ + public function testNegativeRingRemovalIsAtomic(): void + { + $point = new Point(0, null, 8, 4326); + $ring = new LineString([$point], 4326); + $patch = new Polygon([$ring], 4326); + $surface = new PolyhedralSurface([$patch], 2154); + $owner = new PolyhedralSurface([$patch]); + $this->expectException(OutOfBoundsException::class); + + try { + $surface->removeRing(0, -1); + } finally { + static::assertSame([$patch], $surface->getPatches()); + static::assertSame($ring, $patch->getRing(0)); + static::assertSame($point, $ring->getPoint(0)); + static::assertSame([[[[0, null, 8]]]], $surface->toArray()); + static::assertSame([[[[0, null, 8]]]], $owner->toArray()); + static::assertSame(4326, $point->getSrid()); + static::assertSame(4326, $ring->getSrid()); + static::assertSame(4326, $patch->getSrid()); + static::assertSame(2154, $surface->getSrid()); + } + } + + /** Invalid zero-based addressing leaves every shared component unchanged. */ + public function testNegativeRingReplacementIsAtomic(): void + { + $point = new Point(0, null, 8, 4326); + $ring = new LineString([$point], 4326); + $patch = new Polygon([$ring], 4326); + $surface = new PolyhedralSurface([$patch], 2154); + $owner = new PolyhedralSurface([$patch]); + $this->expectException(OutOfBoundsException::class); + + try { + $surface->setRing(0, -1, $ring); + } finally { + static::assertSame([$patch], $surface->getPatches()); + static::assertSame($ring, $patch->getRing(0)); + static::assertSame($point, $ring->getPoint(0)); + static::assertSame([[[[0, null, 8]]]], $surface->toArray()); + static::assertSame([[[[0, null, 8]]]], $owner->toArray()); + static::assertSame(4326, $point->getSrid()); + static::assertSame(4326, $ring->getSrid()); + static::assertSame(4326, $patch->getSrid()); + static::assertSame(2154, $surface->getSrid()); + } + } + + /** Invalid zero-based addressing leaves every shared component unchanged. */ + public function testOversizedPatchInsertionIsAtomic(): void + { + $point = new Point(0, null, 8, 4326); + $ring = new LineString([$point], 4326); + $patch = new Polygon([$ring], 4326); + $surface = new PolyhedralSurface([$patch], 2154); + $owner = new PolyhedralSurface([$patch]); + $this->expectException(OutOfBoundsException::class); + + try { + $surface->insertPatch(2, $patch); + } finally { + static::assertSame([$patch], $surface->getPatches()); + static::assertSame($ring, $patch->getRing(0)); + static::assertSame($point, $ring->getPoint(0)); + static::assertSame([[[[0, null, 8]]]], $surface->toArray()); + static::assertSame([[[[0, null, 8]]]], $owner->toArray()); + static::assertSame(4326, $point->getSrid()); + static::assertSame(4326, $ring->getSrid()); + static::assertSame(4326, $patch->getSrid()); + static::assertSame(2154, $surface->getSrid()); + } + } + + /** Invalid zero-based addressing leaves every shared component unchanged. */ + public function testOversizedRingInsertionIsAtomic(): void + { + $point = new Point(0, null, 8, 4326); + $ring = new LineString([$point], 4326); + $patch = new Polygon([$ring], 4326); + $surface = new PolyhedralSurface([$patch], 2154); + $owner = new PolyhedralSurface([$patch]); + $this->expectException(OutOfBoundsException::class); + + try { + $surface->insertRing(0, 2, $ring); + } finally { + static::assertSame([$patch], $surface->getPatches()); + static::assertSame($ring, $patch->getRing(0)); + static::assertSame($point, $ring->getPoint(0)); + static::assertSame([[[[0, null, 8]]]], $surface->toArray()); + static::assertSame([[[[0, null, 8]]]], $owner->toArray()); + static::assertSame(4326, $point->getSrid()); + static::assertSame(4326, $ring->getSrid()); + static::assertSame(4326, $patch->getSrid()); + static::assertSame(2154, $surface->getSrid()); + } + } +} diff --git a/tests/Unit/Types/MutablePolyhedralSurfaceOwnershipTest.php b/tests/Unit/Types/MutablePolyhedralSurfaceOwnershipTest.php new file mode 100644 index 0000000..da20ada --- /dev/null +++ b/tests/Unit/Types/MutablePolyhedralSurfaceOwnershipTest.php @@ -0,0 +1,538 @@ + 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\SpatialTypes\Tests\Unit\Types; + +use LongitudeOne\SpatialTypes\Exception\InvalidValueException; +use LongitudeOne\SpatialTypes\Interfaces\MutableLineStringInterface; +use LongitudeOne\SpatialTypes\Interfaces\MutablePolygonInterface; +use LongitudeOne\SpatialTypes\Types\Dimension3z\Geometry\LineStringImmutable; +use LongitudeOne\SpatialTypes\Types\Dimension3z\Geometry\PointImmutable; +use LongitudeOne\SpatialTypes\Types\Dimension3z\Geometry\PolygonImmutable; +use LongitudeOne\SpatialTypes\Types\Dimension3z\LineString; +use LongitudeOne\SpatialTypes\Types\Dimension3z\Point; +use LongitudeOne\SpatialTypes\Types\Dimension3z\Polygon; +use LongitudeOne\SpatialTypes\Types\Dimension3z\PolyhedralSurface; +use PHPUnit\Framework\TestCase; + +/** Technical input and indexed failures never partially modify shared descendants. + * @internal + * + * @coversNothing + */ +class MutablePolyhedralSurfaceOwnershipTest extends TestCase +{ + /** Coordinate-array construction still rejects immutable nested spatial values. */ + public function testArrayImmutablePointIsRejected(): void + { + $point = new Point(0, null, 8, 4326); + $ring = new LineString([$point], 4326); + $patch = new Polygon([$ring], 4326); + $surface = new PolyhedralSurface([$patch], 2154); + $owner = new PolyhedralSurface([$patch]); + $invalid = new PointImmutable(3, 4, 5); + $this->expectException(InvalidValueException::class); + + try { + (new \ReflectionClass(PolyhedralSurface::class))->newInstance([[[$invalid]]]); + } finally { + static::assertSame([$patch], $surface->getPatches()); + static::assertSame($ring, $patch->getRing(0)); + static::assertSame($point, $ring->getPoint(0)); + static::assertSame([[[[0, null, 8]]]], $surface->toArray()); + static::assertSame([[[[0, null, 8]]]], $owner->toArray()); + static::assertSame(4326, $point->getSrid()); + static::assertSame(4326, $ring->getSrid()); + static::assertSame(4326, $patch->getSrid()); + static::assertSame(2154, $surface->getSrid()); + } + } + + /** Coordinate-array construction still rejects immutable nested spatial values. */ + public function testArrayImmutableRingIsRejected(): void + { + $point = new Point(0, null, 8, 4326); + $ring = new LineString([$point], 4326); + $patch = new Polygon([$ring], 4326); + $surface = new PolyhedralSurface([$patch], 2154); + $owner = new PolyhedralSurface([$patch]); + $invalid = new LineStringImmutable([[3, 4, 5]]); + $this->expectException(InvalidValueException::class); + + try { + (new \ReflectionClass(PolyhedralSurface::class))->newInstance([[$invalid]]); + } finally { + static::assertSame([$patch], $surface->getPatches()); + static::assertSame($ring, $patch->getRing(0)); + static::assertSame($point, $ring->getPoint(0)); + static::assertSame([[[[0, null, 8]]]], $surface->toArray()); + static::assertSame([[[[0, null, 8]]]], $owner->toArray()); + static::assertSame(4326, $point->getSrid()); + static::assertSame(4326, $ring->getSrid()); + static::assertSame(4326, $patch->getSrid()); + static::assertSame(2154, $surface->getSrid()); + } + } + + /** Typed editing rejects immutable spatial components without implicit conversion. */ + public function testImmutablePatchAdditionIsAtomic(): void + { + $point = new Point(0, null, 8, 4326); + $ring = new LineString([$point], 4326); + $patch = new Polygon([$ring], 4326); + $surface = new PolyhedralSurface([$patch], 2154); + $owner = new PolyhedralSurface([$patch]); + $invalid = new PolygonImmutable([[[3, 4, 5]]]); + $this->expectException(\TypeError::class); + + try { + (new \ReflectionMethod($surface, 'addPatch'))->invoke($surface, $invalid); + } finally { + static::assertSame([$patch], $surface->getPatches()); + static::assertSame($ring, $patch->getRing(0)); + static::assertSame($point, $ring->getPoint(0)); + static::assertSame([[[[0, null, 8]]]], $surface->toArray()); + static::assertSame([[[[0, null, 8]]]], $owner->toArray()); + static::assertSame(4326, $point->getSrid()); + static::assertSame(4326, $ring->getSrid()); + static::assertSame(4326, $patch->getSrid()); + static::assertSame(2154, $surface->getSrid()); + } + } + + /** Typed editing rejects immutable spatial components without implicit conversion. */ + public function testImmutablePatchInsertionIsAtomic(): void + { + $point = new Point(0, null, 8, 4326); + $ring = new LineString([$point], 4326); + $patch = new Polygon([$ring], 4326); + $surface = new PolyhedralSurface([$patch], 2154); + $owner = new PolyhedralSurface([$patch]); + $invalid = new PolygonImmutable([[[3, 4, 5]]]); + $this->expectException(\TypeError::class); + + try { + (new \ReflectionMethod($surface, 'insertPatch'))->invoke($surface, 0, $invalid); + } finally { + static::assertSame([$patch], $surface->getPatches()); + static::assertSame($ring, $patch->getRing(0)); + static::assertSame($point, $ring->getPoint(0)); + static::assertSame([[[[0, null, 8]]]], $surface->toArray()); + static::assertSame([[[[0, null, 8]]]], $owner->toArray()); + static::assertSame(4326, $point->getSrid()); + static::assertSame(4326, $ring->getSrid()); + static::assertSame(4326, $patch->getSrid()); + static::assertSame(2154, $surface->getSrid()); + } + } + + /** Typed editing rejects immutable spatial components without implicit conversion. */ + public function testImmutablePatchReplacementIsAtomic(): void + { + $point = new Point(0, null, 8, 4326); + $ring = new LineString([$point], 4326); + $patch = new Polygon([$ring], 4326); + $surface = new PolyhedralSurface([$patch], 2154); + $owner = new PolyhedralSurface([$patch]); + $invalid = new PolygonImmutable([[[3, 4, 5]]]); + $this->expectException(\TypeError::class); + + try { + (new \ReflectionMethod($surface, 'setPatch'))->invoke($surface, 0, $invalid); + } finally { + static::assertSame([$patch], $surface->getPatches()); + static::assertSame($ring, $patch->getRing(0)); + static::assertSame($point, $ring->getPoint(0)); + static::assertSame([[[[0, null, 8]]]], $surface->toArray()); + static::assertSame([[[[0, null, 8]]]], $owner->toArray()); + static::assertSame(4326, $point->getSrid()); + static::assertSame(4326, $ring->getSrid()); + static::assertSame(4326, $patch->getSrid()); + static::assertSame(2154, $surface->getSrid()); + } + } + + /** Typed editing rejects immutable spatial components without implicit conversion. */ + public function testImmutablePointReplacementIsAtomic(): void + { + $point = new Point(0, null, 8, 4326); + $ring = new LineString([$point], 4326); + $patch = new Polygon([$ring], 4326); + $surface = new PolyhedralSurface([$patch], 2154); + $owner = new PolyhedralSurface([$patch]); + $invalid = new PointImmutable(3, 4, 5); + $this->expectException(\TypeError::class); + + try { + (new \ReflectionMethod($surface, 'setPoint'))->invoke($surface, 0, 0, 0, $invalid); + } finally { + static::assertSame([$patch], $surface->getPatches()); + static::assertSame($ring, $patch->getRing(0)); + static::assertSame($point, $ring->getPoint(0)); + static::assertSame([[[[0, null, 8]]]], $surface->toArray()); + static::assertSame([[[[0, null, 8]]]], $owner->toArray()); + static::assertSame(4326, $point->getSrid()); + static::assertSame(4326, $ring->getSrid()); + static::assertSame(4326, $patch->getSrid()); + static::assertSame(2154, $surface->getSrid()); + } + } + + /** Typed editing rejects immutable spatial components without implicit conversion. */ + public function testImmutableRingReplacementIsAtomic(): void + { + $point = new Point(0, null, 8, 4326); + $ring = new LineString([$point], 4326); + $patch = new Polygon([$ring], 4326); + $surface = new PolyhedralSurface([$patch], 2154); + $owner = new PolyhedralSurface([$patch]); + $invalid = new LineStringImmutable([[3, 4, 5]]); + $this->expectException(\TypeError::class); + + try { + (new \ReflectionMethod($surface, 'setRing'))->invoke($surface, 0, 0, $invalid); + } finally { + static::assertSame([$patch], $surface->getPatches()); + static::assertSame($ring, $patch->getRing(0)); + static::assertSame($point, $ring->getPoint(0)); + static::assertSame([[[[0, null, 8]]]], $surface->toArray()); + static::assertSame([[[[0, null, 8]]]], $owner->toArray()); + static::assertSame(4326, $point->getSrid()); + static::assertSame(4326, $ring->getSrid()); + static::assertSame(4326, $patch->getSrid()); + static::assertSame(2154, $surface->getSrid()); + } + } + + /** Custom mutable components cannot introduce immutable descendants. */ + public function testNestedImmutablePointAdditionIsAtomic(): void + { + $point = new Point(0, null, 8, 4326); + $ring = new LineString([$point], 4326); + $patch = new Polygon([$ring], 4326); + $surface = new PolyhedralSurface([$patch], 2154); + $owner = new PolyhedralSurface([$patch]); + $immutable = new PointImmutable(3, 4, 5); + $invalidRing = static::createStub(MutableLineStringInterface::class); + $invalidRing->method('getPoints')->willReturn([$immutable]); + $invalid = static::createStub(MutablePolygonInterface::class); + $invalid->method('getRings')->willReturn([$invalidRing]); + $this->expectException(InvalidValueException::class); + + try { + $surface->addPatch($invalid); + } finally { + static::assertSame([$patch], $surface->getPatches()); + static::assertSame($ring, $patch->getRing(0)); + static::assertSame($point, $ring->getPoint(0)); + static::assertSame([[[[0, null, 8]]]], $surface->toArray()); + static::assertSame([[[[0, null, 8]]]], $owner->toArray()); + static::assertSame(4326, $point->getSrid()); + static::assertSame(4326, $ring->getSrid()); + static::assertSame(4326, $patch->getSrid()); + static::assertSame(2154, $surface->getSrid()); + } + } + + /** Custom mutable components cannot introduce immutable descendants. */ + public function testNestedImmutablePointConstructionIsAtomic(): void + { + $point = new Point(0, null, 8, 4326); + $ring = new LineString([$point], 4326); + $patch = new Polygon([$ring], 4326); + $surface = new PolyhedralSurface([$patch], 2154); + $owner = new PolyhedralSurface([$patch]); + $immutable = new PointImmutable(3, 4, 5); + $invalidRing = static::createStub(MutableLineStringInterface::class); + $invalidRing->method('getPoints')->willReturn([$immutable]); + $invalid = static::createStub(MutablePolygonInterface::class); + $invalid->method('getRings')->willReturn([$invalidRing]); + $this->expectException(InvalidValueException::class); + + try { + (new \ReflectionClass(PolyhedralSurface::class))->newInstance([$invalid]); + } finally { + static::assertSame([$patch], $surface->getPatches()); + static::assertSame($ring, $patch->getRing(0)); + static::assertSame($point, $ring->getPoint(0)); + static::assertSame([[[[0, null, 8]]]], $surface->toArray()); + static::assertSame([[[[0, null, 8]]]], $owner->toArray()); + static::assertSame(4326, $point->getSrid()); + static::assertSame(4326, $ring->getSrid()); + static::assertSame(4326, $patch->getSrid()); + static::assertSame(2154, $surface->getSrid()); + } + } + + /** Custom mutable components cannot introduce immutable descendants. */ + public function testNestedImmutablePointInsertionIsAtomic(): void + { + $point = new Point(0, null, 8, 4326); + $ring = new LineString([$point], 4326); + $patch = new Polygon([$ring], 4326); + $surface = new PolyhedralSurface([$patch], 2154); + $owner = new PolyhedralSurface([$patch]); + $immutable = new PointImmutable(3, 4, 5); + $invalidRing = static::createStub(MutableLineStringInterface::class); + $invalidRing->method('getPoints')->willReturn([$immutable]); + $invalid = static::createStub(MutablePolygonInterface::class); + $invalid->method('getRings')->willReturn([$invalidRing]); + $this->expectException(InvalidValueException::class); + + try { + $surface->insertPatch(0, $invalid); + } finally { + static::assertSame([$patch], $surface->getPatches()); + static::assertSame($ring, $patch->getRing(0)); + static::assertSame($point, $ring->getPoint(0)); + static::assertSame([[[[0, null, 8]]]], $surface->toArray()); + static::assertSame([[[[0, null, 8]]]], $owner->toArray()); + static::assertSame(4326, $point->getSrid()); + static::assertSame(4326, $ring->getSrid()); + static::assertSame(4326, $patch->getSrid()); + static::assertSame(2154, $surface->getSrid()); + } + } + + /** Custom mutable components cannot introduce immutable descendants. */ + public function testNestedImmutablePointReplacementIsAtomic(): void + { + $point = new Point(0, null, 8, 4326); + $ring = new LineString([$point], 4326); + $patch = new Polygon([$ring], 4326); + $surface = new PolyhedralSurface([$patch], 2154); + $owner = new PolyhedralSurface([$patch]); + $immutable = new PointImmutable(3, 4, 5); + $invalidRing = static::createStub(MutableLineStringInterface::class); + $invalidRing->method('getPoints')->willReturn([$immutable]); + $invalid = static::createStub(MutablePolygonInterface::class); + $invalid->method('getRings')->willReturn([$invalidRing]); + $this->expectException(InvalidValueException::class); + + try { + $surface->setPatch(0, $invalid); + } finally { + static::assertSame([$patch], $surface->getPatches()); + static::assertSame($ring, $patch->getRing(0)); + static::assertSame($point, $ring->getPoint(0)); + static::assertSame([[[[0, null, 8]]]], $surface->toArray()); + static::assertSame([[[[0, null, 8]]]], $owner->toArray()); + static::assertSame(4326, $point->getSrid()); + static::assertSame(4326, $ring->getSrid()); + static::assertSame(4326, $patch->getSrid()); + static::assertSame(2154, $surface->getSrid()); + } + } + + /** Custom mutable components cannot introduce immutable descendants. */ + public function testNestedImmutableRingAdditionIsAtomic(): void + { + $point = new Point(0, null, 8, 4326); + $ring = new LineString([$point], 4326); + $patch = new Polygon([$ring], 4326); + $surface = new PolyhedralSurface([$patch], 2154); + $owner = new PolyhedralSurface([$patch]); + $immutable = new LineStringImmutable([[3, 4, 5]]); + $invalid = static::createStub(MutablePolygonInterface::class); + $invalid->method('getRings')->willReturn([$immutable]); + $this->expectException(InvalidValueException::class); + + try { + $surface->addPatch($invalid); + } finally { + static::assertSame([$patch], $surface->getPatches()); + static::assertSame($ring, $patch->getRing(0)); + static::assertSame($point, $ring->getPoint(0)); + static::assertSame([[[[0, null, 8]]]], $surface->toArray()); + static::assertSame([[[[0, null, 8]]]], $owner->toArray()); + static::assertSame(4326, $point->getSrid()); + static::assertSame(4326, $ring->getSrid()); + static::assertSame(4326, $patch->getSrid()); + static::assertSame(2154, $surface->getSrid()); + } + } + + /** Custom mutable components cannot introduce immutable descendants. */ + public function testNestedImmutableRingConstructionIsAtomic(): void + { + $point = new Point(0, null, 8, 4326); + $ring = new LineString([$point], 4326); + $patch = new Polygon([$ring], 4326); + $surface = new PolyhedralSurface([$patch], 2154); + $owner = new PolyhedralSurface([$patch]); + $immutable = new LineStringImmutable([[3, 4, 5]]); + $invalid = static::createStub(MutablePolygonInterface::class); + $invalid->method('getRings')->willReturn([$immutable]); + $this->expectException(InvalidValueException::class); + + try { + (new \ReflectionClass(PolyhedralSurface::class))->newInstance([$invalid]); + } finally { + static::assertSame([$patch], $surface->getPatches()); + static::assertSame($ring, $patch->getRing(0)); + static::assertSame($point, $ring->getPoint(0)); + static::assertSame([[[[0, null, 8]]]], $surface->toArray()); + static::assertSame([[[[0, null, 8]]]], $owner->toArray()); + static::assertSame(4326, $point->getSrid()); + static::assertSame(4326, $ring->getSrid()); + static::assertSame(4326, $patch->getSrid()); + static::assertSame(2154, $surface->getSrid()); + } + } + + /** Custom mutable components cannot introduce immutable descendants. */ + public function testNestedImmutableRingInsertionIsAtomic(): void + { + $point = new Point(0, null, 8, 4326); + $ring = new LineString([$point], 4326); + $patch = new Polygon([$ring], 4326); + $surface = new PolyhedralSurface([$patch], 2154); + $owner = new PolyhedralSurface([$patch]); + $immutable = new LineStringImmutable([[3, 4, 5]]); + $invalid = static::createStub(MutablePolygonInterface::class); + $invalid->method('getRings')->willReturn([$immutable]); + $this->expectException(InvalidValueException::class); + + try { + $surface->insertPatch(0, $invalid); + } finally { + static::assertSame([$patch], $surface->getPatches()); + static::assertSame($ring, $patch->getRing(0)); + static::assertSame($point, $ring->getPoint(0)); + static::assertSame([[[[0, null, 8]]]], $surface->toArray()); + static::assertSame([[[[0, null, 8]]]], $owner->toArray()); + static::assertSame(4326, $point->getSrid()); + static::assertSame(4326, $ring->getSrid()); + static::assertSame(4326, $patch->getSrid()); + static::assertSame(2154, $surface->getSrid()); + } + } + + /** Custom mutable components cannot introduce immutable descendants. */ + public function testNestedImmutableRingReplacementIsAtomic(): void + { + $point = new Point(0, null, 8, 4326); + $ring = new LineString([$point], 4326); + $patch = new Polygon([$ring], 4326); + $surface = new PolyhedralSurface([$patch], 2154); + $owner = new PolyhedralSurface([$patch]); + $immutable = new LineStringImmutable([[3, 4, 5]]); + $invalid = static::createStub(MutablePolygonInterface::class); + $invalid->method('getRings')->willReturn([$immutable]); + $this->expectException(InvalidValueException::class); + + try { + $surface->setPatch(0, $invalid); + } finally { + static::assertSame([$patch], $surface->getPatches()); + static::assertSame($ring, $patch->getRing(0)); + static::assertSame($point, $ring->getPoint(0)); + static::assertSame([[[[0, null, 8]]]], $surface->toArray()); + static::assertSame([[[[0, null, 8]]]], $owner->toArray()); + static::assertSame(4326, $point->getSrid()); + static::assertSame(4326, $ring->getSrid()); + static::assertSame(4326, $patch->getSrid()); + static::assertSame(2154, $surface->getSrid()); + } + } + + /** Invalid descendants are rejected before editing a shared patch. */ + public function testNestedRingAdditionIsAtomic(): void + { + $point = new Point(0, null, 8, 4326); + $ring = new LineString([$point], 4326); + $patch = new Polygon([$ring], 4326); + $surface = new PolyhedralSurface([$patch], 2154); + $owner = new PolyhedralSurface([$patch]); + $immutable = new PointImmutable(3, 4, 5); + $invalid = static::createStub(MutableLineStringInterface::class); + $invalid->method('getPoints')->willReturn([$immutable]); + $this->expectException(InvalidValueException::class); + + try { + $surface->addRing(0, $invalid); + } finally { + static::assertSame([$patch], $surface->getPatches()); + static::assertSame($ring, $patch->getRing(0)); + static::assertSame($point, $ring->getPoint(0)); + static::assertSame([[[[0, null, 8]]]], $surface->toArray()); + static::assertSame([[[[0, null, 8]]]], $owner->toArray()); + static::assertSame(4326, $point->getSrid()); + static::assertSame(4326, $ring->getSrid()); + static::assertSame(4326, $patch->getSrid()); + static::assertSame(2154, $surface->getSrid()); + static::assertSame([3, 4, 5], $immutable->toArray()); + } + } + + /** Invalid descendants are rejected before editing a shared patch. */ + public function testNestedRingInsertionIsAtomic(): void + { + $point = new Point(0, null, 8, 4326); + $ring = new LineString([$point], 4326); + $patch = new Polygon([$ring], 4326); + $surface = new PolyhedralSurface([$patch], 2154); + $owner = new PolyhedralSurface([$patch]); + $immutable = new PointImmutable(3, 4, 5); + $invalid = static::createStub(MutableLineStringInterface::class); + $invalid->method('getPoints')->willReturn([$immutable]); + $this->expectException(InvalidValueException::class); + + try { + $surface->insertRing(0, 0, $invalid); + } finally { + static::assertSame([$patch], $surface->getPatches()); + static::assertSame($ring, $patch->getRing(0)); + static::assertSame($point, $ring->getPoint(0)); + static::assertSame([[[[0, null, 8]]]], $surface->toArray()); + static::assertSame([[[[0, null, 8]]]], $owner->toArray()); + static::assertSame(4326, $point->getSrid()); + static::assertSame(4326, $ring->getSrid()); + static::assertSame(4326, $patch->getSrid()); + static::assertSame(2154, $surface->getSrid()); + static::assertSame([3, 4, 5], $immutable->toArray()); + } + } + + /** Invalid descendants are rejected before editing a shared patch. */ + public function testNestedRingReplacementIsAtomic(): void + { + $point = new Point(0, null, 8, 4326); + $ring = new LineString([$point], 4326); + $patch = new Polygon([$ring], 4326); + $surface = new PolyhedralSurface([$patch], 2154); + $owner = new PolyhedralSurface([$patch]); + $immutable = new PointImmutable(3, 4, 5); + $invalid = static::createStub(MutableLineStringInterface::class); + $invalid->method('getPoints')->willReturn([$immutable]); + $this->expectException(InvalidValueException::class); + + try { + $surface->setRing(0, 0, $invalid); + } finally { + static::assertSame([$patch], $surface->getPatches()); + static::assertSame($ring, $patch->getRing(0)); + static::assertSame($point, $ring->getPoint(0)); + static::assertSame([[[[0, null, 8]]]], $surface->toArray()); + static::assertSame([[[[0, null, 8]]]], $owner->toArray()); + static::assertSame(4326, $point->getSrid()); + static::assertSame(4326, $ring->getSrid()); + static::assertSame(4326, $patch->getSrid()); + static::assertSame(2154, $surface->getSrid()); + static::assertSame([3, 4, 5], $immutable->toArray()); + } + } +} diff --git a/tests/Unit/Types/MutablePolyhedralSurfaceTest.php b/tests/Unit/Types/MutablePolyhedralSurfaceTest.php new file mode 100644 index 0000000..4c09462 --- /dev/null +++ b/tests/Unit/Types/MutablePolyhedralSurfaceTest.php @@ -0,0 +1,262 @@ + 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\SpatialTypes\Tests\Unit\Types; + +use LongitudeOne\Core\Enum\CoordinateDimensionEnum; +use LongitudeOne\Core\Enum\GeometryTypeEnum; +use LongitudeOne\Core\Enum\SpatialModelEnum; +use LongitudeOne\SpatialTypes\Exception\InvalidDimensionException; +use LongitudeOne\SpatialTypes\Exception\InvalidValueException; +use LongitudeOne\SpatialTypes\Interfaces\MutablePolyhedralSurfaceInterface; +use LongitudeOne\SpatialTypes\Interfaces\MutableSpatialInterface; +use LongitudeOne\SpatialTypes\Types\Dimension3z\Geometry\PolygonImmutable; +use LongitudeOne\SpatialTypes\Types\Dimension3z\Geometry\PolyhedralSurfaceImmutable; +use LongitudeOne\SpatialTypes\Types\Dimension3z\LineString; +use LongitudeOne\SpatialTypes\Types\Dimension3z\Point; +use LongitudeOne\SpatialTypes\Types\Dimension3z\Polygon; +use LongitudeOne\SpatialTypes\Types\Dimension3z\PolyhedralSurface; +use PHPUnit\Framework\TestCase; + +/** Mutable patches preserve ordered data and shared ownership. + * @internal + * + * @coversNothing + */ +class MutablePolyhedralSurfaceTest extends TestCase +{ + /** A late invalid tuple never commits an earlier hydrated patch. */ + public function testCoordinateFailureIsAtomic(): void + { + $point = new Point(0, null, 8, 4326); + $ring = new LineString([$point], 4326); + $patch = new Polygon([$ring], 4326); + $surface = new PolyhedralSurface([$patch], 2154); + $owner = new PolyhedralSurface([$patch]); + $this->expectException(InvalidDimensionException::class); + + try { + (new \ReflectionMethod($surface, 'setArrayOfCoordinates'))->invoke($surface, [[[[3, 4, 5]]], [[[6, 7, 8, 9]]]]); + } finally { + static::assertSame([$patch], $surface->getPatches()); + static::assertSame($ring, $patch->getRing(0)); + static::assertSame($point, $ring->getPoint(0)); + static::assertSame([[[[0, null, 8]]]], $surface->toArray()); + static::assertSame([[[[0, null, 8]]]], $owner->toArray()); + static::assertSame(4326, $point->getSrid()); + } + } + + /** Complete coordinate replacement creates descendants and detaches previous shared patches. */ + public function testCoordinateReplacementIsolation(): void + { + $point = new Point(1, 2, 3, 4326); + $ring = new LineString([$point], 4326); + $patch = new Polygon([$ring], 4326); + $surface = new PolyhedralSurface([$patch], 2154, SpatialModelEnum::GEOGRAPHY); + $owner = new PolyhedralSurface([$patch]); + static::assertSame($surface, $surface->setArrayOfCoordinates([[[[null, 5, 6]]], []])); + static::assertSame([[[[null, 5, 6]]], []], $surface->toArray()); + static::assertInstanceOf(Polygon::class, $surface->getPatch(0)); + static::assertInstanceOf(LineString::class, $surface->getPatch(0)->getRing(0)); + static::assertInstanceOf(Point::class, $surface->getPatch(0)->getRing(0)->getPoint(0)); + static::assertSame(2154, $surface->getPatch(0)->getSrid()); + static::assertSame(2154, $surface->getPatch(0)->getRing(0)->getSrid()); + static::assertSame(2154, $surface->getPatch(0)->getRing(0)->getPoint(0)->getSrid()); + static::assertSame(SpatialModelEnum::GEOGRAPHY, $surface->getPatch(0)->getFamily()); + static::assertSame(SpatialModelEnum::GEOGRAPHY, $surface->getPatch(0)->getRing(0)->getPoint(0)->getFamily()); + static::assertSame([$patch], $owner->getPatches()); + static::assertSame($ring, $patch->getRing(0)); + static::assertSame($point, $ring->getPoint(0)); + static::assertSame([[[[1, 2, 3]]]], $owner->toArray()); + static::assertSame(4326, $point->getSrid()); + } + + /** Empty patches, rings and incomplete points are retained without geometric checks. */ + public function testEmptyAndIncompleteSurface(): void + { + $surface = new PolyhedralSurface(); + static::assertTrue($surface->isEmpty()); + static::assertSame([], $surface->toArray()); + static::assertSame($surface, $surface->setArrayOfCoordinates([[], [[]], [[[0, null, null], [null, 7, 8]]]])); + static::assertFalse($surface->isEmpty()); + static::assertSame([[], [[]], [[[0, null, null], [null, 7, 8]]]], $surface->toArray()); + static::assertSame($surface, $surface->setArrayOfCoordinates([])); + static::assertTrue($surface->isEmpty()); + } + + /** All-null points remain EMPTY without removing containing rings or patches. */ + public function testEmptyPoint(): void + { + $surface = new PolyhedralSurface([[[[null, null, null]]]]); + static::assertFalse($surface->isEmpty()); + static::assertSame([[[[]]]], $surface->toArray()); + static::assertTrue($surface->getPatch(0)->getRing(0)->getPoint(0)->isEmpty()); + static::assertSame(CoordinateDimensionEnum::XYZ, $surface->getPatch(0)->getRing(0)->getPoint(0)->getDimension()); + } + + /** Immutable patches are rejected at construction without changing their state. */ + public function testImmutablePatchConstructorIsRejected(): void + { + $patch = new PolygonImmutable([[[1, 2, 3]]], 4326); + $this->expectException(InvalidValueException::class); + + try { + (new \ReflectionClass(PolyhedralSurface::class))->newInstance([$patch]); + } finally { + static::assertSame([[[1, 2, 3]]], $patch->toArray()); + static::assertSame(4326, $patch->getSrid()); + } + } + + /** Immutable source data stays isolated when its coordinate array creates mutable descendants. */ + public function testImmutableSourceIsolation(): void + { + $immutable = new PolyhedralSurfaceImmutable([[[[1, 2, 3]]]], 4326); + $surface = (new \ReflectionClass(PolyhedralSurface::class))->newInstance($immutable->toArray(), 4326); + $surface->getPatch(0)->getRing(0)->getPoint(0)->setX(9); + static::assertSame([[[[9, 2, 3]]]], $surface->toArray()); + static::assertSame([[[[1, 2, 3]]]], $immutable->toArray()); + static::assertSame(4326, $immutable->getPatch(0)->getRing(0)->getPoint(0)->getSrid()); + } + + /** Immutable aggregate constructors still reject mutable patches. */ + public function testImmutableSurfaceRejectsMutablePatch(): void + { + $patch = new Polygon([[[0, null, 8]]], 4326); + $this->expectException(\TypeError::class); + + try { + (new \ReflectionClass(PolyhedralSurfaceImmutable::class))->newInstance([$patch]); + } finally { + static::assertSame([[[0, null, 8]]], $patch->toArray()); + static::assertSame(4326, $patch->getSrid()); + } + } + + /** Nested editing addresses patch, ring, then point and exposes shared changes. */ + public function testNestedEditing(): void + { + $first = new LineString([[1, 2, 3]]); + $second = new LineString([[4, 5, 6]]); + $empty = new LineString([]); + $patch = new Polygon([$first]); + $surface = new PolyhedralSurface([new Polygon([]), $patch]); + $owner = new PolyhedralSurface([$patch]); + static::assertSame($surface, $surface->addRing(1, $empty)->insertRing(1, 1, $second)); + static::assertSame([$first, $second, $empty], $patch->getRings()); + static::assertSame($surface, $surface->insertRing(1, 3, $first)->setRing(1, 1, $empty)->removeRing(1, 2)); + static::assertSame([$first, $empty, $first], $patch->getRings()); + $replacement = new Point(9, null, 8, 4326); + static::assertSame($surface, $surface->setPoint(1, 0, 0, $replacement)); + static::assertSame($replacement, $owner->getPatch(0)->getRing(0)->getPoint(0)); + static::assertSame([[], [[[9, null, 8]], [], [[9, null, 8]]]], $surface->toArray()); + static::assertSame([[[[9, null, 8]], [], [[9, null, 8]]]], $owner->toArray()); + } + + /** Patches retain supplied instances and order on every membership operation. */ + public function testOrderedPatchEditing(): void + { + $first = new Polygon([[[1, 2, 3]]], 4326); + $second = new Polygon([[[4, 5, 6]]], 2154); + $empty = new Polygon([]); + $surface = new PolyhedralSurface([7 => $first]); + static::assertSame($surface, $surface->addPatch($empty)->insertPatch(1, $second)); + static::assertSame([$first, $second, $empty], $surface->getPatches()); + static::assertSame($surface, $surface->insertPatch(3, $first)->setPatch(1, $empty)->removePatch(2)); + static::assertSame([$first, $empty, $first], $surface->getElements()); + static::assertSame([[[[1, 2, 3]]], [], [[[1, 2, 3]]]], $surface->toArray()); + static::assertSame($first, $surface->getPatch(0)); + static::assertSame(4326, $first->getSrid()); + static::assertSame(2154, $second->getSrid()); + $returned = $surface->getPatches(); + $returned[0] = $second; + static::assertSame($first, $surface->getPatch(0)); + } + + /** Mutable surfaces implement the approved contracts without copy or dimension mutators. */ + public function testPublicContracts(): void + { + $surface = new PolyhedralSurface(); + static::assertContains(MutablePolyhedralSurfaceInterface::class, class_implements($surface)); + static::assertContains(MutableSpatialInterface::class, class_implements($surface)); + static::assertSame(GeometryTypeEnum::POLYHEDRALSURFACE, $surface->getType()); + static::assertSame(CoordinateDimensionEnum::XYZ, $surface->getDimension()); + static::assertSame(SpatialModelEnum::GEOMETRY, $surface->getFamily()); + static::assertFalse((new \ReflectionClass($surface))->hasMethod('withPatch')); + static::assertFalse((new \ReflectionClass($surface))->hasMethod('withArrayOfCoordinates')); + static::assertFalse((new \ReflectionClass($surface))->hasMethod('withSrid')); + static::assertFalse((new \ReflectionClass($surface))->hasMethod('setDimension')); + static::assertSame('static', (string) (new \ReflectionMethod($surface, 'setPoint'))->getReturnType()); + } + + /** Shared point identity changes every occurrence; equal-valued points remain independent. */ + public function testSharedVertexIdentity(): void + { + $shared = new Point(1, 2, 3); + $equal = new Point(1, 2, 3); + $first = new Polygon([new LineString([$shared, $equal, $shared])]); + $second = new Polygon([new LineString([$shared])]); + $surface = new PolyhedralSurface([$first, $second]); + $owner = new PolyhedralSurface([$second]); + static::assertSame($shared, $surface->getPatch(0)->getRing(0)->getPoint(2)); + static::assertSame($shared, $surface->getPatch(1)->getRing(0)->getPoint(0)); + $shared->setZ(null)->setX(9); + static::assertSame([[[[9, 2, null], [1, 2, 3], [9, 2, null]]], [[[9, 2, null]]]], $surface->toArray()); + static::assertSame([[[[9, 2, null]]]], $owner->toArray()); + static::assertSame([1, 2, 3], $equal->toArray()); + $replacement = new Point(7, 8, 9); + $surface->setPoint(0, 0, 0, $replacement); + static::assertSame([[[[7, 8, 9], [1, 2, 3], [9, 2, null]]], [[[9, 2, null]]]], $surface->toArray()); + static::assertSame($shared, $second->getRing(0)->getPoint(0)); + } + + /** Construction retains supplied nested objects without synchronizing context or layout. */ + public function testSuppliedNestedComponents(): void + { + $point = new Point(0, null, 8, 4326, SpatialModelEnum::GEOGRAPHY); + $ring = new LineString([$point], 3857); + $surface = new PolyhedralSurface([[$ring], [[$point]], new \LongitudeOne\SpatialTypes\Types\Dimension3m\Polygon([[[1, 2, 9]]])], 2154); + static::assertSame($ring, $surface->getPatch(0)->getRing(0)); + static::assertSame($point, $surface->getPatch(1)->getRing(0)->getPoint(0)); + static::assertSame(2154, $surface->getPatch(0)->getSrid()); + static::assertSame(3857, $ring->getSrid()); + static::assertSame(4326, $point->getSrid()); + static::assertSame(SpatialModelEnum::GEOGRAPHY, $point->getFamily()); + static::assertSame(CoordinateDimensionEnum::XYZ, $surface->getDimension()); + static::assertSame(CoordinateDimensionEnum::XYM, $surface->getPatch(2)->getDimension()); + static::assertSame([[[[0, null, 8]]], [[[0, null, 8]]], [[[1, 2, 9]]]], $surface->toArray()); + } + + /** XYZM surfaces hydrate the existing measured layout and retain null Z and M ordinates. */ + public function testXyzmLayout(): void + { + $surface = new \LongitudeOne\SpatialTypes\Types\Dimension4zm\PolyhedralSurface([[[[0, null, 8, 9]]]], 4326, SpatialModelEnum::GEOGRAPHY); + static::assertSame(CoordinateDimensionEnum::XYZM, $surface->getDimension()); + static::assertSame(GeometryTypeEnum::POLYHEDRALSURFACE, $surface->getType()); + static::assertSame(SpatialModelEnum::GEOGRAPHY, $surface->getFamily()); + static::assertSame([[[[0, null, 8, 9]]]], $surface->toArray()); + static::assertInstanceOf(\LongitudeOne\SpatialTypes\Types\Dimension4zm\Polygon::class, $surface->getPatch(0)); + static::assertInstanceOf(\LongitudeOne\SpatialTypes\Types\Dimension4zm\LineString::class, $surface->getPatch(0)->getRing(0)); + static::assertInstanceOf(\LongitudeOne\SpatialTypes\Types\Dimension4zm\Point::class, $surface->getPatch(0)->getRing(0)->getPoint(0)); + static::assertSame($surface, $surface->setArrayOfCoordinates([[[[null, 7, null, 0]]]])->setSrid(2154)->setFamily(SpatialModelEnum::GEOMETRY)); + static::assertSame([[[[null, 7, null, 0]]]], $surface->toArray()); + static::assertSame(CoordinateDimensionEnum::XYZM, $surface->getDimension()); + static::assertSame(GeometryTypeEnum::POLYHEDRALSURFACE, $surface->getType()); + static::assertSame(2154, $surface->getPatch(0)->getRing(0)->getPoint(0)->getSrid()); + static::assertSame(SpatialModelEnum::GEOMETRY, $surface->getPatch(0)->getRing(0)->getPoint(0)->getFamily()); + } +}