Obvody/app/Model/SborFacade.php

209 lines
6.1 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Model;
use Nette\Database\Explorer;
use Nette\Utils\Json;
use Nette\Utils\JsonException;
use SimpleXMLElement;
/**
* Hranice sboru. V tabulce je jeden záznam, nový KML ho nahradí.
*/
final class SborFacade
{
public function __construct(
private Explorer $database,
) {
}
/**
* @return array{nazev: string, bodu: int}|null
*/
public function getSbor(): ?array
{
$radek = $this->database->fetch('SELECT TOP 1 NAZEV, GEO FROM SBOR ORDER BY ID');
if ($radek === null) {
return null;
}
try {
$geo = Json::decode((string) $radek->GEO, forceArrays: true);
} catch (JsonException) {
$geo = [];
}
return [
'nazev' => (string) $radek->NAZEV,
'bodu' => is_array($geo) ? $this->pocetBodu($geo) : 0,
];
}
/**
* GeoJSON geometrie jediné uložené hranice sboru.
*/
public function getGeoJson(): ?string
{
$geo = $this->database->fetchField('SELECT TOP 1 GEO FROM SBOR ORDER BY ID');
if (!is_string($geo) || trim($geo) === '') {
return null;
}
return $geo;
}
public function nahradZKml(string $kml): void
{
$nazev = $this->nazevZKml($kml);
$geo = Json::encode($this->geometrieZKml($kml));
$id = $this->database->fetchField('SELECT TOP 1 ID FROM SBOR ORDER BY ID');
if ($id === null) {
$this->database->query('INSERT INTO SBOR (NAZEV, GEO) VALUES (?, ?)', $nazev, $geo);
return;
}
$this->database->query('UPDATE SBOR SET NAZEV = ?, GEO = ? WHERE ID = ?', $nazev, $geo, $id);
$this->database->query('DELETE FROM SBOR WHERE ID <> ?', $id);
}
private function nazevZKml(string $kml): string
{
$xml = $this->nactiKml($kml);
$nazvy = $xml->xpath('//k:Document/k:name');
if ($nazvy === false || $nazvy === []) {
$nazvy = $xml->xpath('//k:Placemark/k:name');
}
$nazev = trim((string) ($nazvy[0] ?? ''));
if ($nazev === '') {
throw new \InvalidArgumentException('V souboru KML chybí název sboru.');
}
if (mb_strlen($nazev) > 50) {
throw new \InvalidArgumentException('Název sboru v souboru je delší než 50 znaků.');
}
return $nazev;
}
/**
* @return array{type: string, coordinates: array<int, mixed>}
*/
private function geometrieZKml(string $kml): array
{
$xml = $this->nactiKml($kml);
$uzly = $xml->xpath('//k:Polygon/k:outerBoundaryIs/k:LinearRing/k:coordinates');
if ($uzly === false || $uzly === []) {
throw new \InvalidArgumentException('V souboru KML není polygon s hranicí.');
}
$prstence = [];
foreach ($uzly as $uzel) {
$prstence[] = $this->prstenec((string) $uzel);
}
if (count($prstence) === 1) {
return [
'type' => 'Polygon',
'coordinates' => [$prstence[0]],
];
}
return [
'type' => 'MultiPolygon',
'coordinates' => array_map(
static fn(array $prstenec): array => [$prstenec],
$prstence,
),
];
}
private function nactiKml(string $kml): SimpleXMLElement
{
$kml = trim($kml);
if ($kml === '' || !str_contains($kml, '<kml')) {
throw new \InvalidArgumentException('Soubor není ve formátu KML.');
}
$predchozi = libxml_use_internal_errors(true);
try {
$xml = simplexml_load_string($kml, SimpleXMLElement::class, LIBXML_NONET);
} finally {
libxml_clear_errors();
libxml_use_internal_errors($predchozi);
}
if (!$xml instanceof SimpleXMLElement) {
throw new \InvalidArgumentException('Soubor KML se nepodařilo přečíst.');
}
$xml->registerXPathNamespace('k', 'http://www.opengis.net/kml/2.2');
return $xml;
}
/**
* @return list<array{0: float, 1: float}>
*/
private function prstenec(string $text): array
{
$kruh = [];
foreach (preg_split('/\s+/', trim($text)) ?: [] as $token) {
if ($token === '') {
continue;
}
$casti = explode(',', $token);
if (count($casti) < 2 || !is_numeric($casti[0]) || !is_numeric($casti[1])) {
throw new \InvalidArgumentException('Bod hranice v KML nemá souřadnice.');
}
$lng = round((float) $casti[0], 7);
$lat = round((float) $casti[1], 7);
if ($lng < 12.0 || $lng > 19.0 || $lat < 48.0 || $lat > 52.0) {
throw new \InvalidArgumentException('Bod hranice sboru je mimo oblast obvodů.');
}
$kruh[] = [$lng, $lat];
}
if (count($kruh) > 1 && $kruh[0] === $kruh[count($kruh) - 1]) {
array_pop($kruh);
}
if (count($kruh) < 3) {
throw new \InvalidArgumentException('Hranice sboru musí mít aspoň tři body.');
}
if (count($kruh) > 50000) {
throw new \InvalidArgumentException('Hranice sboru má příliš mnoho bodů.');
}
$kruh[] = $kruh[0];
return $kruh;
}
/**
* @param array<string, mixed> $geo
*/
private function pocetBodu(array $geo): int
{
$prstence = [];
if (($geo['type'] ?? '') === 'Polygon') {
$prstence[] = $geo['coordinates'][0] ?? [];
} elseif (($geo['type'] ?? '') === 'MultiPolygon') {
foreach ($geo['coordinates'] ?? [] as $polygon) {
$prstence[] = $polygon[0] ?? [];
}
}
$pocet = 0;
foreach ($prstence as $kruh) {
if (!is_array($kruh)) {
continue;
}
$delka = count($kruh);
if ($delka > 1 && $kruh[0] === $kruh[$delka - 1]) {
$delka--;
}
$pocet += $delka;
}
return $pocet;
}
}