src/Controller/LocationController.php line 19

  1. <?php
  2. namespace App\Controller;
  3. use App\Entity\LocationDetail;
  4. use App\Repository\CityRepository;
  5. use App\Repository\LocationRepository;
  6. use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
  7. use Symfony\Component\HttpFoundation\JsonResponse;
  8. use Symfony\Component\HttpFoundation\Request;
  9. use Symfony\Component\HttpFoundation\Response;
  10. use Symfony\Component\Routing\Annotation\Route;
  11. class LocationController extends AbstractController
  12. {
  13.     /**
  14.      * @Route("/map", name="app_map")
  15.      */
  16.     public function map(
  17.         LocationRepository $locationRepository,
  18.         CityRepository $cityRepository,
  19.         \App\Service\Mercure\MercurePublisher $mercure
  20.     ): Response {
  21.         // Fetch all active locations with their details, city and country in one query
  22.         $locations $locationRepository->findAllActiveWithDetails();
  23.         // Cities for the filter dropdown
  24.         $cities $cityRepository->findAllActive();
  25.         /** @var \App\Entity\User|null $user */
  26.         $user  $this->getUser();
  27.         $topic $user !== null
  28.             \App\Service\Mercure\MercurePublisher::userBookingTopic($user->getId())
  29.             : null;
  30.         $response $this->render('@web/page/index.html.twig', [
  31.             'locations'      => $locations,
  32.             'cities'         => $cities,
  33.             'yandex_api_key' => $this->getParameter('app.yandex_api_key'),
  34.             'mercure_url'    => $mercure->getPublicUrl(),
  35.             'mercure_topic'  => $topic,
  36.         ]);
  37.         // A signed-in passenger may subscribe to their own live-order topic.
  38.         if ($topic !== null) {
  39.             $response->headers->setCookie($mercure->authorizationCookie([$topic]));
  40.         }
  41.         return $response;
  42.     }
  43.     /**
  44.      * Live "drive-by" endpoint.
  45.      *
  46.      * The map page polls this with the visitor's current GPS coordinates. If an
  47.      * active location sits within the given radius, we return it together with
  48.      * the audio guide (mp3) so the browser can autoplay the explanation.
  49.      */
  50.     #[Route('/map/nearby'name'app_map_nearby'methods: ['GET'])]
  51.     public function nearby(
  52.         Request $request,
  53.         LocationRepository $locationRepository
  54.     ): JsonResponse {
  55.         $lat $request->query->get('lat');
  56.         $lng $request->query->get('lng');
  57.         if ($lat === null || $lng === null || !is_numeric($lat) || !is_numeric($lng)) {
  58.             return $this->json(
  59.                 ['found' => false'error' => 'lat and lng are required numeric parameters'],
  60.                 Response::HTTP_BAD_REQUEST
  61.             );
  62.         }
  63.         // Trigger radius in metres. Clamp so a caller can't ask for the world.
  64.         $radius = (float) $request->query->get('radius''150');
  65.         $radius max(10.0min($radius2000.0));
  66.         // Optional preferred language for the audio guide (ISO 639-1).
  67.         $lang $request->query->get('lang');
  68.         $result $locationRepository->findNearestActiveWithDetails(
  69.             (float) $lat,
  70.             (float) $lng,
  71.             $radius
  72.         );
  73.         if ($result === null) {
  74.             // Nothing within the radius — still report the closest place and how
  75.             // far it is, so the client can show useful feedback.
  76.             $nearest $locationRepository->findNearestInfo((float) $lat, (float) $lng);
  77.             return $this->json([
  78.                 'found'   => false,
  79.                 'nearest' => $nearest === null null : [
  80.                     'name'     => $nearest['name'],
  81.                     'distance' => round($nearest['distance'], 1),
  82.                 ],
  83.             ]);
  84.         }
  85.         /** @var \App\Entity\Location $location */
  86.         $location $result['location'];
  87.         // Pick the audio guide: active audio details, in sort order, preferring
  88.         // the requested language, then falling back to the first available.
  89.         $audioDetails = [];
  90.         foreach ($location->getDetails() as $detail) {
  91.             if ($detail->isActive()
  92.                 && $detail->getType() === LocationDetail::TYPE_AUDIO
  93.                 && $detail->getFilePath()
  94.             ) {
  95.                 $audioDetails[] = $detail;
  96.             }
  97.         }
  98.         usort($audioDetails, static fn (LocationDetail $aLocationDetail $b) => $a->getSortOrder() <=> $b->getSortOrder());
  99.         $audio null;
  100.         if ($lang !== null && $lang !== '') {
  101.             // Strict: when a language is requested, only play that language's
  102.             // audio (no fallback to other languages).
  103.             foreach ($audioDetails as $detail) {
  104.                 if ($detail->getLanguage() === $lang) {
  105.                     $audio $detail;
  106.                     break;
  107.                 }
  108.             }
  109.         } else {
  110.             $audio $audioDetails[0] ?? null;
  111.         }
  112.         return $this->json([
  113.             'found'    => true,
  114.             'location' => [
  115.                 'id'       => $location->getId(),
  116.                 'name'     => $location->getName(),
  117.                 'slug'     => $location->getSlug(),
  118.                 'category' => $location->getCategory(),
  119.                 'address'  => $location->getAddress(),
  120.                 'lat'      => $location->getLatitude(),
  121.                 'lng'      => $location->getLongitude(),
  122.                 'distance' => round($result['distance'], 1),
  123.             ],
  124.             'audio'    => $audio === null null : [
  125.                 'id'       => $audio->getId(),
  126.                 'title'    => $audio->getTitle(),
  127.                 'filePath' => $audio->getFilePath(),
  128.                 'content'  => $audio->getContent(),
  129.                 'language' => $audio->getLanguage(),
  130.             ],
  131.         ]);
  132.     }
  133. }