1 <?php
declare(strict_types
=1);
4 * This file is part of the Rapsys AirBundle package.
6 * (c) Raphaël Gertz <symfony@rapsys.eu>
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
12 namespace Rapsys\AirBundle\Repository
;
14 use Doctrine\ORM\Query\ResultSetMapping
;
16 use Symfony\Component\Routing\Generator\UrlGeneratorInterface
;
17 use Symfony\Component\Routing\RouterInterface
;
19 use Rapsys\AirBundle\Repository
;
24 * @TODO: use new window function syntax https://mariadb.com/kb/en/window-functions-overview/ MAX(updated) OVER (PARTITION updated) AS modified ???
26 class LocationRepository
extends Repository
{
32 public function findAll(): array {
33 //Get all locations index by id
34 return $this->createQueryBuilder('location', 'location.id')->getQuery()->getResult();
38 * Find locations as array
40 * @param DatePeriod $period The period
41 * @return array The locations array
43 public function findAllAsArray(\DatePeriod
$period): array {
45 //TODO: ajouter pays ???
54 FROM Rapsys\AirBundle\Entity\Location AS l
55 LEFT JOIN Rapsys\AirBundle\Entity\Session AS s ON (l.id = s.location_id)
57 ORDER BY COUNT(IF(s.date BETWEEN :begin AND :end, s.id, NULL)) DESC, COUNT(s.id) DESC, l.id
60 //Replace bundle entity name by table name
61 $req = str_replace($this->tableKeys
, $this->tableValues
, $req);
63 //Get result set mapping instance
64 //XXX: DEBUG: see ../blog.orig/src/Rapsys/BlogBundle/Repository/ArticleRepository.php
65 $rsm = new ResultSetMapping();
68 //XXX: see vendor/doctrine/dbal/lib/Doctrine/DBAL/Types/Types.php
69 //addScalarResult($sqlColName, $resColName, $type = 'string');
70 $rsm->addScalarResult('id', 'id', 'integer')
71 ->addScalarResult('title', 'title', 'string')
72 ->addScalarResult('latitude', 'latitude', 'float')
73 ->addScalarResult('longitude', 'longitude', 'float')
74 ->addScalarResult('indoor', 'indoor', 'boolean')
75 ->addScalarResult('count', 'count', 'integer')
76 ->addScalarResult('updated', 'updated', 'datetime');
80 ->createNativeQuery($req, $rsm)
81 ->setParameter('begin', $period->getStartDate())
82 ->setParameter('end', $period->getEndDate())
88 //Iterate on each city
89 foreach($result as $data) {
93 'title' => $title = $this->translator
->trans($data['title']),
94 'latitude' => $data['latitude'],
95 'longitude' => $data['longitude'],
96 'updated' => $data['updated'],
98 'slug' => $location = $this->slugger
->slug($title),
99 'link' => $this->router
->generate('rapsysair_location_view', ['id' => $data['id'], 'location' => $this->slugger
->slug($location)])
108 * Find cities as array
110 * @param DatePeriod $period The period
111 * @param int $count The session count
112 * @return array The cities array
114 public function findCitiesAsArray(\DatePeriod
$period, int $count = 1): array {
118 SUBSTRING(a.zipcode, 1, 2) AS id,
120 ROUND(AVG(a.latitude), 6) AS latitude,
121 ROUND(AVG(a.longitude), 6) AS longitude,
122 GROUP_CONCAT(a.id ORDER BY a.pcount DESC, a.count DESC, a.id SEPARATOR "\\n") AS ids,
123 GROUP_CONCAT(a.title ORDER BY a.pcount DESC, a.count DESC, a.id SEPARATOR "\\n") AS titles,
124 GROUP_CONCAT(a.latitude ORDER BY a.pcount DESC, a.count DESC, a.id SEPARATOR "\\n") AS latitudes,
125 GROUP_CONCAT(a.longitude ORDER BY a.pcount DESC, a.count DESC, a.id SEPARATOR "\\n") AS longitudes,
126 GROUP_CONCAT(a.indoor ORDER BY a.pcount DESC, a.count DESC, a.id SEPARATOR "\\n") AS indoors,
127 GROUP_CONCAT(a.count ORDER BY a.pcount DESC, a.count DESC, a.id SEPARATOR "\\n") AS counts,
128 MAX(a.modified) AS modified
137 GREATEST(l.created, l.updated, COALESCE(s.created, 0), COALESCE(s.updated, 0)) AS modified,
139 COUNT(s.id) AS count,
140 COUNT(IF(s.date BETWEEN :begin AND :end, s.id, NULL)) AS pcount
141 FROM Rapsys\AirBundle\Entity\Location AS l
142 LEFT JOIN Rapsys\AirBundle\Entity\Session AS s ON (l.id = s.location_id)
147 GROUP BY a.city, SUBSTRING(a.zipcode, 1, 2)
148 ORDER BY a.city, a.zipcode
151 //Replace bundle entity name by table name
152 $req = str_replace($this->tableKeys
, $this->tableValues
, $req);
154 //Get result set mapping instance
155 //XXX: DEBUG: see ../blog.orig/src/Rapsys/BlogBundle/Repository/ArticleRepository.php
156 $rsm = new ResultSetMapping();
159 //XXX: see vendor/doctrine/dbal/lib/Doctrine/DBAL/Types/Types.php
160 //addScalarResult($sqlColName, $resColName, $type = 'string');
161 $rsm->addScalarResult('id', 'id', 'integer')
162 ->addScalarResult('city', 'city', 'string')
163 ->addScalarResult('latitude', 'latitude', 'float')
164 ->addScalarResult('longitude', 'longitude', 'float')
165 //XXX: is a string because of \n separator
166 ->addScalarResult('ids', 'ids', 'string')
167 //XXX: is a string because of \n separator
168 ->addScalarResult('titles', 'titles', 'string')
169 //XXX: is a string because of \n separator
170 ->addScalarResult('latitudes', 'latitudes', 'string')
171 //XXX: is a string because of \n separator
172 ->addScalarResult('longitudes', 'longitudes', 'string')
173 //XXX: is a string because of \n separator
174 ->addScalarResult('indoors', 'indoors', 'string')
175 //XXX: is a string because of \n separator
176 ->addScalarResult('counts', 'counts', 'string')
177 ->addScalarResult('modified', 'modified', 'datetime')
178 ->addIndexByScalar('city');
182 ->createNativeQuery($req, $rsm)
183 ->setParameter('begin', $period->getStartDate())
184 ->setParameter('end', $period->getEndDate())
190 //Iterate on each city
191 foreach($result as $city => $data) {
193 $titles = explode("\n", $data['titles']);
196 $latitudes = explode("\n", $data['latitudes']);
199 $longitudes = explode("\n", $data['longitudes']);
202 $indoors = explode("\n", $data['indoors']);
205 $counts = explode("\n", $data['counts']);
207 //With unsufficient count
208 if ($count && $counts[0] < $count) {
210 //XXX: count are sorted so only check first
215 $data['locations'] = [];
217 //Iterate on each location
218 foreach(explode("\n", $data['ids']) as $k => $id) {
219 //With unsufficient count
220 if ($count && $counts[$k] < $count) {
222 //XXX: count are sorted so only check first
227 $data['locations'][] = [
229 'title' => $location = $this->translator
->trans($titles[$k]),
230 'latitude' => floatval($latitudes[$k]),
231 'longitude' => floatval($longitudes[$k]),
232 'indoor' => $indoors[$k] == 0 ? $this->translator
->trans('outdoor') : $this->translator
->trans('indoor'),
233 'link' => $this->router
->generate('rapsysair_location_view', ['id' => $id, 'location' => $this->slugger
->slug($location)])
240 'city' => $data['city'],
241 'in' => $this->translator
->trans('in '.$data['city']),
242 'indoors' => array_map(function ($v) { return $v
== 0 ? $this
->translator
->trans('outdoor') : $this
->translator
->trans('indoor'); }, array_unique($indoors)),
243 'multimap' => $this->translator
->trans($data['city'].' sector map'),
244 'latitude' => $data['latitude'],
245 'longitude' => $data['longitude'],
246 'modified' => $data['modified'],
248 'slug' => $city = $this->slugger
->slug($data['city']),
249 'link' => $this->router
->generate('rapsysair_city_view', ['city' => $city, 'latitude' => $data['latitude'], 'longitude' => $data['longitude']]),
250 'locations' => $data['locations']
259 * Find city by latitude and longitude as array
261 * @param float $latitude The latitude
262 * @param float $longitude The longitude
263 * @return ?array The cities array
265 public function findCityByLatitudeLongitudeAsArray(float $latitude, float $longitude): ?array {
269 SUBSTRING(l.zipcode, 1, 2) AS id,
271 ROUND(AVG(l.latitude), 6) AS latitude,
272 ROUND(AVG(l.longitude), 6) AS longitude,
273 MAX(l.updated) AS updated
274 FROM Rapsys\AirBundle\Entity\Location AS l
275 GROUP BY city, SUBSTRING(l.zipcode, 1, 2)
276 ORDER BY ACOS(SIN(RADIANS(:latitude))*SIN(RADIANS(l.latitude))+COS(RADIANS(:latitude))*COS(RADIANS(l.latitude))*COS(RADIANS(:longitude - l.longitude)))*40030.17/2/PI()
280 //Replace bundle entity name by table name
281 $req = str_replace($this->tableKeys
, $this->tableValues
, $req);
283 //Get result set mapping instance
284 //XXX: DEBUG: see ../blog.orig/src/Rapsys/BlogBundle/Repository/ArticleRepository.php
285 $rsm = new ResultSetMapping();
288 //XXX: see vendor/doctrine/dbal/lib/Doctrine/DBAL/Types/Types.php
289 //addScalarResult($sqlColName, $resColName, $type = 'string');
290 $rsm->addScalarResult('id', 'id', 'integer')
291 ->addScalarResult('city', 'city', 'string')
292 ->addScalarResult('latitude', 'latitude', 'float')
293 ->addScalarResult('longitude', 'longitude', 'float')
294 ->addScalarResult('updated', 'updated', 'datetime')
295 ->addIndexByScalar('city');
299 ->createNativeQuery($req, $rsm)
300 ->setParameter('latitude', $latitude)
301 ->setParameter('longitude', $longitude)
302 ->getOneOrNullResult();
305 if ($result === null) {
312 'id' => $result['id'],
313 'city' => $result['city'],
314 'latitude' => $result['latitude'],
315 'longitude' => $result['longitude'],
316 'updated' => $result['updated'],
317 'in' => $this->translator
->trans('in '.$result['city']),
318 'multimap' => $this->translator
->trans($result['city'].' sector map'),
320 'slug' => $slug = $this->slugger
->slug($result['city']),
321 'link' => $this->router
->generate('rapsysair_city_view', ['city' => $slug, 'latitude' => $result['latitude'], 'longitude' => $result['longitude']])
326 * Find locations by latitude and longitude sorted by period as array
328 * @TODO: find all other locations when current one has no sessions ???
330 * @param float $latitude The latitude
331 * @param float $longitude The longitude
332 * @param DatePeriod $period The period
333 * @param int $count The session count
334 * @param float $distance The distance
335 * @return array The locations array
337 public function findAllByLatitudeLongitudeAsArray(float $latitude, float $longitude, \DatePeriod
$period, int $count = 1, float $distance = 15): array {
339 $radius = 40030.17/2/pi();
341 //Compute min latitude
342 $minlat = min(rad2deg(asin(sin(deg2rad($latitude))*cos($distance/$radius) +
cos(deg2rad($latitude))*sin($distance/$radius)*cos(deg2rad(180)))), $latitude);
344 //Compute max latitude
345 $maxlat = max(rad2deg(asin(sin(deg2rad($latitude))*cos($distance/$radius) +
cos(deg2rad($latitude))*sin($distance/$radius)*cos(deg2rad(0)))), $latitude);
347 //Compute min longitude
348 $minlong = fmod((rad2deg((deg2rad($longitude) +
atan2(sin(deg2rad(-90))*sin($distance/$radius)*cos(deg2rad($minlat)), cos($distance/$radius) - sin(deg2rad($minlat)) * sin(deg2rad($minlat))))) +
180), 360) - 180;
351 $maxlong = fmod((rad2deg((deg2rad($longitude) +
atan2(sin(deg2rad(90))*sin($distance/$radius)*cos(deg2rad($maxlat)), cos($distance/$radius) - sin(deg2rad($maxlat)) * sin(deg2rad($maxlat))))) +
180), 360) - 180;
354 //TODO: see old request before commit to sort session count, distance and then by id ?
355 //TODO: see to sort by future session count, historical session count, distance and then by id ?
356 //TODO: do the same for cities and city ?
365 MAX(GREATEST(a.modified, COALESCE(s.created, 0), COALESCE(s.updated, 0))) AS modified,
375 GREATEST(l.created, l.updated) AS modified
376 FROM Rapsys\AirBundle\Entity\Location AS l
377 WHERE l.latitude BETWEEN :minlat AND :maxlat AND l.longitude BETWEEN :minlong AND :maxlong
380 LEFT JOIN Rapsys\AirBundle\Entity\Session s ON (s.location_id = a.id)
382 ORDER BY COUNT(IF(s.date BETWEEN :begin AND :end, s.id, NULL)) DESC, count DESC, a.id
385 //Replace bundle entity name by table name
386 $req = str_replace($this->tableKeys
, $this->tableValues
, $req);
388 //Get result set mapping instance
389 //XXX: DEBUG: see ../blog.orig/src/Rapsys/BlogBundle/Repository/ArticleRepository.php
390 $rsm = new ResultSetMapping();
393 //XXX: see vendor/doctrine/dbal/lib/Doctrine/DBAL/Types/Types.php
394 //addScalarResult($sqlColName, $resColName, $type = 'string');
395 $rsm->addScalarResult('id', 'id', 'integer')
396 ->addScalarResult('title', 'title', 'string')
397 ->addScalarResult('latitude', 'latitude', 'float')
398 ->addScalarResult('longitude', 'longitude', 'float')
399 ->addScalarResult('created', 'created', 'datetime')
400 ->addScalarResult('updated', 'updated', 'datetime')
401 ->addScalarResult('modified', 'modified', 'datetime')
402 ->addScalarResult('count', 'count', 'integer');
406 ->createNativeQuery($req, $rsm)
407 ->setParameter('begin', $period->getStartDate())
408 ->setParameter('end', $period->getEndDate())
409 ->setParameter('minlat', $minlat)
410 ->setParameter('maxlat', $maxlat)
411 ->setParameter('minlong', $minlong)
412 ->setParameter('maxlong', $maxlong)
418 //Iterate on each location
419 foreach($result as $id => $data) {
420 //With active locations
421 if ($count && $data['count'] < $count) {
422 //Skip unactive locations
429 'title' => $title = $this->translator
->trans($data['title']),
430 'latitude' => $data['latitude'],
431 'longitude' => $data['longitude'],
432 'created' => $data['created'],
433 'updated' => $data['updated'],
434 'modified' => $data['modified'],
435 'count' => $data['count'],
436 'slug' => $slug = $this->slugger
->slug($title),
437 'link' => $this->router
->generate('rapsysair_location_view', ['id' => $data['id'], 'location' => $slug])
446 * Find locations by user id sorted by period as array
448 * @param int $userId The user id
449 * @param DatePeriod $period The period
450 * @return array The locations array
452 public function findAllByUserIdAsArray(int $userId, \DatePeriod
$period, $distance = 15): array {
454 //TODO: ajouter pays ???
465 COUNT(s3.id) AS tcount
474 COUNT(s2.id) AS pcount,
475 MAX(b.updated) AS modified
489 FROM applications AS a
490 JOIN sessions AS s ON (s.id = a.session_id)
491 JOIN locations AS l ON (l.id = s.location_id)
492 WHERE a.user_id = :id
498 WHERE ACOS(SIN(RADIANS(a.latitude))*SIN(RADIANS(l2.latitude))+COS(RADIANS(a.latitude))*COS(RADIANS(l2.latitude))*COS(RADIANS(a.longitude - l2.longitude)))*40030.17/2/PI() BETWEEN 0 AND :distance
503 LEFT JOIN sessions AS s2 ON (s2.location_id = b.id AND s2.date BETWEEN :begin AND :end)
508 LEFT JOIN sessions AS s3 ON (s3.location_id = a.id)
510 ORDER BY pcount DESC, tcount DESC, a.id
513 //Replace bundle entity name by table name
514 $req = str_replace($this->tableKeys
, $this->tableValues
, $req);
516 //Get result set mapping instance
517 //XXX: DEBUG: see ../blog.orig/src/Rapsys/BlogBundle/Repository/ArticleRepository.php
518 $rsm = new ResultSetMapping();
521 //XXX: see vendor/doctrine/dbal/lib/Doctrine/DBAL/Types/Types.php
522 //addScalarResult($sqlColName, $resColName, $type = 'string');
523 $rsm->addScalarResult('id', 'id', 'integer')
524 ->addScalarResult('title', 'title', 'string')
525 ->addScalarResult('city', 'city', 'string')
526 ->addScalarResult('latitude', 'latitude', 'float')
527 ->addScalarResult('longitude', 'longitude', 'float')
528 ->addScalarResult('updated', 'updated', 'datetime')
529 ->addScalarResult('pcount', 'pcount', 'integer')
530 ->addScalarResult('tcount', 'tcount', 'integer')
531 ->addScalarResult('modified', 'modified', 'datetime');
535 ->createNativeQuery($req, $rsm)
536 ->setParameter('begin', $period->getStartDate())
537 ->setParameter('end', $period->getEndDate())
538 ->setParameter('id', $userId)
539 ->setParameter('distance', $distance)
545 //Iterate on each location
546 foreach($result as $id => $data) {
547 //With active locations
548 if (!empty($result[0]['tcount']) && empty($data['tcount'])) {
549 //Skip unactive locations
556 'city' => $data['city'],
557 'title' => $title = $this->translator
->trans($data['title']),
558 'at' => $this->translator
->trans('at '.$data['title']),
559 'miniature' => $this->translator
->trans($data['title'].' miniature'),
560 'latitude' => $data['latitude'],
561 'longitude' => $data['longitude'],
562 'updated' => $data['updated'],
563 'pcount' => $data['pcount'],
564 'tcount' => $data['tcount'],
565 'modified' => $data['modified'],
566 'slug' => $slug = $this->slugger
->slug($title),
567 'link' => $this->router
->generate('rapsysair_location_view', ['id' => $data['id'], 'location' => $slug])
576 * Find location as array by id
578 * @param int $id The location id
579 * @param string $locale The locale
580 * @return array The location data
582 public function findOneByIdAsArray(int $id, string $locale): ?array {
593 MAX(l2.updated) AS updated,
594 SUBSTRING(l.zipcode, 1, 2) AS city_id,
595 ROUND(AVG(l2.latitude), 6) AS city_latitude,
596 ROUND(AVG(l2.longitude), 6) AS city_longitude
597 FROM Rapsys\AirBundle\Entity\Location AS l
598 JOIN Rapsys\AirBundle\Entity\Location AS l2 ON (l2.city = l.city AND SUBSTRING(l2.zipcode, 1, 3) = SUBSTRING(l.zipcode, 1, 3))
604 //Replace bundle entity name by table name
605 $req = str_replace($this->tableKeys
, $this->tableValues
, $req);
607 //Get result set mapping instance
608 //XXX: DEBUG: see ../blog.orig/src/Rapsys/BlogBundle/Repository/ArticleRepository.php
609 $rsm = new ResultSetMapping();
612 //XXX: see vendor/doctrine/dbal/lib/Doctrine/DBAL/Types/Types.php
613 //addScalarResult($sqlColName, $resColName, $type = 'string');
614 $rsm->addScalarResult('id', 'id', 'integer')
615 ->addScalarResult('title', 'title', 'string')
616 ->addScalarResult('city', 'city', 'string')
617 ->addScalarResult('latitude', 'latitude', 'float')
618 ->addScalarResult('longitude', 'longitude', 'float')
619 ->addScalarResult('indoor', 'indoor', 'boolean')
620 ->addScalarResult('zipcode', 'zipcode', 'string')
621 ->addScalarResult('updated', 'updated', 'datetime')
622 ->addScalarResult('city_id', 'city_id', 'integer')
623 ->addScalarResult('city_latitude', 'city_latitude', 'float')
624 ->addScalarResult('city_longitude', 'city_longitude', 'float')
625 ->addIndexByScalar('id');
629 ->createNativeQuery($req, $rsm)
630 ->setParameter('id', $id)
631 ->getOneOrNullResult();
634 if ($result === null) {
640 $result['alternates'] = [];
643 $route = 'rapsysair_location_view';
646 $routeParams = ['id' => $id];
648 //Iterate on each languages
649 foreach($this->languages
as $languageId => $language) {
650 //Without current locale
651 if ($languageId !== $locale) {
655 //Set route params locale
656 $routeParams['_locale'] = $languageId;
658 //Set route params location
659 $routeParams['location'] = $this->slugger
->slug($this->translator
->trans($result['title'], [], null, $languageId));
661 //Iterate on each locales
662 foreach(array_keys($this->languages
) as $other) {
663 //Without other locale
664 if ($other !== $languageId) {
665 //Set other locale title
666 $titles[$other] = $this->translator
->trans($language, [], null, $other);
670 //Add alternates locale
671 $result['alternates'][substr($languageId, 0, 2)] = $result['alternates'][str_replace('_', '-', $languageId)] = [
672 'absolute' => $this->router
->generate($route, $routeParams, UrlGeneratorInterface
::ABSOLUTE_URL
),
673 'relative' => $this->router
->generate($route, $routeParams),
674 'title' => implode('/', $titles),
675 'translated' => $this->translator
->trans($language, [], null, $languageId)
682 'id' => $result['id'],
684 'id' => $result['city_id'],
685 'title' => $result['city'],
686 'in' => $this->translator
->trans('in '.$result['city']),
687 'link' => $this->router
->generate('rapsysair_city_view', ['city' => $result['city'], 'latitude' => $result['city_latitude'], 'longitude' => $result['city_longitude']])
689 'title' => $title = $this->translator
->trans($result['title']),
690 'latitude' => $result['latitude'],
691 'longitude' => $result['longitude'],
692 'indoor' => $result['indoor'],
693 'updated' => $result['updated'],
694 'around' => $this->translator
->trans('around '.$result['title']),
695 'at' => $this->translator
->trans('at '.$result['title']),
696 'atin' => $this->translator
->trans('at '.$result['title']).' '.$this->translator
->trans('in '.$result['city']),
697 'multimap' => $this->translator
->trans($result['title'].' sector map'),
699 'slug' => $slug = $this->slugger
->slug($title),
700 'link' => $this->router
->generate($route, ['_locale' => $locale, 'location' => $slug]+
$routeParams),
701 'alternates' => $result['alternates']
706 * Find complementary locations by session id
708 * @param int $id The session id
709 * @return array The other locations
711 public function findComplementBySessionId(int $id): array {
712 //Fetch complement locations
713 $ret = $this->getEntityManager()
714 ->createQuery('SELECT l.id, l.title FROM Rapsys\AirBundle\Entity\Session s LEFT JOIN Rapsys\AirBundle\Entity\Session s2 WITH s2.id != s.id AND s2.slot = s.slot AND s2.date = s.date LEFT JOIN Rapsys\AirBundle\Entity\Location l WITH l.id != s.location AND (l.id != s2.location OR s2.location IS NULL) WHERE s.id = :sid GROUP BY l.id ORDER BY l.id')
715 ->setParameter('sid', $id)
718 //TODO: try to improve with:
719 #->addIndexByScalar('city');
722 $ret = array_column($ret, 'id', 'title');
728 * Find locations by user id
730 * @param int $id The user id
731 * @return array The user locations
733 public function findByUserId(int $userId): array {
735 $req = 'SELECT l.id, l.title
736 FROM Rapsys\AirBundle\Entity\UserLocation AS ul
737 JOIN Rapsys\AirBundle\Entity\Location AS l ON (l.id = ul.location_id)
738 WHERE ul.user_id = :id';
740 //Replace bundle entity name by table name
741 $req = str_replace($this->tableKeys
, $this->tableValues
, $req);
743 //Get result set mapping instance
744 //XXX: DEBUG: see ../blog.orig/src/Rapsys/BlogBundle/Repository/ArticleRepository.php
745 $rsm = new ResultSetMapping();
747 //Declare result set for our request
748 $rsm->addEntityResult('Rapsys\AirBundle\Entity\Location', 'l')
749 ->addFieldResult('l', 'id', 'id')
750 ->addFieldResult('l', 'title', 'title');
754 ->createNativeQuery($req, $rsm)
755 ->setParameter('id', $userId)