Skip to content

Commit ccfce33

Browse files
committed
Add convex hull query
1 parent b9f6c6d commit ccfce33

7 files changed

+150
-47
lines changed

API.md

+24
Original file line numberDiff line numberDiff line change
@@ -441,3 +441,27 @@ Place::query()
441441
->exists(); // true
442442
```
443443
</details>
444+
445+
## Available Spatial Queries
446+
447+
### convexHull
448+
449+
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.
450+
451+
| parameter name | type |
452+
|----------------|---------------------|
453+
| `$geometry` | `Geometry \ string` |
454+
455+
<details><summary>Example</summary>
456+
457+
```php
458+
$points = MultiPoint::fromJson('{"type":"MultiPoint","coordinates":[[-1,-1],[1,-1],[1,1],[-1,1],[-1,-1],[0,0]]}');
459+
460+
$wkbHull = SpatialQuery::make()
461+
->convexHull($points)
462+
->first()
463+
->convex_hull;
464+
465+
Polygon::fromWkb($wkbHull) // {"type":"Polygon","coordinates":[[[-1,-1],[1,-1],[1,1],[-1,1],[-1,-1]]]}
466+
```
467+
</details>

src/SpatialBuilder.php

+2-16
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66

77
use Illuminate\Contracts\Database\Query\Expression as ExpressionContract;
88
use Illuminate\Database\Eloquent\Builder;
9-
use Illuminate\Support\Facades\DB;
109
use MatanYadaev\EloquentSpatial\Objects\Geometry;
1110

1211
/**
@@ -18,6 +17,8 @@
1817
*/
1918
class SpatialBuilder extends Builder
2019
{
20+
use SpatialQueryHelpers;
21+
2122
public function withDistance(
2223
ExpressionContract|Geometry|string $column,
2324
ExpressionContract|Geometry|string $geometryOrColumn,
@@ -315,19 +316,4 @@ public function whereSrid(
315316

316317
return $this;
317318
}
318-
319-
protected function toExpressionString(ExpressionContract|Geometry|string $geometryOrColumnOrExpression): string
320-
{
321-
$grammar = $this->getGrammar();
322-
323-
if ($geometryOrColumnOrExpression instanceof ExpressionContract) {
324-
$expression = $geometryOrColumnOrExpression;
325-
} elseif ($geometryOrColumnOrExpression instanceof Geometry) {
326-
$expression = $geometryOrColumnOrExpression->toSqlExpression($this->getConnection());
327-
} else {
328-
$expression = DB::raw($grammar->wrap($geometryOrColumnOrExpression));
329-
}
330-
331-
return (string) $expression->getValue($grammar);
332-
}
333319
}

src/SpatialQuery.php

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?php
2+
3+
namespace MatanYadaev\EloquentSpatial;
4+
5+
use Illuminate\Contracts\Database\Query\Expression as ExpressionContract;
6+
use Illuminate\Database\ConnectionInterface;
7+
use Illuminate\Database\Query\Builder;
8+
use Illuminate\Database\Query\Grammars\Grammar;
9+
use Illuminate\Database\Query\Processors\Processor;
10+
use Illuminate\Support\Facades\DB;
11+
use MatanYadaev\EloquentSpatial\Objects\Geometry;
12+
13+
class SpatialQuery extends Builder
14+
{
15+
use SpatialQueryHelpers;
16+
17+
public function __construct(ConnectionInterface $connection = null, Grammar $grammar = null, Processor $processor = null)
18+
{
19+
parent::__construct($connection ?? DB::connection(), $grammar, $processor);
20+
}
21+
22+
public static function make(ConnectionInterface $connection = null, Grammar $grammar = null, Processor $processor = null): self
23+
{
24+
return new self($connection, $grammar, $processor);
25+
}
26+
27+
public function convexHull(
28+
ExpressionContract|Geometry|string $geometry,
29+
): self
30+
{
31+
$this->selectRaw(
32+
sprintf(
33+
'ST_CONVEXHULL(%s) as convex_hull',
34+
$this->toExpressionString($geometry),
35+
)
36+
);
37+
38+
return $this;
39+
}
40+
}

src/SpatialQueryHelpers.php

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
namespace MatanYadaev\EloquentSpatial;
4+
5+
use Illuminate\Contracts\Database\Query\Expression as ExpressionContract;
6+
use Illuminate\Support\Facades\DB;
7+
use MatanYadaev\EloquentSpatial\Objects\Geometry;
8+
9+
trait SpatialQueryHelpers
10+
{
11+
protected function toExpressionString(ExpressionContract|Geometry|string $geometryOrColumnOrExpression): string
12+
{
13+
$grammar = $this->getGrammar();
14+
15+
if ($geometryOrColumnOrExpression instanceof ExpressionContract) {
16+
$expression = $geometryOrColumnOrExpression;
17+
} elseif ($geometryOrColumnOrExpression instanceof Geometry) {
18+
$expression = $geometryOrColumnOrExpression->toSqlExpression($this->getConnection());
19+
} else {
20+
$expression = DB::raw($grammar->wrap($geometryOrColumnOrExpression));
21+
}
22+
23+
return (string) $expression->getValue($grammar);
24+
}
25+
}

tests/SpatialBuilderTest.php

-31
Original file line numberDiff line numberDiff line change
@@ -408,34 +408,3 @@
408408

409409
expect($testPlaceWithDistance)->not()->toBeNull();
410410
});
411-
412-
it('toExpressionString can handle a Expression input', function (): void {
413-
$spatialBuilder = TestPlace::query();
414-
$toExpressionStringMethod = (new ReflectionClass($spatialBuilder))->getMethod('toExpressionString');
415-
416-
$result = $toExpressionStringMethod->invoke($spatialBuilder, DB::raw('POINT(longitude, latitude)'));
417-
418-
expect($result)->toBe('POINT(longitude, latitude)');
419-
});
420-
421-
it('toExpressionString can handle a Geometry input', function (): void {
422-
$spatialBuilder = TestPlace::query();
423-
$toExpressionStringMethod = (new ReflectionClass($spatialBuilder))->getMethod('toExpressionString');
424-
$polygon = Polygon::fromJson('{"type":"Polygon","coordinates":[[[-1,-1],[1,-1],[1,1],[-1,1],[-1,-1]]]}');
425-
426-
$result = $toExpressionStringMethod->invoke($spatialBuilder, $polygon);
427-
428-
$grammar = $spatialBuilder->getGrammar();
429-
$connection = $spatialBuilder->getConnection();
430-
$sqlSerializedPolygon = $polygon->toSqlExpression($connection)->getValue($grammar);
431-
expect($result)->toBe($sqlSerializedPolygon);
432-
});
433-
434-
it('toExpressionString can handle a string input', function (): void {
435-
$spatialBuilder = TestPlace::query();
436-
$toExpressionStringMethod = (new ReflectionClass($spatialBuilder))->getMethod('toExpressionString');
437-
438-
$result = $toExpressionStringMethod->invoke($spatialBuilder, 'test_places.point');
439-
440-
expect($result)->toBe('`test_places`.`point`');
441-
});

tests/SpatialQueryHelpersTest.php

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
3+
use MatanYadaev\EloquentSpatial\Objects\Polygon;
4+
use MatanYadaev\EloquentSpatial\Tests\TestModels\TestPlace;
5+
6+
it('toExpressionString can handle a Expression input', function (): void {
7+
$spatialBuilder = TestPlace::query();
8+
$toExpressionStringMethod = (new ReflectionClass($spatialBuilder))->getMethod('toExpressionString');
9+
10+
$result = $toExpressionStringMethod->invoke($spatialBuilder, DB::raw('POINT(longitude, latitude)'));
11+
12+
expect($result)->toBe('POINT(longitude, latitude)');
13+
});
14+
15+
it('toExpressionString can handle a Geometry input', function (): void {
16+
$spatialBuilder = TestPlace::query();
17+
$toExpressionStringMethod = (new ReflectionClass($spatialBuilder))->getMethod('toExpressionString');
18+
$polygon = Polygon::fromJson('{"type":"Polygon","coordinates":[[[-1,-1],[1,-1],[1,1],[-1,1],[-1,-1]]]}');
19+
20+
$result = $toExpressionStringMethod->invoke($spatialBuilder, $polygon);
21+
22+
$grammar = $spatialBuilder->getGrammar();
23+
$connection = $spatialBuilder->getConnection();
24+
$sqlSerializedPolygon = $polygon->toSqlExpression($connection)->getValue($grammar);
25+
expect($result)->toBe($sqlSerializedPolygon);
26+
});
27+
28+
it('toExpressionString can handle a string input', function (): void {
29+
$spatialBuilder = TestPlace::query();
30+
$toExpressionStringMethod = (new ReflectionClass($spatialBuilder))->getMethod('toExpressionString');
31+
32+
$result = $toExpressionStringMethod->invoke($spatialBuilder, 'test_places.point');
33+
34+
expect($result)->toBe('`test_places`.`point`');
35+
});

tests/SpatialQueryTest.php

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
use Illuminate\Foundation\Testing\DatabaseMigrations;
4+
use MatanYadaev\EloquentSpatial\Objects\Polygon;
5+
use MatanYadaev\EloquentSpatial\SpatialQuery;
6+
7+
uses(DatabaseMigrations::class);
8+
9+
it('creates a convex hull', function (): void {
10+
$points = \MatanYadaev\EloquentSpatial\Objects\MultiPoint::fromJson('{"type":"MultiPoint","coordinates":[[-1,-1],[1,-1],[1,1],[-1,1],[-1,-1],[0,0]]}', 0);
11+
12+
$hull = SpatialQuery::make()
13+
->convexHull($points)
14+
->first();
15+
16+
// @phpstan-ignore-next-line
17+
$returnedPolygon = Polygon::fromWkb($hull->convex_hull)->toArray();
18+
19+
$expectedPolygon = Polygon::fromJson('{"type":"Polygon","coordinates":[[[-1,-1],[1,-1],[1,1],[-1,1],[-1,-1]]]}')->toArray();
20+
21+
// Compare coordinates like this to prevent ordering differences between database types/versions
22+
// @phpstan-ignore-next-line
23+
expect($returnedPolygon['coordinates'][0])->toEqualCanonicalizing($expectedPolygon['coordinates'][0]);
24+
});

0 commit comments

Comments
 (0)