fix(kml): reconocer Placemark con cualquier xmlns + soporte KMZ
Bug: kmlToGeoJson usaba xpath //kml:Placemark exigiendo el namespace http://www.opengis.net/kml/2.2. KMLs con xmlns de Google Earth (http://earth.google.com/kml/2.1) o sin namespace daban 0 placemarks → la capa se creaba pero SIN elementos. Fix: - Todos los xpath usan local-name() (namespace-agnostic). - parseKmlGeometry navega Point/LineString/Polygon/MultiGeometry sin depender del prefijo del namespace. - Soporte KMZ: descomprime el zip y parsea el .kml interno (habitualmente doc.kml). - Nuevo helper kmlChild() para leer <name>/<description> tolerante al ns. Tests: SpatialFileConverterTest cubre los tres KMLs (opengis 2.2, google 2.1, sin xmlns). Suite 94 passing. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Services\SpatialFileConverter;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Tests\TestCase;
|
||||
|
||||
class SpatialFileConverterTest extends TestCase
|
||||
{
|
||||
private function uploadedKml(string $content, string $name = 'test.kml'): UploadedFile
|
||||
{
|
||||
$tmp = tempnam(sys_get_temp_dir(), 'kml_');
|
||||
file_put_contents($tmp, $content);
|
||||
return new UploadedFile($tmp, $name, 'application/vnd.google-earth.kml+xml', null, true);
|
||||
}
|
||||
|
||||
public function test_kml_with_opengis_namespace_yields_features(): void
|
||||
{
|
||||
$kml = '<?xml version="1.0" encoding="UTF-8"?>
|
||||
<kml xmlns="http://www.opengis.net/kml/2.2">
|
||||
<Document>
|
||||
<Placemark><name>P1</name><Point><coordinates>-3.7,40.4,0</coordinates></Point></Placemark>
|
||||
<Placemark><name>P2</name><Point><coordinates>-3.6,40.5,0</coordinates></Point></Placemark>
|
||||
</Document>
|
||||
</kml>';
|
||||
$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 = '<?xml version="1.0" encoding="UTF-8"?>
|
||||
<kml xmlns="http://earth.google.com/kml/2.1">
|
||||
<Document>
|
||||
<Placemark><name>Pilar 1</name><Point><coordinates>-3.7,40.4</coordinates></Point></Placemark>
|
||||
</Document>
|
||||
</kml>';
|
||||
$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 = '<?xml version="1.0" encoding="UTF-8"?>
|
||||
<kml>
|
||||
<Placemark><name>Muro</name>
|
||||
<LineString><coordinates>-3.7,40.4 -3.6,40.5</coordinates></LineString>
|
||||
</Placemark>
|
||||
</kml>';
|
||||
$g = SpatialFileConverter::convertToGeoJson($this->uploadedKml($kml));
|
||||
$this->assertNotNull($g);
|
||||
$this->assertCount(1, $g['features']);
|
||||
$this->assertEquals('LineString', $g['features'][0]['geometry']['type']);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user