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
;
15 use Symfony\Component\Routing\Generator\UrlGeneratorInterface
;
16 use Symfony\Component\Routing\RouterInterface
;
21 * @TODO: use new window function syntax https://mariadb.com/kb/en/window-functions-overview/ MAX(updated) OVER (PARTITION updated) AS modified ???
23 class LocationRepository
extends EntityRepository
{
29 public function findAll(): array {
30 //Get all locations index by id
31 return $this->createQueryBuilder('location', 'location.id')->getQuery()->getResult();
35 * Find locations as array
37 * @param DatePeriod $period The period
38 * @return array The locations array
40 public function findAllAsArray(\DatePeriod
$period): array {
42 //TODO: ajouter pays ???
51 FROM RapsysAirBundle:Location AS l
52 LEFT JOIN RapsysAirBundle:Session AS s ON (l.id = s.location_id)
54 ORDER BY COUNT(IF(s.date BETWEEN :begin AND :end, s.id, NULL)) DESC, COUNT(s.id) DESC, l.id
57 //Replace bundle entity name by table name
58 $req = str_replace($this->tableKeys
, $this->tableValues
, $req);
60 //Get result set mapping instance
61 //XXX: DEBUG: see ../blog.orig/src/Rapsys/BlogBundle/Repository/ArticleRepository.php
62 $rsm = new ResultSetMapping();
65 //XXX: see vendor/doctrine/dbal/lib/Doctrine/DBAL/Types/Types.php
66 //addScalarResult($sqlColName, $resColName, $type = 'string');
67 $rsm->addScalarResult('id', 'id', 'integer')
68 ->addScalarResult('title', 'title', 'string')
69 ->addScalarResult('latitude', 'latitude', 'float')
70 ->addScalarResult('longitude', 'longitude', 'float')
71 ->addScalarResult('indoor', 'indoor', 'boolean')
72 ->addScalarResult('count', 'count', 'integer')
73 ->addScalarResult('updated', 'updated', 'datetime');
77 ->createNativeQuery($req, $rsm)
78 ->setParameter('begin', $period->getStartDate())
79 ->setParameter('end', $period->getEndDate())
85 //Iterate on each city
86 foreach($result as $data) {
90 'title' => $title = $this->translator
->trans($data['title']),
91 'latitude' => $data['latitude'],
92 'longitude' => $data['longitude'],
93 'updated' => $data['updated'],
95 'slug' => $location = $this->slugger
->slug($title),
96 'link' => $this->router
->generate('rapsys_air_location_view', ['id' => $data['id'], 'location' => $this->slugger
->slug($location)])
105 * Find cities as array
107 * @param DatePeriod $period The period
108 * @param int $count The session count
109 * @return array The cities array
111 public function findCitiesAsArray(\DatePeriod
$period, int $count = 1): array {
115 SUBSTRING(a.zipcode, 1, 2) AS id,
117 ROUND(AVG(a.latitude), 6) AS latitude,
118 ROUND(AVG(a.longitude), 6) AS longitude,
119 GROUP_CONCAT(a.id ORDER BY a.pcount DESC, a.count DESC, a.id SEPARATOR "\\n") AS ids,
120 GROUP_CONCAT(a.title ORDER BY a.pcount DESC, a.count DESC, a.id SEPARATOR "\\n") AS titles,
121 GROUP_CONCAT(a.latitude ORDER BY a.pcount DESC, a.count DESC, a.id SEPARATOR "\\n") AS latitudes,
122 GROUP_CONCAT(a.longitude ORDER BY a.pcount DESC, a.count DESC, a.id SEPARATOR "\\n") AS longitudes,
123 GROUP_CONCAT(a.indoor ORDER BY a.pcount DESC, a.count DESC, a.id SEPARATOR "\\n") AS indoors,
124 GROUP_CONCAT(a.count ORDER BY a.pcount DESC, a.count DESC, a.id SEPARATOR "\\n") AS counts,
125 MAX(a.updated) AS modified
136 COUNT(s.id) AS count,
137 COUNT(IF(s.date BETWEEN :begin AND :end, s.id, NULL)) AS pcount
138 FROM RapsysAirBundle:Location AS l
139 LEFT JOIN RapsysAirBundle:Session AS s ON (l.id = s.location_id)
144 GROUP BY a.city, SUBSTRING(a.zipcode, 1, 3)
145 ORDER BY a.city, SUBSTRING(a.zipcode, 1, 3)
148 //Replace bundle entity name by table name
149 $req = str_replace($this->tableKeys
, $this->tableValues
, $req);
151 //Get result set mapping instance
152 //XXX: DEBUG: see ../blog.orig/src/Rapsys/BlogBundle/Repository/ArticleRepository.php
153 $rsm = new ResultSetMapping();
156 //XXX: see vendor/doctrine/dbal/lib/Doctrine/DBAL/Types/Types.php
157 //addScalarResult($sqlColName, $resColName, $type = 'string');
158 $rsm->addScalarResult('id', 'id', 'integer')
159 ->addScalarResult('city', 'city', 'string')
160 ->addScalarResult('latitude', 'latitude', 'float')
161 ->addScalarResult('longitude', 'longitude', 'float')
162 //XXX: is a string because of \n separator
163 ->addScalarResult('ids', 'ids', 'string')
164 //XXX: is a string because of \n separator
165 ->addScalarResult('titles', 'titles', 'string')
166 //XXX: is a string because of \n separator
167 ->addScalarResult('latitudes', 'latitudes', 'string')
168 //XXX: is a string because of \n separator
169 ->addScalarResult('longitudes', 'longitudes', 'string')
170 //XXX: is a string because of \n separator
171 ->addScalarResult('indoors', 'indoors', 'string')
172 //XXX: is a string because of \n separator
173 ->addScalarResult('counts', 'counts', 'string')
174 ->addScalarResult('modified', 'modified', 'datetime')
175 ->addIndexByScalar('city');
179 ->createNativeQuery($req, $rsm)
180 ->setParameter('begin', $period->getStartDate())
181 ->setParameter('end', $period->getEndDate())
187 //Iterate on each city
188 foreach($result as $city => $data) {
190 $titles = explode("\n", $data['titles']);
193 $latitudes = explode("\n", $data['latitudes']);
196 $longitudes = explode("\n", $data['longitudes']);
199 $indoors = explode("\n", $data['indoors']);
202 $counts = explode("\n", $data['counts']);
204 //With unsufficient count
205 if ($count && $counts[0] < $count) {
207 //XXX: count are sorted so only check first
212 $data['locations'] = [];
214 //Iterate on each location
215 foreach(explode("\n", $data['ids']) as $k => $id) {
216 //With unsufficient count
217 if ($count && $counts[$k] < $count) {
219 //XXX: count are sorted so only check first
224 $data['locations'][] = [
226 'title' => $location = $this->translator
->trans($titles[$k]),
227 'latitude' => floatval($latitudes[$k]),
228 'longitude' => floatval($longitudes[$k]),
229 'indoor' => $indoors[$k] == 0 ? $this->translator
->trans('outdoor') : $this->translator
->trans('indoor'),
230 'link' => $this->router
->generate('rapsys_air_location_view', ['id' => $id, 'location' => $this->slugger
->slug($location)])
237 'city' => $data['city'],
238 'in' => $this->translator
->trans('in '.$data['city']),
239 'indoors' => array_map(function ($v) { return $v
== 0 ? $this
->translator
->trans('outdoor') : $this
->translator
->trans('indoor'); }, array_unique($indoors)),
240 'multimap' => $this->translator
->trans($data['city'].' sector map'),
241 'latitude' => $data['latitude'],
242 'longitude' => $data['longitude'],
243 'modified' => $data['modified'],
245 'slug' => $city = $this->slugger
->slug($data['city']),
246 'link' => $this->router
->generate('rapsys_air_city_view', ['city' => $city, 'latitude' => $data['latitude'], 'longitude' => $data['longitude']]),
247 'locations' => $data['locations']
256 * Find city by latitude and longitude as array
258 * @param float $latitude The latitude
259 * @param float $longitude The longitude
260 * @return ?array The cities array
262 public function findCityByLatitudeLongitudeAsArray(float $latitude, float $longitude): ?array {
266 SUBSTRING(l.zipcode, 1, 2) AS id,
268 ROUND(AVG(l.latitude), 6) AS latitude,
269 ROUND(AVG(l.longitude), 6) AS longitude,
270 MAX(l.updated) AS updated
271 FROM RapsysAirBundle:Location AS l
272 GROUP BY city, SUBSTRING(l.zipcode, 1, 3)
273 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()
277 //Replace bundle entity name by table name
278 $req = str_replace($this->tableKeys
, $this->tableValues
, $req);
280 //Get result set mapping instance
281 //XXX: DEBUG: see ../blog.orig/src/Rapsys/BlogBundle/Repository/ArticleRepository.php
282 $rsm = new ResultSetMapping();
285 //XXX: see vendor/doctrine/dbal/lib/Doctrine/DBAL/Types/Types.php
286 //addScalarResult($sqlColName, $resColName, $type = 'string');
287 $rsm->addScalarResult('id', 'id', 'integer')
288 ->addScalarResult('city', 'city', 'string')
289 ->addScalarResult('latitude', 'latitude', 'float')
290 ->addScalarResult('longitude', 'longitude', 'float')
291 ->addScalarResult('updated', 'updated', 'datetime')
292 ->addIndexByScalar('city');
296 ->createNativeQuery($req, $rsm)
297 ->setParameter('latitude', $latitude)
298 ->setParameter('longitude', $longitude)
299 ->getOneOrNullResult();
302 if ($result === null) {
309 'id' => $result['id'],
310 'city' => $result['city'],
311 'latitude' => $result['latitude'],
312 'longitude' => $result['longitude'],
313 'updated' => $result['updated'],
314 'in' => $this->translator
->trans('in '.$result['city']),
315 'multimap' => $this->translator
->trans($result['city'].' sector map'),
317 'slug' => $slug = $this->slugger
->slug($result['city']),
318 'link' => $this->router
->generate('rapsys_air_city_view', ['city' => $slug, 'latitude' => $result['latitude'], 'longitude' => $result['longitude']])
323 * Find locations by latitude and longitude sorted by period as array
325 * @TODO: find all other locations when current one has no sessions ???
327 * @param float $latitude The latitude
328 * @param float $longitude The longitude
329 * @param DatePeriod $period The period
330 * @param int $count The session count
331 * @param float $distance The distance
332 * @return array The locations array
334 public function findAllByLatitudeLongitudeAsArray(float $latitude, float $longitude, \DatePeriod
$period, int $count = 1, float $distance = 15): array {
336 $radius = 40030.17/2/pi();
338 //Compute min latitude
339 $minlat = min(rad2deg(asin(sin(deg2rad($latitude))*cos($distance/$radius) +
cos(deg2rad($latitude))*sin($distance/$radius)*cos(deg2rad(180)))), $latitude);
341 //Compute max latitude
342 $maxlat = max(rad2deg(asin(sin(deg2rad($latitude))*cos($distance/$radius) +
cos(deg2rad($latitude))*sin($distance/$radius)*cos(deg2rad(0)))), $latitude);
344 //Compute min longitude
345 $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;
348 $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;
351 //TODO: see old request before commit to sort session count, distance and then by id ?
352 //TODO: see to sort by future session count, historical session count, distance and then by id ?
353 //TODO: do the same for cities and city ?
361 MAX(a.updated) AS modified,
370 FROM RapsysAirBundle:Location AS l
371 WHERE l.latitude BETWEEN :minlat AND :maxlat AND l.longitude BETWEEN :minlong AND :maxlong
374 LEFT JOIN RapsysAirBundle:Session s ON (s.location_id = a.id)
376 ORDER BY COUNT(IF(s.date BETWEEN :begin AND :end, s.id, NULL)) DESC, count DESC, a.id
379 //Replace bundle entity name by table name
380 $req = str_replace($this->tableKeys
, $this->tableValues
, $req);
382 //Get result set mapping instance
383 //XXX: DEBUG: see ../blog.orig/src/Rapsys/BlogBundle/Repository/ArticleRepository.php
384 $rsm = new ResultSetMapping();
387 //XXX: see vendor/doctrine/dbal/lib/Doctrine/DBAL/Types/Types.php
388 //addScalarResult($sqlColName, $resColName, $type = 'string');
389 $rsm->addScalarResult('id', 'id', 'integer')
390 ->addScalarResult('title', 'title', 'string')
391 ->addScalarResult('latitude', 'latitude', 'float')
392 ->addScalarResult('longitude', 'longitude', 'float')
393 ->addScalarResult('updated', 'updated', 'datetime')
394 ->addScalarResult('modified', 'modified', 'datetime')
395 ->addScalarResult('count', 'count', 'integer');
399 ->createNativeQuery($req, $rsm)
400 ->setParameter('begin', $period->getStartDate())
401 ->setParameter('end', $period->getEndDate())
402 ->setParameter('minlat', $minlat)
403 ->setParameter('maxlat', $maxlat)
404 ->setParameter('minlong', $minlong)
405 ->setParameter('maxlong', $maxlong)
411 //Iterate on each location
412 foreach($result as $id => $data) {
413 //With active locations
414 if ($count && $data['count'] < $count) {
415 //Skip unactive locations
422 'title' => $title = $this->translator
->trans($data['title']),
423 'latitude' => $data['latitude'],
424 'longitude' => $data['longitude'],
425 'updated' => $data['updated'],
426 'modified' => $data['modified'],
427 'count' => $data['count'],
428 'slug' => $slug = $this->slugger
->slug($title),
429 'link' => $this->router
->generate('rapsys_air_location_view', ['id' => $data['id'], 'location' => $slug])
438 * Find locations by user id sorted by period as array
440 * @param int $userId The user id
441 * @param DatePeriod $period The period
442 * @return array The locations array
444 public function findAllByUserIdAsArray(int $userId, \DatePeriod
$period, $distance = 15): array {
446 //TODO: ajouter pays ???
457 COUNT(s3.id) AS tcount
466 COUNT(s2.id) AS pcount,
467 MAX(b.updated) AS modified
481 FROM applications AS a
482 JOIN sessions AS s ON (s.id = a.session_id)
483 JOIN locations AS l ON (l.id = s.location_id)
484 WHERE a.user_id = :id
490 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
495 LEFT JOIN sessions AS s2 ON (s2.location_id = b.id AND s2.date BETWEEN :begin AND :end)
500 LEFT JOIN sessions AS s3 ON (s3.location_id = a.id)
502 ORDER BY pcount DESC, tcount DESC, a.id
505 //Replace bundle entity name by table name
506 $req = str_replace($this->tableKeys
, $this->tableValues
, $req);
508 //Get result set mapping instance
509 //XXX: DEBUG: see ../blog.orig/src/Rapsys/BlogBundle/Repository/ArticleRepository.php
510 $rsm = new ResultSetMapping();
513 //XXX: see vendor/doctrine/dbal/lib/Doctrine/DBAL/Types/Types.php
514 //addScalarResult($sqlColName, $resColName, $type = 'string');
515 $rsm->addScalarResult('id', 'id', 'integer')
516 ->addScalarResult('title', 'title', 'string')
517 ->addScalarResult('city', 'city', 'string')
518 ->addScalarResult('latitude', 'latitude', 'float')
519 ->addScalarResult('longitude', 'longitude', 'float')
520 ->addScalarResult('updated', 'updated', 'datetime')
521 ->addScalarResult('pcount', 'pcount', 'integer')
522 ->addScalarResult('tcount', 'tcount', 'integer')
523 ->addScalarResult('modified', 'modified', 'datetime');
527 ->createNativeQuery($req, $rsm)
528 ->setParameter('begin', $period->getStartDate())
529 ->setParameter('end', $period->getEndDate())
530 ->setParameter('id', $userId)
531 ->setParameter('distance', $distance)
537 //Iterate on each location
538 foreach($result as $id => $data) {
539 //With active locations
540 if (!empty($result[0]['tcount']) && empty($data['tcount'])) {
541 //Skip unactive locations
548 'city' => $data['city'],
549 'title' => $title = $this->translator
->trans($data['title']),
550 'at' => $this->translator
->trans('at '.$data['title']),
551 'miniature' => $this->translator
->trans($data['title'].' miniature'),
552 'latitude' => $data['latitude'],
553 'longitude' => $data['longitude'],
554 'updated' => $data['updated'],
555 'pcount' => $data['pcount'],
556 'tcount' => $data['tcount'],
557 'modified' => $data['modified'],
558 'slug' => $slug = $this->slugger
->slug($title),
559 'link' => $this->router
->generate('rapsys_air_location_view', ['id' => $data['id'], 'location' => $slug])
568 * Find location as array by id
570 * @param int $id The location id
571 * @param string $locale The locale
572 * @return array The location data
574 public function findOneByIdAsArray(int $id, string $locale): ?array {
585 MAX(l2.updated) AS updated,
586 SUBSTRING(l.zipcode, 1, 2) AS city_id,
587 ROUND(AVG(l2.latitude), 6) AS city_latitude,
588 ROUND(AVG(l2.longitude), 6) AS city_longitude
589 FROM RapsysAirBundle:Location AS l
590 JOIN RapsysAirBundle:Location AS l2 ON (l2.city = l.city AND SUBSTRING(l.zipcode, 1, 3) = SUBSTRING(l.zipcode, 1, 3))
596 //Replace bundle entity name by table name
597 $req = str_replace($this->tableKeys
, $this->tableValues
, $req);
599 //Get result set mapping instance
600 //XXX: DEBUG: see ../blog.orig/src/Rapsys/BlogBundle/Repository/ArticleRepository.php
601 $rsm = new ResultSetMapping();
604 //XXX: see vendor/doctrine/dbal/lib/Doctrine/DBAL/Types/Types.php
605 //addScalarResult($sqlColName, $resColName, $type = 'string');
606 $rsm->addScalarResult('id', 'id', 'integer')
607 ->addScalarResult('title', 'title', 'string')
608 ->addScalarResult('city', 'city', 'string')
609 ->addScalarResult('latitude', 'latitude', 'float')
610 ->addScalarResult('longitude', 'longitude', 'float')
611 ->addScalarResult('indoor', 'indoor', 'boolean')
612 ->addScalarResult('zipcode', 'zipcode', 'string')
613 ->addScalarResult('updated', 'updated', 'datetime')
614 ->addScalarResult('city_id', 'city_id', 'integer')
615 ->addScalarResult('city_latitude', 'city_latitude', 'float')
616 ->addScalarResult('city_longitude', 'city_longitude', 'float')
617 ->addIndexByScalar('id');
621 ->createNativeQuery($req, $rsm)
622 ->setParameter('id', $id)
623 ->getOneOrNullResult();
626 if ($result === null) {
632 $result['alternates'] = [];
635 $route = 'rapsys_air_location_view';
638 $routeParams = ['id' => $id];
640 //Iterate on each languages
641 foreach($this->languages
as $languageId => $language) {
642 //Without current locale
643 if ($languageId !== $locale) {
647 //Set route params locale
648 $routeParams['_locale'] = $languageId;
650 //Set route params location
651 $routeParams['location'] = $this->slugger
->slug($this->translator
->trans($result['title'], [], null, $languageId));
653 //Iterate on each locales
654 foreach(array_keys($this->languages
) as $other) {
655 //Without other locale
656 if ($other !== $languageId) {
657 //Set other locale title
658 $titles[$other] = $this->translator
->trans($language, [], null, $other);
662 //Add alternates locale
663 $result['alternates'][substr($languageId, 0, 2)] = $result['alternates'][str_replace('_', '-', $languageId)] = [
664 'absolute' => $this->router
->generate($route, $routeParams, UrlGeneratorInterface
::ABSOLUTE_URL
),
665 'relative' => $this->router
->generate($route, $routeParams),
666 'title' => implode('/', $titles),
667 'translated' => $this->translator
->trans($language, [], null, $languageId)
674 'id' => $result['id'],
676 'id' => $result['city_id'],
677 'title' => $result['city'],
678 'in' => $this->translator
->trans('in '.$result['city']),
679 'link' => $this->router
->generate('rapsys_air_city_view', ['city' => $result['city'], 'latitude' => $result['city_latitude'], 'longitude' => $result['city_longitude']])
681 'title' => $title = $this->translator
->trans($result['title']),
682 'latitude' => $result['latitude'],
683 'longitude' => $result['longitude'],
684 'indoor' => $result['indoor'],
685 'updated' => $result['updated'],
686 'around' => $this->translator
->trans('around '.$result['title']),
687 'at' => $this->translator
->trans('at '.$result['title']),
688 'atin' => $this->translator
->trans('at '.$result['title']).' '.$this->translator
->trans('in '.$result['city']),
689 'multimap' => $this->translator
->trans($result['title'].' sector map'),
691 'slug' => $slug = $this->slugger
->slug($title),
692 'link' => $this->router
->generate($route, ['_locale' => $locale, 'location' => $slug]+
$routeParams),
693 'alternates' => $result['alternates']
698 * Find complementary locations by session id
700 * @param int $id The session id
701 * @return array The other locations
703 public function findComplementBySessionId(int $id): array {
704 //Fetch complement locations
705 $ret = $this->getEntityManager()
706 ->createQuery('SELECT l.id, l.title FROM RapsysAirBundle:Session s LEFT JOIN RapsysAirBundle:Session s2 WITH s2.id != s.id AND s2.slot = s.slot AND s2.date = s.date LEFT JOIN RapsysAirBundle: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')
707 ->setParameter('sid', $id)
710 //TODO: try to improve with:
711 #->addIndexByScalar('city');
714 $ret = array_column($ret, 'id', 'title');
720 * Find locations by user id
722 * @param int $id The user id
723 * @return array The user locations
725 public function findByUserId(int $userId): array {
727 $req = 'SELECT l.id, l.title
728 FROM RapsysAirBundle:UserLocation AS ul
729 JOIN RapsysAirBundle:Location AS l ON (l.id = ul.location_id)
730 WHERE ul.user_id = :id';
732 //Replace bundle entity name by table name
733 $req = str_replace($this->tableKeys
, $this->tableValues
, $req);
735 //Get result set mapping instance
736 //XXX: DEBUG: see ../blog.orig/src/Rapsys/BlogBundle/Repository/ArticleRepository.php
737 $rsm = new ResultSetMapping();
739 //Declare result set for our request
740 $rsm->addEntityResult('RapsysAirBundle:Location', 'l')
741 ->addFieldResult('l', 'id', 'id')
742 ->addFieldResult('l', 'title', 'title');
746 ->createNativeQuery($req, $rsm)
747 ->setParameter('id', $userId)