Files
construprogress/app/Services/SpatialFileConverter.php
T

423 lines
11 KiB
PHP
Raw Normal View History

2026-05-07 23:31:33 +02:00
<?php
namespace App\Services;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Log;
use Shapefile\ShapefileReader;
class SpatialFileConverter
{
public static function convertToGeoJson(UploadedFile $file): ?array
{
$ext = strtolower($file->getClientOriginalExtension());
$path = $file->getPathname();
$geojson = match ($ext) {
'geojson' => self::parseGeoJson($path),
2026-08-28 13:04:28 +02:00
'kml' => self::kmlToGeoJson($path),
'kmz' => self::kmzToGeoJson($path),
'shp' => self::shapefileToGeoJson($path),
'zip' => self::handleZip($path),
default => null,
2026-05-07 23:31:33 +02:00
};
2026-08-28 13:04:28 +02:00
if (! $geojson) {
return null;
}
2026-05-07 23:31:33 +02:00
return self::postProcess($geojson);
}
/* =======================
POST PROCESADO PRO
======================= */
private static function postProcess(array $geojson): array
{
$features = [];
foreach ($geojson['features'] ?? [] as $feature) {
2026-08-28 13:04:28 +02:00
if (! isset($feature['geometry'])) {
continue;
}
2026-05-07 23:31:33 +02:00
$geometry = self::cleanGeometry($feature['geometry']);
2026-08-28 13:04:28 +02:00
if (! $geometry) {
continue;
}
2026-05-07 23:31:33 +02:00
$features[] = [
'type' => 'Feature',
'geometry' => $geometry,
2026-08-28 13:04:28 +02:00
'properties' => self::normalizeProperties($feature['properties'] ?? []),
2026-05-07 23:31:33 +02:00
];
}
return [
'type' => 'FeatureCollection',
'features' => $features,
'bbox' => self::calculateBBox($features),
2026-08-28 13:04:28 +02:00
'centroid' => self::calculateCentroid($features),
2026-05-07 23:31:33 +02:00
];
}
/* =======================
NORMALIZACIÓN
======================= */
private static function normalizeProperties(array $props): array
{
return array_merge([
'name' => '',
'description' => '',
], $props);
}
/* =======================
GEOMETRY CLEAN
======================= */
private static function cleanGeometry(array $geom): ?array
{
2026-08-28 13:04:28 +02:00
if (! isset($geom['type'], $geom['coordinates'])) {
return null;
}
2026-05-07 23:31:33 +02:00
if ($geom['type'] === 'Polygon') {
$geom['coordinates'] = array_map(function ($ring) {
if ($ring[0] !== end($ring)) {
$ring[] = $ring[0];
}
2026-08-28 13:04:28 +02:00
2026-05-07 23:31:33 +02:00
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']));
}
2026-08-28 13:04:28 +02:00
if (empty($coords)) {
return null;
}
2026-05-07 23:31:33 +02:00
$lons = array_column($coords, 0);
$lats = array_column($coords, 1);
return [
min($lons),
min($lats),
max($lons),
2026-08-28 13:04:28 +02:00
max($lats),
2026-05-07 23:31:33 +02:00
];
}
/* =======================
CENTROIDE SIMPLE
======================= */
private static function calculateCentroid(array $features): ?array
{
$coords = [];
foreach ($features as $f) {
$coords = array_merge($coords, self::flattenCoords($f['geometry']['coordinates']));
}
2026-08-28 13:04:28 +02:00
if (empty($coords)) {
return null;
}
2026-05-07 23:31:33 +02:00
$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) {
2026-08-28 13:04:28 +02:00
if (! is_array($c)) {
return;
}
2026-05-07 23:31:33 +02:00
if (isset($c[0]) && isset($c[1]) && is_numeric($c[0])) {
$result[] = [$c[0], $c[1]];
2026-08-28 13:04:28 +02:00
2026-05-07 23:31:33 +02:00
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);
2026-08-28 13:04:28 +02:00
2026-05-07 23:31:33 +02:00
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);
2026-08-28 13:04:28 +02:00
if (! $xml) {
return null;
}
2026-05-07 23:31:33 +02:00
// 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"]');
2026-08-28 13:04:28 +02:00
if ($placemarks === false) {
$placemarks = [];
}
2026-05-07 23:31:33 +02:00
$features = [];
foreach ($placemarks as $pm) {
$geom = self::parseKmlGeometry($pm);
2026-08-28 13:04:28 +02:00
if (! $geom) {
continue;
}
2026-05-07 23:31:33 +02:00
$features[] = [
'type' => 'Feature',
'geometry' => $geom,
'properties' => [
'name' => trim((string) self::kmlChild($pm, 'name')),
'description' => trim((string) self::kmlChild($pm, 'description')),
],
2026-05-07 23:31:33 +02:00
];
}
return ['type' => 'FeatureCollection', 'features' => $features];
}
/** Descomprime un KMZ y parsea el .kml interno. */
private static function kmzToGeoJson(string $path): ?array
{
2026-08-28 13:04:28 +02:00
$zip = new \ZipArchive;
if ($zip->open($path) !== true) {
return null;
}
2026-08-28 13:04:28 +02:00
$tmp = sys_get_temp_dir().'/kmz_'.uniqid();
if (! @mkdir($tmp, 0777, true) && ! is_dir($tmp)) {
$zip->close();
2026-08-28 13:04:28 +02:00
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);
2026-08-28 13:04:28 +02:00
return $result;
}
private static function rrmdir(string $dir): void
{
2026-08-28 13:04:28 +02:00
if (! is_dir($dir)) {
return;
}
foreach (scandir($dir) ?: [] as $item) {
2026-08-28 13:04:28 +02:00
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
{
2026-08-28 13:04:28 +02:00
$matches = $node->xpath('./*[local-name()="'.$name.'"]');
return $matches ? $matches[0] : '';
}
2026-05-07 23:31:33 +02:00
private static function parseKmlGeometry($pm): ?array
{
// Todos los accesos van por xpath local-name para tolerar cualquier xmlns.
2026-08-28 13:04:28 +02:00
$find = fn (string $tag) => $pm->xpath('./*[local-name()="'.$tag.'"]');
$findDeep = fn (\SimpleXMLElement $n, string $tag) => $n->xpath('.//*[local-name()="'.$tag.'"]');
2026-05-07 23:31:33 +02:00
if ($multi = $find('MultiGeometry')) {
$geoms = [];
foreach ($multi[0]->children() as $g) {
2026-05-07 23:31:33 +02:00
$parsed = self::parseKmlGeometry($g);
2026-08-28 13:04:28 +02:00
if ($parsed) {
$geoms[] = $parsed;
}
2026-05-07 23:31:33 +02:00
}
2026-08-28 13:04:28 +02:00
return ['type' => 'GeometryCollection', 'geometries' => $geoms];
2026-05-07 23:31:33 +02:00
}
if ($point = $find('Point')) {
$coords = self::parseKmlCoords((string) ($findDeep($point[0], 'coordinates')[0] ?? ''));
2026-08-28 13:04:28 +02:00
return $coords ? ['type' => 'Point', 'coordinates' => $coords[0]] : null;
2026-05-07 23:31:33 +02:00
}
if ($line = $find('LineString')) {
$coords = self::parseKmlCoords((string) ($findDeep($line[0], 'coordinates')[0] ?? ''));
2026-08-28 13:04:28 +02:00
return $coords ? ['type' => 'LineString', 'coordinates' => $coords] : null;
2026-05-07 23:31:33 +02:00
}
if ($poly = $find('Polygon')) {
$outer = $findDeep($poly[0], 'coordinates')[0] ?? '';
$coords = self::parseKmlCoords((string) $outer);
2026-08-28 13:04:28 +02:00
return $coords ? ['type' => 'Polygon', 'coordinates' => [$coords]] : null;
2026-05-07 23:31:33 +02:00
}
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) {
2026-08-28 13:04:28 +02:00
$coords[] = [(float) $p[0], (float) $p[1]];
2026-05-07 23:31:33 +02:00
}
}
2026-08-28 13:04:28 +02:00
2026-05-07 23:31:33 +02:00
return $coords;
}
/* =======================
SHP REAL
======================= */
private static function shapefileToGeoJson($path): ?array
{
try {
$reader = new ShapefileReader($path);
$features = [];
while ($record = $reader->fetchRecord()) {
2026-08-28 13:04:28 +02:00
if ($record->isDeleted()) {
continue;
}
2026-05-07 23:31:33 +02:00
$geom = json_decode($record->getGeometry()->toGeoJSON(), true);
2026-08-28 13:04:28 +02:00
if (! $geom) {
continue;
}
2026-05-07 23:31:33 +02:00
$features[] = [
'type' => 'Feature',
'geometry' => $geom,
2026-08-28 13:04:28 +02:00
'properties' => $record->getDataArray(),
2026-05-07 23:31:33 +02:00
];
}
return ['type' => 'FeatureCollection', 'features' => $features];
} catch (\Exception $e) {
Log::error($e->getMessage());
2026-08-28 13:04:28 +02:00
2026-05-07 23:31:33 +02:00
return null;
}
}
/* =======================
ZIP LIMPIO
======================= */
private static function handleZip($zipPath): ?array
{
2026-08-28 13:04:28 +02:00
$zip = new \ZipArchive;
2026-05-07 23:31:33 +02:00
2026-08-28 13:04:28 +02:00
if ($zip->open($zipPath) !== true) {
return null;
}
2026-05-07 23:31:33 +02:00
2026-08-28 13:04:28 +02:00
$dir = sys_get_temp_dir().'/geo_'.uniqid();
2026-05-07 23:31:33 +02:00
mkdir($dir);
$zip->extractTo($dir);
$zip->close();
$result = null;
foreach (scandir($dir) as $file) {
2026-08-28 13:04:28 +02:00
$full = $dir.'/'.$file;
2026-05-07 23:31:33 +02:00
$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);
}
2026-08-28 13:04:28 +02:00
}