Skip to content

Commit

Permalink
SRID support (#47)
Browse files Browse the repository at this point in the history
* Add PhpStorm run configurations

* Add test to `whereSrid`

* Add tests to Point

* Add tests to MultiPoint

* Add tests to LineString

* Add tests to MultiLineString

* Add tests to Polygon

* Add tests to MultiPolygon

* Add tests to GeometryCollection

* Implement `whereSrid`

* Implement SRID

* fixes

* Fixes

* update docs

Co-authored-by: Matan Yadaev <[email protected]>
  • Loading branch information
MatanYadaev and Matan Yadaev authored Jul 20, 2022
1 parent 55119fb commit 4ec31b7
Show file tree
Hide file tree
Showing 23 changed files with 768 additions and 72 deletions.
8 changes: 8 additions & 0 deletions .run/Fix formatting.run.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Fix formatting" type="ComposerRunConfigurationType" factoryName="Composer Script">
<option name="commandLineParameters" value="" />
<option name="pathToComposerJson" value="$PROJECT_DIR$/composer.json" />
<option name="script" value="php-cs-fixer" />
<method v="2" />
</configuration>
</component>
8 changes: 8 additions & 0 deletions .run/Static code analysis.run.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Static code analysis" type="ComposerRunConfigurationType" factoryName="Composer Script">
<option name="commandLineParameters" value="" />
<option name="pathToComposerJson" value="$PROJECT_DIR$/composer.json" />
<option name="script" value="phpstan" />
<method v="2" />
</configuration>
</component>
11 changes: 11 additions & 0 deletions .run/Test.run.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Test" type="PestRunConfigurationType">
<option name="pestRunnerSettings">
<PestRunner method="" scope="ConfigurationFile" />
</option>
<option name="runnerSettings">
<PhpTestRunnerSettings method="" scope="ConfigurationFile" />
</option>
<method v="2" />
</configuration>
</component>
51 changes: 39 additions & 12 deletions API.md
Original file line number Diff line number Diff line change
@@ -1,23 +1,28 @@
# API

## Available spatial classes
## Available geometry classes

* `Point(float $latitude, float $longitude)` - [MySQL Point](https://dev.mysql.com/doc/refman/8.0/en/gis-class-point.html)
* `MultiPoint(Point[] | Collection<Point>)` - [MySQL MultiPoint](https://dev.mysql.com/doc/refman/8.0/en/gis-class-multipoint.html)
* `LineString(Point[] | Collection<Point>)` - [MySQL LineString](https://dev.mysql.com/doc/refman/8.0/en/gis-class-linestring.html)
* `MultiLineString(LineString[] | Collection<LineString>)` - [MySQL MultiLineString](https://dev.mysql.com/doc/refman/8.0/en/gis-class-multilinestring.html)
* `Polygon(LineString[] | Collection<LineString>)` - [MySQL Polygon](https://dev.mysql.com/doc/refman/8.0/en/gis-class-polygon.html)
* `MultiPolygon(Polygon[] | Collection<Polygon>)` - [MySQL MultiPolygon](https://dev.mysql.com/doc/refman/8.0/en/gis-class-multipolygon.html)
* `GeometryCollection(Geometry[] | Collection<Geometry>)` - [MySQL GeometryCollection](https://dev.mysql.com/doc/refman/8.0/en/gis-class-geometrycollection.html)
* `Point(float $latitude, float $longitude, int $srid = 0)` - [MySQL Point](https://dev.mysql.com/doc/refman/8.0/en/gis-class-point.html)
* `MultiPoint(Point[] | Collection<Point>, int $srid = 0)` - [MySQL MultiPoint](https://dev.mysql.com/doc/refman/8.0/en/gis-class-multipoint.html)
* `LineString(Point[] | Collection<Point>, int $srid = 0)` - [MySQL LineString](https://dev.mysql.com/doc/refman/8.0/en/gis-class-linestring.html)
* `MultiLineString(LineString[] | Collection<LineString>, int $srid = 0)` - [MySQL MultiLineString](https://dev.mysql.com/doc/refman/8.0/en/gis-class-multilinestring.html)
* `Polygon(LineString[] | Collection<LineString>, int $srid = 0)` - [MySQL Polygon](https://dev.mysql.com/doc/refman/8.0/en/gis-class-polygon.html)
* `MultiPolygon(Polygon[] | Collection<Polygon>, int $srid = 0)` - [MySQL MultiPolygon](https://dev.mysql.com/doc/refman/8.0/en/gis-class-multipolygon.html)
* `GeometryCollection(Geometry[] | Collection<Geometry>, int $srid = 0)` - [MySQL GeometryCollection](https://dev.mysql.com/doc/refman/8.0/en/gis-class-geometrycollection.html)

## Available spatial functions
Geometry classes can be also created by these static methods:

Every geometry class has these functions:
* `fromJson(string $geoJson, int $srid = 0)` - Creates a geometry object from a [GeoJSON](https://en.wikipedia.org/wiki/GeoJSON) string.
* `fromWkt(string $wkt, int $srid = 0)` - Creates a geometry object from a [WKT](https://en.wikipedia.org/wiki/Well-known_text_representation_of_geometry).
* `fromWkb(string $wkb, int $srid = 0)` - Creates a geometry object from a [WKB](https://en.wikipedia.org/wiki/Well-known_text_representation_of_geometry#Well-known_binary).

## Available geometry class methods

* `toArray()` - Serializes the geometry object into a GeoJSON associative array.
* `toJson()` - Serializes the geometry object into an GeoJSON string.
* `fromJson(string $geoJson)` - Deserializes a geometry object from a GeoJSON string. (static method)
* `toFeatureCollectionJson()` - Serializes the geometry object into an GeoJSON's FeatureCollection string.
* `toWkt()` - Serializes the geometry object into a WKT.
* `toWkb()` - Serializes the geometry object into a WKB.
* `getCoordinates()` - Returns the coordinates of the geometry object.

In addition, `GeometryCollection` also has these functions:
Expand All @@ -40,7 +45,7 @@ $geometryCollection = new GeometryCollection([
]);

echo $geometryCollection->getGeometries()[1]->latitude; // 180
// can also access as an array:
// or access as an array:
echo $geometryCollection[1]->latitude; // 180
```

Expand All @@ -59,6 +64,7 @@ echo $geometryCollection[1]->latitude; // 180
* [whereCrosses](#whereCrosses)
* [whereDisjoint](#whereDisjoint)
* [whereEquals](#whereEquals)
* [whereSrid](#whereSrid)

### withDistance

Expand Down Expand Up @@ -372,3 +378,24 @@ Place::query()
```
</details>

### whereSrid

Filters records by the [ST_Srid](https://dev.mysql.com/doc/refman/8.0/en/gis-general-property-functions.html#function_st-srid) function.

| parameter name | type
| ------------------ | --------------------
| `$column` | `string`
| `$operator` | `string`
| `$value` | `int`

<details><summary>Example</summary>

```php
Place::create(['location' => new Point(0, 0, 4326)]);

Place::query()
->whereSrid('location', '=', 4326)
->exists(); // true
```
</details>

26 changes: 16 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,12 @@ use MatanYadaev\EloquentSpatial\Objects\Point;

$londonEye = Place::create([
'name' => 'London Eye',
'location' => new Point(51.5032973, -0.1195537)
'location' => new Point(51.5032973, -0.1217424),
]);

$whiteHouse = Place::create([
'name' => 'White House',
'location' => new Point(38.8976763, -77.0365298, 4326), // with SRID
]);

$vaticanCity = Place::create([
Expand All @@ -119,16 +124,18 @@ $vaticanCity = Place::create([
new Point(12.457734346389769, 41.905918239316286),
new Point(12.45572805404663, 41.90637337450963),
new Point(12.455363273620605, 41.90746728266806),
])
])
]),
]),
])
```

Retrieve a record with spatial data:

```php
echo $londonEye->location->latitude; // 51.5032973
echo $londonEye->location->longitude; // -0.1195537
echo $londonEye->location->longitude; // -0.1217424

echo $whiteHouse->location->srid; // 4326

echo $vacationCity->area->toJson(); // {"type":"Polygon","coordinates":[[[41.90746728266806,12.455363273620605],[41.906636872349075,12.450309991836548],[41.90197359839437,12.445632219314575],[41.90027269624499,12.447413206100464],[41.90000118654431,12.457906007766724],[41.90281205461268,12.458517551422117],[41.903107507989986,12.457584142684937],[41.905918239316286,12.457734346389769],[41.90637337450963,12.45572805404663],[41.90746728266806,12.455363273620605]]]}
```
Expand Down Expand Up @@ -170,13 +177,12 @@ Place::query()->whereDistance(...); // This is IDE-friendly
Place::whereDistance(...); // This is not
```

## Tests
## Development

``` bash
composer phpunit
# or with coverage
composer phpunit-coverage
```
* Test: `composer pest`
* Test with coverage: `composer pest-coverage`
* Type check: `composer phpstan`
* Format: `composer php-cs-fixer`

## Changelog

Expand Down
27 changes: 11 additions & 16 deletions src/Factory.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,8 @@

class Factory
{
public static function parse(string $value, bool $isWkb): Geometry
public static function parse(string $value): Geometry
{
if ($isWkb) {
// MySQL adds 4 NULL bytes at the start of the WKB
$value = substr($value, 4);
}

try {
/** @var geoPHPGeometry|false $geoPHPGeometry */
$geoPHPGeometry = geoPHP::load($value);
Expand All @@ -46,14 +41,14 @@ public static function parse(string $value, bool $isWkb): Geometry

protected static function createFromGeometry(geoPHPGeometry $geometry): Geometry
{
$srid = is_int($geometry->getSRID()) ? $geometry->getSRID() : 0;

if ($geometry instanceof geoPHPPoint) {
if ($geometry->coords[0] === null || $geometry->coords[1] === null) {
if (! isset($geoPHPGeometry) || ! $geoPHPGeometry) {
throw new InvalidArgumentException('Invalid spatial value');
}
throw new InvalidArgumentException('Invalid spatial value');
}

return new Point($geometry->coords[1], $geometry->coords[0]);
return new Point($geometry->coords[1], $geometry->coords[0], $srid);
}

/** @var geoPHPGeometryCollection $geometry */
Expand All @@ -63,25 +58,25 @@ protected static function createFromGeometry(geoPHPGeometry $geometry): Geometry
});

if ($geometry::class === geoPHPMultiPoint::class) {
return new MultiPoint($components);
return new MultiPoint($components, $srid);
}

if ($geometry::class === geoPHPLineString::class) {
return new LineString($components);
return new LineString($components, $srid);
}

if ($geometry::class === geoPHPPolygon::class) {
return new Polygon($components);
return new Polygon($components, $srid);
}

if ($geometry::class === geoPHPMultiLineString::class) {
return new MultiLineString($components);
return new MultiLineString($components, $srid);
}

if ($geometry::class === geoPHPMultiPolygon::class) {
return new MultiPolygon($components);
return new MultiPolygon($components, $srid);
}

return new GeometryCollection($components);
return new GeometryCollection($components, $srid);
}
}
38 changes: 23 additions & 15 deletions src/GeometryCast.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,56 +27,64 @@ public function __construct(string $className)
/**
* @param Model $model
* @param string $key
* @param string|Expression|null $wkbOrWKt
* @param string|Expression|null $value
* @param array<string, mixed> $attributes
* @return Geometry|null
*/
public function get($model, string $key, $wkbOrWKt, array $attributes): ?Geometry
public function get($model, string $key, $value, array $attributes): ?Geometry
{
if (! $wkbOrWKt) {
if (! $value) {
return null;
}

if ($wkbOrWKt instanceof Expression) {
$wkt = $this->extractWktFromExpression($wkbOrWKt);
if ($value instanceof Expression) {
$wkt = $this->extractWktFromExpression($value);
$srid = $this->extractSridFromExpression($value);

return $this->className::fromWkt($wkt);
return $this->className::fromWkt($wkt, $srid);
}

return $this->className::fromWkb($wkbOrWKt);
return $this->className::fromWkb($value);
}

/**
* @param Model $model
* @param string $key
* @param Geometry|mixed|null $geometry
* @param Geometry|mixed|null $value
* @param array<string, mixed> $attributes
* @return Expression|null
*
* @throws InvalidArgumentException
*/
public function set($model, string $key, $geometry, array $attributes): Expression|null
public function set($model, string $key, $value, array $attributes): Expression|null
{
if (! $geometry) {
if (! $value) {
return null;
}

if (! ($geometry instanceof $this->className)) {
$geometryType = is_object($geometry) ? $geometry::class : gettype($geometry);
if (! ($value instanceof $this->className)) {
$geometryType = is_object($value) ? $value::class : gettype($value);
throw new InvalidArgumentException(
sprintf('Expected %s, %s given.', static::class, $geometryType)
);
}

$wkt = $geometry->toWkt(withFunction: true);
$wkt = $value->toWkt();

return DB::raw("ST_GeomFromText('{$wkt}')");
return DB::raw("ST_GeomFromText('{$wkt}', {$value->srid})");
}

private function extractWktFromExpression(Expression $expression): string
{
preg_match('/ST_GeomFromText\(\'(.+)\'\)/', (string) $expression, $match);
preg_match('/ST_GeomFromText\(\'(.+)\', .+\)/', (string) $expression, $match);

return $match[1];
}

private function extractSridFromExpression(Expression $expression): int
{
preg_match('/ST_GeomFromText\(\'.+\', (.+)\)/', (string) $expression, $match);

return (int) $match[1];
}
}
31 changes: 24 additions & 7 deletions src/Objects/Geometry.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@

abstract class Geometry implements Castable, Arrayable, Jsonable, JsonSerializable
{
public int $srid = 0;

abstract public function toWkt(bool $withFunction = true): string;

/**
Expand All @@ -33,10 +35,14 @@ public function toJson($options = 0): string

public function toWkb(): string
{
$geoPHPGeometry = geoPHP::load($this->toWkt());
$geoPHPGeometry = geoPHP::load($this->toJson());

$sridInBinary = pack('L', $this->srid);

// @phpstan-ignore-next-line
return (new geoPHPWkb)->write($geoPHPGeometry, true);
$wkbWithoutSrid = (new geoPHPWkb)->write($geoPHPGeometry);

return $sridInBinary.$wkbWithoutSrid;
}

/**
Expand All @@ -47,7 +53,14 @@ public function toWkb(): string
*/
public static function fromWkb(string $wkb): static
{
$geometry = Factory::parse($wkb, true);
$srid = substr($wkb, 0, 4);
// @phpstan-ignore-next-line
$srid = unpack('L', $srid)[1];

$wkb = substr($wkb, 4);

$geometry = Factory::parse($wkb);
$geometry->srid = $srid;

if (! ($geometry instanceof static)) {
throw new InvalidArgumentException(
Expand All @@ -60,13 +73,15 @@ public static function fromWkb(string $wkb): static

/**
* @param string $wkt
* @param int $srid
* @return static
*
* @throws InvalidArgumentException
*/
public static function fromWkt(string $wkt): static
public static function fromWkt(string $wkt, int $srid = 0): static
{
$geometry = Factory::parse($wkt, false);
$geometry = Factory::parse($wkt);
$geometry->srid = $srid;

if (! ($geometry instanceof static)) {
throw new InvalidArgumentException(
Expand All @@ -79,13 +94,15 @@ public static function fromWkt(string $wkt): static

/**
* @param string $geoJson
* @param int $srid
* @return static
*
* @throws InvalidArgumentException
*/
public static function fromJson(string $geoJson): static
public static function fromJson(string $geoJson, int $srid = 0): static
{
$geometry = Factory::parse($geoJson, false);
$geometry = Factory::parse($geoJson);
$geometry->srid = $srid;

if (! ($geometry instanceof static)) {
throw new InvalidArgumentException(
Expand Down
Loading

0 comments on commit 4ec31b7

Please sign in to comment.