Blocco 2 - Esiti delle azioni Livewire come avvisi (trait Avvisa) invece di flash che comparivano alla pagina dopo. - PDF, miniature e versioni dei territori non più serviti da nginx su /storage: TerritorioFileController con login, permessi e cache privata. - Ripristino tutto o niente: verifica dell'archivio, una transazione con import, piantine, blocco account e scambio dei file annullabile. - Backup formato 2 con piantine, storico PDF e area KML. - Posto unico preso in un passo (Cache::lock) e rispettato anche da import XML e PDF. - Import PDF da cartella: abbinamento corretto (1-A, 12 B, ambigui), caricamento a lotti, file non PDF ignorati, PDF sostituito nello storico. - L'import XML elimina i file dei territori che non usa più; comando territori:file-orfani per quelli accumulati prima. Blocco 3 - Password attuale richiesta al cambio volontario; AuthenticateSession chiude le sessioni aperte altrove. - Niente nomi dei proclamatori in chiaro nel registro attività. - Export XML senza remember_token; chi riceve l'hash dal file deve cambiare password. - Campagne: ultimo giorno incluso, percentuale sui territori distinti. - Coda: retry_after 960 s, stop_grace_period 16m, Redis volatile-lru, impostazioni rilette a ogni job. - Azzeramento che cancella anche piantine, area KML e anteprime. - Pulsanti profilo e menu che non si nascondevano; icona del sito. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
175 lines
6.2 KiB
PHP
175 lines
6.2 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.
|
|
*
|
|
* Questa cartella sta accanto al disco `public`, non dentro: BackupArchive la
|
|
* impacchetta a parte (entry piantine/, dal formato 2 dell'archivio) e il
|
|
* ripristino la sostituisce insieme al disco. Un archivio di formato 1 non la
|
|
* contiene, e ripristinandolo l'area in uso resta com'e'.
|
|
*/
|
|
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 ['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;
|
|
}
|
|
}
|