Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions docs/immutability.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,10 @@ Mutable `Point` classes are available directly under each dimension namespace.
See [mutable points](instantiable-spatial-types.md#mutable-points) for constructors
and in-place operations. Mutable `LineString` and `CircularString` classes are also available directly under
each dimension namespace. See [mutable paths](instantiable-spatial-types.md#mutable-line-strings-and-circular-strings)
for point ownership, propagation and arc indexing. Other mutable aggregates arrive
in later Stories of Epic #38.
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
arrive in later Stories of Epic #38.
Existing factories create the explicitly named immutable classes and expose
immutable return contracts.

Expand Down
66 changes: 66 additions & 0 deletions docs/instantiable-spatial-types.md
Original file line number Diff line number Diff line change
Expand Up @@ -711,3 +711,69 @@ throws `OutOfBoundsException` otherwise. Arc helpers check only this sequence
structure. They do not validate circle geometry, coordinate completeness or
continuity, close the path, repair points, or enforce geometric minimum counts.
Null ordinates, EMPTY points and incomplete point sequences remain representable.

## Mutable compound curves

`Types\Dimension2\CompoundCurve`, `Types\Dimension3z\CompoundCurve`,
`Types\Dimension3m\CompoundCurve` and `Types\Dimension4zm\CompoundCurve`
implement `MutableCompoundCurveInterface`. Their constructor is
`__construct(array $curves = [], int|SpatialReference $srid = 0,
SpatialModelEnum $family = SpatialModelEnum::GEOMETRY)`.

Only `MutableLineStringInterface` and `MutableCircularStringInterface` components
are accepted. Coordinate arrays, immutable curves and nested compound curves are
rejected without conversion. Components retain their own declared layout,
family and reference; compatibility and geometric continuity require explicit
validation. Incomplete and empty component sequences remain representable.

| Operation | Behavior |
| --- | --- |
| `addCurve($curve)` | Append a component |
| `insertCurve($index, $curve)` | Insert before an index; the component count appends |
| `setCurve($index, $curve)` | Replace an existing component |
| `removeCurve($index)` | Remove an existing component and reindex |
| `setCurves($curves)` | Replace the entire ordered sequence atomically |
| `getCurve($index)` | Read an existing component |
| `getCurves()`, `getElements()` | Return the ordered component instances |

All indices are zero-based. Negative indices and missing components raise
`OutOfBoundsException`; indexed access does not wrap. Invalid component arrays
raise `InvalidValueException`, while incompatible typed mutation arguments raise
PHP `TypeError`. Failures preserve membership and supplied components.
Successful mutations return the receiver as `static` for chaining. No `with…`
methods or dimension setter are exposed.

```php
use LongitudeOne\Core\Enum\SpatialModelEnum;
use LongitudeOne\SpatialTypes\Types\Dimension2\CircularString;
use LongitudeOne\SpatialTypes\Types\Dimension2\CompoundCurve;
use LongitudeOne\SpatialTypes\Types\Dimension2\LineString;
use LongitudeOne\SpatialTypes\Types\Dimension2\Point;

$point = new Point(0, null, 4326);
$line = new LineString([$point], 4326);
$arc = new CircularString([[10, 0], [11, 1], [12, 0]], 4326);
$path = new CompoundCurve([$line], 4326);
$other = new CompoundCurve([$line], 4326);
$path->addCurve($arc)->insertCurve(1, $line)->removeCurve(1);

assert($path->getCurve(0) === $line);
$point->setY(2); // Both paths now expose the changed point.
$path->setSrid(2154); // Updates both curves and all their points in place.
assert($other->getCurve(0)->getSrid() === 2154);
assert($other->getSrid() === 4326);
$path->setFamily(SpatialModelEnum::GEOGRAPHY, false); // Only this compound changes.
```

Construction, insertion and replacement retain the supplied instances without
cloning. Getters expose those instances; editing a component or its points is
visible to every owner. Modifying the returned PHP membership array itself does
not change the compound.

`setSrid()`, `setSpatialReference()` and `setFamily()` accept a second boolean
`$propagate` argument, defaulting to `true`. Propagation updates existing
components and their points recursively, including instances shared by other
owners. Passing `false` updates only the receiver. No context setter recalculates
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.
8 changes: 8 additions & 0 deletions docs/spatial-reference-systems.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,11 @@ $lambert93 = $lambert93->withCoordinates(Coordinates::xy(652469.0227, 6862035.25

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
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
member context while retaining their own aggregate context. Coordinates are
never recalculated.
12 changes: 12 additions & 0 deletions docs/type-class-hierarchy.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,15 @@ coordinate tuples create mutable points, and immutable components are rejected.
Their interfaces refine the common read methods to return mutable points and
expose type-specific mutation, including optional context propagation. The common
interfaces remain read-only, and mutable paths expose no copy operations.

Mutable compound curves use a separate base:

```text
AbstractSpatialType
└── AbstractCompoundCurve
└── Dimension{2,3m,3z,4zm}\CompoundCurve
```

`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.
58 changes: 57 additions & 1 deletion lib/Interfaces/MutableCompoundCurveInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,63 @@

namespace LongitudeOne\SpatialTypes\Interfaces;

/** Mutable compoundcurve contract; type-specific editing is introduced separately. */
use LongitudeOne\Core\Enum\SpatialModelEnum;
use LongitudeOne\SpatialTypes\Reference\SpatialReference;

/** In-place ordered mutable line-string and circular-string components. */
interface MutableCompoundCurveInterface extends CompoundCurveInterface, MutableCurveInterface
{
/** @param MutableCircularStringInterface|MutableLineStringInterface $curve Component retained without cloning */
public function addCurve(MutableCircularStringInterface|MutableLineStringInterface $curve): static;

/** @param int $index Existing zero-based component index */
public function getCurve(int $index): MutableCircularStringInterface|MutableLineStringInterface;

/** @return list<MutableCircularStringInterface|MutableLineStringInterface> */
public function getCurves(): array;

/** @return list<MutableCircularStringInterface|MutableLineStringInterface> */
public function getElements(): array;

/** Return the final component's endpoint, or null. */
public function getEndPoint(): ?MutablePointInterface;

/** Return the first component's start point, or null. */
public function getStartPoint(): ?MutablePointInterface;

/**
* @param int $index Zero-based insertion index, up to the component count
* @param MutableCircularStringInterface|MutableLineStringInterface $curve Component retained without cloning
*/
public function insertCurve(int $index, MutableCircularStringInterface|MutableLineStringInterface $curve): static;

/** @param int $index Existing zero-based component index */
public function removeCurve(int $index): static;

/**
* @param int $index Existing zero-based component index
* @param MutableCircularStringInterface|MutableLineStringInterface $curve Component retained without cloning
*/
public function setCurve(int $index, MutableCircularStringInterface|MutableLineStringInterface $curve): static;

/** @param array<MutableCircularStringInterface|MutableLineStringInterface> $curves Complete ordered replacement sequence */
public function setCurves(array $curves): static;

/**
* @param SpatialModelEnum $family Declared family
* @param bool $propagate Whether to update components and their points
*/
public function setFamily(SpatialModelEnum $family, bool $propagate = true): static;

/**
* @param SpatialReference $reference Declared reference
* @param bool $propagate Whether to update components and their points
*/
public function setSpatialReference(SpatialReference $reference, bool $propagate = true): static;

/**
* @param int $srid Declared reference identifier
* @param bool $propagate Whether to update components and their points
*/
public function setSrid(int $srid, bool $propagate = true): static;
}
222 changes: 222 additions & 0 deletions lib/Types/AbstractCompoundCurve.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
<?php
/**
* This file is part of the spatial project.
*
* PHP 8.4 | 8.5
*
* Copyright Alexandre Tranchant <alexandre.tranchant@gmail.com> 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\GeometryTypeEnum;
use LongitudeOne\Core\Enum\SpatialModelEnum;
use LongitudeOne\SpatialTypes\Exception\InvalidValueException;
use LongitudeOne\SpatialTypes\Exception\OutOfBoundsException;
use LongitudeOne\SpatialTypes\Interfaces\MutableCircularStringInterface;
use LongitudeOne\SpatialTypes\Interfaces\MutableCompoundCurveInterface;
use LongitudeOne\SpatialTypes\Interfaces\MutableLineStringInterface;
use LongitudeOne\SpatialTypes\Interfaces\MutablePointInterface;
use LongitudeOne\SpatialTypes\Reference\SpatialReference;

/** Mutable ordered curve storage without geometric validation.
* @internal
*/
abstract class AbstractCompoundCurve extends AbstractSpatialType implements MutableCompoundCurveInterface
{
/** @var list<MutableCircularStringInterface|MutableLineStringInterface> */
protected array $curves = [];

protected SpatialModelEnum $family;

/**
* @param array<MutableCircularStringInterface|MutableLineStringInterface> $curves Ordered mutable components
* @param int|SpatialReference $srid Declared reference
* @param SpatialModelEnum $family Declared family
*/
final public function __construct(array $curves = [], int|SpatialReference $srid = 0, SpatialModelEnum $family = SpatialModelEnum::GEOMETRY)
{
$this->family = $family;
$this->initializeSpatialReference($srid);
$this->setCurves($curves);
}

/** @param MutableCircularStringInterface|MutableLineStringInterface $curve Component retained without cloning */
public function addCurve(MutableCircularStringInterface|MutableLineStringInterface $curve): static
{
$this->curves[] = $curve;

return $this;
}

/** @param int $index Existing zero-based component index */
public function getCurve(int $index): MutableCircularStringInterface|MutableLineStringInterface
{
$this->requireCurveIndex($index);

return $this->curves[$index];
}

/** @return list<MutableCircularStringInterface|MutableLineStringInterface> */
public function getCurves(): array
{
return $this->curves;
}

/** @return list<MutableCircularStringInterface|MutableLineStringInterface> */
public function getElements(): array
{
return $this->curves;
}

/** Return the final component's endpoint, or null. */
public function getEndPoint(): ?MutablePointInterface
{
return $this->isEmpty() ? null : $this->curves[count($this->curves) - 1]->getEndPoint();
}

/** Return the declared family. */
public function getFamily(): SpatialModelEnum
{
return $this->family;
}

/** Return the first component's start point, or null. */
public function getStartPoint(): ?MutablePointInterface
{
return $this->isEmpty() ? null : $this->curves[0]->getStartPoint();
}

/** Return the fixed spatial type. */
public function getType(): GeometryTypeEnum
{
return GeometryTypeEnum::COMPOUNDCURVE;
}

/**
* @param int $index Zero-based insertion index, up to the component count
* @param MutableCircularStringInterface|MutableLineStringInterface $curve Component retained without cloning
*/
public function insertCurve(int $index, MutableCircularStringInterface|MutableLineStringInterface $curve): static
{
if ($index < 0 || $index > count($this->curves)) {
throw new OutOfBoundsException(sprintf('Curve index %d is outside the insertion range.', $index));
}
array_splice($this->curves, $index, 0, [$curve]);

return $this;
}

/** Whether the first and final endpoints are present and equal. */
public function isClosed(): bool
{
$start = $this->getStartPoint();
$end = $this->getEndPoint();

return null !== $start && null !== $end && $start->equalsTo($end);
}

/** Whether there are no components. */
public function isEmpty(): bool
{
return [] === $this->curves;
}

/** @param int $index Existing zero-based component index */
public function removeCurve(int $index): static
{
$this->requireCurveIndex($index);
array_splice($this->curves, $index, 1);

return $this;
}

/**
* @param int $index Existing zero-based component index
* @param MutableCircularStringInterface|MutableLineStringInterface $curve Component retained without cloning
*/
public function setCurve(int $index, MutableCircularStringInterface|MutableLineStringInterface $curve): static
{
$this->requireCurveIndex($index);
array_splice($this->curves, $index, 1, [$curve]);

return $this;
}

/** @param array<MutableCircularStringInterface|MutableLineStringInterface> $curves Complete ordered replacement sequence */
public function setCurves(array $curves): static
{
$checked = [];
foreach ($curves as $curve) {
if (!$curve instanceof MutableLineStringInterface && !$curve instanceof MutableCircularStringInterface) {
throw new InvalidValueException('A mutable compound curve requires mutable line-string or circular-string components.');
}
$checked[] = $curve;
}
$this->curves = $checked;

return $this;
}

/**
* @param SpatialModelEnum $family Declared family
* @param bool $propagate Whether to update components and their points
*/
public function setFamily(SpatialModelEnum $family, bool $propagate = true): static
{
if ($propagate) {
foreach ($this->curves as $curve) {
$curve->setFamily($family);
}
}
$this->family = $family;

return $this;
}

/**
* @param SpatialReference $reference Declared reference
* @param bool $propagate Whether to update components and their points
*/
public function setSpatialReference(SpatialReference $reference, bool $propagate = true): static
{
if ($propagate) {
foreach ($this->curves as $curve) {
$curve->setSpatialReference($reference);
}
}
$this->spatialReference = $reference;

return $this;
}

/**
* @param int $srid Declared reference identifier
* @param bool $propagate Whether to update components 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 (MutableCircularStringInterface|MutableLineStringInterface $curve): array => $curve->toArray(), $this->curves);
}

/** @param int $index Existing zero-based component index */
private function requireCurveIndex(int $index): void
{
if ($index < 0 || $index >= count($this->curves)) {
throw new OutOfBoundsException(sprintf('Curve index %d does not address an existing curve.', $index));
}
}
}
Loading
Loading