Files
termanager2/app/Services/Piantine/PiantinaRenderer.php
Francesco Picone 056d95cfcb 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>
2026-10-12 12:08:34 +02:00

118 lines
4.3 KiB
PHP

<?php
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;
/**
* Client del container `map-renderer`: gli passa la geometria di una piantina,
* riceve l'immagine ad alta definizione e la archivia sul disco pubblico
* accanto a PDF e miniature.
*/
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();
if ($geometry === null) {
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';
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());
throw new RuntimeException('Il renderer delle mappe ha risposto con un errore: ' . $dettaglio);
}
$immagine = $response->body();
if (! $this->immagineValida($immagine, $formato)) {
throw new RuntimeException('Il renderer delle mappe non ha restituito un\'immagine valida.');
}
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.');
}
if ($mappa->image_path && $mappa->image_path !== $path) {
$disco->delete($mappa->image_path);
}
$mappa->update([
'image_path' => $path,
'rendered_at' => now(),
]);
return $path;
}
/** I primi byte bastano a distinguere un'immagine da una pagina d'errore. */
private function immagineValida(string $dati, string $formato): bool
{
if ($dati === '') {
return false;
}
return $formato === 'png'
? str_starts_with($dati, "\x89PNG")
: str_starts_with($dati, "\xFF\xD8\xFF");
}
}