src/Controller/GisserverController.php line 123

  1. <?php
  2. //src/Controller/GisserverController.php
  3. namespace App\Controller;
  4. use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
  5. use Symfony\Component\HttpFoundation\Response;
  6. use Symfony\Component\HttpFoundation\JsonResponse;
  7. use Symfony\Component\HttpFoundation\Request;
  8. use Doctrine\DBAL\DriverManager;
  9. use Doctrine\DBAL\Driver\PDO\PgSQL;
  10. use Psr\Log\LoggerInterface;
  11. use PDO;
  12. class GisserverController extends AbstractController
  13. {
  14.     private const TIMEOUT 240;
  15.     private LoggerInterface $logger;
  16.     public function __construct(LoggerInterface $logger)
  17.     {
  18.         $this->logger $logger;
  19.     }
  20.     
  21.     /**
  22.      * Obtener conexión DBAL
  23.      */
  24.     private function getConnection()
  25.     {
  26.         $databaseUrl $_ENV['DATABASE_URL_GIS'];
  27.         
  28.         $connectionParams = [
  29.             'url' => $databaseUrl,
  30.             'driverOptions' => [PDO::ATTR_TIMEOUT => intval(self::TIMEOUT)]
  31.         ];
  32.         
  33.         return DriverManager::getConnection($connectionParams);
  34.     }
  35.     public function geoJson(Request $request)
  36.     {
  37.         $layerTable $request->get('typeName');
  38.         $SrsName $request->get('SrsName''EPSG:4326');
  39.         $fields $request->get('PropertyName');
  40.         $srsCode $this->extractSRID($SrsName);
  41.         $fieldsKey = empty($fields) ? 'all' md5($fields);
  42.         $cacheDir $this->getParameter('kernel.project_dir') . "/var/cache/geojson";
  43.         $cacheFile "{$cacheDir}/{$layerTable}_{$srsCode}_{$fieldsKey}.json";
  44.         
  45.         if (file_exists($cacheFile)) 
  46.         {
  47.             $response = new Response(file_get_contents($cacheFile));
  48.             $response->headers->set('Content-Type''application/json');
  49.             $response->headers->set('Access-Control-Allow-Origin''*');
  50.             $response->headers->set('Cache-Control''public, max-age=3600');
  51.             $response->headers->set('X-Cache''HIT');
  52.             return $response;
  53.         }
  54.         if ($fields == null || $fields == '') {
  55.             $fields '*';
  56.         }
  57.         else
  58.         {
  59.             $fields 'id,geom,'.$fields;
  60.         }
  61.         try 
  62.         {
  63.             $this->connection $this->getConnection();
  64.             $sql "
  65.                 SELECT jsonb_build_object(
  66.                     'type', 'FeatureCollection',
  67.                     'features', jsonb_agg(features.feature)
  68.                 ) as geojson
  69.                 FROM (
  70.                     SELECT jsonb_build_object(
  71.                         'type', 'Feature',
  72.                         'id', id,
  73.                         'geometry', ST_AsGeoJSON(ST_Transform(geom, {$srsCode}))::jsonb,
  74.                         'properties', to_jsonb(row) - 'geom'
  75.                     ) AS feature
  76.                     FROM (
  77.                         SELECT {$fields}
  78.                         FROM \"{$layerTable}\"
  79.                     ) row
  80.                 ) features
  81.             ";
  82.             
  83.             $result $this->connection->executeQuery($sql);
  84.             $data $result->fetchAssociative();
  85.             $geojson $data['geojson'] ?? '{"type":"FeatureCollection","features":[]}';
  86.         
  87.             if (!is_dir($cacheDir)) {
  88.                 mkdir($cacheDir0755true);
  89.             }
  90.             file_put_contents($cacheFile$geojson);
  91.             
  92.             $response = new Response($geojson);
  93.             $response->headers->set('Content-Type''application/json');
  94.             $response->headers->set('Access-Control-Allow-Origin''*');
  95.             $response->headers->set('Cache-Control''public, max-age=3600');
  96.             $response->headers->set('X-Cache''MISS');
  97.             return $response;
  98.         } catch (\Exception $e) {
  99.             return $this->json([
  100.                 'error' => 'Error al obtener GeoJSON',
  101.                 'message' => $e->getMessage()
  102.             ], Response::HTTP_INTERNAL_SERVER_ERROR);
  103.         }
  104.         
  105.     }
  106.     #[Route('/gis/tiles/{tabla}/{z}/{x}/{y}.pbf'name'gis_tiles'methods: ['GET'])]
  107.     public function tiles(string $tablaint $zint $xint $yRequest $request): Response
  108.     {
  109.         $fields $request->get('fields');
  110.         $arrFields explode(',',$fields);
  111.         $comarca $request->get('comarca'); // Obtener el parámetro comarca
  112.         
  113.         foreach ($arrFields as $i => $f) {
  114.             $arrFields[$i] = '"' trim($f) . '"';
  115.         }
  116.         $fieldsQuoted implode(', '$arrFields);
  117.         $tabla urldecode($tabla);
  118.         // Determinar qué materialized view usar según el zoom
  119.         $materializedView $this->getMaterializedViewName($tabla$z);
  120.         
  121.         $this->logger->info("Preparando generación de tiles", [
  122.             'tabla_solicitada' => $tabla,
  123.             'zoom' => $z,
  124.             'x' => $x,
  125.             'y' => $y,
  126.             'vista_candidata' => $materializedView,
  127.             'comarca_filtro' => $comarca
  128.         ]);
  129.         
  130.         try 
  131.         {
  132.             $conDbal $this->getConnection();
  133.             // Verificar si existe la materialized view
  134.             $viewExists $this->checkMaterializedViewExists($conDbal$materializedView);
  135.             
  136.             if (!$viewExists) {
  137.                 $this->logger->warning("Materialized view {$materializedView} no existe, usando tabla original {$tabla}");
  138.                 $materializedView $tabla;
  139.             }
  140.             $this->logger->info("Vista/Tabla que se usará para executeQuery", [
  141.                 'fuente_datos' => $materializedView,
  142.                 'es_vista_materializada' => $viewExists,
  143.                 'zoom' => $z
  144.             ]);
  145.             // Construir la cláusula WHERE con el filtro de comarca si se proporciona
  146.             $whereClause "ST_Intersects(geom_3857, ST_TileEnvelope(:z, :x, :y))";
  147.             
  148.             if (!empty($comarca)) {
  149.                 $whereClause .= " AND comarca = :comarca";
  150.             }
  151.             $sql "
  152.                 WITH mvtgeom AS (
  153.                     SELECT 
  154.                         id,
  155.                         {$fieldsQuoted},
  156.                         ST_AsMVTGeom(
  157.                             geom_3857,
  158.                             ST_TileEnvelope(:z, :x, :y),
  159.                             4096,
  160.                             64,
  161.                             true
  162.                         ) AS geom
  163.                     FROM \"{$materializedView}\"
  164.                     WHERE {$whereClause}
  165.                 )
  166.                 SELECT ST_AsMVT(mvtgeom, :layer, 4096, 'geom') as mvt
  167.                 FROM mvtgeom
  168.                 WHERE geom IS NOT NULL
  169.             ";
  170.             $this->logger->debug("SQL generado", [
  171.                 'sql' => str_replace([':z'':x'':y'':layer'':comarca'], [$z$x$y$tabla$comarca ?? NULL], $sql),
  172.                 'fuente' => $materializedView,
  173.                 'comarca' => $comarca ?? NULL
  174.             ]);
  175.             $stmt $conDbal->prepare($sql);
  176.             $stmt->bindValue('layer'$tabla);
  177.             $stmt->bindValue('z'$z);
  178.             $stmt->bindValue('x'$x);
  179.             $stmt->bindValue('y'$y);
  180.             // Bindear el parámetro comarca si existe
  181.             if (!empty($comarca)) {
  182.                 $stmt->bindValue('comarca'$comarca\PDO::PARAM_STR);
  183.             }
  184.             
  185.             $result $stmt->executeQuery();
  186.             $data $result->fetchAssociative();
  187.             $this->logger->info("Resultado MVT", [
  188.                 'fuente_usada' => $materializedView,
  189.                 'has_mvt' => isset($data['mvt']),
  190.                 'mvt_empty' => empty($data['mvt']),
  191.                 'mvt_type' => gettype($data['mvt'] ?? null),
  192.                 'comarca_aplicada' => $comarca ?? 'N/A'
  193.             ]);
  194.             if ($data && !empty($data['mvt'])) 
  195.             {
  196.                 $mvtField $data['mvt'];
  197.                 if (is_resource($mvtField)) {
  198.                     $mvt stream_get_contents($mvtField);
  199.                 } elseif (is_string($mvtField) && str_starts_with($mvtField'\\x')) {
  200.                     $mvt hex2bin(substr($mvtField2));
  201.                 } else {
  202.                     $mvt = (string)$mvtField;
  203.                 }
  204.                 
  205.                 if ($mvt === '' || $mvt === null) {
  206.                     $this->logger->warning("MVT vacío", ['fuente' => $materializedView'comarca' => $comarca ?? 'N/A']);
  207.                     return new Response(''204, [
  208.                         'Content-Type' => 'application/x-protobuf'
  209.                     ]);
  210.                 }
  211.                 
  212.                 $response = new Response($mvt);
  213.                 $response->headers->set('Content-Type''application/x-protobuf');
  214.                 $response->headers->set('Content-Length'strlen($mvt));
  215.                 $response->headers->set('Cache-Control''public, max-age=86400');
  216.                 $response->headers->set('Access-Control-Allow-Origin''*');
  217.                 $response->headers->set('X-Cache''MISS');
  218.                 $response->headers->set('X-Source'$materializedView);
  219.                 
  220.                 $this->logger->info("Tile generado exitosamente", [
  221.                     'fuente' => $materializedView,
  222.                     'bytes' => strlen($mvt),
  223.                     'comarca' => $comarca ?? 'N/A'
  224.                 ]);
  225.                 
  226.                 return $response;
  227.             }
  228.             
  229.             $this->logger->warning("No hay datos para el tile", [
  230.                 'fuente' => $materializedView,
  231.                 'zoom' => $z,
  232.                 'tile' => "{$x}/{$y}",
  233.                 'comarca' => $comarca ?? 'N/A'
  234.             ]);
  235.             
  236.             return new Response(''204, [
  237.                 'Content-Type' => 'application/x-protobuf'
  238.             ]);
  239.             
  240.         } catch (\Exception $e) {
  241.             $this->logger->error("Error en tiles", [
  242.                 'tabla' => $tabla,
  243.                 'fuente_intentada' => $materializedView ?? 'desconocida',
  244.                 'error' => $e->getMessage(),
  245.                 'comarca' => $comarca ?? 'N/A'
  246.             ]);
  247.             return new Response(
  248.                 'Error: ' $e->getMessage(), 
  249.                 Response::HTTP_INTERNAL_SERVER_ERROR
  250.             );
  251.         }
  252.     }
  253.     /**
  254.      * Determina qué materialized view usar según el zoom level
  255.      */
  256.     private function getMaterializedViewName(string $tablaint $z): string
  257.     {
  258.         if ($z <= 8) {
  259.             return "{$tabla}_mvt_z0_8";      // Tolerancia 200m
  260.         } elseif ($z <= 10) {
  261.             return "{$tabla}_mvt_z9_10";     // Tolerancia 75m
  262.         } elseif ($z <= 12) {
  263.             return "{$tabla}_mvt_z11_12";    // Tolerancia 30m
  264.         } elseif ($z <= 14) {
  265.             return "{$tabla}_mvt_z13_14";    // Tolerancia 10m
  266.         } elseif ($z <= 16) {
  267.             return "{$tabla}_mvt_z15_16";    // Tolerancia 3m
  268.         } elseif ($z <= 18) {
  269.             return "{$tabla}_mvt_z17_18";    // Tolerancia 1m
  270.         } else {
  271.             return $tabla;                    // Original sin simplificar
  272.         }
  273.     }
  274.     /**
  275.      * Verifica si existe una materialized view
  276.      */
  277.     private function checkMaterializedViewExists($connectionstring $viewName): bool
  278.     {
  279.         $sql "
  280.             SELECT EXISTS (
  281.                 SELECT 1 
  282.                 FROM pg_matviews 
  283.                 WHERE matviewname = :viewName
  284.             ) as exists
  285.         ";
  286.         
  287.         try {
  288.             $stmt $connection->prepare($sql);
  289.             $stmt->bindValue('viewName'$viewName);
  290.             $result $stmt->executeQuery();
  291.             $data $result->fetchAssociative();
  292.             
  293.             return (bool) $data['exists'];
  294.         } catch (\Exception $e) {
  295.             $this->logger->error("Error verificando materialized view: " $e->getMessage());
  296.             return false;
  297.         }
  298.     }
  299.     private function extractSRID(string $srsName): int
  300.     {
  301.         $parts explode(':'$srsName);
  302.         return isset($parts[1]) ? intval($parts[1]) : 4326;
  303.     }
  304. }