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
15 changes: 8 additions & 7 deletions docs/immutability.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ as immutable makes that reuse safe and makes a value's location explicit.

## Current contract

Every currently available concrete spatial class has an `Immutable` suffix and
is final and immutable through the public API. Former concrete names are removed,
Concrete immutable spatial classes have an `Immutable` suffix and
are final and immutable through the public API. Former concrete names are removed,
without deprecated aliases. `Value\Coordinates` and `Reference\SpatialReference`
remain immutable auxiliary values with their existing names. Its coordinates, spatial reference,
remain immutable auxiliary values with their existing names. An immutable object's coordinates, spatial reference,
and aggregate membership are set by its constructor, and it has no public
setter or membership mutator. `getCoordinates()` returns an immutable
`Value\Coordinates` value. `withCoordinates()` creates a distinct point with
Expand Down Expand Up @@ -98,9 +98,10 @@ returning `static`. These metadata operations must preserve ordinates. Neither
variant has a dimension setter. Immutable concrete namespaces fix the family
and dimension; `withSpatialReference()` and `withSrid()` preserve them.

Concrete mutable classes and detailed type-specific editing methods arrive in
the following Stories of Epic #38. They are not available in this change. Existing
factories currently create the explicitly named immutable classes and expose
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.
Existing factories create the explicitly named immutable classes and expose
immutable return contracts.

Immutable aggregate constructors and replacement paths accept only immutable
Expand Down Expand Up @@ -139,7 +140,7 @@ multi-line string would both change as an indirect side effect. A caller that
only holds `$multiLine` could therefore observe a geometry change without any
operation being made on it.

Because points are immutable, that operation does not exist. Code that needs
Because these points are immutable, that operation does not exist. Code that needs
another location creates another point and explicitly builds the spatial value
that should contain it. The represented value remains separate from functional validity checks.

Expand Down
52 changes: 51 additions & 1 deletion docs/instantiable-spatial-types.md
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,56 @@ ISO/IEC 13249-3 or OGC requirements. Request coordinate-completeness validation
explicitly through `spatial-validator`; this library does not invoke it or
depend on it. See [Validation](validators.md).

### Mutable points

Mutable points live directly in the dimension namespace:

| Layout | Class | Constructor ordinates |
| --- | --- | --- |
| XY | `Types\Dimension2\Point` | X, Y |
| XYZ | `Types\Dimension3z\Point` | X, Y, Z |
| XYM | `Types\Dimension3m\Point` | X, Y, M |
| XYZM | `Types\Dimension4zm\Point` | X, Y, Z, M |

All ordinates default to `null`. Each constructor ends with
`int|SpatialReference $srid = 0` and
`SpatialModelEnum $family = SpatialModelEnum::GEOMETRY`.
The family is instance state: the same class supports Geometry and Geography.

```php
use LongitudeOne\Core\Enum\SpatialModelEnum;
use LongitudeOne\SpatialTypes\Reference\SpatialReference;
use LongitudeOne\SpatialTypes\Types\Dimension3z\Point;

$point = new Point(0, null, null, 4326);
$samePoint = $point->setX("2°30'E")
->setY(48.5)
->setZ(35)
->setFamily(SpatialModelEnum::GEOGRAPHY)
->setSpatialReference(SpatialReference::epsg(2154));
assert($samePoint === $point);
assert([2.5, 48.5, 35] === $point->toArray());
```

`MutablePointInterface` exposes `setX()`, `setY()`, `setZ()` and `setM()`;
`MutableSpatialInterface` provides `setFamily()`, `setSpatialReference()` and
`setSrid()`. Every successful setter returns the same instance as `static`.
X/Y accept `float|int|string|null` and use the constructor's geo-parser path
for strings. Z/M accept `float|int|null`, without string parsing.
Rejected technical or parsing input leaves the receiver unchanged.

Calling a Z/M setter for an axis outside the declared layout throws
`LongitudeOne\SpatialTypes\Exception\BadMethodCallException`, even for `null`.
Type and dimension remain fixed; there is no dimension setter or `with…` method.
Supported axes accept null independently: `[0, null]` is not EMPTY. All-null
ordinates produce EMPTY, with `toArray() === []` and `getCoordinates() === null`.
A previously obtained `Coordinates` value remains an immutable snapshot.

Family and declared-reference setters preserve every ordinate without coordinate
transformation. Geometric validation remains explicit in the consumer. Mutable
points cannot be inserted into immutable aggregates and are not implicitly
converted. Factories continue to create immutable types.

### Immutable point coordinates

`LongitudeOne\SpatialTypes\Value\Coordinates` is a public immutable value
Expand Down Expand Up @@ -403,7 +453,7 @@ Functional validity checks belong to explicit consumer validation; see

## Immutability contract

Every spatial type is immutable through the public API: construction sets its
Every spatial type with the `Immutable` suffix is immutable through the public API: construction sets its
ordinates, SRID, and aggregate membership, and no public mutator exists.
`withCoordinates(Coordinates $coordinates): static` returns a point with
replacement coordinates of the same dimension; `withSpatialReference(SpatialReference $reference): static`
Expand Down
4 changes: 4 additions & 0 deletions docs/spatial-reference-systems.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ checked explicitly by consumers. See [Spatial validation](validators.md).
reference. It does not change coordinates and therefore does not perform a
coordinate transformation. `withSrid()` is the legacy integer equivalent.

Mutable points provide `setSpatialReference()`, `setSrid()` and `setFamily()`
for changing the declared context on the same instance. These setters return
that instance and preserve every ordinate, including null values.

To obtain a valid value in a different SRS, calculate new coordinates with an
external transformer, then create or update a value with those coordinates and
the target `SpatialReference`.
Expand Down
7 changes: 5 additions & 2 deletions docs/type-class-hierarchy.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@ one branch for each listed coordinate dimension and `{Geometry, Geography}` one
concrete class for each spatial model.

`AbstractSpatialTypeImmutable` extends the read-only `AbstractSpatialType` base.
The following hierarchy contains only immutable implementation bases; future mutable
classes do not inherit their copy operations.
The following hierarchy contains immutable implementation bases. Mutable points
extend `AbstractSpatialType` through `AbstractPoint`, with concrete
`Dimension{2,3m,3z,4zm}\Point` classes. `PointBehaviorTrait` shares point reads,
coordinate initialization and parsing between `AbstractPoint` and
`AbstractPointImmutable`; mutable points do not inherit copy operations.

```text
AbstractSpatialTypeImmutable
Expand Down
13 changes: 12 additions & 1 deletion lib/Interfaces/MutablePointInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,18 @@

namespace LongitudeOne\SpatialTypes\Interfaces;

/** Mutable point contract; type-specific editing is introduced separately. */
/** In-place point editing; unavailable Z/M axes reject even null values. */
interface MutablePointInterface extends MutableSpatialInterface, PointInterface
{
/** @param null|float|int $m Measure ordinate; the layout must contain M */
public function setM(float|int|null $m): static;

/** @param null|float|int|string $x X ordinate; strings use constructor parsing */
public function setX(float|int|string|null $x): static;

/** @param null|float|int|string $y Y ordinate; strings use constructor parsing */
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;
}
147 changes: 147 additions & 0 deletions lib/Types/AbstractPoint.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
<?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\BadMethodCallException;
use LongitudeOne\SpatialTypes\Interfaces\MutablePointInterface;
use LongitudeOne\SpatialTypes\Reference\SpatialReference;

/** Common in-place point editing, without geometric validation.
* @internal
*/
abstract class AbstractPoint extends AbstractSpatialType implements MutablePointInterface
{
use PointBehaviorTrait;

protected SpatialModelEnum $family = SpatialModelEnum::GEOMETRY;

protected float|int|null $m = null;

protected float|int|null $z = null;

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

/** Return the m. */
public function getM(): float|int|null
{
if (!$this->hasM()) {
throw BadMethodCallException::create(__METHOD__, $this->getDimension());
}

return $this->m;
}

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

/** Return the z. */
public function getZ(): float|int|null
{
if (!$this->hasZ()) {
throw BadMethodCallException::create(__METHOD__, $this->getDimension());
}

return $this->z;
}

/** @param SpatialModelEnum $family Declared family */
public function setFamily(SpatialModelEnum $family): static
{
$this->family = $family;

return $this;
}

/** @param null|float|int $m Measure ordinate */
public function setM(float|int|null $m): static
{
if (!$this->hasM()) {
throw BadMethodCallException::create(__METHOD__, $this->getDimension());
}

$this->m = $m;
$this->refreshEmptyState();

return $this;
}

/** @param SpatialReference $reference Declared reference */
public function setSpatialReference(SpatialReference $reference): static
{
$this->spatialReference = $reference;

return $this;
}

/** @param int $srid Declared reference identifier */
public function setSrid(int $srid): static
{
return $this->setSpatialReference(SpatialReference::fromSrid($srid));
}

/** @param null|float|int|string $x X ordinate, using constructor parsing */
public function setX(float|int|string|null $x): static
{
$this->initializeX($x);
$this->refreshEmptyState();

return $this;
}

/** @param null|float|int|string $y Y ordinate, using constructor parsing */
public function setY(float|int|string|null $y): static
{
$this->initializeY($y);
$this->refreshEmptyState();

return $this;
}

/** @param null|float|int $z Elevation ordinate */
public function setZ(float|int|null $z): static
{
if (!$this->hasZ()) {
throw BadMethodCallException::create(__METHOD__, $this->getDimension());
}

$this->z = $z;
$this->refreshEmptyState();

return $this;
}

/** @return (null|float|int)[] */
public function toArray(): array
{
return $this->getCoordinates()?->toArray() ?? [];
}

/** Recompute EMPTY after a successful ordinate edit. */
private function refreshEmptyState(): void
{
$this->hasOnlyNullCoordinates($this->x, $this->y, $this->z, $this->m);
}
}
Loading
Loading