209 lines
6.1 KiB
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;
|
|
}
|
|
}
|