Reputation: 10940
In a CakePHP (v3) application, how can I retrieve the closest results based on passed lat lng values?
I'd like to have them back as native CakePHP entities, so something like this:
public function closest($lat, $lng) {
$sightings = //records within given lat lng
$this->set(compact('sightings'));
$this->set('_serialize', ['sightings']);
}
I know this SQL works:
SELECT *,
( 3959 * acos( cos( radians(50.7) ) * cos( radians( latitude ) ) * cos( radians( longitude ) - radians(-1.8) ) + sin( radians(50.7) ) * sin( radians( latitude ) ) ) ) AS distance
FROM sightings
HAVING distance < 10
ORDER BY distance
LIMIT 0 , 20
Struggling to combine the two.
UPDATE
I've added
class Sighting extends Entity {
public $_virtual = ['distance'];
//rest of class...
}
So now the distance
is showing up in my json output (with the value of null as would be expected right now, but I feel it's a step at lease).
I've taken a look here: http://www.mrthun.com/2014/11/26/search-distance-cakephp/ which seems to be what I'm trying to achieve so assumed something like this:
$latitude = 51.145;
$longitude = -1.45;
$distance = 100;
$this->Sightings->virtualFields
= array('Sightings.distance'
=> '(3959 * acos (cos ( radians('.$latitude.') )
* cos( radians( Sightings.latitude ) )
* cos( radians( Sightings.longitude )
- radians('.$longitude.') )
+ sin ( radians('.$latitude.') )
* sin( radians( Sightings.latitude ) )))');
$sightings = $this->Sightings->find('all', [
'conditions' => ['Sightings.distance <' => $distance]
]);
$this->set(compact('sightings'));
$this->set('_serialize', ['sightings']);
Results in: Column not found: 1054 Unknown column 'Sightings.distance' in 'where clause'
Not sure if it's possible CakePHP v2 as opposed to v3?
Upvotes: 12
Views: 2404
Reputation: 2605
finally i got the solution for this. below is the working example
$distance = 25;
$start_latitude = 12.920479;
$start_longitude = 77.670547;
$contents = $this->Sightings->find()->select(['latitude', 'longitude', 'distance' => 'SQRT(
POW(69.1 * (latitude - '.$start_latitude.'), 2) +
POW(69.1 * ('.$start_longitude.' - longitude) * COS(latitude / 57.3), 2))'])->having(["distance < " => $distance]);
output
{"lat": xx.920479,
"lng": xx.670547,
"distance": "0"
}
Upvotes: 0
Reputation: 10940
People might want to look at the CakePHP GeoDistance Plugin:
CakePHP-GeoDistance is a CakePHP 3 behavior for querying geocoded data based on cartographic distance using the spherical cosine law. It is great for 'find my nearest X' or 'find Y near me' type queries.
https://github.com/chris48s/cakephp-geodistance
Did the job.
Upvotes: -1
Reputation: 10044
There are several approaches to achieve your result.
Instead of struggling with the ORM, you could perform a raw query?
$conn = ConnectionManager::get('my_connection');
And then you can run a custom query like:
$results = $conn->execute('MY RAW SQL HERE');
If you want to run a query and eager load the relations, you will need to use the contains method like:
$query = $articles->find('all', ['contain' => ['Authors', 'Comments']]);
If you want to use virtual fields like in your example above, you need to add it in your entity model class like:
protected function _getDistance()
{
//access model members using $this->_properties e.g.
//return $this->_properties['latitude'] . ' ' .
// $this->_properties['longitude'];
}
This should ensure your distance is no longer null in your json.
And you could use with the distance method from CakephpGeo library to construct a query
public function distance(array $pointX, array $pointY) {
$res = $this->calculateDistance($pointX, $pointY);
return ceil($res);
}
public static function calculateDistance($pointX, $pointY) {
$res = 69.09 * rad2deg(acos(sin(deg2rad($pointX['lat'])) * sin(deg2rad($pointY['lat']))
+ cos(deg2rad($pointX['lat'])) * cos(deg2rad($pointY['lat'])) * cos(deg2rad($pointX['lng']
- $pointY['lng']))));
return $res;
}
Lastly, and not recommended, you could retrieve all the info you need and use PHP to narrow down your results. Painful, slow and not recommended.
Upvotes: 0
Reputation: 9398
there are no more virtualFields in cake 3 but you still can create an alias for your calculated field
As suggested by @ndm you'd better bind $latitude
and $longitude
to prevent SQL injections
$distanceField = '(3959 * acos (cos ( radians(:latitude) )
* cos( radians( Sightings.latitude ) )
* cos( radians( Sightings.longitude )
- radians(:longitude) )
+ sin ( radians(:latitude) )
* sin( radians( Sightings.latitude ) )))';
using where
$sightings = $this->Sightings->find()
->select([
'distance' => $distanceField
])
->where(["$distanceField < " => $distance])
->bind(':latitude', $latitude, 'float')
->bind(':longitude', $longitude, 'float')
->contain(['Photos', 'Tags']);
using having
$sightings = $this->Sightings->find()
->select([
'distance' => $distanceField
])
->having(['distance < ' => $distance])
->bind(':latitude', $latitude, 'float')
->bind(':longitude', $longitude, 'float')
->contain(['Photos', 'Tags']);
Upvotes: 10
Reputation: 8606
You need to use ConnectionManager here.
For example:
use Cake\Datasource\ConnectionManager; // Mention this first, preferably on top of the page.
$connection = ConnectionManager::get('default');
$results = $connection
->execute('SELECT * FROM articles WHERE id = :id', ['id' => 1])
->fetchAll('assoc');
You could try and set up your query in this fashion. It will definitely work.
Use this as a reference:
SELECT Statements with CakePHP 3
Upvotes: 0