kmlInGeoJson($contenuto); $quante = count($geojson['features']); if ($quante === 0) { throw new RuntimeException( 'Nel file KML non è stato trovato nessun poligono o linea: ' . 'serve almeno un perimetro che delimiti l\'area della città.' ); } Storage::disk('local')->makeDirectory(self::DIR); Storage::disk('local')->put(self::FILE_KML, $contenuto); Storage::disk('local')->put(self::FILE_GEOJSON, json_encode($geojson, JSON_UNESCAPED_UNICODE)); $config = $this->config(); $config['kml_nome'] = $nomeOriginale; $config['aggiornato_il'] = now()->toDateTimeString(); $this->salvaConfig($config); return $quante; } /** GeoJSON FeatureCollection dell'area attiva, o null se mai caricata. */ public function area(): ?array { if (! Storage::disk('local')->exists(self::FILE_GEOJSON)) { return null; } $decoded = json_decode(Storage::disk('local')->get(self::FILE_GEOJSON), true); return is_array($decoded) ? $decoded : null; } public function config(): array { if (! Storage::disk('local')->exists(self::FILE_CONFIG)) { return ['kml_nome' => null, 'aggiornato_il' => null]; } $decoded = json_decode(Storage::disk('local')->get(self::FILE_CONFIG), true); return is_array($decoded) ? $decoded + ['kml_nome' => null, 'aggiornato_il' => null] : ['kml_nome' => null, 'aggiornato_il' => null]; } private function salvaConfig(array $config): void { Storage::disk('local')->makeDirectory(self::DIR); Storage::disk('local')->put(self::FILE_CONFIG, json_encode($config, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)); } // ─── KML → GeoJSON ────────────────────────────────────────────────────── /** * Estrae poligoni e linee dai Placemark di un KML (anche annidati in * Document/Folder e dentro MultiGeometry) e li restituisce come * FeatureCollection GeoJSON. * * Le xpath usano local-name(): i prefissi registrati con * registerXPathNamespace non si ereditano sui nodi restituiti da xpath(), * e i KML in circolazione dichiarano il namespace in almeno tre modi * diversi (OGC, Google legacy, nessuno). Cosi' funzionano tutti. */ public function kmlInGeoJson(string $kml): array { $precedente = libxml_use_internal_errors(true); $xml = simplexml_load_string($kml); libxml_clear_errors(); libxml_use_internal_errors($precedente); if ($xml === false) { throw new RuntimeException('Il file non è un XML/KML leggibile.'); } $placemarks = $xml->xpath("//*[local-name()='Placemark']") ?: []; $features = []; foreach ($placemarks as $placemark) { $nodiNome = $placemark->xpath("./*[local-name()='name']") ?: []; $nome = trim((string) ($nodiNome[0] ?? '')); foreach ($this->geometrieDelPlacemark($placemark) as $geometry) { $features[] = [ 'type' => 'Feature', 'properties' => ['name' => $nome], 'geometry' => $geometry, ]; } } return ['type' => 'FeatureCollection', 'features' => $features]; } private function geometrieDelPlacemark(\SimpleXMLElement $placemark): array { $geometrie = []; foreach ($placemark->xpath(".//*[local-name()='Polygon']") ?: [] as $polygon) { $anelli = []; foreach ($polygon->xpath(".//*[local-name()='LinearRing']/*[local-name()='coordinates']") ?: [] as $coordinates) { $anello = $this->coordinateKml((string) $coordinates); if (count($anello) >= 3) { $anelli[] = $anello; } } if ($anelli !== []) { $geometrie[] = ['type' => 'Polygon', 'coordinates' => $anelli]; } } // Le LineString servono ai KML che tracciano il perimetro come linea // invece che come poligono chiuso. foreach ($placemark->xpath(".//*[local-name()='LineString']/*[local-name()='coordinates']") ?: [] as $coordinates) { $punti = $this->coordinateKml((string) $coordinates); if (count($punti) >= 2) { $geometrie[] = ['type' => 'LineString', 'coordinates' => $punti]; } } return $geometrie; } /** "lng,lat[,alt] lng,lat[,alt] ..." → [[lng, lat], ...] */ private function coordinateKml(string $testo): array { $punti = []; foreach (preg_split('/\s+/', trim($testo)) ?: [] as $tripla) { if ($tripla === '') { continue; } $parti = explode(',', $tripla); if (count($parti) >= 2 && is_numeric($parti[0]) && is_numeric($parti[1])) { $punti[] = [(float) $parti[0], (float) $parti[1]]; } } return $punti; } }