getClientOriginalExtension()); $path = $file->getPathname(); $geojson = match ($ext) { 'geojson' => self::parseGeoJson($path), 'kml' => self::kmlToGeoJson($path), 'kmz' => self::kmzToGeoJson($path), 'shp' => self::shapefileToGeoJson($path), 'zip' => self::handleZip($path), default => null, }; if (! $geojson) { return null; } return self::postProcess($geojson); } /* ======================= POST PROCESADO PRO ======================= */ private static function postProcess(array $geojson): array { $features = []; foreach ($geojson['features'] ?? [] as $feature) { if (! isset($feature['geometry'])) { continue; } $geometry = self::cleanGeometry($feature['geometry']); if (! $geometry) { continue; } $features[] = [ 'type' => 'Feature', 'geometry' => $geometry, 'properties' => self::normalizeProperties($feature['properties'] ?? []), ]; } return [ 'type' => 'FeatureCollection', 'features' => $features, 'bbox' => self::calculateBBox($features), 'centroid' => self::calculateCentroid($features), ]; } /* ======================= NORMALIZACIÓN ======================= */ private static function normalizeProperties(array $props): array { return array_merge([ 'name' => '', 'description' => '', ], $props); } /* ======================= GEOMETRY CLEAN ======================= */ private static function cleanGeometry(array $geom): ?array { if (! isset($geom['type'], $geom['coordinates'])) { return null; } if ($geom['type'] === 'Polygon') { $geom['coordinates'] = array_map(function ($ring) { if ($ring[0] !== end($ring)) { $ring[] = $ring[0]; } return $ring; }, $geom['coordinates']); } return $geom; } /* ======================= BBOX ======================= */ private static function calculateBBox(array $features): ?array { $coords = []; foreach ($features as $f) { $coords = array_merge($coords, self::flattenCoords($f['geometry']['coordinates'])); } if (empty($coords)) { return null; } $lons = array_column($coords, 0); $lats = array_column($coords, 1); return [ min($lons), min($lats), max($lons), max($lats), ]; } /* ======================= CENTROIDE SIMPLE ======================= */ private static function calculateCentroid(array $features): ?array { $coords = []; foreach ($features as $f) { $coords = array_merge($coords, self::flattenCoords($f['geometry']['coordinates'])); } if (empty($coords)) { return null; } $x = array_sum(array_column($coords, 0)) / count($coords); $y = array_sum(array_column($coords, 1)) / count($coords); return [$x, $y]; } private static function flattenCoords($coords): array { $result = []; $iterator = function ($c) use (&$result, &$iterator) { if (! is_array($c)) { return; } if (isset($c[0]) && isset($c[1]) && is_numeric($c[0])) { $result[] = [$c[0], $c[1]]; return; } foreach ($c as $item) { $iterator($item); } }; $iterator($coords); return $result; } /* ======================= GEOJSON ======================= */ private static function parseGeoJson($path): ?array { $data = json_decode(file_get_contents($path), true); return json_last_error() === JSON_ERROR_NONE ? $data : null; } /* ======================= KML (MEJORADO) ======================= */ private static function kmlToGeoJson($path): ?array { libxml_use_internal_errors(true); $xml = simplexml_load_file($path); if (! $xml) { return null; } // Namespace-agnostic: usamos local-name() en el XPath para aceptar KMLs // con cualquier xmlns (opengis 2.2, earth.google 2.1/2.0, o sin xmlns). $placemarks = $xml->xpath('//*[local-name()="Placemark"]'); if ($placemarks === false) { $placemarks = []; } $features = []; foreach ($placemarks as $pm) { $geom = self::parseKmlGeometry($pm); if (! $geom) { continue; } $features[] = [ 'type' => 'Feature', 'geometry' => $geom, 'properties' => [ 'name' => trim((string) self::kmlChild($pm, 'name')), 'description' => trim((string) self::kmlChild($pm, 'description')), ], ]; } return ['type' => 'FeatureCollection', 'features' => $features]; } /** Descomprime un KMZ y parsea el .kml interno. */ private static function kmzToGeoJson(string $path): ?array { $zip = new \ZipArchive; if ($zip->open($path) !== true) { return null; } $tmp = sys_get_temp_dir().'/kmz_'.uniqid(); if (! @mkdir($tmp, 0777, true) && ! is_dir($tmp)) { $zip->close(); return null; } $zip->extractTo($tmp); $zip->close(); // Buscar el primer .kml (habitualmente doc.kml) $kml = null; $rii = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($tmp)); foreach ($rii as $file) { if ($file->isFile() && strtolower($file->getExtension()) === 'kml') { $kml = $file->getPathname(); break; } } $result = $kml ? self::kmlToGeoJson($kml) : null; // Limpieza self::rrmdir($tmp); return $result; } private static function rrmdir(string $dir): void { if (! is_dir($dir)) { return; } foreach (scandir($dir) ?: [] as $item) { if ($item === '.' || $item === '..') { continue; } $p = $dir.DIRECTORY_SEPARATOR.$item; is_dir($p) ? self::rrmdir($p) : @unlink($p); } @rmdir($dir); } /** Devuelve el primer hijo cuyo local-name coincida (sin depender del prefijo). */ private static function kmlChild(\SimpleXMLElement $node, string $name): \SimpleXMLElement|string { $matches = $node->xpath('./*[local-name()="'.$name.'"]'); return $matches ? $matches[0] : ''; } private static function parseKmlGeometry($pm): ?array { // Todos los accesos van por xpath local-name para tolerar cualquier xmlns. $find = fn (string $tag) => $pm->xpath('./*[local-name()="'.$tag.'"]'); $findDeep = fn (\SimpleXMLElement $n, string $tag) => $n->xpath('.//*[local-name()="'.$tag.'"]'); if ($multi = $find('MultiGeometry')) { $geoms = []; foreach ($multi[0]->children() as $g) { $parsed = self::parseKmlGeometry($g); if ($parsed) { $geoms[] = $parsed; } } return ['type' => 'GeometryCollection', 'geometries' => $geoms]; } if ($point = $find('Point')) { $coords = self::parseKmlCoords((string) ($findDeep($point[0], 'coordinates')[0] ?? '')); return $coords ? ['type' => 'Point', 'coordinates' => $coords[0]] : null; } if ($line = $find('LineString')) { $coords = self::parseKmlCoords((string) ($findDeep($line[0], 'coordinates')[0] ?? '')); return $coords ? ['type' => 'LineString', 'coordinates' => $coords] : null; } if ($poly = $find('Polygon')) { $outer = $findDeep($poly[0], 'coordinates')[0] ?? ''; $coords = self::parseKmlCoords((string) $outer); return $coords ? ['type' => 'Polygon', 'coordinates' => [$coords]] : null; } return null; } private static function parseKmlCoords(string $text): array { $coords = []; foreach (preg_split('/\s+/', trim($text)) as $pair) { $p = explode(',', $pair); if (count($p) >= 2) { $coords[] = [(float) $p[0], (float) $p[1]]; } } return $coords; } /* ======================= SHP REAL ======================= */ private static function shapefileToGeoJson($path): ?array { try { $reader = new ShapefileReader($path); $features = []; while ($record = $reader->fetchRecord()) { if ($record->isDeleted()) { continue; } $geom = json_decode($record->getGeometry()->toGeoJSON(), true); if (! $geom) { continue; } $features[] = [ 'type' => 'Feature', 'geometry' => $geom, 'properties' => $record->getDataArray(), ]; } return ['type' => 'FeatureCollection', 'features' => $features]; } catch (\Exception $e) { Log::error($e->getMessage()); return null; } } /* ======================= ZIP LIMPIO ======================= */ private static function handleZip($zipPath): ?array { $zip = new \ZipArchive; if ($zip->open($zipPath) !== true) { return null; } $dir = sys_get_temp_dir().'/geo_'.uniqid(); mkdir($dir); $zip->extractTo($dir); $zip->close(); $result = null; foreach (scandir($dir) as $file) { $full = $dir.'/'.$file; $ext = strtolower(pathinfo($file, PATHINFO_EXTENSION)); if ($ext === 'shp') { $result = self::shapefileToGeoJson($full); break; } if ($ext === 'kml') { $result = self::kmlToGeoJson($full); break; } } self::deleteDir($dir); return $result; } private static function deleteDir($dir) { foreach (glob("$dir/*") as $file) { is_dir($file) ? self::deleteDir($file) : unlink($file); } rmdir($dir); } }