197 lines
5.9 KiB
PHP
197 lines
5.9 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Model;
|
|
|
|
use Nette;
|
|
use Nette\Database\Explorer;
|
|
use Nette\Database\Row;
|
|
use Nette\Security\User;
|
|
use Nette\Utils\DateTime;
|
|
use Nette\Utils\Json;
|
|
use Nette\Utils\JsonException;
|
|
|
|
/**
|
|
* Třída pro načítání informací o obvodech z DB.
|
|
*/
|
|
final class ObvodFacade
|
|
{
|
|
/** Sloupce bez GEO, aby seznam obvodů nenačítal hranice. */
|
|
private const SLOUPCE = 'ID, NAZEV, CISLO, SKUPINA, AKTIVNI';
|
|
|
|
public function __construct(
|
|
private Explorer $database,
|
|
private User $user,
|
|
) {
|
|
}
|
|
|
|
/**
|
|
* Základní informace o obvodu.
|
|
* @param int $id
|
|
* @return Nette\Database\Table\ActiveRow|null
|
|
*/
|
|
public function getObvod(int $id): Row|null
|
|
{
|
|
$sql = 'SELECT ' . self::SLOUPCE . '
|
|
FROM OBVOD
|
|
WHERE OBVOD.ID = ?';
|
|
return $this->database->fetch($sql, $id);
|
|
}
|
|
|
|
public const SKUPINA_COOKIE = 'obvodSkupina';
|
|
|
|
/**
|
|
* Základní informace o obvodech.
|
|
* @return array<int, Row>
|
|
*/
|
|
public function getObvody(?string $skupina = null): array
|
|
{
|
|
if ($skupina === null || $skupina === '') {
|
|
return $this->database->fetchAll('SELECT ' . self::SLOUPCE . ' FROM OBVOD ORDER BY CISLO');
|
|
}
|
|
|
|
return $this->database->fetchAll(
|
|
'SELECT ' . self::SLOUPCE . ' FROM OBVOD WHERE SKUPINA = ? ORDER BY CISLO',
|
|
$skupina,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @return list<string>
|
|
*/
|
|
public function getSkupiny(): array
|
|
{
|
|
$radky = $this->database->fetchAll(
|
|
"SELECT DISTINCT SKUPINA
|
|
FROM OBVOD
|
|
WHERE SKUPINA IS NOT NULL AND LTRIM(RTRIM(SKUPINA)) <> ''
|
|
ORDER BY SKUPINA",
|
|
);
|
|
|
|
return array_map(static fn(Row $radek): string => (string) $radek->SKUPINA, $radky);
|
|
}
|
|
|
|
/**
|
|
* Předchozí a další existující obvod podle čísla.
|
|
* Čísla nemusí jít po sobě.
|
|
*
|
|
* @return array{predchozi: int|null, dalsi: int|null}
|
|
*/
|
|
public function getSousede(int $id): array
|
|
{
|
|
$prazdne = ['predchozi' => null, 'dalsi' => null];
|
|
$radky = $this->database->fetchAll('SELECT ID FROM OBVOD ORDER BY CISLO, ID');
|
|
$ids = array_map(static fn(Row $radek): int => (int) $radek->ID, $radky);
|
|
$index = array_search($id, $ids, true);
|
|
if ($index === false) {
|
|
return $prazdne;
|
|
}
|
|
|
|
return [
|
|
'predchozi' => $index > 0 ? $ids[$index - 1] : null,
|
|
'dalsi' => $index < count($ids) - 1 ? $ids[$index + 1] : null,
|
|
];
|
|
}
|
|
|
|
public function vyberSkupinu(?string $pozadovana, ?string $ulozena): string
|
|
{
|
|
$skupiny = $this->getSkupiny();
|
|
if ($pozadovana !== null && in_array($pozadovana, $skupiny, true)) {
|
|
return $pozadovana;
|
|
}
|
|
if ($ulozena !== null && in_array($ulozena, $skupiny, true)) {
|
|
return $ulozena;
|
|
}
|
|
|
|
return $skupiny[0] ?? '';
|
|
}
|
|
|
|
/**
|
|
* Hranice obvodů jako GeoJSON FeatureCollection.
|
|
* GEO v databázi je GeoJSON geometrie (Polygon nebo MultiPolygon).
|
|
*/
|
|
public function getGeoJson(): string
|
|
{
|
|
$radky = $this->database->fetchAll(
|
|
"SELECT CISLO, GEO
|
|
FROM OBVOD
|
|
WHERE GEO IS NOT NULL AND LTRIM(RTRIM(GEO)) <> ''
|
|
ORDER BY CISLO",
|
|
);
|
|
|
|
$features = [];
|
|
foreach ($radky as $radek) {
|
|
$features[] = '{"type":"Feature","properties":{"number":'
|
|
. (int) $radek->CISLO
|
|
. '},"geometry":'
|
|
. $radek->GEO
|
|
. '}';
|
|
}
|
|
|
|
return '{"type":"FeatureCollection","features":[' . implode(',', $features) . ']}';
|
|
}
|
|
|
|
/**
|
|
* Uloží hranici obvodu. GEO je GeoJSON Polygon s jedním prstencem.
|
|
*/
|
|
public function ulozHranici(int $id, string $geo): void
|
|
{
|
|
if ($this->database->fetchField('SELECT ID FROM OBVOD WHERE ID = ?', $id) === null) {
|
|
throw new \InvalidArgumentException('Obvod nenalezen.');
|
|
}
|
|
|
|
$this->database->query(
|
|
'UPDATE OBVOD SET GEO = ? WHERE ID = ?',
|
|
$this->overHranici($geo),
|
|
$id,
|
|
);
|
|
}
|
|
|
|
private function overHranici(string $geo): string
|
|
{
|
|
try {
|
|
$data = Json::decode($geo, forceArrays: true);
|
|
} catch (JsonException) {
|
|
throw new \InvalidArgumentException('Hranice nejsou platný JSON.');
|
|
}
|
|
|
|
if (!is_array($data) || ($data['type'] ?? '') !== 'Polygon' || !isset($data['coordinates']) || !is_array($data['coordinates'])) {
|
|
throw new \InvalidArgumentException('Hranice musí být polygon.');
|
|
}
|
|
if (count($data['coordinates']) !== 1 || !is_array($data['coordinates'][0])) {
|
|
throw new \InvalidArgumentException('Polygon musí mít jeden prstenec bez děr.');
|
|
}
|
|
|
|
$kruh = [];
|
|
foreach ($data['coordinates'][0] as $bod) {
|
|
if (!is_array($bod) || count($bod) < 2 || !is_numeric($bod[0]) || !is_numeric($bod[1])) {
|
|
throw new \InvalidArgumentException('Bod hranice nemá souřadnice.');
|
|
}
|
|
$lng = round((float) $bod[0], 7);
|
|
$lat = round((float) $bod[1], 7);
|
|
if ($lng < 12.0 || $lng > 19.0 || $lat < 48.0 || $lat > 52.0) {
|
|
throw new \InvalidArgumentException('Bod hranice 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 musí mít aspoň tři body.');
|
|
}
|
|
if (count($kruh) > 2000) {
|
|
throw new \InvalidArgumentException('Hranice má příliš mnoho bodů.');
|
|
}
|
|
|
|
$kruh[] = $kruh[0];
|
|
|
|
return Json::encode([
|
|
'type' => 'Polygon',
|
|
'coordinates' => [$kruh],
|
|
]);
|
|
}
|
|
|
|
} |