I'm attempting to order events by their distance from a user submitted postcode and distance.
I've attached a sample of my database tables and their relationships, as you can see geom is associated with multiple addresses via postcode and addresses can be associated to multiple tables (in this instance the events table).
I'm taking a postcode from the end user as well as a radius in miles to retrieve appropriate events, here is a sample of how I am achieving this in Eloquent.
/**
* Extend locale method which initially only gets lat/long for given postcode to search
*
* #param \Illuminate\Database\Eloquent\Builder $query The query builder
* #param \App\Http\Requests\SearchRequest $request The search request
* #return void
*/
protected function locale(Builder $query, SearchRequest $request)
{
$postcode = $this->formatPostcode($request->postcode);
$geom = Geom::query()->where('postcode', $postcode)->first();
if (! $geom || Cache::has('postcodeAPIFailed')) {
return;
}
$lat = $geom->geo_location['lat'];
$long = $geom->geo_location['long'];
// Top-left point of bounding box
$lat1 = $lat - ($request->within / 69);
$long1 = $long - $request->within / abs(cos(deg2rad($lat)) * 69);
// Bottom-right point of bounding box
$lat2 = $lat + ($request->within / 69);
$long2 = $long + $request->within / abs(cos(deg2rad($lat)) * 69);
$query->whereHas('address', function (Builder $query) use ($request, $lat, $long, $lat1, $long1, $lat2, $long2) {
$query->whereHas('geom', function (Builder $query) use ($request, $lat, $long, $lat1, $long1, $lat2, $long2) {
$query->whereRaw('st_within(geo_location, envelope(linestring(point(?, ?), point(?, ?))))', [$long1, $lat1, $long2, $lat2]);
});
});
}
In the controller after we have retrieved the search results, we calculate the distances for each of the results.
if ($request->has('postcode')) {
$postcodeDistances = $this->getDistances($results, $request);
}
This produces an array with a key of postcode and value of distance, i.e $postcodeDistances['L1 0AA'] = '3';, we send this array to the view.
In the view we then use the following logic to display distance on a record where applicable
#if($postcodeDistances)
<span>
{{ $postcodeDistances[$result->address->postcode] }}
mile{{ $postcodeDistances[$result->address->postcode] != 1 ? 's' : '' }} away
</span>
#endif
I've tried a few methods but I've been unable to update my function locale() to do the ordering by distance. I've considered maybe I can attached the distance to the collection and use a Laravel method to order the collections that way but achieving this from the database layer would be ideal if the latter is even possible.
My first attempt was to addSelect a distance field after whereHas('geom') and order by the new field
$query->addSelect(\DB::raw("ST_DISTANCE_SPHERE(geo_location, POINT({$long}, {$lat})) AS distance"));
I receive the following error:
SQLSTATE[21000]: Cardinality violation: 1241 Operand should contain 2 column(s) (SQL: select count(*) as aggregate from `event` where (select count(*) from `address` where `address`.`addressable_id` = `event`.`id` and `address`.`addressable_type` = event and (select count(*), ST_DISTANCE_SPHERE(geo_location, POINT(-2.717472, 53.427078)) AS distance from `geom` where `geom`.`postcode` = `address`.`postcode` and st_within(geo_location, envelope(linestring(point(-3.6903924055016, 52.847367855072), point(-1.7445515944984, 54.006788144928))))) >= 1) >= 1 and (select count(*) from `organisation` where `event`.`organisation_id` = `organisation`.`id` and `status` = 1) >= 1 and `event_template_id` is not null and `date_start` >= 2018-07-31 00:00:00 and `status` in (1, 5))
I also attempted to use orderByRaw in the same place instead, whilst I did not receive an error the results were not ordered accordingly.
$query->orderByRaw('ST_DISTANCE_SPHERE(geo_location, POINT(?, ?)) ASC', [$long, $lat]);
I'll have a go for solving your issue. But. As I mentioned in a comment, you will have to split up geo_location to lat and lng.
When that is done, the formula is as follows. This will calculate distance in km.
$distance = 50; //max distance in km
$limit = 100; //the amount of selected records
$earthRadiusKm = 6371;
$earthRadiusMiles = 3959;
$postcode = $this->formatPostcode($request->postcode);
$geom = Geom::query()->where('postcode', $postcode)->first();
$lat = $geom->lat;
$lng = $geom->lng;
//assuming your Geom model db name is geoms and the 'id' is id
$postcodeDistances = \DB::table('geoms')->selectRaw("
geoms.id, ( $earthRadiusKm * acos( cos( radians($lat) ) * cos( radians( geoms.lat ) )
* cos( radians( geoms.lng ) - radians($lng) ) + sin( radians($lat) ) *
sin(radians(geoms.lat)) ) ) AS distance, lat, lng
")->havingRaw("distance < $distance")->orderBy('distance')->limit($limit)->get();
For your interest, if you want the results in imperial miles, I have added the earth radius in both metrics. As the formula does make use of the earth's radius, longer distances will be more accurate.
The result (json) should look like this (2 records result, the first coord is always the starting point). Fyi, the coords are in the Philippines.
all: [
{#3627
+"id": 1128,
+"distance": 0.0,
+"lat": "15.6672998",
+"lng": "120.7349950",
},
{#3595
+"id": 1535,
+"distance": 9.564007130831,
+"lat": "15.6732128",
+"lng": "120.6458749",
},
]
Related
I have a location column of type jsonb in my issue_points table:
{
"geo": {
"lat": 57.994434,
"lng": 78.35716
},
"fias": {
"city": "some city fias code",
"region": "some region fias code"
},
"address": "some address"
}
Using the Laravel DB facade, I want to find pickup points relative to the received coordinates using the Haversine formula:
select * from "issue_points" where acos(
sin(radians(?))
* sin(radians(location->'geo'->'lat'))
+ cos(radians(?))
* cos(radians(location->'geo'->'lat'))
* cos(radians(?)- radians(location->'geo'->'lng'))
) * 6371 <= 20 order by "updated_at" desc
The problem is that if I want to get lat or lng using location->'geo'->'lat', the following postgresql exception is thrown:
"SQLSTATE[42883]: Undefined function: 7 ERROR: function
radians(jsonb) does not exist\nLINE 3: *
sin(radians(location->'geo'->'lat'))\n
^\nHINT: No function matches the given name and argument types. You
might need to add explicit type casts. (SQL: select * from
"issue_points" where acos(\n sin(radians(55.03441))\n
sin(radians(location->'geo'->'lat'))\n + cos(radians(55.03441))\n *
cos(radians(location->'geo'->'lat'))\n *
cos(radians(73.46783)- radians(location->'geo'->'lng'))\n ) *
6371 <= 20 order by "updated_at" desc)",
p.s If you use atomic fields with scalar data types: lat (float) and lng (float), and try to use the following code, then issue items will return correctly:
select * from "issue_points" where acos(
sin(radians(?))
* sin(radians(lat))
+ cos(radians(?))
* cos(radians(lat))
* cos(radians(?)- radians(lng))
) * 6371 <= 20 order by "updated_at" desc
Every operator and function has a type. In the case of the -> you are using it is jsonb => jsonb - it can't be anything else really, your result could be a scalar value, an object, an array, an array of objects contiaining...
So - if you are confident that the values you are extracting will always have the type you expect then you can do: cos(radians( (location->'geo'->'lat')::double precision )).
However, if you do know that your json is always structured like this then there's no reason to make it json is there?
I am looking for a solution to filte jobs basing on the salary.
I have a Job model which has 2 column min_salary and max_salary, and the user is going to use a slider to pick two values as min and max salary.
I have tried the whereBetween and orWhereBetween but the problem is the query gets closed and I can't proceed with other filters, so am trying to get the average between the model min/max salary and use it instead of the two slaries.
Here's what i tried:
$salary = explode(',',$request->salary); // geting the salary range from the request as array
$jobs = Job::whereBetween('min_salary',$salary)->orWhereBetween('max_salary',$salary);
This solution does not work as I want, as I can't proceed with other filters.
I also tried to do a custom orWhere function and do seperated whereBetween queries, but I got the same result.
What I am trying now is to get the average between the model max and min salary without creating another field*, and then proceed to do something like that :
$jobs = Job::whereBetween('theCalculatedAvg',$salary);
I appreciate any help with any kind of solution that does not require creating another field in the database, and i wont mind an sql raw solution if it does the job.
Thank you.
Update
Heres the whole function u had :
if($request->has('offset')) {
$salary = explode(',',$request->salary);
$jobs = Job::whereBetween('min_salary',$salary)->orWhereBetween('max_salary',$salary);
if ($request->has('lat')) {
$sqlDistance = DB::raw
('
( 6371 * acos
( cos
( radians
(' . $request->lat . ')
)
* cos
( radians
( lat )
)
* cos
( radians
( lon )
- radians
(' . $request->lon . ')
)
+ sin
( radians
(' . $request->lat . ')
)
* sin
( radians
( lat )
)
)
)
');
$jobs->when($sqlDistance != null, function ($query) use ($sqlDistance,$request){
$query->whereHas('address', function ($subQuery) use ($sqlDistance,$request) {
$subQuery->addSelect(DB::raw("{$sqlDistance} AS distance"));
$subQuery->havingRaw("distance <= ?", [(int)$request->range]);
});
})
->with('company')
->with('address');
}
if ($request->has('key')) {
$jobs->where('title', 'like', '%' . $request->key . '%');
}
if ($request->has('cat')) {
$cat = explode(',',$request->cat);
$jobs->whereIn('category_id', $cat);
}
if ($request->has('type')) {
$type = explode(',',$request->type);
$jobs->whereIn('type', $type);
}
if($request->has('hs')) {
return view("General::browseJobs", [
'jobs' => $jobs->orderBy('created_at','desc')->skip($request->offset * 2)->take(2)->get(),
'count' => count($jobs->get())
]);
}
$view = view('General::loaders.jobs', [
'jobs' => $jobs->orderBy('created_at','desc')->skip($request->offset * 2)->take(2)->get()
])->render();
return response()->json(['html' => $view , 'count' => count($jobs->get()) ]);
}
return view("General::browseJobs", [
'jobs' => Job::orderBy('created_at','desc')->take(2)->get(),
'count' => count(Job::all())
]);
The issue is that i just want the salary filter to work this way:
lets say the user picked twho values : $min and $max (will be stored in an array)
now i want to show him the jobs where the min_salary is between [$min, $max]
or the max_salary is between [$min, $max].
NB : if i use :
Job::whereBetween('min_salary',$salary)->whereBetween('max_salary',$salary);
without the or it works just fine, but i want the or logic to be implemented.
You need to scope the or to those 2 conditions only:
Jobs::where(function ($q) use ($salary) {
$q->whereBetween('min_salary',$salary)->orWhereBetween('max_salary',$salary);
})
To understand why this is required, you need to consider the sql query produced by eloquent.
If you write:
$jobs = Jobs::where([condition1])->orWhere([condition2]);
.. some other code..
$jobs->where([condition3]);
...
$jobs->where([condition4]);
The resulting query will be:
SELECT *
FROM jobs
WHERE [condition1] OR [condition2] AND [condition3] AND [condition4]
But in SQL, the AND operator has precedence over the OR, so the conditions are logically considered like this:
([condition1]) OR ([condition2] AND [condition3] AND [condition4])
This is not the behavior you want.
By scoping the OR condition in a where closure, you basically tell eloquent to add a parenthesis around the conditions, so:
$jobs = Jobs::where(function ($q) {
$q->where([condition1])->orWhere([condition2]);
});
.. some other code..
$jobs->where([condition3]);
...
$jobs->where([condition4]);
Results in the following query:
SELECT *
FROM jobs
WHERE ([condition1] OR [condition2]) AND [condition3] AND [condition4]
which is the desidered one
Ok, thank you for updating the question. I'm pretty sure I'm not able to answer it in one go, but I'll do my best.
As far as I can see, you are working on a Job site where you want to allow users to enter filters, and only show jobs matching those filters.
I see you have filters for min/max salary, distance, title, categories and types.
You also have a hs key which decides which view to open with which data.
I would consider the min/max salary to be an OR statament, and the rest an AND.
In that case I would do
$mainQuery = Job::where(function($query) use($salary)
{
$query->whereBetween('min_salary', $salary)->orWhereBetween('max_salary', $salary);
});
So you can do $mainQuery->where('anothermust-have', $somevalue);
I'm having the following function to calculate distances between two points using Haversine formula:
public static function getByDistance($distance)
{
$pubsFiltered = DB::select(
'SELECT * FROM
(SELECT *, (6371 * acos(cos(radians(40.4169473)) * cos(radians(latitude)) *
cos(radians(longitude) - radians(-3.7035285)) +
sin(radians(40.4169473)) * sin(radians(latitude))))
AS distance
FROM pubs) AS distances
WHERE distance < ' . $distance . '
ORDER BY distance
;
');
return $pubsFiltered;
}
This is returning an array instead of a collection, that is what I need.
If I change DB to Pub it returns an error because of the subquery where I calculate "distance" using my table "pubs".
How can I change the whole function or simply the subquery to have an instance of my Pub model?...Do I have to use set/declare variables in mysql?
Thanks a lot!!
$pubs = Pubs::hydrate($pubsFiltered)
For now, I am using this query to obtain nearby records:
public function scopeNearby($query, $lat, $long, $distance = 10)
{
if($lat AND $long) {
// 3959 for miles, 6371 for kilometers
$distanceQuery = '(6371 * acos(cos(radians(?)) * cos(radians(geo_lat)) * ' .
'cos(radians(geo_long) - radians(?)) + sin(radians(?)) ' .
'* sin(radians(geo_lat))))';
return $query->select('*', DB::raw("{$distanceQuery} as distance"))
->addBinding([$lat, $long, $lat], "select")
->whereNotNull('geo_lat')
->whereNotNull('geo_long')
->whereRaw("{$distanceQuery} < ?")
->addBinding([$lat, $long, $lat, $distance], "where")
->orderBy('distance', "ASC");
}
}
You may notice that I have used the $distanceQuery for both SELECT and WHERE clauses. I am currently having issues when this scope is used alongside with the paginate() method. This might need some refactoring, but for now you can do the same.
I've finally worked out how to put together a complex query to get related models.
This is what my query currently looks like...
$campaign = Campaign::find($campaign_id);
$buyers = $campaign->buyers()->with('notes')->with(['emails' => function($q){
$q->where('campaign_id', '13');
}])->get();
The complex part is I'm trying to get entries from emails that have both a matching buyer_id & campaign_id. This query achieves exactly what I'm after in a pretty efficient way...
BUT... I can't work out how to pass in parameters to the with closure. At the moment I've hard coded the id 13 into the where query in the closure but I want it to be equal to $campaign_id passed in to the original function.
How do I do this?
Worked it out if anyone has same problem... need to use use statement
$campaign = Campaign::find($campaign_id);
$buyers = $campaign->buyers()->with('notes')->with(['emails' => function($q) use ($campaign_id){
$q->where('campaign_id', $campaign_id);
}])->get();
Is this documented anywhere?
TRY With This
$mainQuery=StockTransactionLog::with(['supplier','customer','warehouse','stockInDetails'=>function($query) use ($productId){
$query->with(['product'])->where('product_stock_in_details.product_id',$productId);
},'stockOutDetails'=>function($query) use ($productId){
$query->with(['product'])->where('product_stock_out_details.product_id',$productId);
},'stockDamage'=>function($query) use ($productId){
$query->with(['product'])->where('product_damage_details.product_id',$productId);
},'stockReturn'=>function($query) use ($productId){
$query->select('id','return_id','product_id');
$query->with(['product'])->where('product_return_details.product_id',$productId);
}]);
$latitude = $request->input('latitude', '44.4562319000');
$longitude = $request->input('longitude', '26.1003480000');
$radius = 1000000;
$locations = Locations::selectRaw("id, name, address, latitude, longitude, image_path, rating, city_id, created_at, active,
( 6371 * acos( cos( radians(?) ) *
cos( radians( latitude ) )
* cos( radians( longitude ) - radians(?)
) + sin( radians(?) ) *
sin( radians( latitude ) ) )
) AS distance", [$latitude, $longitude, $latitude])
->where('active', '1')
->having("distance", "<", $radius)
->orderBy("distance")
->get();
$fpr = FPR::with([
'detail','of_permintaan.detail',
'of_permintaan.detail.pembelian'=>function($q)use($id){
return $q->where('pembelian_id',$id);}
])->findOrFail($id);
Well, in case u need to pass it on its child-child relation, but i don't recomend it
This is FULLTEXT query, searching food from restaurants-menu
$query = "SELECT *, MATCH(food_row) AGAINST('cheese' IN BOOLEAN MODE) AS yyy
FROM table_menu
WHERE MATCH(food_row) AGAINST('cheese' IN BOOLEAN MODE)
AND restaurant IN (SELECT id FROM table_restaurant
WHERE country IN ('FR') AND town='paris')";
I want to modify. Considering Max distance = 5km. this is working.
latitude and longitude are of user (google geocode), lat and lng of restaurants in mysql (they are in table_restaurant)
$xxx = " (((acos(sin((".$latitude."*pi()/180)) *
sin((`lat`*pi()/180))+cos((".$latitude."*pi()/180)) *
cos((`lat`*pi()/180)) * cos(((".$longitude."- `lng`)
*pi()/180))))*180/pi())*60*1.1515*1.609344) ";
$query .= " AND ($xxx <= 5 )"; // 5 = the max distance
yes working, but I can't write each restaurant distance in the output.
I don't know how insert 'AS distance' If not FULLTEXT, would be easy.
$query = "SELECT *, $xxx as distance FROM table2 WHERE ... ORDER BY distance ";
output: distance = row['distance']
how can I do? Sorry for my english, tanks
I assume the reason you're modifying the query is because the distance test is optional.
Try it like this:
if ($max_dist) {
$dist = " (((acos(sin((".$latitude."*pi()/180)) *
sin((`lat`*pi()/180))+cos((".$latitude."*pi()/180)) *
cos((`lat`*pi()/180)) * cos(((".$longitude."- `lng`)
*pi()/180))))*180/pi())*60*1.1515*1.609344) ";
$having = "HAVING distance < $max_dist";
} else {
$dist = " NULL ";
$having = '';
}
$query = "SELECT m.*, MATCH(food_row) AGAINST('cheese' IN BOOLEAN MODE) AS yyy,
$dist AS distance
FROM table_menu m
JOIN table_restaurant r ON m.restaurant = r.id
WHERE MATCH(food_row) AGAINST('cheese' IN BOOLEAN MODE)
AND country IN ('FR') AND town='paris'
$having";