++ Prima versione del designer piantine + Skill grafica

This commit is contained in:
2026-08-28 15:32:14 +02:00
parent 8aa29cc467
commit 7e4c8aa9b8
62 changed files with 16340 additions and 4 deletions

View File

@@ -0,0 +1,189 @@
<?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. Stando dentro storage/, entra nei backup completi
* senza bisogno di altro.
*/
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 ['localita' => null, 'kml_nome' => null, 'aggiornato_il' => null];
}
$decoded = json_decode(Storage::disk('local')->get(self::FILE_CONFIG), true);
return is_array($decoded)
? $decoded + ['localita' => null, 'kml_nome' => null, 'aggiornato_il' => null]
: ['localita' => null, 'kml_nome' => null, 'aggiornato_il' => null];
}
/** La "Località" stampata sull'intestazione del modulo S-12. */
public function localita(): string
{
$config = $this->config();
if (! empty($config['localita'])) {
return $config['localita'];
}
return (string) \App\Models\Setting::getValue('congregazione_nome', '');
}
public function impostaLocalita(?string $localita): void
{
$config = $this->config();
$config['localita'] = $localita !== null && trim($localita) !== '' ? trim($localita) : null;
$this->salvaConfig($config);
}
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;
}
}