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
18 changes: 16 additions & 2 deletions docs/immutability.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,10 @@ and dimension; `withSpatialReference()` and `withSrid()` preserve them.

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 aggregates arrive in later Stories of Epic #38.
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.
Existing factories create the explicitly named immutable classes and expose
immutable return contracts.

Expand Down Expand Up @@ -202,7 +205,6 @@ creates a deep copy whose descendants all receive the requested reference.
See [Instantiable spatial types](instantiable-spatial-types.md#immutability-contract)
for the complete current mutability contract.


Circular strings also provide `withArrayOfCoordinates()` without functional
validity checks. Reference replacements preserve defining coordinates and copy
the contained points. Editing an endpoint does not automatically edit the
Expand All @@ -226,3 +228,15 @@ $partial = $paris->withCoordinates(Coordinates::xy(0, null));
assert([0, null] === $partial->toArray());
assert(!$partial->isEmpty());
```

### Mutable path ownership

Mutable line strings and circular strings reject immutable point components
without conversion. Supplied mutable points are retained by reference on
construction, insertion and replacement; getters expose those same instances.
Mutating a shared point is observable through every path containing it.

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.
88 changes: 88 additions & 0 deletions docs/instantiable-spatial-types.md
Original file line number Diff line number Diff line change
Expand Up @@ -623,3 +623,91 @@ Committee Draft: clauses 4.2.7 and 7.4.1 (component types, continuity and empty
values), and 7.4.6–7.4.7 (endpoints, including `null` for EMPTY). The library's
point equality semantics apply to endpoint predicates; continuity is not enforced. This
feature adds no serialization, decoding, linearization or factory entry points.

## Mutable line strings and circular strings

`Types\Dimension2`, `Types\Dimension3z`, `Types\Dimension3m` and
`Types\Dimension4zm` each provide `LineString` and `CircularString`. They implement
`MutableLineStringInterface` and `MutableCircularStringInterface` respectively.
Their constructor is `__construct(array $points, int|SpatialReference $srid = 0,
SpatialModelEnum $family = SpatialModelEnum::GEOMETRY)`. The coordinate layout and
spatial type remain fixed; the declared family is instance state.

Supply mutable point objects or indexed coordinate tuples in the selected layout.
Tuples create mutable points using the line's declared family and reference,
including the existing X/Y coordinate-string parser. Numeric and null Z/M values
follow the corresponding point constructor. Immutable point objects are rejected
without conversion. Supplied mutable points keep their identity, ordinates,
family and reference; construction and membership edits do not synchronize them.
Functional compatibility and geometric validity remain consumer concerns.

| Operation | Effect |
| --- | --- |
| `addPoint($point)` | Append the supplied mutable point |
| `insertPoint($index, $point)` | Insert before the zero-based index; the point count appends |
| `setPoint($index, $point)` | Replace an existing point |
| `removePoint($index)` | Remove an existing point and reindex the remaining points |
| `setArrayOfCoordinates($tuples)` | Replace all points with newly constructed mutable points |
| `getPoint($index)` | Return the same mutable point instance at an existing index |
| `getPoints()` / `getElements()` | Return an ordered array of the same mutable point instances |

Indexed access, replacement and removal require an existing non-negative index;
invalid addressing throws `OutOfBoundsException`. Unlike the immutable variants,
mutable paths do not wrap indices or accept negative indices. Failed membership,
coordinate or arc edits preserve the path and all referenced points. All mutation
methods return the receiver with return type `static`; there are no `with…`
methods or dimension setter.

```php
use LongitudeOne\SpatialTypes\Types\Dimension2\LineString;
use LongitudeOne\SpatialTypes\Types\Dimension2\Point;

$shared = new Point(0, null, 4326);
$first = new LineString([$shared], 4326);
$second = new LineString([$shared], 4326);
$first->addPoint(new Point(2, 3))->insertPoint(1, new Point(1, null));
$shared->setY(4); // Both lines now expose [0, 4] for their first point.
$first->setSrid(2154); // Updates the shared point, also observed through $second.
$first->setSrid(3857, false); // Updates only $first's declared reference.
```

`setSrid($srid, $propagate = true)`,
`setSpatialReference($reference, $propagate = true)` and
`setFamily($family, $propagate = true)` change the path's declared context.
By default they also update every contained point, including shared instances in
other paths. Passing `false` changes only the receiver. None of these operations
transforms ordinates or updates the metadata of another containing path.
Returned point arrays cannot change membership, but their mutable points remain shared.

### Circular arc indexing

General point editing is available even for EMPTY or incomplete circular paths.
`setOrigin($point)` inserts the first point into an empty circular string or
replaces only its existing first point. `appendArc($passingPoint,
$destinationPoint)` appends two points after an origin followed by zero or more
complete pairs. Without an origin or after an incomplete pair, it throws
`InvalidValueException` without mutation.

| Arc index | Origin point index | Passing point index | Destination point index |
| --- | --- | --- | --- |
| `0` | `0` | `1` | `2` |
| `1` | `2` | `3` | `4` |
| `i` | `2 * i` | `2 * i + 1` | `2 * i + 2` |

```php
use LongitudeOne\SpatialTypes\Types\Dimension2\CircularString;

$path = new CircularString([]);
$path->setOrigin(new Point(0, 0))
->appendArc(new Point(1, 1), new Point(2, 0))
->appendArc(new Point(3, -1), new Point(4, 0));
$path->replaceArc(0, new Point(1, 2), new Point(2, 1));
// Point 0 is retained; points 1 and 2 are replaced.
// Point 2 is also the origin of arc 1, whose points 3 and 4 are retained.
```

`replaceArc()` requires all three positions of the addressed arc to exist and
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.
19 changes: 19 additions & 0 deletions docs/type-class-hierarchy.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,22 @@ storage; they are not line strings and do not inherit point-set
simplicity checks. Compound curves retain ordered `ImmutableCurveInterface` components
and require curve interfaces and point tuples with numeric or null ordinates; functional compatibility
and endpoint continuity are not checked.

Mutable point-defined paths extend the read-only spatial base independently of
immutable implementations:

```text
AbstractSpatialType
└── AbstractPointSequence
├── AbstractLineString
│ └── Dimension{2,3m,3z,4zm}\LineString
└── AbstractCircularString
└── Dimension{2,3m,3z,4zm}\CircularString
```

`PointSequenceEditingTrait` shares ordered membership editing between the two
mutable path bases. These paths contain only `MutablePointInterface` instances;
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.
2 changes: 1 addition & 1 deletion lib/Interfaces/CircularStringInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public function getElements(): array;
/**
* Return a point from the circular string.
*
* @param int $index index of the point. -1 is the last point. -2 is the penultimate point, etc.
* @param int $index index of the point; immutable variants wrap negative indices, mutable variants require an existing zero-based index
*/
public function getPoint(int $index): PointInterface;

Expand Down
2 changes: 1 addition & 1 deletion lib/Interfaces/LineStringInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public function getElements(): array;
/**
* Return a point from the line string.
*
* @param int $index index of the point. -1 is the last point. -2 is the penultimate point, etc.
* @param int $index index of the point; immutable variants wrap negative indices, mutable variants require an existing zero-based index
*/
public function getPoint(int $index): PointInterface;

Expand Down
76 changes: 75 additions & 1 deletion lib/Interfaces/MutableCircularStringInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,81 @@

namespace LongitudeOne\SpatialTypes\Interfaces;

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

/** In-place ordered point editing; immutable components are rejected. */
interface MutableCircularStringInterface extends CircularStringInterface, MutableCurveInterface
{
/** @param MutablePointInterface $point Point retained without cloning */
public function addPoint(MutablePointInterface $point): static;

/**
* @param MutablePointInterface $passingPoint Intermediate point retained without cloning
* @param MutablePointInterface $destinationPoint End point retained without cloning
*/
public function appendArc(MutablePointInterface $passingPoint, MutablePointInterface $destinationPoint): static;

/** @return MutablePointInterface[] */
public function getElements(): array;

/** Return the last point, or null for EMPTY. */
public function getEndPoint(): ?MutablePointInterface;

/** @param int $index Existing zero-based point index */
public function getPoint(int $index): MutablePointInterface;

/** @return MutablePointInterface[] */
public function getPoints(): array;

/** Return the first point, or null for EMPTY. */
public function getStartPoint(): ?MutablePointInterface;

/**
* @param int $index Zero-based insertion index, up to the point count
* @param MutablePointInterface $point Point retained without cloning
*/
public function insertPoint(int $index, MutablePointInterface $point): static;

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

/**
* @param int $index Zero-based arc index; all three defining points must exist
* @param MutablePointInterface $passingPoint Replacement intermediate point
* @param MutablePointInterface $destinationPoint Replacement shared endpoint
*/
public function replaceArc(int $index, MutablePointInterface $passingPoint, MutablePointInterface $destinationPoint): static;

/**
* @param array<array{0: null|float|int|string, 1: null|float|int|string, 2 ?: null|float|int, 3 ?: null|float|int}> $coordinates Replacement tuples
*/
public function setArrayOfCoordinates(array $coordinates): static;

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

/** @param MutablePointInterface $point First point retained without cloning */
public function setOrigin(MutablePointInterface $point): static;

/**
* @param int $index Existing zero-based point index
* @param MutablePointInterface $point Point retained without cloning
*/
public function setPoint(int $index, MutablePointInterface $point): static;

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

/**
* @param int $srid Declared reference identifier
* @param bool $propagate Whether to update every contained point
*/
public function setSrid(int $srid, bool $propagate = true): static;
}
60 changes: 59 additions & 1 deletion lib/Interfaces/MutableLineStringInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,65 @@

namespace LongitudeOne\SpatialTypes\Interfaces;

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

/** In-place ordered point editing; immutable components are rejected. */
interface MutableLineStringInterface extends LineStringInterface, MutableCurveInterface
{
/** @param MutablePointInterface $point Point retained without cloning */
public function addPoint(MutablePointInterface $point): static;

/** @return MutablePointInterface[] */
public function getElements(): array;

/** Return the last point, or null for EMPTY. */
public function getEndPoint(): ?MutablePointInterface;

/** @param int $index Existing zero-based point index */
public function getPoint(int $index): MutablePointInterface;

/** @return MutablePointInterface[] */
public function getPoints(): array;

/** Return the first point, or null for EMPTY. */
public function getStartPoint(): ?MutablePointInterface;

/**
* @param int $index Zero-based insertion index, up to the point count
* @param MutablePointInterface $point Point retained without cloning
*/
public function insertPoint(int $index, MutablePointInterface $point): static;

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

/**
* @param array<array{0: null|float|int|string, 1: null|float|int|string, 2 ?: null|float|int, 3 ?: null|float|int}> $coordinates Replacement tuples
*/
public function setArrayOfCoordinates(array $coordinates): static;

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

/**
* @param int $index Existing zero-based point index
* @param MutablePointInterface $point Point retained without cloning
*/
public function setPoint(int $index, MutablePointInterface $point): static;

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

/**
* @param int $srid Declared reference identifier
* @param bool $propagate Whether to update every contained point
*/
public function setSrid(int $srid, bool $propagate = true): static;
}
3 changes: 3 additions & 0 deletions lib/Interfaces/MutablePointInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,7 @@ public function setY(float|int|string|null $y): static;

/** @param null|float|int $z Elevation ordinate; the layout must contain Z */
public function setZ(float|int|null $z): static;

/** @return (null|float|int)[] */
public function toArray(): array;
}
Loading
Loading