diff --git a/API.md b/API.md
index 1864eb4..64ad150 100644
--- a/API.md
+++ b/API.md
@@ -441,3 +441,49 @@ Place::query()
->exists(); // true
```
+
+## 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` |
+
+Example
+
+```php
+ $point1 = new Point(41.9631174, -87.6770458);
+ $point2 = new Point(40.7628267, -73.9898293);
+
+ $distance = GeometryUtilities::make()
+ ->distanceSphere($point1, $point2); // 1148798.720296128 (meters)
+```
+
+
+
+### 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` |
+
+Example
+
+```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]]]}
+```
+
diff --git a/src/Exceptions/GeometryQueryException.php b/src/Exceptions/GeometryQueryException.php
new file mode 100644
index 0000000..950154d
--- /dev/null
+++ b/src/Exceptions/GeometryQueryException.php
@@ -0,0 +1,14 @@
+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;
+ }
+}
diff --git a/src/SpatialBuilder.php b/src/SpatialBuilder.php
index fdbd2a6..c17ae2b 100644
--- a/src/SpatialBuilder.php
+++ b/src/SpatialBuilder.php
@@ -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);
- }
}
diff --git a/src/SpatialQueryHelpers.php b/src/SpatialQueryHelpers.php
new file mode 100644
index 0000000..9baa572
--- /dev/null
+++ b/src/SpatialQueryHelpers.php
@@ -0,0 +1,25 @@
+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);
+ }
+}
diff --git a/tests/GeometryUtilitiesTest.php b/tests/GeometryUtilitiesTest.php
new file mode 100644
index 0000000..e23aa84
--- /dev/null
+++ b/tests/GeometryUtilitiesTest.php
@@ -0,0 +1,45 @@
+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);
+});
diff --git a/tests/SpatialBuilderTest.php b/tests/SpatialBuilderTest.php
index af7dfc5..f981e8e 100644
--- a/tests/SpatialBuilderTest.php
+++ b/tests/SpatialBuilderTest.php
@@ -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`');
-});
diff --git a/tests/SpatialQueryHelpersTest.php b/tests/SpatialQueryHelpersTest.php
new file mode 100644
index 0000000..686dfc5
--- /dev/null
+++ b/tests/SpatialQueryHelpersTest.php
@@ -0,0 +1,35 @@
+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`');
+});