Files
termanager2/app/Models/TerritorioMappa.php
Francesco Picone e7148c7b32 Piantine pubblicate anche dopo un ripristino; posto unico senza gare
- Lo stato "Pubblicata" non dipende più dall'id del territorio, che il
  ripristino ricrea: l'impronta usa il numero, e la piantina ricorda il
  percorso del PDF pubblicato (pdf_pubblicato, con migrazione che riconosce
  le piantine già pubblicate).
- BackupOperationState: corrente() in sola lettura e liberaCorrente() sotto
  lo stesso mutex di occupa(), così una lettura non cancella il puntatore
  di un'operazione appena avviata.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 15:33:07 +02:00

190 lines
6.2 KiB
PHP

<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
/**
* Il disegno di un'area sulla mappa.
*
* Puo' essere collegato a un territorio oppure no: si disegna guardando la
* cartografia, il numero si decide dopo. Finche' resta scollegato vale il
* campo `nome` come etichetta; una volta collegato comanda il numero del
* territorio, che e' il riferimento con cui lo chiamano tutti.
*
* `geojson` e' la bozza: l'editor la salva da solo a ogni modifica, e non
* tocca il PDF del territorio. Il PDF cambia solo con la pubblicazione, che
* ricorda cosa e' stato pubblicato (`geojson_pubblicato`,
* `impronta_pubblicata`, `pubblicata_at`).
*/
class TerritorioMappa extends Model
{
public const STATO_BOZZA = 'bozza';
public const STATO_PUBBLICATA = 'pubblicata';
public const STATO_MODIFICATA = 'modificata';
protected $table = 'territorio_mappe';
protected $fillable = [
'territorio_id',
'nome',
'geojson',
'geojson_pubblicato',
'impronta_pubblicata',
'pdf_pubblicato',
'pubblicata_at',
'image_path',
'rendered_at',
];
protected function casts(): array
{
return [
'rendered_at' => 'datetime',
'pubblicata_at' => 'datetime',
];
}
/**
* withTrashed: un territorio nel cestino resta collegato. Senza, la
* relazione tornava null, l'editor mostrava la piantina come scollegata e
* il primo salvataggio cancellava il collegamento — che al ripristino del
* territorio non c'era piu'.
*/
public function territorio()
{
return $this->belongsTo(Territorio::class, 'territorio_id')->withTrashed();
}
/**
* La geometria decodificata (Polygon/MultiPolygon GeoJSON) o null se
* il contenuto non è più interpretabile.
*/
public function geometria(): ?array
{
$decoded = json_decode((string) $this->geojson, true);
return is_array($decoded) ? $decoded : null;
}
/** Collegata a un territorio vivo: è la condizione per pubblicare. */
public function collegata(): bool
{
return $this->territorio_id !== null
&& $this->territorio !== null
&& ! $this->territorio->trashed();
}
public function territorioNelCestino(): bool
{
return $this->territorio !== null && $this->territorio->trashed();
}
/**
* Come si chiama questa piantina nell'interfaccia: il numero del
* territorio se c'è, altrimenti il nome scelto a mano, altrimenti un
* ripiego che almeno la distingue dalle altre.
*/
public function etichetta(): string
{
if ($this->territorio !== null) {
return 'Terr. ' . $this->territorio->numero;
}
$nome = trim((string) $this->nome);
return $nome !== '' ? $nome : 'Piantina #' . $this->id;
}
// ─── Bozza e pubblicazione ──────────────────────────────────────────────
/**
* Impronta di tutto cio' che finisce nel PDF: confine, territorio e i
* suoi dati stampati sulla cartolina (numero, zona come «Località»,
* confini). Se cambia uno qualunque di questi, il PDF in vigore non
* corrisponde piu' e la piantina ha modifiche da pubblicare — anche quando
* il disegno e' rimasto lo stesso.
*/
public function impronta(): string
{
$territorio = $this->territorio;
// Il territorio entra con il numero, non con l'id: il numero è unico e
// il ripristino di un backup ricrea i territori con id nuovi, che
// facevano risultare da pubblicare ogni piantina appena ripristinata.
return hash('sha256', json_encode([
$this->geometria(),
$territorio ? trim((string) $territorio->numero) : null,
$territorio?->zona?->nome,
$territorio ? trim((string) $territorio->confini) : null,
]));
}
public function stato(): string
{
if ($this->pubblicata_at === null) {
return self::STATO_BOZZA;
}
return $this->impronta() === $this->impronta_pubblicata && $this->pdfInUsoDallaPiantina()
? self::STATO_PUBBLICATA
: self::STATO_MODIFICATA;
}
/**
* Il PDF in vigore del territorio è ancora quello prodotto dall'ultima
* pubblicazione. Se nel frattempo è stato sostituito per altre vie
* (ripristino di una versione, caricamento a mano, import), quello che
* arriva ai proclamatori non è più questo disegno, anche se l'impronta
* torna. Si confronta il percorso, che il backup conserva: prima si
* cercava l'id del territorio nel nome del file, e dopo un ripristino
* (id nuovi) non tornava più.
*/
public function pdfInUsoDallaPiantina(): bool
{
return $this->pdf_pubblicato !== null
&& $this->territorio !== null
&& $this->territorio->pdf_path === $this->pdf_pubblicato;
}
/** Il testo dell'etichetta di stato, uguale in ogni pagina. */
public function statoEtichetta(): string
{
return match ($this->stato()) {
self::STATO_BOZZA => 'Bozza',
self::STATO_PUBBLICATA => 'Pubblicata',
self::STATO_MODIFICATA => 'Modifiche da pubblicare',
};
}
/**
* Perché non si può pubblicare, o null se si può. Il testo va a schermo
* accanto al pulsante: dice cosa fare, non solo che manca qualcosa.
*/
public function motivoNonPubblicabile(): ?string
{
if ($this->geometria() === null) {
return 'Disegna il confine della piantina.';
}
if ($this->territorioNelCestino()) {
return 'Il territorio collegato è nel cestino: ripristinalo o collega un altro territorio.';
}
if (! $this->collegata()) {
return 'Collega la piantina a un territorio: il PDF è quello del territorio.';
}
return null;
}
/** Il confine pubblicato è diverso da quello in bozza: si può scartare. */
public function bozzaScartabile(): bool
{
return $this->geojson_pubblicato !== null
&& json_decode($this->geojson_pubblicato, true) != $this->geometria();
}
}