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()); + } +}