diff --git a/app/Services/SpatialFileConverter.php b/app/Services/SpatialFileConverter.php
index aad11c9..6b7d4fe 100644
--- a/app/Services/SpatialFileConverter.php
+++ b/app/Services/SpatialFileConverter.php
@@ -16,6 +16,7 @@ class SpatialFileConverter
$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,
@@ -176,9 +177,10 @@ class SpatialFileConverter
if (!$xml) return null;
- $xml->registerXPathNamespace('kml', 'http://www.opengis.net/kml/2.2');
-
- $placemarks = $xml->xpath('//kml:Placemark');
+ // 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 = [];
@@ -190,54 +192,92 @@ class SpatialFileConverter
'type' => 'Feature',
'geometry' => $geom,
'properties' => [
- 'name' => (string)$pm->name,
- 'description' => (string)$pm->description
- ]
+ '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
{
- if (isset($pm->MultiGeometry)) {
- $geoms = [];
+ // 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 . '"]');
- foreach ($pm->MultiGeometry->children() as $g) {
+ if ($multi = $find('MultiGeometry')) {
+ $geoms = [];
+ foreach ($multi[0]->children() as $g) {
$parsed = self::parseKmlGeometry($g);
if ($parsed) $geoms[] = $parsed;
}
-
- return [
- 'type' => 'GeometryCollection',
- 'geometries' => $geoms
- ];
+ return ['type' => 'GeometryCollection', 'geometries' => $geoms];
}
- if (isset($pm->Point)) {
- return [
- 'type' => 'Point',
- 'coordinates' => self::parseKmlCoords((string)$pm->Point->coordinates)[0]
- ];
+ if ($point = $find('Point')) {
+ $coords = self::parseKmlCoords((string) ($findDeep($point[0], 'coordinates')[0] ?? ''));
+ return $coords ? ['type' => 'Point', 'coordinates' => $coords[0]] : null;
}
- if (isset($pm->LineString)) {
- return [
- 'type' => 'LineString',
- 'coordinates' => self::parseKmlCoords((string)$pm->LineString->coordinates)
- ];
+ if ($line = $find('LineString')) {
+ $coords = self::parseKmlCoords((string) ($findDeep($line[0], 'coordinates')[0] ?? ''));
+ return $coords ? ['type' => 'LineString', 'coordinates' => $coords] : null;
}
- if (isset($pm->Polygon)) {
- return [
- 'type' => 'Polygon',
- 'coordinates' => [
- self::parseKmlCoords(
- (string)$pm->Polygon->outerBoundaryIs->LinearRing->coordinates
- )
- ]
- ];
+ if ($poly = $find('Polygon')) {
+ $outer = $findDeep($poly[0], 'coordinates')[0] ?? '';
+ $coords = self::parseKmlCoords((string) $outer);
+ return $coords ? ['type' => 'Polygon', 'coordinates' => [$coords]] : null;
}
return null;
diff --git a/tests/Feature/SpatialFileConverterTest.php b/tests/Feature/SpatialFileConverterTest.php
new file mode 100644
index 0000000..a72ae7d
--- /dev/null
+++ b/tests/Feature/SpatialFileConverterTest.php
@@ -0,0 +1,61 @@
+
+
+
+ P1-3.7,40.4,0
+ P2-3.6,40.5,0
+
+';
+ $g = SpatialFileConverter::convertToGeoJson($this->uploadedKml($kml));
+ $this->assertNotNull($g);
+ $this->assertCount(2, $g['features']);
+ $this->assertEquals('Point', $g['features'][0]['geometry']['type']);
+ }
+
+ public function test_kml_with_google_earth_namespace_still_yields_features(): void
+ {
+ // Este era el bug: xpath //kml:Placemark fallaba con este xmlns.
+ $kml = '
+
+
+ Pilar 1-3.7,40.4
+
+';
+ $g = SpatialFileConverter::convertToGeoJson($this->uploadedKml($kml));
+ $this->assertNotNull($g);
+ $this->assertCount(1, $g['features']);
+ $this->assertEquals('Pilar 1', $g['features'][0]['properties']['name']);
+ }
+
+ public function test_kml_without_namespace_still_yields_features(): void
+ {
+ $kml = '
+
+ Muro
+ -3.7,40.4 -3.6,40.5
+
+';
+ $g = SpatialFileConverter::convertToGeoJson($this->uploadedKml($kml));
+ $this->assertNotNull($g);
+ $this->assertCount(1, $g['features']);
+ $this->assertEquals('LineString', $g['features'][0]['geometry']['type']);
+ }
+}