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`'); +});