Editor piantine: bozza automatica e pubblicazione con anteprima
- Il disegno è bozza e si salva da solo; i dati del territorio si modificano dall'editor e valgono subito nel gestionale. - Un solo "Pubblica" (immagine + PDF), uguale in editor e gestione: anteprima preparata in coda su disco privato, conferma obbligatoria, rifiutata se nel frattempo è cambiato qualcosa di stampato. - Stati Bozza / Pubblicata / Modifiche da pubblicare, calcolati sull'impronta di confine e dati stampati e sul PDF in uso. - Gestione riscritta: stato, ricerca, filtri, storico paginato. - Editor ridisegnato a tre zone, utilizzabile anche su tablet e telefono. - Avvisi via evento (trait Avvisa + <x-avvisi>). - .gitattributes: script shell sempre LF, altrimenti il container locale su Windows non parte. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
namespace App\Services\Piantine;
|
||||
|
||||
use App\Models\TerritorioMappa;
|
||||
use Illuminate\Http\Client\ConnectionException;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use RuntimeException;
|
||||
@@ -14,6 +15,11 @@ use RuntimeException;
|
||||
*/
|
||||
class PiantinaRenderer
|
||||
{
|
||||
/**
|
||||
* Rendering e archiviazione come immagine della piantina.
|
||||
*
|
||||
* @return string il percorso sul disco pubblico
|
||||
*/
|
||||
public function render(TerritorioMappa $mappa): string
|
||||
{
|
||||
$geometry = $mappa->geometria();
|
||||
@@ -22,19 +28,38 @@ class PiantinaRenderer
|
||||
throw new RuntimeException('La geometria salvata per questa piantina non è leggibile: ridisegna il poligono.');
|
||||
}
|
||||
|
||||
[$immagine, $estensione] = $this->immagine($geometry);
|
||||
|
||||
return $this->archivia($mappa, $immagine, $estensione);
|
||||
}
|
||||
|
||||
/**
|
||||
* Solo il rendering, senza toccare niente: serve all'anteprima della
|
||||
* pubblicazione, che non deve cambiare la piantina finché non si conferma.
|
||||
*
|
||||
* @return array{0: string, 1: string} contenuto e estensione ('jpg'|'png')
|
||||
*/
|
||||
public function immagine(array $geometry): array
|
||||
{
|
||||
$formato = config('piantine.formato') === 'png' ? 'png' : 'jpeg';
|
||||
|
||||
$response = Http::timeout((int) config('piantine.renderer_timeout'))
|
||||
->post(rtrim((string) config('piantine.renderer_url'), '/') . '/render', [
|
||||
'geometry' => $geometry,
|
||||
'marginRatio' => (float) config('piantine.margin_ratio'),
|
||||
'maxSide' => (int) config('piantine.max_side'),
|
||||
'aspect' => (float) config('piantine.aspetto'),
|
||||
'strokeWidth' => (float) config('piantine.tratto'),
|
||||
'strokeOpacity' => (float) config('piantine.tratto_opacita'),
|
||||
'format' => $formato,
|
||||
'quality' => (int) config('piantine.qualita'),
|
||||
]);
|
||||
try {
|
||||
$response = Http::timeout((int) config('piantine.renderer_timeout'))
|
||||
->post(rtrim((string) config('piantine.renderer_url'), '/') . '/render', [
|
||||
'geometry' => $geometry,
|
||||
'marginRatio' => (float) config('piantine.margin_ratio'),
|
||||
'maxSide' => (int) config('piantine.max_side'),
|
||||
'aspect' => (float) config('piantine.aspetto'),
|
||||
'strokeWidth' => (float) config('piantine.tratto'),
|
||||
'strokeOpacity' => (float) config('piantine.tratto_opacita'),
|
||||
'format' => $formato,
|
||||
'quality' => (int) config('piantine.qualita'),
|
||||
]);
|
||||
} catch (ConnectionException $e) {
|
||||
// Container fermo, nome non risolto o tempo scaduto: il messaggio
|
||||
// di cURL in inglese non dice niente a chi sta pubblicando.
|
||||
throw new RuntimeException('Il servizio che disegna le mappe non risponde (container map-renderer). Riprova fra qualche minuto; se continua, va controllato il server.', 0, $e);
|
||||
}
|
||||
|
||||
if (! $response->successful()) {
|
||||
$dettaglio = $response->json('error') ?? ('HTTP ' . $response->status());
|
||||
@@ -47,15 +72,28 @@ class PiantinaRenderer
|
||||
throw new RuntimeException('Il renderer delle mappe non ha restituito un\'immagine valida.');
|
||||
}
|
||||
|
||||
// Il vecchio file puo' avere un'estensione diversa da quella nuova (si
|
||||
// e' passati da PNG a JPEG): senza questa rimozione resterebbe sul
|
||||
// volume per sempre, senza che niente lo referenzi piu'.
|
||||
if ($mappa->image_path && $mappa->image_path !== $this->percorso($mappa, $formato)) {
|
||||
Storage::disk('public')->delete($mappa->image_path);
|
||||
return [$immagine, $formato === 'png' ? 'png' : 'jpg'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Salva l'immagine come quella della piantina, rimuovendo la precedente.
|
||||
*
|
||||
* Il nome cambia a ogni rendering: nginx e Cloudflare tengono in cache i
|
||||
* file di /storage per giorni, e con un nome fisso il link mostrava
|
||||
* l'immagine vecchia anche dopo averla rigenerata.
|
||||
*/
|
||||
public function archivia(TerritorioMappa $mappa, string $immagine, string $estensione): string
|
||||
{
|
||||
$disco = Storage::disk('public');
|
||||
$path = 'territori-mappe/piantina-' . $mappa->id . '-' . now()->format('YmdHis') . '.' . $estensione;
|
||||
|
||||
if (! $disco->put($path, $immagine)) {
|
||||
throw new RuntimeException('Non è stato possibile salvare l\'immagine della piantina sul disco.');
|
||||
}
|
||||
|
||||
$path = $this->percorso($mappa, $formato);
|
||||
Storage::disk('public')->put($path, $immagine);
|
||||
if ($mappa->image_path && $mappa->image_path !== $path) {
|
||||
$disco->delete($mappa->image_path);
|
||||
}
|
||||
|
||||
$mappa->update([
|
||||
'image_path' => $path,
|
||||
@@ -65,11 +103,6 @@ class PiantinaRenderer
|
||||
return $path;
|
||||
}
|
||||
|
||||
private function percorso(TerritorioMappa $mappa, string $formato): string
|
||||
{
|
||||
return 'territori-mappe/piantina-' . $mappa->id . '.' . ($formato === 'png' ? 'png' : 'jpg');
|
||||
}
|
||||
|
||||
/** I primi byte bastano a distinguere un'immagine da una pagina d'errore. */
|
||||
private function immagineValida(string $dati, string $formato): bool
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user