Sfoglia il codice sorgente

add the custom DistanceFilter

Olivier Massot 3 anni fa
parent
commit
7cded68af4

+ 2 - 0
config/packages/doctrine.yaml

@@ -45,6 +45,8 @@ doctrine:
                 dql:
                     string_functions:
                         find_in_set: DoctrineExtensions\Query\Mysql\FindInSet
+                    numeric_functions:
+                        spherical_distance: App\Doctrine\ORM\AST\SphericalDistance
             audit:
                 connection: audit
             adminassos:

+ 21 - 19
src/Doctrine/ORM/AST/SphericDistance.php → src/Doctrine/ORM/AST/SphericalDistance.php

@@ -3,7 +3,7 @@
 namespace App\Doctrine\ORM\AST;
 
 use Doctrine\ORM\Query\AST\ASTException;
-use Doctrine\ORM\Query\AST\ComparisonExpression;
+use Doctrine\ORM\Query\AST\Functions\FunctionNode;
 use Doctrine\ORM\Query\AST\Node;
 use Doctrine\ORM\Query\Lexer;
 use Doctrine\ORM\Query\Parser;
@@ -11,7 +11,9 @@ use Doctrine\ORM\Query\QueryException;
 use Doctrine\ORM\Query\SqlWalker;
 
 /**
- * Calcules la distance en km à vol d'oiseau entre les coordonnées géographiques données (latitude + longitude) de deux points.
+ * SphericalDistanceFunction ::= "SPHERICAL_DISTANCE" "(" ArithmeticPrimary "," ArithmeticPrimary "," ArithmeticPrimary "," ArithmeticPrimary ")"
+ *
+ * Calcule la distance en km à vol d'oiseau entre les coordonnées géographiques données (latitude, longitude) de deux points.
  *
  * Implémentation de la formule de Haversine, dont la précision est de l'ordre de la dizaine de mètres dans les cas les plus courants.
  *
@@ -19,10 +21,12 @@ use Doctrine\ORM\Query\SqlWalker;
  *
  *     SPHERICAL_DISTANCE(latitude1, longitude1, latitude2, longitude2)
  *
+ * WARNING: passing latitude2 and longitude2 as parameter, even named, is not properly interpreted, pass them directly.
+ *
  * @see https://fr.wikipedia.org/wiki/Coordonn%C3%A9es_sph%C3%A9riques
  * @see https://fr.wikipedia.org/wiki/Formule_de_haversine
  */
-class SphericDistance
+class SphericalDistance extends FunctionNode
 {
     private Node | string $latitude1;
     private Node | string $longitude1;
@@ -60,24 +64,22 @@ class SphericDistance
     {
         $R = 6371;  // Rayon terrestre, en km
 
-        $rLat1 = '({lat1} * PI() / 180)'; // Latitude 1, en radians
-        $rLon1 = '({lon1} * PI() / 180)'; // Longitude 1, en radians
-        $rLat2 = '({lat2} * PI() / 180)'; // Latitude 2, en radians
-        $rLon2 = '({lon2} * PI() / 180)'; // Longitude 2, en radians
+        $lat2 = $this->latitude2->dispatch($sqlWalker);
+        $lat1 = $this->latitude1->dispatch($sqlWalker);
+
+        // Call two additional dispatch so doctrine complete the parameters stack (careful: the order is important)
+        $this->latitude1->dispatch($sqlWalker);
+        $this->latitude2->dispatch($sqlWalker);
 
-        $sql = sprintf(
-            '2 * %1$s * ASIN(SQRT(POW(SIN((%4$s - %2$s) / 2), 2) + COS(%2$s) * COS(%4$s) * POW(SIN((%5$s - %3$s) / 2), 2)))',
-            $R, $rLat1, $rLon1, $rLat2, $rLon2
-        );
+        $lon1 = $this->longitude1->dispatch($sqlWalker);
+        $lon2 = $this->longitude2->dispatch($sqlWalker);
 
-        $sql = str_replace(array('{lat1}', '{lon1}', '{lat2}', '{lon2}'), array('(%1$s)', '(%2$s)', '(%3$s)', '(%4$s)'), $sql);
+        // Latitudes et longitudes en radians
+        $rLat1 = "($lat1 * PI() / 180)";
+        $rLon1 = "($lon1 * PI() / 180)";
+        $rLat2 = "($lat2 * PI() / 180)";
+        $rLon2 = "($lon2 * PI() / 180)";
 
-        return sprintf(
-            $sql,
-            $this->latitude1->dispatch($sqlWalker),
-            $this->longitude1->dispatch($sqlWalker),
-            $this->latitude2->dispatch($sqlWalker),
-            $this->longitude2->dispatch($sqlWalker)
-        );
+        return "2 * $R * ASIN(SQRT(POW(SIN(($rLat2 - $rLat1) / 2), 2) + COS($rLat1) * COS($rLat2) * POW(SIN(($rLon2 - $rLon1) / 2), 2)))";
     }
 }

+ 2 - 0
src/Entity/Public/PublicEvent.php

@@ -8,6 +8,7 @@ use ApiPlatform\Core\Annotation\ApiResource;
 use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\DateFilter;
 use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\NumericFilter;
 use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\SearchFilter;
+use App\Filter\Utils\DistanceFilter;
 use App\Repository\Public\PublicEventRepository;
 use Doctrine\ORM\Mapping as ORM;
 
@@ -35,6 +36,7 @@ use Doctrine\ORM\Mapping as ORM;
 #[ApiFilter(SearchFilter::class, properties: ['name' => 'partial', 'city' => 'exact'])]
 #[ApiFilter(NumericFilter::class, properties: ['organizationId'])]
 #[ApiFilter(DateFilter::class, properties: ['datetimeStart', 'datetimeEnd'])]
+#[ApiFilter(DistanceFilter::class)]
 class PublicEvent
 {
     #[ORM\Id]

+ 91 - 0
src/Filter/Utils/DistanceFilter.php

@@ -0,0 +1,91 @@
+<?php
+declare(strict_types=1);
+
+namespace App\Filter\Utils;
+
+use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface;
+use Doctrine\ORM\QueryBuilder;
+use Dunglas\ApiBundle\Api\ResourceInterface;
+use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\AbstractFilter;
+use JetBrains\PhpStorm\ArrayShape;
+
+/**
+ * Add a distance filter to en entity
+ *
+ * To use it, add the following query :
+ *
+ *     withinDistance=({latitude}, {longitude}, {distance})
+ *
+ * Where latitude and longitude are the coordinates of the origine point, and distance the maximum
+ * distance in Km.
+ *
+ * /!\ The subject entity shall have a longitude and a latitude properties
+ */
+final class DistanceFilter extends AbstractFilter
+{
+    /**
+     * API docs
+     * @param string|ResourceInterface $resource
+     * @return array[]
+     */
+    #[ArrayShape(['search' => "array"])]
+    public function getDescription(string|ResourceInterface $resource): array
+    {
+        if (!property_exists($resource, 'latitude') || !property_exists($resource, 'longitude')) {
+            throw new \RuntimeException('DistanceFilter can only used with resources having both latitude and longitude properties');
+        }
+        return [
+            'search' => [
+                'property' => 'withinDistance',
+                'type' => 'string',
+                'required' => false,
+                'swagger' => [
+                    'description' => "Filtre une entity selon sa distance (km) à un point (latitude, longitude). " .
+                                     "L'entité doit-elle aussi posséder des propriétés 'latitude' et 'longitude'." .
+                                     "Pass the following query to use it : `withinDistance=({latitude}, {longitude}, {distance})`, " .
+                                     "where {latitude} and {longitude} are the coordinates of the origine point, and {distance} the maximum distance in Km.",
+                    'name' => 'Distance Filter',
+                    'type' => 'Utils Filter',
+                ],
+            ]
+        ];
+    }
+
+    protected function filterProperty(
+        string $property,
+        $value,
+        QueryBuilder $queryBuilder,
+        QueryNameGeneratorInterface
+        $queryNameGenerator, string
+        $resourceClass,
+        string $operationName = null
+    ): void
+    {
+        if ($property !== 'withinDistance') {
+            return;
+        }
+        if (!preg_match('/^(\d+(\.\d+)?,){2}\d+(\.\d+)?$/', $value)) {
+            throw new \RuntimeException('DistanceFilter : Invalid argument, please pass latitude, longitude and distance to the parameter as comma separated floating numbers.');
+        }
+
+        [$latitude, $longitude, $distance] = explode(',', $value);
+
+        $alias = $queryBuilder->getRootAliases()[0];
+
+        // Generate unique parameters names to avoid collisions with other filters
+        $latitudeParameterName = $queryNameGenerator->generateParameterName('latitude');
+        $longitudeParameterName = $queryNameGenerator->generateParameterName('longitude');
+        $distanceParameterName = $queryNameGenerator->generateParameterName('distance');
+
+        $queryBuilder
+            ->andWhere(
+                sprintf(
+                    'SPHERICAL_DISTANCE(%1$s.latitude, %1$s.longitude, :%2$s, :%3$s) <= :%4$s',
+                    $alias, $latitudeParameterName, $longitudeParameterName, $distanceParameterName
+                )
+            )
+            ->setParameter($latitudeParameterName, $latitude)
+            ->setParameter($longitudeParameterName, $longitude)
+            ->setParameter($distanceParameterName, $distance);
+    }
+}