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
19 changes: 19 additions & 0 deletions docs/immutability.md
Original file line number Diff line number Diff line change
Expand Up @@ -246,3 +246,22 @@ Their declared-context setters accept a second `$propagate` argument, defaulting
to `true`. Propagation edits all contained points in place, so other paths sharing
those points observe the updated point metadata. Passing `false` updates only the
receiving path. Ordinates never change during a declared-context update.

### Mutable aggregate ownership

Mutable multi-points, multi-line strings, multi-polygons and `SpatialCollection`
retain supplied members and descendants by reference. Their common
`getElement()`/`getElements()` methods expose those same instances.
`setElement()` replaces one membership entry; editing a retained object is
visible through every aggregate referencing it. Complete `setElements()`
replacement checks all inputs before changing membership.

These aggregates reject immutable spatial components recursively. Containment
cycles are also rejected atomically; repeated members and shared descendants
remain supported when their references form an acyclic graph. Coordinate
arrays supported by the multi-object constructors and `setElements()` create
mutable descendants. The three context setters propagate through all members
by default; `false` changes only the receiver. Propagation modifies shared
instances without recalculating coordinates. See the
[aggregate API reference](instantiable-spatial-types.md#mutable-multi-objects-and-spatial-collections)
for signatures and examples.
104 changes: 104 additions & 0 deletions docs/instantiable-spatial-types.md
Original file line number Diff line number Diff line change
Expand Up @@ -941,3 +941,107 @@ EMPTY or incomplete patches, open rings and null ordinates remain representable.
Mutation does not enforce face validity, adjacency, closure or manifold rules.
ISO/IEC CD 13249-3:201x(E), clause 8.5.1, defines polyhedral-surface validity;
this representation API leaves its evaluation to explicit spatial-validator use.

## Mutable multi-objects and spatial collections

`MultiPoint`, `MultiLineString`, `MultiPolygon` and `SpatialCollection` are
available directly under each of `Types\Dimension2`, `Types\Dimension3z`,
`Types\Dimension3m` and `Types\Dimension4zm`. All four use this constructor:

```php
new MultiPoint(array $elements = [], int|SpatialReference $srid = 0,
SpatialModelEnum $family = SpatialModelEnum::GEOMETRY);
```

| Mutable aggregate | Accepted members | Coordinate-array input |
| --- | --- | --- |
| `MultiPoint` | `MutablePointInterface` | Point tuples |
| `MultiLineString` | `MutableLineStringInterface` | Arrays of points or tuples |
| `MultiPolygon` | `MutablePolygonInterface` | Arrays of rings, points or tuples |
| `SpatialCollection` | `MutableSpatialInterface`, including nested collections | Not accepted: arrays do not identify member types |

Construction and `setElements()` support the listed coordinate arrays. Arrays
create mutable descendants in the receiver's current layout, family and
reference. Supplied spatial objects retain their own context, layout and
identity. Immutable members and immutable descendants are rejected without
conversion or cloning. A mutable triangle implements `MutablePolygonInterface`
and can therefore be retained by a mutable multi-polygon.

`SpatialCollection` is the family-independent mutable counterpart of
`GeometryCollectionImmutable` and `GeographyCollectionImmutable`. Changing its
family preserves both its concrete class and its instance identity.

| Operation, common to all four kinds | Result |
| --- | --- |
| `addElement($element)` | Append a mutable member |
| `insertElement($index, $element)` | Insert before an index; the member count appends |
| `setElement($index, $element)` | Replace one existing member |
| `removeElement($index)` | Remove one existing member and reindex |
| `setElements($elements)` | Replace complete ordered membership atomically; `[]` empties it |
| `getElement($index)`, `getElements()` | Expose the retained mutable instances |

`MultiPoint::getPoints()`, `MultiLineString::getLineStrings()` and
`MultiPolygon::getPolygons()` also expose the retained instances. Single-member
mutations take mutable spatial objects; coordinate arrays are accepted by the
constructor and complete replacement. Indices start at zero. Access,
replacement and removal require existing members; insertion accepts indices
from zero through the count. Invalid addressing raises `OutOfBoundsException`.
Wrong mutable member kinds, invalid membership arrays and immutable descendants
raise `InvalidValueException`; immutable arguments to typed single-member
mutators raise PHP `TypeError`. Technical input or addressing failures preserve
membership and referenced descendants, including late failures during complete
replacement. Successful mutations return the receiver as `static`.

Containment must be acyclic. Construction and every member-mutation path reject
already cyclic member graphs or a new direct/indirect reference back to the
receiver with `InvalidValueException` and the message
`Mutable aggregate members must not form reference cycles.` The failure changes
neither membership nor the context of any referenced instance. This is a
technical object-graph restriction, independent of geometric validation.
Repeated members and shared descendants in an acyclic graph remain supported.

```php
use LongitudeOne\Core\Enum\SpatialModelEnum;
use LongitudeOne\SpatialTypes\Types\Dimension2\LineString;
use LongitudeOne\SpatialTypes\Types\Dimension2\MultiLineString;
use LongitudeOne\SpatialTypes\Types\Dimension2\MultiPoint;
use LongitudeOne\SpatialTypes\Types\Dimension2\Point;
use LongitudeOne\SpatialTypes\Types\Dimension2\SpatialCollection;

$point = new Point(0, null, 4326);
$points = new MultiPoint([$point], 4326);
$line = new LineString([$point], 4326);
$lines = new MultiLineString([$line], 4326);
$collection = new SpatialCollection([$points, $lines], 4326);

$points->addElement(new Point(3, 4))->insertElement(1, $point);
$point->setX(7); // Both occurrences in $points and the point in $line change.
assert($point === $points->getElement(0));
assert([[[7, null]]] === $lines->toArray());

$collection->setFamily(SpatialModelEnum::GEOGRAPHY, false); // Only this collection.
$collection->setSrid(2154); // Nested aggregates, their members and all points.
assert(2154 === $point->getSrid());
assert([7, null] === $point->toArray()); // Coordinates are unchanged.
```

`setSrid()`, `setSpatialReference()` and `setFamily()` each accept
`bool $propagate = true`. By default changes reach all descendants recursively,
including points in compound curves and polyhedral surfaces held by a
`SpatialCollection`. Propagation updates existing shared instances and is visible
to other owners of those instances. Passing `false` updates only the receiver.
Membership arrays returned by getters are PHP array values: editing that array
does not edit membership, while editing a returned spatial object does.

`toArray()` preserves member boundaries, including nested collection boundaries.
Complete coordinate replacement creates new mutable descendants and detaches the
previous members from this receiver without modifying their other owners.
Spatial type and coordinate layout remain fixed. Mutable aggregates expose no
`with…` operations or dimension setter.

EMPTY members, incomplete ordinates, duplicate points, open rings and other
geometrically invalid representable data are retained. Editing does not validate,
repair, transform or convert dimensions. ISO/IEC CD 13249-3:201x(E), clause 9.1.1,
defines the collection model and its validity requirements, including shared
spatial references. This representation API leaves those functional checks to
explicit [spatial-validator](validators.md) use.
24 changes: 24 additions & 0 deletions docs/type-class-hierarchy.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,3 +114,27 @@ AbstractSpatialType
addressing. The base retains patches and their mutable descendants by reference.
Coordinate arrays create mutable polygons in the fixed XYZ or XYZM layout;
immutable surface implementations and their copy contracts remain separate.

Mutable multi-objects and heterogeneous collections share ordered membership:

```text
AbstractSpatialType
└── AbstractMutableCollection
├── AbstractMultiPoint
│ └── Dimension{2,3m,3z,4zm}\MultiPoint
├── AbstractMultiLineString
│ └── Dimension{2,3m,3z,4zm}\MultiLineString
├── AbstractMultiPolygon
│ └── Dimension{2,3m,3z,4zm}\MultiPolygon
└── AbstractSpatialCollection
└── Dimension{2,3m,3z,4zm}\SpatialCollection
```

`MutableCollectionInterface` provides ordered member editing and recursive
context propagation. Each `MutableMulti<Type>Interface` refines member reads to
the corresponding mutable type. `SpatialCollection` accepts mutable spatial
objects, including nested collections, through `MutableSpatialInterface`.
`MutableComponentGuard` rejects immutable descendants without geometric checks.
Mutable implementations remain independent of immutable classes and expose no
copy operations. The public operations and ownership rules are documented in
[mutable multi-objects and spatial collections](instantiable-spatial-types.md#mutable-multi-objects-and-spatial-collections).
63 changes: 62 additions & 1 deletion lib/Interfaces/MutableCollectionInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,68 @@

namespace LongitudeOne\SpatialTypes\Interfaces;

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

/** Ordered mutable membership, retaining supplied instances.
* @template T of MutableSpatialInterface
* @template TInput
*/
interface MutableCollectionInterface extends CollectionInterface, MutableSpatialInterface
{
/**
* @param MutableSpatialInterface $element Member retained without copying
*
* @phpstan-param T $element */
public function addElement(MutableSpatialInterface $element): static;

/**
* @param int $index Existing zero-based member index
*
* @return T
*/
public function getElement(int $index): MutableSpatialInterface;

/** @return list<T> */
public function getElements(): array;

/**
* @param int $index Insertion index, including the member count
* @param MutableSpatialInterface $element Member retained without copying
*
* @phpstan-param T $element
*/
public function insertElement(int $index, MutableSpatialInterface $element): static;

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

/**
* @param int $index Existing zero-based member index
* @param MutableSpatialInterface $element Replacement retained without copying
*
* @phpstan-param T $element
*/
public function setElement(int $index, MutableSpatialInterface $element): static;

/** @param array<T|TInput> $elements Complete ordered membership, including supported coordinate arrays */
public function setElements(array $elements): static;

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

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

/**
* @param int $srid Declared reference identifier
* @param bool $propagate Whether to update all descendants
*/
public function setSrid(int $srid, bool $propagate = true): static;
}
9 changes: 8 additions & 1 deletion lib/Interfaces/MutableMultiLineStringInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,14 @@

namespace LongitudeOne\SpatialTypes\Interfaces;

/** Mutable multilinestring contract; type-specific editing is introduced separately. */
/** Mutable MultiLineString membership.
* @extends MutableCollectionInterface<MutableLineStringInterface, array<array{0: null|float|int|string, 1: null|float|int|string, 2?: null|float|int, 3?: null|float|int}|\LongitudeOne\SpatialTypes\Interfaces\MutablePointInterface>>
*/
interface MutableMultiLineStringInterface extends MultiLineStringInterface, MutableCollectionInterface
{
/** @return list<MutableLineStringInterface> */
public function getElements(): array;

/** @return list<MutableLineStringInterface> */
public function getLineStrings(): array;
}
9 changes: 8 additions & 1 deletion lib/Interfaces/MutableMultiPointInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,14 @@

namespace LongitudeOne\SpatialTypes\Interfaces;

/** Mutable multipoint contract; type-specific editing is introduced separately. */
/** Mutable MultiPoint membership.
* @extends MutableCollectionInterface<MutablePointInterface, array{0: null|float|int|string, 1: null|float|int|string, 2?: null|float|int, 3?: null|float|int}>
*/
interface MutableMultiPointInterface extends MultiPointInterface, MutableCollectionInterface
{
/** @return list<MutablePointInterface> */
public function getElements(): array;

/** @return list<MutablePointInterface> */
public function getPoints(): array;
}
9 changes: 8 additions & 1 deletion lib/Interfaces/MutableMultiPolygonInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,14 @@

namespace LongitudeOne\SpatialTypes\Interfaces;

/** Mutable multipolygon contract; type-specific editing is introduced separately. */
/** Mutable MultiPolygon membership.
* @extends MutableCollectionInterface<MutablePolygonInterface, array<array<array{0: null|float|int|string, 1: null|float|int|string, 2?: null|float|int, 3?: null|float|int}|\LongitudeOne\SpatialTypes\Interfaces\MutablePointInterface>|\LongitudeOne\SpatialTypes\Interfaces\MutableLineStringInterface>>
*/
interface MutableMultiPolygonInterface extends MultiPolygonInterface, MutableCollectionInterface
{
/** @return list<MutablePolygonInterface> */
public function getElements(): array;

/** @return list<MutablePolygonInterface> */
public function getPolygons(): array;
}
69 changes: 69 additions & 0 deletions lib/Types/AbstractMultiLineString.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<?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\CoordinateDimensionEnum;
use LongitudeOne\Core\Enum\GeometryTypeEnum;
use LongitudeOne\SpatialTypes\Exception\InvalidValueException;
use LongitudeOne\SpatialTypes\Interfaces\MutableLineStringInterface;
use LongitudeOne\SpatialTypes\Interfaces\MutableMultiLineStringInterface;
use LongitudeOne\SpatialTypes\Interfaces\MutablePointInterface;

/** Mutable MultiLineString members without geometric validation.
* @internal
*
* @extends AbstractMutableCollection<MutableLineStringInterface, array<array{0: null|float|int|string, 1: null|float|int|string, 2?: null|float|int, 3?: null|float|int}|\LongitudeOne\SpatialTypes\Interfaces\MutablePointInterface>>
*/
abstract class AbstractMultiLineString extends AbstractMutableCollection implements MutableMultiLineStringInterface
{
/** @return list<MutableLineStringInterface> */
public function getLineStrings(): array
{
return $this->getElements();
}

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

/** @return (null|float|int)[][][] */
public function toArray(): array
{
return array_map(static fn (MutableLineStringInterface $element): array => $element->toArray(), $this->getElements());
}

/** @param array<array{0: null|float|int|string, 1: null|float|int|string, 2?: null|float|int, 3?: null|float|int}|MutablePointInterface>|MutableLineStringInterface $element Mutable member or coordinate array */
protected function hydrateElement(mixed $element): MutableLineStringInterface
{
if (is_array($element)) {
$element = match ($this->getDimension()) {
CoordinateDimensionEnum::XY => new Dimension2\LineString($element, $this->spatialReference, $this->family),
CoordinateDimensionEnum::XYZ => new Dimension3z\LineString($element, $this->spatialReference, $this->family),
CoordinateDimensionEnum::XYM => new Dimension3m\LineString($element, $this->spatialReference, $this->family),
CoordinateDimensionEnum::XYZM => new Dimension4zm\LineString($element, $this->spatialReference, $this->family),
};
}
if (!$element instanceof MutableLineStringInterface) {
throw new InvalidValueException('Mutable MultiLineString members must be mutable LineString objects or coordinate arrays.');
}
MutableComponentGuard::check($element, $this);

return $element;
}
}
Loading
Loading