- 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>
118 lines
4.3 KiB
PHP
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");
|
|
}
|
|
}
|