Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for convex hulls #83

Closed
wants to merge 4 commits into from
Closed
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
46 changes: 46 additions & 0 deletions API.md
Original file line number Diff line number Diff line change
Expand Up @@ -441,3 +441,49 @@ Place::query()
->exists(); // true
```
</details>

## Available Geometry Helpers


### distanceSphere

Compute the spherical distance between two points using the [ST_Distance_Sphere](https://dev.mysql.com/doc/refman/5.7/en/spatial-convenience-functions.html#function_st-distance-sphere) function.

| parameter name | type |
|----------------|------------------|
| `$point1` | `Point \ string` |
| `$point2` | `Point \ string` |

<details><summary>Example</summary>

```php
$point1 = new Point(41.9631174, -87.6770458);
$point2 = new Point(40.7628267, -73.9898293);

$distance = GeometryUtilities::make()
->distanceSphere($point1, $point2); // 1148798.720296128 (meters)
```
</details>


### convexHull

Creates a ConvexHull using the specified geometry using the [ST_ConvexHull](https://dev.mysql.com/doc/refman/8.0/en/spatial-operator-functions.html#function_st-convexhull) function.

| parameter name | type |
|----------------|---------------------|
| `$geometry` | `Geometry \ string` |

<details><summary>Example</summary>

```php
$points = MultiPoint::fromJson('{"type":"MultiPoint","coordinates":[[-1,-1],[1,-1],[1,1],[-1,1],[-1,-1],[0,0]]}');

$wkbHull = SpatialQuery::make()
->convexHull($points)
->first()
->convex_hull;

Polygon::fromWkb($wkbHull) // {"type":"Polygon","coordinates":[[[-1,-1],[1,-1],[1,1],[-1,1],[-1,-1]]]}
```
</details>
14 changes: 14 additions & 0 deletions src/Exceptions/GeometryQueryException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

namespace MatanYadaev\EloquentSpatial\Exceptions;

use Exception;
use Throwable;

class GeometryQueryException extends Exception
{
public static function noData(int $code = 0, ?Throwable $previous = null): self
{
return new self('No data was returned by the query', $code, $previous);
}
}
90 changes: 90 additions & 0 deletions src/GeometryUtilities.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<?php

namespace MatanYadaev\EloquentSpatial;

use Illuminate\Contracts\Database\Query\Expression as ExpressionContract;
use Illuminate\Database\Query\Grammars\Grammar;
use Illuminate\Support\Facades\DB;
use MatanYadaev\EloquentSpatial\Exceptions\GeometryQueryException;
use MatanYadaev\EloquentSpatial\Objects\Geometry;
use MatanYadaev\EloquentSpatial\Objects\Point;

class GeometryUtilities
{
use SpatialQueryHelpers;

public function __construct(protected ?string $connection = null)
{

}

public static function make(string $connection = null): self
{
return new self($connection);
}

private function getGrammar(): Grammar
{
return DB::connection($this->connection)->getQueryGrammar();
}

private function getConnection(): \Illuminate\Database\Connection
{
return DB::connection($this->connection);
}

public function convexHull(
ExpressionContract|Geometry|string $geometry,
): Geometry
{
$queryResult = DB::connection($this->connection)
->query()
->selectRaw(
sprintf(
'ST_CONVEXHULL(%s) as result',
$this->toExpressionString($geometry),
)
)->first();

throw_unless($queryResult, GeometryQueryException::noData());

// @phpstan-ignore-next-line
return Geometry::fromWkb($queryResult->result);
}

/**
* Compute the distance between two points on a sphere in meters.
*
* @param ExpressionContract|Point|string $point1
* @param ExpressionContract|Point|string $point2
* @param float|null $sphereSize Size of the sphere
* @return float Distance between points
*/
public function distanceSphere(ExpressionContract|Point|string $point1, ExpressionContract|Point|string $point2, float $sphereSize = null): float
{
$queryResult = DB::connection($this->connection)
->query()
->when($sphereSize, fn ($query) => $query
->selectRaw(
sprintf(
'ST_DISTANCE_SPHERE(%s, %s, %s) as result',
$this->toExpressionString($point1),
$this->toExpressionString($point2),
$sphereSize
)
), fn ($query) => $query
->selectRaw(
sprintf(
'ST_DISTANCE_SPHERE(%s, %s) as result',
$this->toExpressionString($point1),
$this->toExpressionString($point2),
)
)
)
->first();

throw_unless($queryResult, GeometryQueryException::noData());

return $queryResult->result;
}
}
18 changes: 2 additions & 16 deletions src/SpatialBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@

use Illuminate\Contracts\Database\Query\Expression as ExpressionContract;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\DB;
use MatanYadaev\EloquentSpatial\Objects\Geometry;

/**
Expand All @@ -18,6 +17,8 @@
*/
class SpatialBuilder extends Builder
{
use SpatialQueryHelpers;

public function withDistance(
ExpressionContract|Geometry|string $column,
ExpressionContract|Geometry|string $geometryOrColumn,
Expand Down Expand Up @@ -315,19 +316,4 @@ public function whereSrid(

return $this;
}

protected function toExpressionString(ExpressionContract|Geometry|string $geometryOrColumnOrExpression): string
{
$grammar = $this->getGrammar();

if ($geometryOrColumnOrExpression instanceof ExpressionContract) {
$expression = $geometryOrColumnOrExpression;
} elseif ($geometryOrColumnOrExpression instanceof Geometry) {
$expression = $geometryOrColumnOrExpression->toSqlExpression($this->getConnection());
} else {
$expression = DB::raw($grammar->wrap($geometryOrColumnOrExpression));
}

return (string) $expression->getValue($grammar);
}
}
25 changes: 25 additions & 0 deletions src/SpatialQueryHelpers.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

namespace MatanYadaev\EloquentSpatial;

use Illuminate\Contracts\Database\Query\Expression as ExpressionContract;
use Illuminate\Support\Facades\DB;
use MatanYadaev\EloquentSpatial\Objects\Geometry;

trait SpatialQueryHelpers
{
protected function toExpressionString(ExpressionContract|Geometry|string $geometryOrColumnOrExpression): string
{
$grammar = $this->getGrammar();

if ($geometryOrColumnOrExpression instanceof ExpressionContract) {
$expression = $geometryOrColumnOrExpression;
} elseif ($geometryOrColumnOrExpression instanceof Geometry) {
$expression = $geometryOrColumnOrExpression->toSqlExpression($this->getConnection());
} else {
$expression = DB::raw($grammar->wrap($geometryOrColumnOrExpression));
}

return (string) $expression->getValue($grammar);
}
}
45 changes: 45 additions & 0 deletions tests/GeometryUtilitiesTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

use Illuminate\Foundation\Testing\DatabaseMigrations;
use MatanYadaev\EloquentSpatial\GeometryUtilities;
use MatanYadaev\EloquentSpatial\Objects\Point;
use MatanYadaev\EloquentSpatial\Objects\Polygon;

uses(DatabaseMigrations::class);

it('creates a convex hull', function (): void {
$points = \MatanYadaev\EloquentSpatial\Objects\MultiPoint::fromJson('{"type":"MultiPoint","coordinates":[[-1,-1],[1,-1],[1,1],[-1,1],[-1,-1],[0,0]]}', 0);

$hull = GeometryUtilities::make()
->convexHull($points);

$expectedPolygon = Polygon::fromJson('{"type":"Polygon","coordinates":[[[-1,-1],[1,-1],[1,1],[-1,1],[-1,-1]]]}')->toArray();

expect($hull)->toBeInstanceOf(Polygon::class);

// Compare coordinates like this to prevent ordering differences between database types/versions
// @phpstan-ignore-next-line
expect($hull->toArray()['coordinates'][0])->toEqualCanonicalizing($expectedPolygon['coordinates'][0]);
});

it('calculates the distance between points on a sphere', function (): void {

$point1 = new Point(41.9631174, -87.6770458);
$point2 = new Point(40.7628267, -73.9898293);

$distance = GeometryUtilities::make()
->distanceSphere($point1, $point2);

expect($distance)->toBe(1148798.720296128);
});

it('calculates the distance between points on a sphere with sphere size', function (): void {

$point1 = new Point(41.9631174, -87.6770458);
$point2 = new Point(40.7628267, -73.9898293);

$distance = GeometryUtilities::make()
->distanceSphere($point1, $point2, 1);

expect($distance)->toBe(0.18031725706132895);
});
31 changes: 0 additions & 31 deletions tests/SpatialBuilderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -408,34 +408,3 @@

expect($testPlaceWithDistance)->not()->toBeNull();
});

it('toExpressionString can handle a Expression input', function (): void {
$spatialBuilder = TestPlace::query();
$toExpressionStringMethod = (new ReflectionClass($spatialBuilder))->getMethod('toExpressionString');

$result = $toExpressionStringMethod->invoke($spatialBuilder, DB::raw('POINT(longitude, latitude)'));

expect($result)->toBe('POINT(longitude, latitude)');
});

it('toExpressionString can handle a Geometry input', function (): void {
$spatialBuilder = TestPlace::query();
$toExpressionStringMethod = (new ReflectionClass($spatialBuilder))->getMethod('toExpressionString');
$polygon = Polygon::fromJson('{"type":"Polygon","coordinates":[[[-1,-1],[1,-1],[1,1],[-1,1],[-1,-1]]]}');

$result = $toExpressionStringMethod->invoke($spatialBuilder, $polygon);

$grammar = $spatialBuilder->getGrammar();
$connection = $spatialBuilder->getConnection();
$sqlSerializedPolygon = $polygon->toSqlExpression($connection)->getValue($grammar);
expect($result)->toBe($sqlSerializedPolygon);
});

it('toExpressionString can handle a string input', function (): void {
$spatialBuilder = TestPlace::query();
$toExpressionStringMethod = (new ReflectionClass($spatialBuilder))->getMethod('toExpressionString');

$result = $toExpressionStringMethod->invoke($spatialBuilder, 'test_places.point');

expect($result)->toBe('`test_places`.`point`');
});
35 changes: 35 additions & 0 deletions tests/SpatialQueryHelpersTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

use MatanYadaev\EloquentSpatial\Objects\Polygon;
use MatanYadaev\EloquentSpatial\Tests\TestModels\TestPlace;

it('toExpressionString can handle a Expression input', function (): void {
$spatialBuilder = TestPlace::query();
$toExpressionStringMethod = (new ReflectionClass($spatialBuilder))->getMethod('toExpressionString');

$result = $toExpressionStringMethod->invoke($spatialBuilder, DB::raw('POINT(longitude, latitude)'));

expect($result)->toBe('POINT(longitude, latitude)');
});

it('toExpressionString can handle a Geometry input', function (): void {
$spatialBuilder = TestPlace::query();
$toExpressionStringMethod = (new ReflectionClass($spatialBuilder))->getMethod('toExpressionString');
$polygon = Polygon::fromJson('{"type":"Polygon","coordinates":[[[-1,-1],[1,-1],[1,1],[-1,1],[-1,-1]]]}');

$result = $toExpressionStringMethod->invoke($spatialBuilder, $polygon);

$grammar = $spatialBuilder->getGrammar();
$connection = $spatialBuilder->getConnection();
$sqlSerializedPolygon = $polygon->toSqlExpression($connection)->getValue($grammar);
expect($result)->toBe($sqlSerializedPolygon);
});

it('toExpressionString can handle a string input', function (): void {
$spatialBuilder = TestPlace::query();
$toExpressionStringMethod = (new ReflectionClass($spatialBuilder))->getMethod('toExpressionString');

$result = $toExpressionStringMethod->invoke($spatialBuilder, 'test_places.point');

expect($result)->toBe('`test_places`.`point`');
});