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
Changes from 1 commit
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
Next Next commit
Add convex hull query
Riley19280 committed Feb 28, 2023
commit ccfce33821fb502a3caba863a6510e88846210c8
24 changes: 24 additions & 0 deletions API.md
Original file line number Diff line number Diff line change
@@ -441,3 +441,27 @@ Place::query()
->exists(); // true
```
</details>

## Available Spatial Queries

### 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>
18 changes: 2 additions & 16 deletions src/SpatialBuilder.php
Original file line number Diff line number Diff line change
@@ -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;

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

public function withDistance(
ExpressionContract|Geometry|string $column,
ExpressionContract|Geometry|string $geometryOrColumn,
@@ -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);
}
}
40 changes: 40 additions & 0 deletions src/SpatialQuery.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

namespace MatanYadaev\EloquentSpatial;

use Illuminate\Contracts\Database\Query\Expression as ExpressionContract;
use Illuminate\Database\ConnectionInterface;
use Illuminate\Database\Query\Builder;
use Illuminate\Database\Query\Grammars\Grammar;
use Illuminate\Database\Query\Processors\Processor;
use Illuminate\Support\Facades\DB;
use MatanYadaev\EloquentSpatial\Objects\Geometry;

class SpatialQuery extends Builder
{
use SpatialQueryHelpers;

public function __construct(ConnectionInterface $connection = null, Grammar $grammar = null, Processor $processor = null)
{
parent::__construct($connection ?? DB::connection(), $grammar, $processor);
}

public static function make(ConnectionInterface $connection = null, Grammar $grammar = null, Processor $processor = null): self
{
return new self($connection, $grammar, $processor);
}

public function convexHull(
ExpressionContract|Geometry|string $geometry,
): self
{
$this->selectRaw(
sprintf(
'ST_CONVEXHULL(%s) as convex_hull',
$this->toExpressionString($geometry),
)
);

return $this;
}
}
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);
}
}
31 changes: 0 additions & 31 deletions tests/SpatialBuilderTest.php
Original file line number Diff line number Diff line change
@@ -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`');
});
24 changes: 24 additions & 0 deletions tests/SpatialQueryTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

use Illuminate\Foundation\Testing\DatabaseMigrations;
use MatanYadaev\EloquentSpatial\Objects\Polygon;
use MatanYadaev\EloquentSpatial\SpatialQuery;

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 = SpatialQuery::make()
->convexHull($points)
->first();

// @phpstan-ignore-next-line
$returnedPolygon = Polygon::fromWkb($hull->convex_hull)->toArray();

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

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