From 60fd333fd695168e2f3b7a68ac317c9eacf57d36 Mon Sep 17 00:00:00 2001 From: Alexandre Tranchant Date: Thu, 29 Oct 2026 17:18:14 +0100 Subject: [PATCH] feat(types): edit mutable polygons and triangles in place Add mutable XY, XYZ, XYM and XYZM surfaces with shared ring ownership, atomic indexed editing and recursive context propagation. Document the contracts and cover representable invalid states and component rejection. --- docs/immutability.md | 4 +- docs/instantiable-spatial-types.md | 79 +++ docs/spatial-reference-systems.md | 3 +- docs/type-class-hierarchy.md | 15 + lib/Interfaces/MutablePolygonInterface.php | 59 +- lib/Interfaces/MutableTriangleInterface.php | 2 +- lib/Types/AbstractPolygon.php | 246 +++++++ lib/Types/Dimension2/Polygon.php | 30 + lib/Types/Dimension2/Triangle.php | 38 ++ lib/Types/Dimension3m/Polygon.php | 30 + lib/Types/Dimension3m/Triangle.php | 38 ++ lib/Types/Dimension3z/Polygon.php | 30 + lib/Types/Dimension3z/Triangle.php | 38 ++ lib/Types/Dimension4zm/Polygon.php | 30 + lib/Types/Dimension4zm/Triangle.php | 38 ++ .../Unit/Types/MutablePolygonContextTest.php | 160 +++++ tests/Unit/Types/MutablePolygonLayoutTest.php | 178 +++++ tests/Unit/Types/MutablePolygonTest.php | 611 ++++++++++++++++++ 18 files changed, 1625 insertions(+), 4 deletions(-) create mode 100644 lib/Types/AbstractPolygon.php create mode 100644 lib/Types/Dimension2/Polygon.php create mode 100644 lib/Types/Dimension2/Triangle.php create mode 100644 lib/Types/Dimension3m/Polygon.php create mode 100644 lib/Types/Dimension3m/Triangle.php create mode 100644 lib/Types/Dimension3z/Polygon.php create mode 100644 lib/Types/Dimension3z/Triangle.php create mode 100644 lib/Types/Dimension4zm/Polygon.php create mode 100644 lib/Types/Dimension4zm/Triangle.php create mode 100644 tests/Unit/Types/MutablePolygonContextTest.php create mode 100644 tests/Unit/Types/MutablePolygonLayoutTest.php create mode 100644 tests/Unit/Types/MutablePolygonTest.php diff --git a/docs/immutability.md b/docs/immutability.md index 0532978..05edac9 100644 --- a/docs/immutability.md +++ b/docs/immutability.md @@ -104,7 +104,9 @@ and in-place operations. Mutable `LineString` and `CircularString` classes are a each dimension namespace. See [mutable paths](instantiable-spatial-types.md#mutable-line-strings-and-circular-strings) for point ownership, propagation and arc indexing. Mutable `CompoundCurve` classes also retain mutable curves and their points by reference. See [mutable compound curves](instantiable-spatial-types.md#mutable-compound-curves) -for component editing and recursive context propagation. Other mutable aggregates +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 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 cf70369..e255f51 100644 --- a/docs/instantiable-spatial-types.md +++ b/docs/instantiable-spatial-types.md @@ -777,3 +777,82 @@ owners. Passing `false` updates only the receiver. No context setter recalculate coordinates, changes the compound's spatial type or changes its layout. `toArray()` preserves component boundaries and their defining point tuples; circular components are never flattened or approximated. + +## Mutable polygons and triangles + +`Polygon` and `Triangle` are available directly in `Types\Dimension2`, +`Types\Dimension3z`, `Types\Dimension3m`, and `Types\Dimension4zm`. +Both constructors have the same signature: + +```php +new Polygon(array $rings, int|SpatialReference $srid = 0, + SpatialModelEnum $family = SpatialModelEnum::GEOMETRY); +new Triangle(array $rings, int|SpatialReference $srid = 0, + SpatialModelEnum $family = SpatialModelEnum::GEOMETRY); +``` + +Each input ring is a `MutableLineStringInterface` or an array of mutable points +and coordinate tuples. Arrays create mutable line strings and points using the +surface's current layout, family and reference. Supplied spatial objects retain +their own context and identity. Immutable rings and points are rejected without +conversion, including immutable points exposed by a custom ring. + +| Operation | Effect | +| --- | --- | +| `addRing($ring)` | Append a mutable line string | +| `insertRing($index, $ring)` | Insert before the addressed ring; the count appends | +| `removeRing($index)` | Remove an existing ring and reindex | +| `setRing($index, $ring)` | Replace one existing ring | +| `setPoint($ringIndex, $pointIndex, $point)` | Replace one point in an existing ring | +| `setArrayOfCoordinates($coordinates)` | Replace all rings from coordinate tuples | +| `getRing($index)`, `getRings()`, `getElements()` | Return the retained mutable ring instances | + +All successful mutations return the receiver as `static`. Ring and point indices +start at zero; access, replacement and removal require an existing element. +Negative indices and indices beyond the addressed sequence are rejected with +`OutOfBoundsException`. Failed addressing or technical input checks leave the +surface and shared descendants unchanged, including a late invalid tuple during +complete coordinate replacement. + +```php +use LongitudeOne\Core\Enum\SpatialModelEnum; +use LongitudeOne\SpatialTypes\Types\Dimension2\LineString; +use LongitudeOne\SpatialTypes\Types\Dimension2\Point; +use LongitudeOne\SpatialTypes\Types\Dimension2\Polygon; +use LongitudeOne\SpatialTypes\Types\Dimension2\Triangle; + +$ring = new LineString([[0, null], [3, 4]], 4326); +$polygon = new Polygon([$ring], 4326); +$triangle = new Triangle([$ring], 4326); +$replacement = new Point(7, null, 4326); + +$samePolygon = $polygon->setPoint(0, 0, $replacement); +assert($polygon === $samePolygon); +assert($replacement === $triangle->getRing(0)->getPoint(0)); +assert([[[7, null], [3, 4]]] === $triangle->toArray()); + +$polygon->setFamily(SpatialModelEnum::GEOGRAPHY, false); // Only the polygon. +$polygon->setSrid(2154); // Polygon, shared ring and every point. +assert(2154 === $triangle->getRing(0)->getSrid()); +assert(4326 === $triangle->getSrid()); +``` + +Construction, insertion and replacement retain supplied components without +cloning; getters expose the same instances. Changes to a shared ring or point +are visible in every surface containing it. Coordinate replacement creates new +descendants and leaves the previous rings and their other owners unchanged. + +`setSrid()`, `setSpatialReference()` and `setFamily()` each accept a second +`bool $propagate = true` parameter. By default they update all rings and points +recursively; `false` updates only the receiver. No ordinates are recalculated. +The instance's spatial type and coordinate layout remain fixed. Mutable surfaces +have no `with…` methods or dimension setter; immutable counterparts keep their +copy-producing operations and reject mutable components. + +Open or incomplete rings, EMPTY rings, null ordinates and triangles with extra +points or rings remain representable. Editing preserves the supplied order and +does not close rings, synchronize endpoints, enforce orientation or validate +geometry. ISO/IEC CD 13249-3:201x(E), clauses 8.3.1 and 8.4.1, define the polygon +and triangle models; representing a value does not establish conformity with +those geometric definitions. Request functional checks through +[spatial-validator](validators.md) explicitly. diff --git a/docs/spatial-reference-systems.md b/docs/spatial-reference-systems.md index efcf021..8d4aab6 100644 --- a/docs/spatial-reference-systems.md +++ b/docs/spatial-reference-systems.md @@ -49,7 +49,8 @@ The Lambert 93 ordinates in this example must be calculated by that external transformer; this library intentionally does not provide it. Mutable line strings and circular strings propagate declared-context updates to -contained points by default. Mutable compound curves recursively propagate to +contained points by default. Mutable polygons and triangles recursively propagate to rings and their points. +Mutable compound curves recursively propagate to all components and their points. Their `setSrid()`, `setSpatialReference()` and `setFamily()` methods accept `$propagate = false` to change only the receiver. Propagation changes shared instances, so their other owners observe the updated diff --git a/docs/type-class-hierarchy.md b/docs/type-class-hierarchy.md index 4271d80..3ebee7b 100644 --- a/docs/type-class-hierarchy.md +++ b/docs/type-class-hierarchy.md @@ -84,3 +84,18 @@ AbstractSpatialType `MutableCompoundCurveInterface` refines component getters to mutable line-string or circular-string interfaces and exposes ordered membership editing. Immutable compound curves retain their separate immutable base and copy contracts. + +Mutable polygons and triangles share ring storage and editing in a separate base: + +```text +AbstractSpatialType +└── AbstractPolygon + ├── Dimension{2,3m,3z,4zm}\Polygon + └── Dimension{2,3m,3z,4zm}\Triangle +``` + +`MutablePolygonInterface` refines ring accessors to `MutableLineStringInterface` +and provides ring editing and ring-then-point replacement. `MutableTriangleInterface` +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. diff --git a/lib/Interfaces/MutablePolygonInterface.php b/lib/Interfaces/MutablePolygonInterface.php index f278f63..a0ab79f 100644 --- a/lib/Interfaces/MutablePolygonInterface.php +++ b/lib/Interfaces/MutablePolygonInterface.php @@ -16,7 +16,64 @@ namespace LongitudeOne\SpatialTypes\Interfaces; -/** Mutable polygon contract; type-specific editing is introduced separately. */ +use LongitudeOne\Core\Enum\SpatialModelEnum; +use LongitudeOne\SpatialTypes\Reference\SpatialReference; + +/** In-place ordered ring and point editing without geometric validation. */ interface MutablePolygonInterface extends MutableSpatialInterface, PolygonInterface { + /** @param MutableLineStringInterface $ring Ring retained without cloning */ + public function addRing(MutableLineStringInterface $ring): static; + + /** @return list */ + public function getElements(): array; + + /** @param int $index Existing zero-based ring index */ + public function getRing(int $index): MutableLineStringInterface; + + /** @return list */ + public function getRings(): array; + + /** + * @param int $index Zero-based insertion index, up to the ring count + * @param MutableLineStringInterface $ring Ring retained without cloning + */ + public function insertRing(int $index, MutableLineStringInterface $ring): static; + + /** @param int $index Existing zero-based ring index */ + public function removeRing(int $index): static; + + /** @param array> $coordinates Complete replacement ring coordinates */ + public function setArrayOfCoordinates(array $coordinates): static; + + /** + * @param SpatialModelEnum $family Declared family + * @param bool $propagate Whether to update rings and their points + */ + public function setFamily(SpatialModelEnum $family, bool $propagate = true): static; + + /** + * @param int $ringIndex Existing zero-based ring index + * @param int $pointIndex Existing zero-based point index within the ring + * @param MutablePointInterface $point Point retained without cloning + */ + public function setPoint(int $ringIndex, int $pointIndex, MutablePointInterface $point): static; + + /** + * @param int $index Existing zero-based ring index + * @param MutableLineStringInterface $ring Ring retained without cloning + */ + public function setRing(int $index, MutableLineStringInterface $ring): static; + + /** + * @param SpatialReference $reference Declared reference + * @param bool $propagate Whether to update rings and their points + */ + public function setSpatialReference(SpatialReference $reference, bool $propagate = true): static; + + /** + * @param int $srid Declared reference identifier + * @param bool $propagate Whether to update rings and their points + */ + public function setSrid(int $srid, bool $propagate = true): static; } diff --git a/lib/Interfaces/MutableTriangleInterface.php b/lib/Interfaces/MutableTriangleInterface.php index 2e5b5a8..7a081f0 100644 --- a/lib/Interfaces/MutableTriangleInterface.php +++ b/lib/Interfaces/MutableTriangleInterface.php @@ -16,7 +16,7 @@ namespace LongitudeOne\SpatialTypes\Interfaces; -/** Mutable triangle contract; type-specific editing is introduced separately. */ +/** Mutable triangle contract with inherited ordered ring and point editing. */ interface MutableTriangleInterface extends MutablePolygonInterface, TriangleInterface { } diff --git a/lib/Types/AbstractPolygon.php b/lib/Types/AbstractPolygon.php new file mode 100644 index 0000000..8de4af8 --- /dev/null +++ b/lib/Types/AbstractPolygon.php @@ -0,0 +1,246 @@ + 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\Reference\SpatialReference; + +/** Mutable ordered rings without geometric validation. + * @internal + */ +abstract class AbstractPolygon extends AbstractSpatialType implements MutablePolygonInterface +{ + protected SpatialModelEnum $family; + + /** @var list */ + protected array $rings = []; + + /** + * @param array|MutableLineStringInterface> $rings Ordered rings or point sequences + * @param int|SpatialReference $srid Declared reference + * @param SpatialModelEnum $family Declared family + */ + final public function __construct(array $rings, int|SpatialReference $srid = 0, SpatialModelEnum $family = SpatialModelEnum::GEOMETRY) + { + $this->family = $family; + $this->initializeSpatialReference($srid); + $this->rings = $this->hydrateRings($rings); + } + + /** @param MutableLineStringInterface $ring Ring retained without cloning */ + public function addRing(MutableLineStringInterface $ring): static + { + $this->checkRingPoints($ring); + $this->rings[] = $ring; + + return $this; + } + + /** @return list */ + public function getElements(): array + { + return $this->rings; + } + + /** Return the declared family. */ + public function getFamily(): SpatialModelEnum + { + return $this->family; + } + + /** @param int $index Existing zero-based ring index */ + public function getRing(int $index): MutableLineStringInterface + { + $this->requireRingIndex($index); + + return $this->rings[$index]; + } + + /** @return list */ + public function getRings(): array + { + return $this->rings; + } + + /** Return the fixed spatial type. */ + public function getType(): GeometryTypeEnum + { + return GeometryTypeEnum::POLYGON; + } + + /** + * @param int $index Zero-based insertion index, up to the ring count + * @param MutableLineStringInterface $ring Ring retained without cloning + */ + public function insertRing(int $index, MutableLineStringInterface $ring): static + { + if ($index < 0 || $index > count($this->rings)) { + throw new OutOfBoundsException(sprintf('Ring index %d is outside the insertion range.', $index)); + } + $this->checkRingPoints($ring); + array_splice($this->rings, $index, 0, [$ring]); + + return $this; + } + + /** Whether there are no rings. */ + public function isEmpty(): bool + { + return [] === $this->rings; + } + + /** @param int $index Existing zero-based ring index */ + public function removeRing(int $index): static + { + $this->requireRingIndex($index); + array_splice($this->rings, $index, 1); + + return $this; + } + + /** @param array> $coordinates Complete replacement ring coordinates */ + public function setArrayOfCoordinates(array $coordinates): static + { + $this->rings = $this->hydrateRings($coordinates); + + return $this; + } + + /** + * @param SpatialModelEnum $family Declared family + * @param bool $propagate Whether to update rings and their points + */ + public function setFamily(SpatialModelEnum $family, bool $propagate = true): static + { + if ($propagate) { + foreach ($this->rings as $ring) { + $ring->setFamily($family); + } + } + $this->family = $family; + + return $this; + } + + /** + * @param int $ringIndex Existing zero-based ring index + * @param int $pointIndex Existing zero-based point index within the ring + * @param MutablePointInterface $point Point retained without cloning + */ + public function setPoint(int $ringIndex, int $pointIndex, MutablePointInterface $point): static + { + $this->getRing($ringIndex)->setPoint($pointIndex, $point); + + return $this; + } + + /** + * @param int $index Existing zero-based ring index + * @param MutableLineStringInterface $ring Ring retained without cloning + */ + public function setRing(int $index, MutableLineStringInterface $ring): static + { + $this->requireRingIndex($index); + $this->checkRingPoints($ring); + array_splice($this->rings, $index, 1, [$ring]); + + return $this; + } + + /** + * @param SpatialReference $reference Declared reference + * @param bool $propagate Whether to update rings and their points + */ + public function setSpatialReference(SpatialReference $reference, bool $propagate = true): static + { + if ($propagate) { + foreach ($this->rings as $ring) { + $ring->setSpatialReference($reference); + } + } + $this->spatialReference = $reference; + + return $this; + } + + /** + * @param int $srid Declared reference identifier + * @param bool $propagate Whether to update rings and their 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 (MutableLineStringInterface $ring): array => $ring->toArray(), $this->rings); + } + + /** @param MutableLineStringInterface $ring Ring whose descendants must be mutable */ + private function checkRingPoints(MutableLineStringInterface $ring): void + { + foreach ($ring->getPoints() as $point) { + if (!$point instanceof MutablePointInterface) { + throw new InvalidValueException('Mutable polygon rings require mutable point components.'); + } + } + } + + /** + * @param array|MutableLineStringInterface> $rings Ordered rings or point sequences + * + * @return list + */ + private function hydrateRings(array $rings): array + { + $hydrated = []; + foreach ($rings as $ring) { + if (is_array($ring)) { + $ring = match ($this->getDimension()) { + CoordinateDimensionEnum::XY => new Dimension2\LineString($ring, $this->spatialReference, $this->family), + CoordinateDimensionEnum::XYZ => new Dimension3z\LineString($ring, $this->spatialReference, $this->family), + CoordinateDimensionEnum::XYM => new Dimension3m\LineString($ring, $this->spatialReference, $this->family), + CoordinateDimensionEnum::XYZM => new Dimension4zm\LineString($ring, $this->spatialReference, $this->family), + }; + } + if (!$ring instanceof MutableLineStringInterface) { + throw new InvalidValueException('Mutable polygons require mutable line-string rings or point sequences.'); + } + $this->checkRingPoints($ring); + $hydrated[] = $ring; + } + + return $hydrated; + } + + /** @param int $index Existing zero-based ring index */ + private function requireRingIndex(int $index): void + { + if ($index < 0 || $index >= count($this->rings)) { + throw new OutOfBoundsException(sprintf('Ring index %d does not address an existing ring.', $index)); + } + } +} diff --git a/lib/Types/Dimension2/Polygon.php b/lib/Types/Dimension2/Polygon.php new file mode 100644 index 0000000..accb081 --- /dev/null +++ b/lib/Types/Dimension2/Polygon.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\Dimension2; + +use LongitudeOne\Core\Enum\CoordinateDimensionEnum; +use LongitudeOne\SpatialTypes\Types\AbstractPolygon; + +/** Mutable XY Polygon with instance-level declared family. */ +final class Polygon extends AbstractPolygon +{ + /** Return the fixed coordinate layout. */ + public function getDimension(): CoordinateDimensionEnum + { + return CoordinateDimensionEnum::XY; + } +} diff --git a/lib/Types/Dimension2/Triangle.php b/lib/Types/Dimension2/Triangle.php new file mode 100644 index 0000000..fae64d0 --- /dev/null +++ b/lib/Types/Dimension2/Triangle.php @@ -0,0 +1,38 @@ + 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\Dimension2; + +use LongitudeOne\Core\Enum\CoordinateDimensionEnum; +use LongitudeOne\Core\Enum\GeometryTypeEnum; +use LongitudeOne\SpatialTypes\Interfaces\MutableTriangleInterface; +use LongitudeOne\SpatialTypes\Types\AbstractPolygon; + +/** Mutable XY Triangle with instance-level declared family. */ +final class Triangle extends AbstractPolygon implements MutableTriangleInterface +{ + /** Return the fixed coordinate layout. */ + public function getDimension(): CoordinateDimensionEnum + { + return CoordinateDimensionEnum::XY; + } + + /** Return the fixed spatial type. */ + public function getType(): GeometryTypeEnum + { + return GeometryTypeEnum::TRIANGLE; + } +} diff --git a/lib/Types/Dimension3m/Polygon.php b/lib/Types/Dimension3m/Polygon.php new file mode 100644 index 0000000..a1459a4 --- /dev/null +++ b/lib/Types/Dimension3m/Polygon.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\Dimension3m; + +use LongitudeOne\Core\Enum\CoordinateDimensionEnum; +use LongitudeOne\SpatialTypes\Types\AbstractPolygon; + +/** Mutable XYM Polygon with instance-level declared family. */ +final class Polygon extends AbstractPolygon +{ + /** Return the fixed coordinate layout. */ + public function getDimension(): CoordinateDimensionEnum + { + return CoordinateDimensionEnum::XYM; + } +} diff --git a/lib/Types/Dimension3m/Triangle.php b/lib/Types/Dimension3m/Triangle.php new file mode 100644 index 0000000..cb874c7 --- /dev/null +++ b/lib/Types/Dimension3m/Triangle.php @@ -0,0 +1,38 @@ + 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\Dimension3m; + +use LongitudeOne\Core\Enum\CoordinateDimensionEnum; +use LongitudeOne\Core\Enum\GeometryTypeEnum; +use LongitudeOne\SpatialTypes\Interfaces\MutableTriangleInterface; +use LongitudeOne\SpatialTypes\Types\AbstractPolygon; + +/** Mutable XYM Triangle with instance-level declared family. */ +final class Triangle extends AbstractPolygon implements MutableTriangleInterface +{ + /** Return the fixed coordinate layout. */ + public function getDimension(): CoordinateDimensionEnum + { + return CoordinateDimensionEnum::XYM; + } + + /** Return the fixed spatial type. */ + public function getType(): GeometryTypeEnum + { + return GeometryTypeEnum::TRIANGLE; + } +} diff --git a/lib/Types/Dimension3z/Polygon.php b/lib/Types/Dimension3z/Polygon.php new file mode 100644 index 0000000..5367243 --- /dev/null +++ b/lib/Types/Dimension3z/Polygon.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\AbstractPolygon; + +/** Mutable XYZ Polygon with instance-level declared family. */ +final class Polygon extends AbstractPolygon +{ + /** Return the fixed coordinate layout. */ + public function getDimension(): CoordinateDimensionEnum + { + return CoordinateDimensionEnum::XYZ; + } +} diff --git a/lib/Types/Dimension3z/Triangle.php b/lib/Types/Dimension3z/Triangle.php new file mode 100644 index 0000000..8b2ce64 --- /dev/null +++ b/lib/Types/Dimension3z/Triangle.php @@ -0,0 +1,38 @@ + 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\Core\Enum\GeometryTypeEnum; +use LongitudeOne\SpatialTypes\Interfaces\MutableTriangleInterface; +use LongitudeOne\SpatialTypes\Types\AbstractPolygon; + +/** Mutable XYZ Triangle with instance-level declared family. */ +final class Triangle extends AbstractPolygon implements MutableTriangleInterface +{ + /** Return the fixed coordinate layout. */ + public function getDimension(): CoordinateDimensionEnum + { + return CoordinateDimensionEnum::XYZ; + } + + /** Return the fixed spatial type. */ + public function getType(): GeometryTypeEnum + { + return GeometryTypeEnum::TRIANGLE; + } +} diff --git a/lib/Types/Dimension4zm/Polygon.php b/lib/Types/Dimension4zm/Polygon.php new file mode 100644 index 0000000..e891234 --- /dev/null +++ b/lib/Types/Dimension4zm/Polygon.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\AbstractPolygon; + +/** Mutable XYZM Polygon with instance-level declared family. */ +final class Polygon extends AbstractPolygon +{ + /** Return the fixed coordinate layout. */ + public function getDimension(): CoordinateDimensionEnum + { + return CoordinateDimensionEnum::XYZM; + } +} diff --git a/lib/Types/Dimension4zm/Triangle.php b/lib/Types/Dimension4zm/Triangle.php new file mode 100644 index 0000000..2726b61 --- /dev/null +++ b/lib/Types/Dimension4zm/Triangle.php @@ -0,0 +1,38 @@ + 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\Core\Enum\GeometryTypeEnum; +use LongitudeOne\SpatialTypes\Interfaces\MutableTriangleInterface; +use LongitudeOne\SpatialTypes\Types\AbstractPolygon; + +/** Mutable XYZM Triangle with instance-level declared family. */ +final class Triangle extends AbstractPolygon implements MutableTriangleInterface +{ + /** Return the fixed coordinate layout. */ + public function getDimension(): CoordinateDimensionEnum + { + return CoordinateDimensionEnum::XYZM; + } + + /** Return the fixed spatial type. */ + public function getType(): GeometryTypeEnum + { + return GeometryTypeEnum::TRIANGLE; + } +} diff --git a/tests/Unit/Types/MutablePolygonContextTest.php b/tests/Unit/Types/MutablePolygonContextTest.php new file mode 100644 index 0000000..c261d07 --- /dev/null +++ b/tests/Unit/Types/MutablePolygonContextTest.php @@ -0,0 +1,160 @@ + 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\Dimension2\LineString; +use LongitudeOne\SpatialTypes\Types\Dimension2\Point; +use LongitudeOne\SpatialTypes\Types\Dimension2\Polygon; +use PHPUnit\Framework\TestCase; + +/** Recursive polygon context and shared ownership. + * @internal + * + * @coversNothing + */ +class MutablePolygonContextTest extends TestCase +{ + /** Declared context preserves coordinates and shared component identity. */ + public function testFamilyLocalOnly(): void + { + $initial = SpatialReference::epsg(4326); + $point = new Point(0, null, $initial); + $line = new LineString([$point], $initial); + $arc = new LineString([$point], $initial); + $compound = new Polygon([$line, $arc], $initial); + $owner = new Polygon([$line], $initial); + static::assertSame($compound, $compound->setFamily(SpatialModelEnum::GEOGRAPHY, false)); + static::assertSame(SpatialModelEnum::GEOGRAPHY, $compound->getFamily()); + static::assertSame(SpatialModelEnum::GEOMETRY, $line->getFamily()); + static::assertSame(SpatialModelEnum::GEOMETRY, $arc->getFamily()); + static::assertSame(SpatialModelEnum::GEOMETRY, $point->getFamily()); + static::assertSame(SpatialModelEnum::GEOMETRY, $owner->getRing(0)->getFamily()); + static::assertSame(SpatialModelEnum::GEOMETRY, $owner->getFamily()); + static::assertSame($line, $compound->getRing(0)); + static::assertSame($point, $arc->getPoint(0)); + static::assertSame([[[0, null]], [[0, null]]], $compound->toArray()); + } + + /** Declared context preserves coordinates and shared component identity. */ + public function testFamilyPropagation(): void + { + $initial = SpatialReference::epsg(4326); + $point = new Point(0, null, $initial); + $line = new LineString([$point], $initial); + $arc = new LineString([$point], $initial); + $compound = new Polygon([$line, $arc], $initial); + $owner = new Polygon([$line], $initial); + static::assertSame($compound, $compound->setFamily(SpatialModelEnum::GEOGRAPHY)); + static::assertSame(SpatialModelEnum::GEOGRAPHY, $compound->getFamily()); + static::assertSame(SpatialModelEnum::GEOGRAPHY, $line->getFamily()); + static::assertSame(SpatialModelEnum::GEOGRAPHY, $arc->getFamily()); + static::assertSame(SpatialModelEnum::GEOGRAPHY, $point->getFamily()); + static::assertSame(SpatialModelEnum::GEOGRAPHY, $owner->getRing(0)->getFamily()); + static::assertSame(SpatialModelEnum::GEOMETRY, $owner->getFamily()); + static::assertSame($line, $compound->getRing(0)); + static::assertSame($point, $arc->getPoint(0)); + static::assertSame([[[0, null]], [[0, null]]], $compound->toArray()); + } + + /** Declared context preserves coordinates and shared component identity. */ + public function testReferenceLocalOnly(): void + { + $initial = SpatialReference::epsg(4326); + $reference = SpatialReference::epsg(2154); + $point = new Point(0, null, $initial); + $line = new LineString([$point], $initial); + $arc = new LineString([$point], $initial); + $compound = new Polygon([$line, $arc], $initial); + $owner = new Polygon([$line], $initial); + static::assertSame($compound, $compound->setSpatialReference($reference, false)); + static::assertSame($reference, $compound->getSpatialReference()); + static::assertSame($initial, $line->getSpatialReference()); + static::assertSame($initial, $arc->getSpatialReference()); + static::assertSame($initial, $point->getSpatialReference()); + static::assertSame($initial, $owner->getRing(0)->getSpatialReference()); + static::assertSame($initial, $owner->getSpatialReference()); + static::assertSame($line, $compound->getRing(0)); + static::assertSame($point, $arc->getPoint(0)); + static::assertSame([[[0, null]], [[0, null]]], $compound->toArray()); + } + + /** Declared context preserves coordinates and shared component identity. */ + public function testReferencePropagation(): void + { + $initial = SpatialReference::epsg(4326); + $reference = SpatialReference::epsg(2154); + $point = new Point(0, null, $initial); + $line = new LineString([$point], $initial); + $arc = new LineString([$point], $initial); + $compound = new Polygon([$line, $arc], $initial); + $owner = new Polygon([$line], $initial); + static::assertSame($compound, $compound->setSpatialReference($reference)); + static::assertSame($reference, $compound->getSpatialReference()); + static::assertSame($reference, $line->getSpatialReference()); + static::assertSame($reference, $arc->getSpatialReference()); + static::assertSame($reference, $point->getSpatialReference()); + static::assertSame($reference, $owner->getRing(0)->getSpatialReference()); + static::assertSame($initial, $owner->getSpatialReference()); + static::assertSame($line, $compound->getRing(0)); + static::assertSame($point, $arc->getPoint(0)); + static::assertSame([[[0, null]], [[0, null]]], $compound->toArray()); + } + + /** Declared context preserves coordinates and shared component identity. */ + public function testSridLocalOnly(): void + { + $initial = SpatialReference::epsg(4326); + $point = new Point(0, null, $initial); + $line = new LineString([$point], $initial); + $arc = new LineString([$point], $initial); + $compound = new Polygon([$line, $arc], $initial); + $owner = new Polygon([$line], $initial); + static::assertSame($compound, $compound->setSrid(3857, false)); + static::assertSame(3857, $compound->getSrid()); + static::assertSame(4326, $line->getSrid()); + static::assertSame(4326, $arc->getSrid()); + static::assertSame(4326, $point->getSrid()); + static::assertSame(4326, $owner->getRing(0)->getSrid()); + static::assertSame(4326, $owner->getSrid()); + static::assertSame($line, $compound->getRing(0)); + static::assertSame($point, $arc->getPoint(0)); + static::assertSame([[[0, null]], [[0, null]]], $compound->toArray()); + } + + /** Declared context preserves coordinates and shared component identity. */ + public function testSridPropagation(): void + { + $initial = SpatialReference::epsg(4326); + $point = new Point(0, null, $initial); + $line = new LineString([$point], $initial); + $arc = new LineString([$point], $initial); + $compound = new Polygon([$line, $arc], $initial); + $owner = new Polygon([$line], $initial); + static::assertSame($compound, $compound->setSrid(3857)); + static::assertSame(3857, $compound->getSrid()); + static::assertSame(3857, $line->getSrid()); + static::assertSame(3857, $arc->getSrid()); + static::assertSame(3857, $point->getSrid()); + static::assertSame(3857, $owner->getRing(0)->getSrid()); + static::assertSame(4326, $owner->getSrid()); + static::assertSame($line, $compound->getRing(0)); + static::assertSame($point, $arc->getPoint(0)); + static::assertSame([[[0, null]], [[0, null]]], $compound->toArray()); + } +} diff --git a/tests/Unit/Types/MutablePolygonLayoutTest.php b/tests/Unit/Types/MutablePolygonLayoutTest.php new file mode 100644 index 0000000..8c59809 --- /dev/null +++ b/tests/Unit/Types/MutablePolygonLayoutTest.php @@ -0,0 +1,178 @@ + 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\Types\Dimension3m\LineString; +use LongitudeOne\SpatialTypes\Types\Dimension3m\Point; +use LongitudeOne\SpatialTypes\Types\Dimension3m\Polygon; +use LongitudeOne\SpatialTypes\Types\Dimension3m\Triangle; +use PHPUnit\Framework\TestCase; + +/** Every mutable polygon and triangle layout hydrates mutable descendants. + * @internal + * + * @coversNothing + */ +class MutablePolygonLayoutTest extends TestCase +{ + /** XYM Polygon retains ordinates, type and layout through replacement. */ + public function testXymPolygon(): void + { + $surface = new Polygon([[[0, null, 9]]], 4326, SpatialModelEnum::GEOGRAPHY); + static::assertSame(CoordinateDimensionEnum::XYM, $surface->getDimension()); + static::assertSame(GeometryTypeEnum::POLYGON, $surface->getType()); + static::assertSame(SpatialModelEnum::GEOGRAPHY, $surface->getFamily()); + static::assertSame([[[0, null, 9]]], $surface->toArray()); + static::assertInstanceOf(LineString::class, $surface->getRing(0)); + static::assertInstanceOf(Point::class, $surface->getRing(0)->getPoint(0)); + static::assertSame($surface, $surface->setArrayOfCoordinates([[[null, 7, null]]])->setSrid(2154)->setFamily(SpatialModelEnum::GEOMETRY)); + static::assertSame([[[null, 7, null]]], $surface->toArray()); + static::assertSame(CoordinateDimensionEnum::XYM, $surface->getDimension()); + static::assertSame(GeometryTypeEnum::POLYGON, $surface->getType()); + static::assertSame(2154, $surface->getRing(0)->getPoint(0)->getSrid()); + static::assertSame(SpatialModelEnum::GEOMETRY, $surface->getRing(0)->getPoint(0)->getFamily()); + } + + /** XYM Triangle retains ordinates, type and layout through replacement. */ + public function testXymTriangle(): void + { + $surface = new Triangle([[[0, null, 9]]], 4326, SpatialModelEnum::GEOGRAPHY); + static::assertSame(CoordinateDimensionEnum::XYM, $surface->getDimension()); + static::assertSame(GeometryTypeEnum::TRIANGLE, $surface->getType()); + static::assertSame(SpatialModelEnum::GEOGRAPHY, $surface->getFamily()); + static::assertSame([[[0, null, 9]]], $surface->toArray()); + static::assertInstanceOf(LineString::class, $surface->getRing(0)); + static::assertInstanceOf(Point::class, $surface->getRing(0)->getPoint(0)); + static::assertSame($surface, $surface->setArrayOfCoordinates([[[null, 7, null]]])->setSrid(2154)->setFamily(SpatialModelEnum::GEOMETRY)); + static::assertSame([[[null, 7, null]]], $surface->toArray()); + static::assertSame(CoordinateDimensionEnum::XYM, $surface->getDimension()); + static::assertSame(GeometryTypeEnum::TRIANGLE, $surface->getType()); + static::assertSame(2154, $surface->getRing(0)->getPoint(0)->getSrid()); + static::assertSame(SpatialModelEnum::GEOMETRY, $surface->getRing(0)->getPoint(0)->getFamily()); + } + + /** XY Polygon retains ordinates, type and layout through replacement. */ + public function testXyPolygon(): void + { + $surface = new \LongitudeOne\SpatialTypes\Types\Dimension2\Polygon([[[0, null]]], 4326, SpatialModelEnum::GEOGRAPHY); + static::assertSame(CoordinateDimensionEnum::XY, $surface->getDimension()); + static::assertSame(GeometryTypeEnum::POLYGON, $surface->getType()); + static::assertSame(SpatialModelEnum::GEOGRAPHY, $surface->getFamily()); + static::assertSame([[[0, null]]], $surface->toArray()); + static::assertInstanceOf(\LongitudeOne\SpatialTypes\Types\Dimension2\LineString::class, $surface->getRing(0)); + static::assertInstanceOf(\LongitudeOne\SpatialTypes\Types\Dimension2\Point::class, $surface->getRing(0)->getPoint(0)); + static::assertSame($surface, $surface->setArrayOfCoordinates([[[null, 7]]])->setSrid(2154)->setFamily(SpatialModelEnum::GEOMETRY)); + static::assertSame([[[null, 7]]], $surface->toArray()); + static::assertSame(CoordinateDimensionEnum::XY, $surface->getDimension()); + static::assertSame(GeometryTypeEnum::POLYGON, $surface->getType()); + static::assertSame(2154, $surface->getRing(0)->getPoint(0)->getSrid()); + static::assertSame(SpatialModelEnum::GEOMETRY, $surface->getRing(0)->getPoint(0)->getFamily()); + } + + /** XY Triangle retains ordinates, type and layout through replacement. */ + public function testXyTriangle(): void + { + $surface = new \LongitudeOne\SpatialTypes\Types\Dimension2\Triangle([[[0, null]]], 4326, SpatialModelEnum::GEOGRAPHY); + static::assertSame(CoordinateDimensionEnum::XY, $surface->getDimension()); + static::assertSame(GeometryTypeEnum::TRIANGLE, $surface->getType()); + static::assertSame(SpatialModelEnum::GEOGRAPHY, $surface->getFamily()); + static::assertSame([[[0, null]]], $surface->toArray()); + static::assertInstanceOf(\LongitudeOne\SpatialTypes\Types\Dimension2\LineString::class, $surface->getRing(0)); + static::assertInstanceOf(\LongitudeOne\SpatialTypes\Types\Dimension2\Point::class, $surface->getRing(0)->getPoint(0)); + static::assertSame($surface, $surface->setArrayOfCoordinates([[[null, 7]]])->setSrid(2154)->setFamily(SpatialModelEnum::GEOMETRY)); + static::assertSame([[[null, 7]]], $surface->toArray()); + static::assertSame(CoordinateDimensionEnum::XY, $surface->getDimension()); + static::assertSame(GeometryTypeEnum::TRIANGLE, $surface->getType()); + static::assertSame(2154, $surface->getRing(0)->getPoint(0)->getSrid()); + static::assertSame(SpatialModelEnum::GEOMETRY, $surface->getRing(0)->getPoint(0)->getFamily()); + } + + /** XYZM Polygon retains ordinates, type and layout through replacement. */ + public function testXyzmPolygon(): void + { + $surface = new \LongitudeOne\SpatialTypes\Types\Dimension4zm\Polygon([[[0, null, 8, 9]]], 4326, SpatialModelEnum::GEOGRAPHY); + static::assertSame(CoordinateDimensionEnum::XYZM, $surface->getDimension()); + static::assertSame(GeometryTypeEnum::POLYGON, $surface->getType()); + static::assertSame(SpatialModelEnum::GEOGRAPHY, $surface->getFamily()); + static::assertSame([[[0, null, 8, 9]]], $surface->toArray()); + static::assertInstanceOf(\LongitudeOne\SpatialTypes\Types\Dimension4zm\LineString::class, $surface->getRing(0)); + static::assertInstanceOf(\LongitudeOne\SpatialTypes\Types\Dimension4zm\Point::class, $surface->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::POLYGON, $surface->getType()); + static::assertSame(2154, $surface->getRing(0)->getPoint(0)->getSrid()); + static::assertSame(SpatialModelEnum::GEOMETRY, $surface->getRing(0)->getPoint(0)->getFamily()); + } + + /** XYZM Triangle retains ordinates, type and layout through replacement. */ + public function testXyzmTriangle(): void + { + $surface = new \LongitudeOne\SpatialTypes\Types\Dimension4zm\Triangle([[[0, null, 8, 9]]], 4326, SpatialModelEnum::GEOGRAPHY); + static::assertSame(CoordinateDimensionEnum::XYZM, $surface->getDimension()); + static::assertSame(GeometryTypeEnum::TRIANGLE, $surface->getType()); + static::assertSame(SpatialModelEnum::GEOGRAPHY, $surface->getFamily()); + static::assertSame([[[0, null, 8, 9]]], $surface->toArray()); + static::assertInstanceOf(\LongitudeOne\SpatialTypes\Types\Dimension4zm\LineString::class, $surface->getRing(0)); + static::assertInstanceOf(\LongitudeOne\SpatialTypes\Types\Dimension4zm\Point::class, $surface->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::TRIANGLE, $surface->getType()); + static::assertSame(2154, $surface->getRing(0)->getPoint(0)->getSrid()); + static::assertSame(SpatialModelEnum::GEOMETRY, $surface->getRing(0)->getPoint(0)->getFamily()); + } + + /** XYZ Polygon retains ordinates, type and layout through replacement. */ + public function testXyzPolygon(): void + { + $surface = new \LongitudeOne\SpatialTypes\Types\Dimension3z\Polygon([[[0, null, 8]]], 4326, SpatialModelEnum::GEOGRAPHY); + static::assertSame(CoordinateDimensionEnum::XYZ, $surface->getDimension()); + static::assertSame(GeometryTypeEnum::POLYGON, $surface->getType()); + static::assertSame(SpatialModelEnum::GEOGRAPHY, $surface->getFamily()); + static::assertSame([[[0, null, 8]]], $surface->toArray()); + static::assertInstanceOf(\LongitudeOne\SpatialTypes\Types\Dimension3z\LineString::class, $surface->getRing(0)); + static::assertInstanceOf(\LongitudeOne\SpatialTypes\Types\Dimension3z\Point::class, $surface->getRing(0)->getPoint(0)); + static::assertSame($surface, $surface->setArrayOfCoordinates([[[null, 7, null]]])->setSrid(2154)->setFamily(SpatialModelEnum::GEOMETRY)); + static::assertSame([[[null, 7, null]]], $surface->toArray()); + static::assertSame(CoordinateDimensionEnum::XYZ, $surface->getDimension()); + static::assertSame(GeometryTypeEnum::POLYGON, $surface->getType()); + static::assertSame(2154, $surface->getRing(0)->getPoint(0)->getSrid()); + static::assertSame(SpatialModelEnum::GEOMETRY, $surface->getRing(0)->getPoint(0)->getFamily()); + } + + /** XYZ Triangle retains ordinates, type and layout through replacement. */ + public function testXyzTriangle(): void + { + $surface = new \LongitudeOne\SpatialTypes\Types\Dimension3z\Triangle([[[0, null, 8]]], 4326, SpatialModelEnum::GEOGRAPHY); + static::assertSame(CoordinateDimensionEnum::XYZ, $surface->getDimension()); + static::assertSame(GeometryTypeEnum::TRIANGLE, $surface->getType()); + static::assertSame(SpatialModelEnum::GEOGRAPHY, $surface->getFamily()); + static::assertSame([[[0, null, 8]]], $surface->toArray()); + static::assertInstanceOf(\LongitudeOne\SpatialTypes\Types\Dimension3z\LineString::class, $surface->getRing(0)); + static::assertInstanceOf(\LongitudeOne\SpatialTypes\Types\Dimension3z\Point::class, $surface->getRing(0)->getPoint(0)); + static::assertSame($surface, $surface->setArrayOfCoordinates([[[null, 7, null]]])->setSrid(2154)->setFamily(SpatialModelEnum::GEOMETRY)); + static::assertSame([[[null, 7, null]]], $surface->toArray()); + static::assertSame(CoordinateDimensionEnum::XYZ, $surface->getDimension()); + static::assertSame(GeometryTypeEnum::TRIANGLE, $surface->getType()); + static::assertSame(2154, $surface->getRing(0)->getPoint(0)->getSrid()); + static::assertSame(SpatialModelEnum::GEOMETRY, $surface->getRing(0)->getPoint(0)->getFamily()); + } +} diff --git a/tests/Unit/Types/MutablePolygonTest.php b/tests/Unit/Types/MutablePolygonTest.php new file mode 100644 index 0000000..97f8a83 --- /dev/null +++ b/tests/Unit/Types/MutablePolygonTest.php @@ -0,0 +1,611 @@ + 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\Exception\OutOfBoundsException; +use LongitudeOne\SpatialTypes\Interfaces\MutableLineStringInterface; +use LongitudeOne\SpatialTypes\Interfaces\MutablePolygonInterface; +use LongitudeOne\SpatialTypes\Interfaces\MutableSpatialInterface; +use LongitudeOne\SpatialTypes\Interfaces\MutableTriangleInterface; +use LongitudeOne\SpatialTypes\Types\Dimension2\Geometry\LineStringImmutable; +use LongitudeOne\SpatialTypes\Types\Dimension2\Geometry\PointImmutable; +use LongitudeOne\SpatialTypes\Types\Dimension2\Geometry\PolygonImmutable; +use LongitudeOne\SpatialTypes\Types\Dimension2\LineString; +use LongitudeOne\SpatialTypes\Types\Dimension2\Point; +use LongitudeOne\SpatialTypes\Types\Dimension2\Polygon; +use LongitudeOne\SpatialTypes\Types\Dimension2\Triangle; +use PHPUnit\Framework\TestCase; + +/** Mutable surface editing preserves identity and representable states. + * @internal + * + * @coversNothing + */ +class MutablePolygonTest extends TestCase +{ + /** All-null coordinate tuples create empty mutable points without removing their ring. */ + public function testAllNullPointRemainsInRing(): void + { + $polygon = new Polygon([[[null, null]]]); + static::assertFalse($polygon->isEmpty()); + static::assertSame([[[]]], $polygon->toArray()); + static::assertTrue($polygon->getRing(0)->getPoint(0)->isEmpty()); + static::assertSame(CoordinateDimensionEnum::XY, $polygon->getRing(0)->getPoint(0)->getDimension()); + } + + /** A late invalid tuple never commits earlier hydrated rings. */ + public function testCoordinateFailureIsAtomic(): void + { + $point = new Point(0, null, 4326); + $ring = new LineString([$point], 4326); + $polygon = new Polygon([$ring], 2154); + $owner = new Triangle([$ring]); + $this->expectException(InvalidDimensionException::class); + + try { + (new \ReflectionMethod($polygon, 'setArrayOfCoordinates'))->invoke($polygon, [[[3, 4]], [[5, 6, 7]]]); + } finally { + static::assertSame([$ring], $polygon->getRings()); + static::assertSame($point, $ring->getPoint(0)); + static::assertSame([[[0, null]]], $polygon->toArray()); + static::assertSame([[[0, null]]], $owner->toArray()); + static::assertSame(4326, $ring->getSrid()); + static::assertSame(4326, $point->getSrid()); + } + } + + /** Coordinate replacement creates descendants against current context and leaves former owners unchanged. */ + public function testCoordinateReplacementIsolation(): void + { + $point = new Point(1, 2, 4326); + $ring = new LineString([$point], 4326); + $polygon = new Polygon([$ring], 2154, SpatialModelEnum::GEOGRAPHY); + $owner = new Polygon([$ring]); + static::assertSame($polygon, $polygon->setArrayOfCoordinates([[[0, null], [null, 5]], []])); + static::assertInstanceOf(LineString::class, $polygon->getRing(0)); + static::assertInstanceOf(Point::class, $polygon->getRing(0)->getPoint(0)); + static::assertSame([[[0, null], [null, 5]], []], $polygon->toArray()); + static::assertSame(2154, $polygon->getRing(0)->getSrid()); + static::assertSame(2154, $polygon->getRing(0)->getPoint(0)->getSrid()); + static::assertSame(SpatialModelEnum::GEOGRAPHY, $polygon->getRing(0)->getFamily()); + static::assertSame(SpatialModelEnum::GEOGRAPHY, $polygon->getRing(0)->getPoint(0)->getFamily()); + static::assertSame([[[1, 2]]], $owner->toArray()); + static::assertSame($ring, $owner->getRing(0)); + static::assertSame($point, $ring->getPoint(0)); + static::assertSame(4326, $point->getSrid()); + } + + /** Invalid addressing preserves the receiver and all referenced descendants. */ + public function testCountAccessIsAtomic(): void + { + $point = new Point(0, null, 4326); + $ring = new LineString([$point], 4326); + $polygon = new Polygon([$ring], 2154); + $this->expectException(OutOfBoundsException::class); + + try { + $polygon->getRing(1); + } finally { + static::assertSame([$ring], $polygon->getRings()); + static::assertSame($point, $ring->getPoint(0)); + static::assertSame([[[0, null]]], $polygon->toArray()); + static::assertSame(4326, $ring->getSrid()); + static::assertSame(4326, $point->getSrid()); + static::assertSame(2154, $polygon->getSrid()); + } + } + + /** Invalid addressing preserves the receiver and all referenced descendants. */ + public function testCountPointIsAtomic(): void + { + $point = new Point(0, null, 4326); + $ring = new LineString([$point], 4326); + $polygon = new Polygon([$ring], 2154); + $this->expectException(OutOfBoundsException::class); + + try { + $polygon->setPoint(0, 1, $point); + } finally { + static::assertSame([$ring], $polygon->getRings()); + static::assertSame($point, $ring->getPoint(0)); + static::assertSame([[[0, null]]], $polygon->toArray()); + static::assertSame(4326, $ring->getSrid()); + static::assertSame(4326, $point->getSrid()); + static::assertSame(2154, $polygon->getSrid()); + } + } + + /** Invalid addressing preserves the receiver and all referenced descendants. */ + public function testCountRemovalIsAtomic(): void + { + $point = new Point(0, null, 4326); + $ring = new LineString([$point], 4326); + $polygon = new Polygon([$ring], 2154); + $this->expectException(OutOfBoundsException::class); + + try { + $polygon->removeRing(1); + } finally { + static::assertSame([$ring], $polygon->getRings()); + static::assertSame($point, $ring->getPoint(0)); + static::assertSame([[[0, null]]], $polygon->toArray()); + static::assertSame(4326, $ring->getSrid()); + static::assertSame(4326, $point->getSrid()); + static::assertSame(2154, $polygon->getSrid()); + } + } + + /** Invalid addressing preserves the receiver and all referenced descendants. */ + public function testCountReplacementIsAtomic(): void + { + $point = new Point(0, null, 4326); + $ring = new LineString([$point], 4326); + $polygon = new Polygon([$ring], 2154); + $this->expectException(OutOfBoundsException::class); + + try { + $polygon->setRing(1, $ring); + } finally { + static::assertSame([$ring], $polygon->getRings()); + static::assertSame($point, $ring->getPoint(0)); + static::assertSame([[[0, null]]], $polygon->toArray()); + static::assertSame(4326, $ring->getSrid()); + static::assertSame(4326, $point->getSrid()); + static::assertSame(2154, $polygon->getSrid()); + } + } + + /** Supplied components keep their own layout, family and reference. */ + public function testDifferentRingContextRemainsRepresentable(): void + { + $ring = new \LongitudeOne\SpatialTypes\Types\Dimension3m\LineString([[0, null, 9]], 4326, SpatialModelEnum::GEOGRAPHY); + $polygon = new Polygon([$ring], 2154); + static::assertSame($ring, $polygon->getRing(0)); + static::assertSame([[[0, null, 9]]], $polygon->toArray()); + static::assertSame(CoordinateDimensionEnum::XY, $polygon->getDimension()); + static::assertSame(CoordinateDimensionEnum::XYM, $ring->getDimension()); + static::assertSame(4326, $ring->getSrid()); + static::assertSame(SpatialModelEnum::GEOGRAPHY, $ring->getFamily()); + } + + /** Empty and incomplete boundaries remain representable, including triangles with holes. */ + public function testEmptyAndInvalidTriangleStates(): void + { + $triangle = new Triangle([]); + static::assertTrue($triangle->isEmpty()); + static::assertSame([], $triangle->toArray()); + static::assertSame($triangle, $triangle->setArrayOfCoordinates([[], [[0, null]], [[1, 2], [3, 4], [5, 6], [7, 8], [9, 10]]])); + static::assertFalse($triangle->isEmpty()); + static::assertSame([[], [[0, null]], [[1, 2], [3, 4], [5, 6], [7, 8], [9, 10]]], $triangle->toArray()); + static::assertSame(GeometryTypeEnum::TRIANGLE, $triangle->getType()); + static::assertSame($triangle, $triangle->setArrayOfCoordinates([])); + static::assertTrue($triangle->isEmpty()); + } + + /** Replacing a ring endpoint never closes or repairs the ring. */ + public function testEndpointReplacementDoesNotRepairClosure(): void + { + $ring = new LineString([[1, 2], [3, 4], [1, 2]]); + $polygon = new Polygon([$ring]); + $point = new Point(9, null); + static::assertSame($polygon, $polygon->setPoint(0, 0, $point)); + static::assertSame([[[9, null], [3, 4], [1, 2]]], $polygon->toArray()); + static::assertFalse($ring->isClosed()); + static::assertSame($point, $ring->getPoint(0)); + } + + /** Typed ring editing rejects immutable components without mutation. */ + public function testImmutableAdditionIsRejected(): void + { + $ring = new LineString([[0, null]], 4326); + $immutable = new LineStringImmutable([[3, 4]], 2154); + $polygon = new Polygon([$ring]); + $this->expectException(\TypeError::class); + + try { + (new \ReflectionMethod($polygon, 'addRing'))->invoke($polygon, $immutable); + } finally { + static::assertSame([$ring], $polygon->getRings()); + static::assertSame([[[0, null]]], $polygon->toArray()); + static::assertSame([[3, 4]], $immutable->toArray()); + static::assertSame(4326, $ring->getSrid()); + } + } + + /** Immutable rings are rejected at construction. */ + public function testImmutableConstructorIsRejected(): void + { + $immutable = new LineStringImmutable([[3, 4]], 2154); + $this->expectException(InvalidValueException::class); + + try { + (new \ReflectionClass(Polygon::class))->newInstance([$immutable]); + } finally { + static::assertSame([[3, 4]], $immutable->toArray()); + static::assertSame(2154, $immutable->getSrid()); + } + } + + /** Typed ring editing rejects immutable components without mutation. */ + public function testImmutableInsertionIsRejected(): void + { + $ring = new LineString([[0, null]], 4326); + $immutable = new LineStringImmutable([[3, 4]], 2154); + $polygon = new Polygon([$ring]); + $this->expectException(\TypeError::class); + + try { + (new \ReflectionMethod($polygon, 'insertRing'))->invoke($polygon, 0, $immutable); + } finally { + static::assertSame([$ring], $polygon->getRings()); + static::assertSame([[[0, null]]], $polygon->toArray()); + static::assertSame([[3, 4]], $immutable->toArray()); + static::assertSame(4326, $ring->getSrid()); + } + } + + /** Nested immutable points cannot enter through point-array construction. */ + public function testImmutablePointArrayIsRejected(): void + { + $point = new PointImmutable(3, 4, 2154); + $this->expectException(InvalidValueException::class); + + try { + (new \ReflectionClass(Triangle::class))->newInstance([[$point]]); + } finally { + static::assertSame([3, 4], $point->toArray()); + static::assertSame(2154, $point->getSrid()); + } + } + + /** Immutable point replacement cannot alter a shared ring. */ + public function testImmutablePointReplacementIsRejected(): void + { + $ring = new LineString([[0, null]]); + $polygon = new Polygon([$ring]); + $point = new PointImmutable(3, 4); + $this->expectException(\TypeError::class); + + try { + (new \ReflectionMethod($polygon, 'setPoint'))->invoke($polygon, 0, 0, $point); + } finally { + static::assertSame([$ring], $polygon->getRings()); + static::assertSame([[[0, null]]], $polygon->toArray()); + static::assertSame([3, 4], $point->toArray()); + } + } + + /** Immutable aggregate paths continue to reject mutable rings. */ + public function testImmutablePolygonRejectsMutableRing(): void + { + $ring = new LineString([[0, null]], 4326); + $this->expectException(InvalidValueException::class); + + try { + (new \ReflectionClass(PolygonImmutable::class))->newInstance([$ring]); + } finally { + static::assertSame([[0, null]], $ring->toArray()); + static::assertSame(4326, $ring->getSrid()); + } + } + + /** Typed ring editing rejects immutable components without mutation. */ + public function testImmutableReplacementIsRejected(): void + { + $ring = new LineString([[0, null]], 4326); + $immutable = new LineStringImmutable([[3, 4]], 2154); + $polygon = new Polygon([$ring]); + $this->expectException(\TypeError::class); + + try { + (new \ReflectionMethod($polygon, 'setRing'))->invoke($polygon, 0, $immutable); + } finally { + static::assertSame([$ring], $polygon->getRings()); + static::assertSame([[[0, null]]], $polygon->toArray()); + static::assertSame([[3, 4]], $immutable->toArray()); + static::assertSame(4326, $ring->getSrid()); + } + } + + /** Invalid addressing preserves the receiver and all referenced descendants. */ + public function testMissingRingPointIsAtomic(): void + { + $point = new Point(0, null, 4326); + $ring = new LineString([$point], 4326); + $polygon = new Polygon([$ring], 2154); + $this->expectException(OutOfBoundsException::class); + + try { + $polygon->setPoint(1, 0, $point); + } finally { + static::assertSame([$ring], $polygon->getRings()); + static::assertSame($point, $ring->getPoint(0)); + static::assertSame([[[0, null]]], $polygon->toArray()); + static::assertSame(4326, $ring->getSrid()); + static::assertSame(4326, $point->getSrid()); + static::assertSame(2154, $polygon->getSrid()); + } + } + + /** Invalid addressing preserves the receiver and all referenced descendants. */ + public function testNegativeAccessIsAtomic(): void + { + $point = new Point(0, null, 4326); + $ring = new LineString([$point], 4326); + $polygon = new Polygon([$ring], 2154); + $this->expectException(OutOfBoundsException::class); + + try { + $polygon->getRing(-1); + } finally { + static::assertSame([$ring], $polygon->getRings()); + static::assertSame($point, $ring->getPoint(0)); + static::assertSame([[[0, null]]], $polygon->toArray()); + static::assertSame(4326, $ring->getSrid()); + static::assertSame(4326, $point->getSrid()); + static::assertSame(2154, $polygon->getSrid()); + } + } + + /** Invalid addressing preserves the receiver and all referenced descendants. */ + public function testNegativeInsertionIsAtomic(): void + { + $point = new Point(0, null, 4326); + $ring = new LineString([$point], 4326); + $polygon = new Polygon([$ring], 2154); + $this->expectException(OutOfBoundsException::class); + + try { + $polygon->insertRing(-1, $ring); + } finally { + static::assertSame([$ring], $polygon->getRings()); + static::assertSame($point, $ring->getPoint(0)); + static::assertSame([[[0, null]]], $polygon->toArray()); + static::assertSame(4326, $ring->getSrid()); + static::assertSame(4326, $point->getSrid()); + static::assertSame(2154, $polygon->getSrid()); + } + } + + /** Invalid addressing preserves the receiver and all referenced descendants. */ + public function testNegativePointIsAtomic(): void + { + $point = new Point(0, null, 4326); + $ring = new LineString([$point], 4326); + $polygon = new Polygon([$ring], 2154); + $this->expectException(OutOfBoundsException::class); + + try { + $polygon->setPoint(0, -1, $point); + } finally { + static::assertSame([$ring], $polygon->getRings()); + static::assertSame($point, $ring->getPoint(0)); + static::assertSame([[[0, null]]], $polygon->toArray()); + static::assertSame(4326, $ring->getSrid()); + static::assertSame(4326, $point->getSrid()); + static::assertSame(2154, $polygon->getSrid()); + } + } + + /** Invalid addressing preserves the receiver and all referenced descendants. */ + public function testNegativeRemovalIsAtomic(): void + { + $point = new Point(0, null, 4326); + $ring = new LineString([$point], 4326); + $polygon = new Polygon([$ring], 2154); + $this->expectException(OutOfBoundsException::class); + + try { + $polygon->removeRing(-1); + } finally { + static::assertSame([$ring], $polygon->getRings()); + static::assertSame($point, $ring->getPoint(0)); + static::assertSame([[[0, null]]], $polygon->toArray()); + static::assertSame(4326, $ring->getSrid()); + static::assertSame(4326, $point->getSrid()); + static::assertSame(2154, $polygon->getSrid()); + } + } + + /** Invalid addressing preserves the receiver and all referenced descendants. */ + public function testNegativeReplacementIsAtomic(): void + { + $point = new Point(0, null, 4326); + $ring = new LineString([$point], 4326); + $polygon = new Polygon([$ring], 2154); + $this->expectException(OutOfBoundsException::class); + + try { + $polygon->setRing(-1, $ring); + } finally { + static::assertSame([$ring], $polygon->getRings()); + static::assertSame($point, $ring->getPoint(0)); + static::assertSame([[[0, null]]], $polygon->toArray()); + static::assertSame(4326, $ring->getSrid()); + static::assertSame(4326, $point->getSrid()); + static::assertSame(2154, $polygon->getSrid()); + } + } + + /** A custom mutable ring cannot introduce immutable point descendants. */ + public function testNestedImmutablePointAdditionIsRejected(): void + { + $ring = new LineString([[0, null]], 4326); + $polygon = new Polygon([$ring], 2154); + $immutable = new PointImmutable(3, 4, 4326); + $invalid = static::createStub(MutableLineStringInterface::class); + $invalid->method('getPoints')->willReturn([$immutable]); + $this->expectException(InvalidValueException::class); + + try { + $polygon->addRing($invalid); + } finally { + static::assertSame([$ring], $polygon->getRings()); + static::assertSame([[[0, null]]], $polygon->toArray()); + static::assertSame([3, 4], $immutable->toArray()); + static::assertSame(4326, $ring->getSrid()); + } + } + + /** A custom mutable ring cannot introduce immutable point descendants. */ + public function testNestedImmutablePointConstructionIsRejected(): void + { + $ring = new LineString([[0, null]], 4326); + $polygon = new Polygon([$ring], 2154); + $immutable = new PointImmutable(3, 4, 4326); + $invalid = static::createStub(MutableLineStringInterface::class); + $invalid->method('getPoints')->willReturn([$immutable]); + $this->expectException(InvalidValueException::class); + + try { + (new \ReflectionClass(Polygon::class))->newInstance([$invalid]); + } finally { + static::assertSame([$ring], $polygon->getRings()); + static::assertSame([[[0, null]]], $polygon->toArray()); + static::assertSame([3, 4], $immutable->toArray()); + static::assertSame(4326, $ring->getSrid()); + } + } + + /** A custom mutable ring cannot introduce immutable point descendants. */ + public function testNestedImmutablePointInsertionIsRejected(): void + { + $ring = new LineString([[0, null]], 4326); + $polygon = new Polygon([$ring], 2154); + $immutable = new PointImmutable(3, 4, 4326); + $invalid = static::createStub(MutableLineStringInterface::class); + $invalid->method('getPoints')->willReturn([$immutable]); + $this->expectException(InvalidValueException::class); + + try { + $polygon->insertRing(0, $invalid); + } finally { + static::assertSame([$ring], $polygon->getRings()); + static::assertSame([[[0, null]]], $polygon->toArray()); + static::assertSame([3, 4], $immutable->toArray()); + static::assertSame(4326, $ring->getSrid()); + } + } + + /** A custom mutable ring cannot introduce immutable point descendants. */ + public function testNestedImmutablePointReplacementIsRejected(): void + { + $ring = new LineString([[0, null]], 4326); + $polygon = new Polygon([$ring], 2154); + $immutable = new PointImmutable(3, 4, 4326); + $invalid = static::createStub(MutableLineStringInterface::class); + $invalid->method('getPoints')->willReturn([$immutable]); + $this->expectException(InvalidValueException::class); + + try { + $polygon->setRing(0, $invalid); + } finally { + static::assertSame([$ring], $polygon->getRings()); + static::assertSame([[[0, null]]], $polygon->toArray()); + static::assertSame([3, 4], $immutable->toArray()); + static::assertSame(4326, $ring->getSrid()); + } + } + + /** Ring edits retain supplied instances and preserve ordering. */ + public function testOrderedEditingAndSharing(): void + { + $point = new Point(0, null, 4326); + $first = new LineString([$point], 4326); + $second = new LineString([[3, 4]]); + $third = new LineString([]); + $polygon = new Polygon([7 => $first]); + $owner = new Triangle([$first]); + static::assertSame($polygon, $polygon->addRing($third)->insertRing(1, $second)); + static::assertSame([$first, $second, $third], $polygon->getRings()); + static::assertSame($polygon, $polygon->insertRing(3, $first)); + static::assertSame($polygon, $polygon->setRing(1, $third)->removeRing(2)); + static::assertSame([$first, $third, $first], $polygon->getElements()); + static::assertSame($first, $polygon->getRing(0)); + static::assertSame($point, $polygon->getRing(0)->getPoint(0)); + $replacement = new Point(null, 5, 2154); + static::assertSame($polygon, $polygon->setPoint(0, 0, $replacement)); + static::assertSame($replacement, $first->getPoint(0)); + $replacement->setX(8); + static::assertSame([[[8, 5]]], $owner->toArray()); + static::assertSame([[[8, 5]], [], [[8, 5]]], $polygon->toArray()); + static::assertSame(4326, $point->getSrid()); + static::assertSame([0, null], $point->toArray()); + $returned = $polygon->getRings(); + $returned[0] = $second; + static::assertSame($first, $polygon->getRing(0)); + } + + /** Invalid addressing preserves the receiver and all referenced descendants. */ + public function testOversizedInsertionIsAtomic(): void + { + $point = new Point(0, null, 4326); + $ring = new LineString([$point], 4326); + $polygon = new Polygon([$ring], 2154); + $this->expectException(OutOfBoundsException::class); + + try { + $polygon->insertRing(2, $ring); + } finally { + static::assertSame([$ring], $polygon->getRings()); + static::assertSame($point, $ring->getPoint(0)); + static::assertSame([[[0, null]]], $polygon->toArray()); + static::assertSame(4326, $ring->getSrid()); + static::assertSame(4326, $point->getSrid()); + static::assertSame(2154, $polygon->getSrid()); + } + } + + /** Point-array construction retains supplied point objects without changing their context. */ + public function testPointArrayConstruction(): void + { + $point = new Point(0, null, 4326, SpatialModelEnum::GEOGRAPHY); + $polygon = new Polygon([[$point]], 2154); + static::assertSame($point, $polygon->getRing(0)->getPoint(0)); + static::assertSame(2154, $polygon->getRing(0)->getSrid()); + static::assertSame(4326, $point->getSrid()); + static::assertSame(SpatialModelEnum::GEOGRAPHY, $point->getFamily()); + static::assertSame([[[0, null]]], $polygon->toArray()); + } + + /** Mutable variants have the approved public contracts and fixed layout/type. */ + public function testPublicContracts(): void + { + $polygon = new Polygon([]); + $triangle = new Triangle([]); + static::assertContains(MutablePolygonInterface::class, class_implements($polygon)); + static::assertContains(MutableSpatialInterface::class, class_implements($polygon)); + static::assertContains(MutableTriangleInterface::class, class_implements($triangle)); + static::assertContains(MutableSpatialInterface::class, class_implements($triangle)); + static::assertSame(GeometryTypeEnum::POLYGON, $polygon->getType()); + static::assertSame(GeometryTypeEnum::TRIANGLE, $triangle->getType()); + static::assertSame(CoordinateDimensionEnum::XY, $polygon->getDimension()); + static::assertSame(CoordinateDimensionEnum::XY, $triangle->getDimension()); + static::assertSame(SpatialModelEnum::GEOMETRY, $polygon->getFamily()); + static::assertSame(SpatialModelEnum::GEOMETRY, $triangle->getFamily()); + static::assertFalse((new \ReflectionClass($polygon))->hasMethod('withPoint')); + static::assertFalse((new \ReflectionClass($triangle))->hasMethod('withRing')); + static::assertFalse((new \ReflectionClass($polygon))->hasMethod('withSrid')); + static::assertFalse((new \ReflectionClass($triangle))->hasMethod('setDimension')); + static::assertSame('static', (string) (new \ReflectionMethod($polygon, 'setPoint'))->getReturnType()); + } +}