336 lines
8.5 KiB
PHP
336 lines
8.5 KiB
PHP
|
|
<?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),
|
||
|
|
'kml' => self::kmlToGeoJson($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;
|
||
|
|
|
||
|
|
$xml->registerXPathNamespace('kml', 'http://www.opengis.net/kml/2.2');
|
||
|
|
|
||
|
|
$placemarks = $xml->xpath('//kml:Placemark');
|
||
|
|
|
||
|
|
$features = [];
|
||
|
|
|
||
|
|
foreach ($placemarks as $pm) {
|
||
|
|
$geom = self::parseKmlGeometry($pm);
|
||
|
|
if (!$geom) continue;
|
||
|
|
|
||
|
|
$features[] = [
|
||
|
|
'type' => 'Feature',
|
||
|
|
'geometry' => $geom,
|
||
|
|
'properties' => [
|
||
|
|
'name' => (string)$pm->name,
|
||
|
|
'description' => (string)$pm->description
|
||
|
|
]
|
||
|
|
];
|
||
|
|
}
|
||
|
|
|
||
|
|
return ['type' => 'FeatureCollection', 'features' => $features];
|
||
|
|
}
|
||
|
|
|
||
|
|
private static function parseKmlGeometry($pm): ?array
|
||
|
|
{
|
||
|
|
if (isset($pm->MultiGeometry)) {
|
||
|
|
$geoms = [];
|
||
|
|
|
||
|
|
foreach ($pm->MultiGeometry->children() as $g) {
|
||
|
|
$parsed = self::parseKmlGeometry($g);
|
||
|
|
if ($parsed) $geoms[] = $parsed;
|
||
|
|
}
|
||
|
|
|
||
|
|
return [
|
||
|
|
'type' => 'GeometryCollection',
|
||
|
|
'geometries' => $geoms
|
||
|
|
];
|
||
|
|
}
|
||
|
|
|
||
|
|
if (isset($pm->Point)) {
|
||
|
|
return [
|
||
|
|
'type' => 'Point',
|
||
|
|
'coordinates' => self::parseKmlCoords((string)$pm->Point->coordinates)[0]
|
||
|
|
];
|
||
|
|
}
|
||
|
|
|
||
|
|
if (isset($pm->LineString)) {
|
||
|
|
return [
|
||
|
|
'type' => 'LineString',
|
||
|
|
'coordinates' => self::parseKmlCoords((string)$pm->LineString->coordinates)
|
||
|
|
];
|
||
|
|
}
|
||
|
|
|
||
|
|
if (isset($pm->Polygon)) {
|
||
|
|
return [
|
||
|
|
'type' => 'Polygon',
|
||
|
|
'coordinates' => [
|
||
|
|
self::parseKmlCoords(
|
||
|
|
(string)$pm->Polygon->outerBoundaryIs->LinearRing->coordinates
|
||
|
|
)
|
||
|
|
]
|
||
|
|
];
|
||
|
|
}
|
||
|
|
|
||
|
|
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);
|
||
|
|
}
|
||
|
|
}
|