190 lines
6.6 KiB
PHP
190 lines
6.6 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Piantine;
|
|
|
|
use Illuminate\Support\Facades\Storage;
|
|
use RuntimeException;
|
|
|
|
/**
|
|
* L'area della città su cui si disegnano i territori.
|
|
*
|
|
* Vive su file (storage/app/piantine/), non su tabelle: c'è una sola area
|
|
* attiva, caricare un KML nuovo la sostituisce e i poligoni già disegnati
|
|
* restano al loro posto. Stando dentro storage/, entra nei backup completi
|
|
* senza bisogno di altro.
|
|
*/
|
|
class PiantineArea
|
|
{
|
|
private const DIR = 'piantine';
|
|
private const FILE_KML = 'piantine/area.kml';
|
|
private const FILE_GEOJSON = 'piantine/area.geojson';
|
|
private const FILE_CONFIG = 'piantine/config.json';
|
|
|
|
/**
|
|
* Registra un nuovo KML come area attiva.
|
|
*
|
|
* @return int Numero di geometrie riconosciute nel file.
|
|
*/
|
|
public function salvaKml(string $contenuto, string $nomeOriginale): int
|
|
{
|
|
$geojson = $this->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 ['localita' => null, 'kml_nome' => null, 'aggiornato_il' => null];
|
|
}
|
|
|
|
$decoded = json_decode(Storage::disk('local')->get(self::FILE_CONFIG), true);
|
|
|
|
return is_array($decoded)
|
|
? $decoded + ['localita' => null, 'kml_nome' => null, 'aggiornato_il' => null]
|
|
: ['localita' => null, 'kml_nome' => null, 'aggiornato_il' => null];
|
|
}
|
|
|
|
/** La "Località" stampata sull'intestazione del modulo S-12. */
|
|
public function localita(): string
|
|
{
|
|
$config = $this->config();
|
|
|
|
if (! empty($config['localita'])) {
|
|
return $config['localita'];
|
|
}
|
|
|
|
return (string) \App\Models\Setting::getValue('congregazione_nome', '');
|
|
}
|
|
|
|
public function impostaLocalita(?string $localita): void
|
|
{
|
|
$config = $this->config();
|
|
$config['localita'] = $localita !== null && trim($localita) !== '' ? trim($localita) : null;
|
|
$this->salvaConfig($config);
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|