If you have a MySQL table with locations (latitude
and longitude
columns) you can get a list of locations near a specific point using the following query:
SELECT *, ((ACOS(SIN(<latitude> * PI() / 180) * SIN(latitude * PI() / 180) + COS(<latitude> * PI() / 180) * COS(latitude * PI() / 180) * COS((<longitude> - longitude) * PI() / 180)) * 180 / PI()) * 60 * 1.1515 * 1.609344) as distance FROM locations HAVING distance <= <distance> ORDER BY distance ASC
In this query we have noted the latitude of the reference point with <latitude>
, its longitude with <longitude>
and the maximum distance to search for with <distance>
(in kilometers).
To make this calculate the distance in miles, just remove the * 1.609344 from the end of the calculation:
SELECT *, ((ACOS(SIN(<latitude> * PI() / 180) * SIN(latitude * PI() / 180) + COS(<latitude> * PI() / 180) * COS(latitude * PI() / 180) * COS((<longitude> - longitude) * PI() / 180)) * 180 / PI()) * 60 * 1.1515) as distance FROM locations HAVING distance <= <distance> ORDER BY distance ASC
You can test the results using Google Maps. To measure distances in Google Maps you need to right-click and choose “Measure distance” from the popup menu.
How to Get Nearby Locations in Symfony with Doctrine
If you’re like me and use Symfony a lot, here’s how you can add a repository method for your entity to return objects ordered by distance from a specific location:
<?php namespace AppBundle\Entity; class LocationRepository extends \Doctrine\ORM\EntityRepository { public function findClosest($lat, $lng) { $qb = $this->createQueryBuilder('l'); $qb->addSelect('((ACOS(SIN(:lat * PI() / 180) * SIN(l.latitude * PI() / 180) + COS(:lat * PI() / 180) * COS(l.latitude * PI() / 180) * COS((:lng - l.longitude) * PI() / 180)) * 180 / PI()) * 60 * 1.1515) as HIDDEN distance'); $qb->orderBy('distance'); $qb->setParameter('lat', $lat); $qb->setParameter('lng', $lng); return $qb->getQuery()->getResult(); } }
To make this work we need to add the ACOS
, SIN
, COS
and PI
functions to Doctrine’s DQL as it does not have them by default.
First create 4 new classes in a new AppBundle/DQL
folder, one for each function we need to define.
AppBundle/DQL/Acos.php
<?php namespace ApiBundle\DQL; use Doctrine\ORM\Query\AST\Functions\FunctionNode, Doctrine\ORM\Query\Lexer; class Acos extends FunctionNode { public $arithmeticExpression; public function getSql(\Doctrine\ORM\Query\SqlWalker $sqlWalker) { return 'ACOS(' . $sqlWalker->walkSimpleArithmeticExpression( $this->arithmeticExpression ) . ')'; } public function parse(\Doctrine\ORM\Query\Parser $parser) { $parser->match(Lexer::T_IDENTIFIER); $parser->match(Lexer::T_OPEN_PARENTHESIS); $this->arithmeticExpression = $parser->SimpleArithmeticExpression(); $parser->match(Lexer::T_CLOSE_PARENTHESIS); } }
AppBundle/DQL/Sin.php
<?php namespace ApiBundle\DQL; use Doctrine\ORM\Query\AST\Functions\FunctionNode, Doctrine\ORM\Query\Lexer; class Sin extends FunctionNode { public $arithmeticExpression; public function getSql(\Doctrine\ORM\Query\SqlWalker $sqlWalker) { return 'SIN(' . $sqlWalker->walkSimpleArithmeticExpression( $this->arithmeticExpression ) . ')'; } public function parse(\Doctrine\ORM\Query\Parser $parser) { $parser->match(Lexer::T_IDENTIFIER); $parser->match(Lexer::T_OPEN_PARENTHESIS); $this->arithmeticExpression = $parser->SimpleArithmeticExpression(); $parser->match(Lexer::T_CLOSE_PARENTHESIS); } }
AppBundle/DQL/Cos.php
<?php namespace ApiBundle\DQL; use Doctrine\ORM\Query\AST\Functions\FunctionNode, Doctrine\ORM\Query\Lexer; class Cos extends FunctionNode { public $arithmeticExpression; public function getSql(\Doctrine\ORM\Query\SqlWalker $sqlWalker) { return 'COS(' . $sqlWalker->walkSimpleArithmeticExpression( $this->arithmeticExpression ) . ')'; } public function parse(\Doctrine\ORM\Query\Parser $parser) { $parser->match(Lexer::T_IDENTIFIER); $parser->match(Lexer::T_OPEN_PARENTHESIS); $this->arithmeticExpression = $parser->SimpleArithmeticExpression(); $parser->match(Lexer::T_CLOSE_PARENTHESIS); } }
AppBundle/DQL/Pi.php
<?php namespace ApiBundle\DQL; use Doctrine\ORM\Query\AST\Functions\FunctionNode, Doctrine\ORM\Query\Lexer; class Pi extends FunctionNode { public $arithmeticExpression; public function getSql(\Doctrine\ORM\Query\SqlWalker $sqlWalker) { return 'PI()'; } public function parse(\Doctrine\ORM\Query\Parser $parser) { $parser->match(Lexer::T_IDENTIFIER); $parser->match(Lexer::T_OPEN_PARENTHESIS); $parser->match(Lexer::T_CLOSE_PARENTHESIS); } }
The next and the last step is to add the above definitions to your app’s config.yml
file:
doctrine: dbal: # ... orm: auto_generate_proxy_classes: '%kernel.debug%' naming_strategy: doctrine.orm.naming_strategy.underscore auto_mapping: true dql: numeric_functions: acos: ApiBundle\DQL\Acos sin: ApiBundle\DQL\Sin cos: ApiBundle\DQL\Cos pi: ApiBundle\DQL\Pi
That’s it. Now it will all work as expected. If you need to add more specific functions to DQL, just look at the above classes and make your own.